From 7e96da95f9743aeb6f115f137af20db68e137517 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Mon, 30 May 2022 16:15:17 +0400 Subject: [PATCH 01/89] ios: enable notifications (#713) --- apps/ios/Shared/AppDelegate.swift | 21 +++++++++---------- .../Views/UserSettings/SettingsView.swift | 12 +++++------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 95e21a2513..b3cf044614 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -21,17 +21,16 @@ class AppDelegate: NSObject, UIApplicationDelegate { logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)") let m = ChatModel.shared m.deviceToken = token - UserDefaults.standard.set(false, forKey: DEFAULT_USE_NOTIFICATIONS) -// let useNotifications = UserDefaults.standard.bool(forKey: "useNotifications") -// if useNotifications { -// Task { -// do { -// m.tokenStatus = try await apiRegisterToken(token: token) -// } catch { -// logger.error("apiRegisterToken error: \(responseError(error))") -// } -// } -// } + let useNotifications = UserDefaults.standard.bool(forKey: "useNotifications") + if useNotifications { + Task { + do { + m.tokenStatus = try await apiRegisterToken(token: token) + } catch { + logger.error("apiRegisterToken error: \(responseError(error))") + } + } + } } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index f531634c26..0e5f46aeda 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -150,12 +150,12 @@ struct SettingsView: View { } label: { settingsRow("gauge") { Text("Experimental features") } } -// if let token = chatModel.deviceToken { -// HStack { -// notificationsIcon() -// notificationsToggle(token) -// } -// } + if let token = chatModel.deviceToken { + HStack { + notificationsIcon() + notificationsToggle(token) + } + } Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") } } From fa844c48e993a4ad67b2e28bfe8f977a8ae8e159 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 31 May 2022 07:55:13 +0100 Subject: [PATCH 02/89] ios: SimpleXChat framework to be shared by app/NSE (#714) * ios: SimpleXChat framework to be shared by app/NSE * remove bridging headers from pp/NSE * embed & sign --- apps/ios/Shared/AppDelegate.swift | 1 + apps/ios/Shared/Model/ChatModel.swift | 1 + apps/ios/Shared/Model/NtfManager.swift | 1 + apps/ios/Shared/Model/Shared/CallTypes.swift | 72 ---- apps/ios/Shared/Model/SimpleXAPI.swift | 1 + .../Shared/SimpleX (iOS)-Bridging-Header.h | 11 - apps/ios/Shared/SimpleXApp.swift | 1 + .../Shared/Views/Call/ActiveCallView.swift | 1 + .../Shared/Views/Call/CallController.swift | 1 + apps/ios/Shared/Views/Call/CallManager.swift | 1 + .../Shared/Views/Call/IncomingCallView.swift | 1 + apps/ios/Shared/Views/Call/WebRTC.swift | 1 + apps/ios/Shared/Views/Call/WebRTCView.swift | 1 + .../Shared/Views/Chat/ChatInfoToolbar.swift | 1 + apps/ios/Shared/Views/Chat/ChatInfoView.swift | 1 + .../Views/Chat/ChatItem/CICallItemView.swift | 1 + .../Views/Chat/ChatItem/CIFileView.swift | 3 +- .../Views/Chat/ChatItem/CIImageView.swift | 1 + .../Views/Chat/ChatItem/CILinkView.swift | 1 + .../Views/Chat/ChatItem/CIMetaView.swift | 1 + .../Views/Chat/ChatItem/DeletedItemView.swift | 1 + .../Views/Chat/ChatItem/EmojiItemView.swift | 1 + .../Views/Chat/ChatItem/FramedItemView.swift | 1 + .../ChatItem/IntegrityErrorItemView.swift | 1 + .../Views/Chat/ChatItem/MsgContentView.swift | 1 + apps/ios/Shared/Views/Chat/ChatItemView.swift | 1 + apps/ios/Shared/Views/Chat/ChatView.swift | 1 + .../ComposeMessage/ComposeImageView.swift | 1 + .../Chat/ComposeMessage/ComposeLinkView.swift | 2 +- .../Chat/ComposeMessage/ComposeView.swift | 1 + .../Chat/ComposeMessage/ContextItemView.swift | 1 + .../Chat/ComposeMessage/SendMessageView.swift | 1 + .../Views/ChatList/ChatListNavLink.swift | 1 + .../Shared/Views/ChatList/ChatListView.swift | 1 + .../Views/ChatList/ChatPreviewView.swift | 1 + .../ChatList/ContactConnectionView.swift | 1 + .../Views/ChatList/ContactRequestView.swift | 1 + .../Shared/Views/Helpers/ChatInfoImage.swift | 1 + .../Shared/Views/Helpers/ProfileImage.swift | 1 + .../Shared/Views/NewChat/NewChatButton.swift | 1 + .../Views/Onboarding/CreateProfile.swift | 1 + .../Views/Onboarding/MakeConnection.swift | 1 + apps/ios/Shared/Views/TerminalView.swift | 1 + .../Views/UserSettings/SMPServers.swift | 1 + .../Views/UserSettings/SettingsView.swift | 1 + .../Views/UserSettings/UserProfile.swift | 1 + apps/ios/Shared/dummy.m | 23 -- .../ios/SimpleX NSE/NotificationService.swift | 10 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 368 ++++++++++++++---- .../Model/Shared => SimpleXChat}/API.swift | 8 +- .../Shared => SimpleXChat}/APITypes.swift | 40 +- apps/ios/SimpleXChat/CallTypes.swift | 99 +++++ .../Shared => SimpleXChat}/ChatTypes.swift | 351 +++++++++-------- .../{Shared => SimpleXChat}/FileUtils.swift | 28 +- .../GroupDefaults.swift | 12 +- .../Model/Shared => SimpleXChat}/JSON.swift | 4 +- .../Notifications.swift | 24 +- .../SimpleX.h} | 11 +- .../SimpleXChat.docc/SimpleXChat.md | 13 + apps/ios/SimpleXChat/SimpleXChat.h | 17 + apps/ios/{SimpleX NSE => SimpleXChat}/dummy.m | 4 +- 61 files changed, 733 insertions(+), 409 deletions(-) delete mode 100644 apps/ios/Shared/Model/Shared/CallTypes.swift delete mode 100644 apps/ios/Shared/SimpleX (iOS)-Bridging-Header.h delete mode 100644 apps/ios/Shared/dummy.m rename apps/ios/{Shared/Model/Shared => SimpleXChat}/API.swift (88%) rename apps/ios/{Shared/Model/Shared => SimpleXChat}/APITypes.swift (96%) create mode 100644 apps/ios/SimpleXChat/CallTypes.swift rename apps/ios/{Shared/Model/Shared => SimpleXChat}/ChatTypes.swift (71%) rename apps/ios/{Shared => SimpleXChat}/FileUtils.swift (87%) rename apps/ios/{Shared/Model/Shared => SimpleXChat}/GroupDefaults.swift (54%) rename apps/ios/{Shared/Model/Shared => SimpleXChat}/JSON.swift (92%) rename apps/ios/{Shared/Model/Shared => SimpleXChat}/Notifications.swift (76%) rename apps/ios/{SimpleX NSE/SimpleX NSE-Bridging-Header.h => SimpleXChat/SimpleX.h} (54%) create mode 100755 apps/ios/SimpleXChat/SimpleXChat.docc/SimpleXChat.md create mode 100644 apps/ios/SimpleXChat/SimpleXChat.h rename apps/ios/{SimpleX NSE => SimpleXChat}/dummy.m (89%) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index b3cf044614..ee981ffbba 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import SimpleXChat class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 4d4ae6ad91..0696b0b6e9 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -10,6 +10,7 @@ import Foundation import Combine import SwiftUI import WebKit +import SimpleXChat final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 146075ded4..8a189ef726 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -9,6 +9,7 @@ import Foundation import UserNotifications import UIKit +import SimpleXChat let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT" let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL" diff --git a/apps/ios/Shared/Model/Shared/CallTypes.swift b/apps/ios/Shared/Model/Shared/CallTypes.swift deleted file mode 100644 index acb14f2199..0000000000 --- a/apps/ios/Shared/Model/Shared/CallTypes.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// CallTypes.swift -// SimpleX (iOS) -// -// Created by Evgeny on 05/05/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import Foundation -import SwiftUI - -struct WebRTCCallOffer: Encodable { - var callType: CallType - var rtcSession: WebRTCSession -} - -struct WebRTCSession: Codable { - var rtcSession: String - var rtcIceCandidates: String -} - -struct WebRTCExtraInfo: Codable { - var rtcIceCandidates: String -} - -struct CallInvitation { - var contact: Contact - var callkitUUID: UUID? - var peerMedia: CallMediaType - var sharedKey: String? - var callTs: Date - var callTypeText: LocalizedStringKey { - get { - switch peerMedia { - case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call" - case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call" - } - } - } - - static let sampleData = CallInvitation( - contact: Contact.sampleData, - peerMedia: .audio, - callTs: .now - ) -} - -struct CallType: Codable { - var media: CallMediaType - var capabilities: CallCapabilities -} - -enum CallMediaType: String, Codable, Equatable { - case video = "video" - case audio = "audio" -} - -enum VideoCamera: String, Codable, Equatable { - case user = "user" - case environment = "environment" -} - -struct CallCapabilities: Codable, Equatable { - var encryption: Bool -} - -enum WebRTCCallStatus: String, Encodable { - case connected = "connected" - case connecting = "connecting" - case disconnected = "disconnected" - case failed = "failed" -} diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e44e40ec5e..530c5735c9 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -12,6 +12,7 @@ import Dispatch import BackgroundTasks import SwiftUI import CallKit +import SimpleXChat private var chatController: chat_ctrl? diff --git a/apps/ios/Shared/SimpleX (iOS)-Bridging-Header.h b/apps/ios/Shared/SimpleX (iOS)-Bridging-Header.h deleted file mode 100644 index bc28b42d38..0000000000 --- a/apps/ios/Shared/SimpleX (iOS)-Bridging-Header.h +++ /dev/null @@ -1,11 +0,0 @@ -// -// Use this file to import your target's public headers that you would like to expose to Swift. -// - -extern void hs_init(int argc, char **argv[]); - -typedef void* chat_ctrl; - -extern chat_ctrl chat_init(char *path); -extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); -extern char *chat_recv_msg(chat_ctrl ctl); diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index e1a54748b3..26ec3490ca 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -7,6 +7,7 @@ import SwiftUI import OSLog +import SimpleXChat let logger = Logger() diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index f61105997c..ad90ecf84d 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -8,6 +8,7 @@ import SwiftUI import WebKit +import SimpleXChat struct ActiveCallView: View { @EnvironmentObject var m: ChatModel diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index ff8339b9b2..57af8acca2 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -9,6 +9,7 @@ import Foundation import CallKit import AVFoundation +import SimpleXChat class CallController: NSObject, CXProviderDelegate, ObservableObject { static let useCallKit = false diff --git a/apps/ios/Shared/Views/Call/CallManager.swift b/apps/ios/Shared/Views/Call/CallManager.swift index 5d0f5af0c6..493a3a1d64 100644 --- a/apps/ios/Shared/Views/Call/CallManager.swift +++ b/apps/ios/Shared/Views/Call/CallManager.swift @@ -7,6 +7,7 @@ // import Foundation +import SimpleXChat class CallManager { func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID { diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift index 089ffea5f5..45651b89b1 100644 --- a/apps/ios/Shared/Views/Call/IncomingCallView.swift +++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct IncomingCallView: View { @EnvironmentObject var m: ChatModel diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift index 0850c0f6bb..7e63f4f499 100644 --- a/apps/ios/Shared/Views/Call/WebRTC.swift +++ b/apps/ios/Shared/Views/Call/WebRTC.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI +import SimpleXChat class Call: ObservableObject, Equatable { static func == (lhs: Call, rhs: Call) -> Bool { diff --git a/apps/ios/Shared/Views/Call/WebRTCView.swift b/apps/ios/Shared/Views/Call/WebRTCView.swift index 93a61c3f4d..ab48d93a39 100644 --- a/apps/ios/Shared/Views/Call/WebRTCView.swift +++ b/apps/ios/Shared/Views/Call/WebRTCView.swift @@ -8,6 +8,7 @@ import SwiftUI import WebKit +import SimpleXChat class WebRTCCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate { var rtcWebView: Binding diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 774e8aa1f7..6e3fcfd96d 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat private let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9) private let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 ) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index b7bd7ef587..fc82d543b6 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift index 6b3498900b..a2204cb9b0 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct CICallItemView: View { @EnvironmentObject var m: ChatModel diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index a7a7119c74..31b90e2ee3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct CIFileView: View { @Environment(\.colorScheme) var colorScheme @@ -133,7 +134,7 @@ struct CIFileView: View { struct CIFileView_Previews: PreviewProvider { static var previews: some View { - let sentFile = ChatItem( + let sentFile: ChatItem = ChatItem( chatDir: .directSnd, meta: CIMeta.getSample(1, .now, "", .sndSent, false, true, false), content: .sndMsgContent(msgContent: .file("")), diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 7b8d2efdfd..9c131cc1c9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct CIImageView: View { @Environment(\.colorScheme) var colorScheme diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index a2a4ecedeb..4c12c7312a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct CILinkView: View { @Environment(\.colorScheme) var colorScheme diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index 390281c34a..b6de98d816 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct CIMetaView: View { var chatItem: ChatItem diff --git a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift index 210a4dfd2d..4ac47188ce 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct DeletedItemView: View { var chatItem: ChatItem diff --git a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift index 260e6e9fe6..e45b5bd183 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct EmojiItemView: View { var chatItem: ChatItem diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index e720ab7416..ddaf6d1d28 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index 3a75789210..7a4fa46822 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct IntegrityErrorItemView: View { var chatItem: ChatItem diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index b0b9928c98..6707f8bde2 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) private let linkColor = Color(uiColor: uiLinkColor) diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index d826856953..62edc343ac 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ChatItemView: View { var chatInfo: ChatInfo diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 48d581d2c8..801177e472 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat private let memberImageSize: CGFloat = 34 diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift index c23de72229..3e65600ce5 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ComposeImageView: View { @Environment(\.colorScheme) var colorScheme diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index 7b24a06f82..335755c00f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -8,7 +8,7 @@ import SwiftUI import LinkPresentation - +import SimpleXChat func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { logger.debug("getLinkMetadata: fetching URL preview") diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 0de0ea5555..87f6c44e39 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat enum ComposePreview { case noPreview diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index c1245633c3..b9fec65005 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ContextItemView: View { @Environment(\.colorScheme) var colorScheme diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 22510808ca..c5269c3804 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct SendMessageView: View { @Binding var composeState: ComposeState diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index f23d29583c..c04fdeb3ba 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ChatListNavLink: View { @EnvironmentObject var chatModel: ChatModel diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 746763583a..f0674af969 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index eec99991d6..d1490eaf0c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ChatPreviewView: View { @ObservedObject var chat: Chat diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift index 5243188c6b..7f9388fc57 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ContactConnectionView: View { var contactConnection: PendingContactConnection diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift index ac873fc8f9..2edb1ad0ee 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ContactRequestView: View { var contactRequest: UserContactRequest diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift index c9a2bdf166..1b344148c0 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ChatInfoImage: View { @ObservedObject var chat: Chat diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index f8f75d74d2..cc4f09ae3b 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct ProfileImage: View { var imageStr: String? = nil diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index ddd693c8f6..3adf3d4c27 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat enum NewChatAction: Identifiable { case createLink diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 639a161adf..578cd85d64 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct CreateProfile: View { @EnvironmentObject var m: ChatModel diff --git a/apps/ios/Shared/Views/Onboarding/MakeConnection.swift b/apps/ios/Shared/Views/Onboarding/MakeConnection.swift index 3251b16535..88676535b8 100644 --- a/apps/ios/Shared/Views/Onboarding/MakeConnection.swift +++ b/apps/ios/Shared/Views/Onboarding/MakeConnection.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct MakeConnection: View { @EnvironmentObject var m: ChatModel diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 75198c0977..6aa9e8804c 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat private let terminalFont = Font.custom("Menlo", size: 16) diff --git a/apps/ios/Shared/Views/UserSettings/SMPServers.swift b/apps/ios/Shared/Views/UserSettings/SMPServers.swift index 35909320d6..07913c59ac 100644 --- a/apps/ios/Shared/Views/UserSettings/SMPServers.swift +++ b/apps/ios/Shared/Views/UserSettings/SMPServers.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat private let serversFont = Font.custom("Menlo", size: 14) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 0e5f46aeda..017360e30c 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")! diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 13238971bf..52137f476c 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct UserProfile: View { @EnvironmentObject var chatModel: ChatModel diff --git a/apps/ios/Shared/dummy.m b/apps/ios/Shared/dummy.m deleted file mode 100644 index 301c29909a..0000000000 --- a/apps/ios/Shared/dummy.m +++ /dev/null @@ -1,23 +0,0 @@ -// -// dummy.m -// SimpleX -// -// Created by Evgeny Poberezkin on 22/01/2022. -// - -#import - -#if defined(__x86_64__) && TARGET_IPHONE_SIMULATOR - -#import - -int readdir_r$INODE64(DIR *restrict dirp, struct dirent *restrict entry, - struct dirent **restrict result) { - return readdir_r(dirp, entry, result); -} - -DIR *opendir$INODE64(const char *name) { - return opendir(name); -} - -#endif diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index a615ac5b71..c0a06a1232 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -8,6 +8,7 @@ import UserNotifications import OSLog +import SimpleXChat let logger = Logger() @@ -59,7 +60,7 @@ func startChat() -> User? { try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) return user } catch { - logger.error("NotificationService startChat error: \(responseError(error))") + logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)") } } else { logger.debug("no active user") @@ -120,8 +121,11 @@ func apiGetActiveUser() -> User? { func apiStartChat() throws { let r = sendSimpleXCmd(.startChat) - if case .chatStarted = r { return } - throw r + switch r { + case .chatStarted: return + case .chatRunning: return + default: throw r + } } func apiSetFilesFolder(filesFolder: String) throws { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 1ec19cdf6f..636ab1fec7 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -38,23 +38,17 @@ 5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; }; 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; }; 5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; }; - 5C5E5D3D282447AB00B0488A /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; }; - 5C5E5D3E282447BF00B0488A /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; }; 5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; }; 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; }; 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; }; - 5C764E80279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; }; - 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7B279C71D4000C6508 /* libiconv.tbd */; }; - 5C764E83279C748B000C6508 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7C279C71DB000C6508 /* libz.tbd */; }; 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; }; 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; - 5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96A27A56D4D0075386C /* JSON.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; 5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; }; @@ -81,21 +75,29 @@ 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; - 5CDCAD5328186F9500503DA2 /* GroupDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */; }; - 5CDCAD5428186F9700503DA2 /* GroupDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */; }; - 5CDCAD5828187C7500503DA2 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5728187C7500503DA2 /* dummy.m */; }; - 5CDCAD5F28187D6900503DA2 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */; }; - 5CDCAD6128187D8000503DA2 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDCAD6028187D7900503DA2 /* libz.tbd */; }; - 5CDCAD682818876500503DA2 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96A27A56D4D0075386C /* JSON.swift */; }; - 5CDCAD7628188D3600503DA2 /* APITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7428188D2900503DA2 /* APITypes.swift */; }; - 5CDCAD7728188D3800503DA2 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; }; - 5CDCAD7828188FD300503DA2 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; }; - 5CDCAD7928188FD600503DA2 /* APITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7428188D2900503DA2 /* APITypes.swift */; }; - 5CDCAD7C2818924D00503DA2 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DAE1502809D9F5000DA960 /* FileUtils.swift */; }; - 5CDCAD7E2818941F00503DA2 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7D2818941F00503DA2 /* API.swift */; }; - 5CDCAD7F281894FB00503DA2 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7D2818941F00503DA2 /* API.swift */; }; - 5CDCAD81281A7E2700503DA2 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD80281A7E2700503DA2 /* Notifications.swift */; }; - 5CDCAD82281A7E2700503DA2 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD80281A7E2700503DA2 /* Notifications.swift */; }; + 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; + 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; }; + 5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */ = {isa = PBXBuildFile; fileRef = 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */; }; + 5CE2BA85284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */; }; + 5CE2BA86284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */; }; + 5CE2BA87284532AD00EC33A6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907D28376BB90076573F /* libffi.a */; }; + 5CE2BA88284532AD00EC33A6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907F28376BB90076573F /* libgmp.a */; }; + 5CE2BA89284532AD00EC33A6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907E28376BB90076573F /* libgmpxx.a */; }; + 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; }; + 5CE2BA8C284533A300EC33A6 /* GroupDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */; }; + 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; }; + 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7D2818941F00503DA2 /* API.swift */; }; + 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7428188D2900503DA2 /* APITypes.swift */; }; + 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96A27A56D4D0075386C /* JSON.swift */; }; + 5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD80281A7E2700503DA2 /* Notifications.swift */; }; + 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DAE1502809D9F5000DA960 /* FileUtils.swift */; }; + 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */; }; + 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDCAD6028187D7900503DA2 /* libz.tbd */; }; + 5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA8A2845332200EC33A6 /* SimpleX.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CE2BA96284537A800EC33A6 /* dummy.m */; }; + 5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 5CE2BAA62845617C00EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; platformFilter = ios; }; 5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407127ADB1D0007B033A /* Emoji.swift */; }; 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; @@ -109,19 +111,8 @@ 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; - 64A6908128376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */; }; - 64A6908228376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */; }; - 64A6908328376BBA0076573F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907D28376BB90076573F /* libffi.a */; }; - 64A6908428376BBA0076573F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907D28376BB90076573F /* libffi.a */; }; - 64A6908528376BBA0076573F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907E28376BB90076573F /* libgmpxx.a */; }; - 64A6908628376BBA0076573F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907E28376BB90076573F /* libgmpxx.a */; }; - 64A6908728376BBA0076573F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907F28376BB90076573F /* libgmp.a */; }; - 64A6908828376BBA0076573F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907F28376BB90076573F /* libgmp.a */; }; - 64A6908928376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */; }; - 64A6908A28376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; - 64DAE1512809D9F5000DA960 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DAE1502809D9F5000DA960 /* FileUtils.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -132,8 +123,54 @@ remoteGlobalIDString = 5CA059C9279559F40002BEB4; remoteInfo = "SimpleX (iOS)"; }; + 5CE2BA6E2845308900EC33A6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5CE2BA672845308900EC33A6; + remoteInfo = SimpleXChat; + }; + 5CE2BA9E284555F500EC33A6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5CDCAD442818589900503DA2; + remoteInfo = "SimpleX NSE"; + }; + 5CE2BAA82845617C00EC33A6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5CE2BA672845308900EC33A6; + remoteInfo = SimpleXChat; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 5CE2BA722845308900EC33A6 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 5CE2BAA0284555F500EC33A6 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 3C714776281C081000CB4D4B /* WebRTCView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCView.swift; sourceTree = ""; }; 3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../android/app/src/main/assets/www; sourceTree = ""; }; @@ -177,8 +214,6 @@ 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = ""; }; 5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; }; 5C764E7C279C71DB000C6508 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; - 5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (iOS)-Bridging-Header.h"; sourceTree = ""; }; - 5C764E7F279C7276000C6508 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = ""; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = ""; }; 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = ""; }; 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = ""; }; @@ -216,14 +251,17 @@ 5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX NSE.entitlements"; sourceTree = ""; }; 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDefaults.swift; sourceTree = ""; }; - 5CDCAD5628187C7500503DA2 /* SimpleX NSE-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX NSE-Bridging-Header.h"; sourceTree = ""; }; - 5CDCAD5728187C7500503DA2 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = ""; }; 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; }; 5CDCAD6028187D7900503DA2 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTypes.swift; sourceTree = ""; }; 5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = ""; }; 5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; 5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; + 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SimpleXChat.h; sourceTree = ""; }; + 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = SimpleXChat.docc; sourceTree = ""; }; + 5CE2BA8A2845332200EC33A6 /* SimpleX.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimpleX.h; sourceTree = ""; }; + 5CE2BA96284537A800EC33A6 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = ""; }; 5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; @@ -252,15 +290,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 64A6908928376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */, + 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */, 646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */, - 64A6908528376BBA0076573F /* libgmpxx.a in Frameworks */, - 64A6908728376BBA0076573F /* libgmp.a in Frameworks */, 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, - 5C764E83279C748B000C6508 /* libz.tbd in Frameworks */, - 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */, - 64A6908328376BBA0076573F /* libffi.a in Frameworks */, - 64A6908128376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -275,13 +307,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CDCAD5F28187D6900503DA2 /* libiconv.tbd in Frameworks */, - 64A6908628376BBA0076573F /* libgmpxx.a in Frameworks */, - 64A6908A28376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */, - 5CDCAD6128187D8000503DA2 /* libz.tbd in Frameworks */, - 64A6908228376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */, - 64A6908828376BBA0076573F /* libgmp.a in Frameworks */, - 64A6908428376BBA0076573F /* libffi.a in Frameworks */, + 5CE2BAA62845617C00EC33A6 /* SimpleXChat.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5CE2BA652845308900EC33A6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, + 5CE2BA85284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */, + 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + 5CE2BA89284532AD00EC33A6 /* libgmpxx.a in Frameworks */, + 5CE2BA87284532AD00EC33A6 /* libffi.a in Frameworks */, + 5CE2BA88284532AD00EC33A6 /* libgmp.a in Frameworks */, + 5CE2BA86284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -394,6 +434,7 @@ 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, 5CA059DA279559F40002BEB4 /* Tests iOS */, + 5CE2BA692845308900EC33A6 /* SimpleXChat */, 5CA059CB279559F40002BEB4 /* Products */, 5C764E7A279C71D4000C6508 /* Frameworks */, ); @@ -405,12 +446,9 @@ 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */, 5C36027227F47AD5009F19D9 /* AppDelegate.swift */, 5CA059C4279559F40002BEB4 /* ContentView.swift */, - 64DAE1502809D9F5000DA960 /* FileUtils.swift */, 5C764E87279CBC8E000C6508 /* Model */, 5C2E260D27A30E2400F70299 /* Views */, 5CA059C5279559F40002BEB4 /* Assets.xcassets */, - 5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */, - 5C764E7F279C7276000C6508 /* dummy.m */, 5C13730C2815740A00F43030 /* DebugJSON.playground */, ); path = Shared; @@ -422,6 +460,7 @@ 5CA059CA279559F40002BEB4 /* SimpleX.app */, 5CA059D7279559F40002BEB4 /* Tests iOS.xctest */, 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */, + 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */, ); name = Products; sourceTree = ""; @@ -497,13 +536,18 @@ 5CDCAD492818589900503DA2 /* Info.plist */, 5CB0BA892826CB3A00B3292C /* Localizable.strings */, 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */, - 5CDCAD5728187C7500503DA2 /* dummy.m */, - 5CDCAD5628187C7500503DA2 /* SimpleX NSE-Bridging-Header.h */, ); path = "SimpleX NSE"; sourceTree = ""; }; 5CDCAD7128188CEB00503DA2 /* Shared */ = { + isa = PBXGroup; + children = ( + ); + path = Shared; + sourceTree = ""; + }; + 5CE2BA692845308900EC33A6 /* SimpleXChat */ = { isa = PBXGroup; children = ( 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */, @@ -513,8 +557,13 @@ 5C9FD96A27A56D4D0075386C /* JSON.swift */, 5CDCAD7D2818941F00503DA2 /* API.swift */, 5CDCAD80281A7E2700503DA2 /* Notifications.swift */, + 64DAE1502809D9F5000DA960 /* FileUtils.swift */, + 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */, + 5CE2BA8A2845332200EC33A6 /* SimpleX.h */, + 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */, + 5CE2BA96284537A800EC33A6 /* dummy.m */, ); - path = Shared; + path = SimpleXChat; sourceTree = ""; }; 5CE4407427ADB657007B033A /* ChatItem */ = { @@ -549,6 +598,18 @@ }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + 5CE2BA632845308900EC33A6 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */, + 5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ 5CA059C9279559F40002BEB4 /* SimpleX (iOS) */ = { isa = PBXNativeTarget; @@ -557,10 +618,14 @@ 5CA059C6279559F40002BEB4 /* Sources */, 5CA059C7279559F40002BEB4 /* Frameworks */, 5CA059C8279559F40002BEB4 /* Resources */, + 5CE2BA722845308900EC33A6 /* Embed Frameworks */, + 5CE2BAA0284555F500EC33A6 /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( + 5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */, + 5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */, ); name = "SimpleX (iOS)"; packageProductDependencies = ( @@ -599,12 +664,31 @@ buildRules = ( ); dependencies = ( + 5CE2BAA92845617C00EC33A6 /* PBXTargetDependency */, ); name = "SimpleX NSE"; productName = "SimpleX NSE"; productReference = 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */; productType = "com.apple.product-type.app-extension"; }; + 5CE2BA672845308900EC33A6 /* SimpleXChat */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5CE2BA752845308900EC33A6 /* Build configuration list for PBXNativeTarget "SimpleXChat" */; + buildPhases = ( + 5CE2BA632845308900EC33A6 /* Headers */, + 5CE2BA642845308900EC33A6 /* Sources */, + 5CE2BA652845308900EC33A6 /* Frameworks */, + 5CE2BA662845308900EC33A6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SimpleXChat; + productName = SimpleXChat; + productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -628,6 +712,10 @@ CreatedOnToolsVersion = 13.3; LastSwiftMigration = 1330; }; + 5CE2BA672845308900EC33A6 = { + CreatedOnToolsVersion = 13.3; + LastSwiftMigration = 1330; + }; }; }; buildConfigurationList = 5CA059C1279559F40002BEB4 /* Build configuration list for PBXProject "SimpleX" */; @@ -649,6 +737,7 @@ 5CA059C9279559F40002BEB4 /* SimpleX (iOS) */, 5CA059D6279559F40002BEB4 /* Tests iOS */, 5CDCAD442818589900503DA2 /* SimpleX NSE */, + 5CE2BA672845308900EC33A6 /* SimpleXChat */, ); }; /* End PBXProject section */ @@ -682,6 +771,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5CE2BA662845308900EC33A6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -691,16 +787,12 @@ files = ( 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, - 5CDCAD7F281894FB00503DA2 /* API.swift in Sources */, 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */, - 5CDCAD81281A7E2700503DA2 /* Notifications.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, - 5CDCAD7728188D3800503DA2 /* ChatTypes.swift in Sources */, 5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */, - 5CDCAD5328186F9500503DA2 /* GroupDefaults.swift in Sources */, 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, @@ -709,7 +801,6 @@ 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */, 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, - 5C764E80279C7276000C6508 /* dummy.m in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, @@ -721,8 +812,6 @@ 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */, 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, - 5CDCAD7628188D3600503DA2 /* APITypes.swift in Sources */, - 5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, 5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */, 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */, @@ -751,7 +840,6 @@ 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */, 5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */, 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */, - 64DAE1512809D9F5000DA960 /* FileUtils.swift in Sources */, 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */, @@ -761,7 +849,6 @@ 3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */, 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */, 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, - 5C5E5D3D282447AB00B0488A /* CallTypes.swift in Sources */, 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */, 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */, 5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */, @@ -784,17 +871,25 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5CDCAD7928188FD600503DA2 /* APITypes.swift in Sources */, - 5C5E5D3E282447BF00B0488A /* CallTypes.swift in Sources */, - 5CDCAD5828187C7500503DA2 /* dummy.m in Sources */, 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */, - 5CDCAD82281A7E2700503DA2 /* Notifications.swift in Sources */, - 5CDCAD7C2818924D00503DA2 /* FileUtils.swift in Sources */, - 5CDCAD682818876500503DA2 /* JSON.swift in Sources */, 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */, - 5CDCAD7828188FD300503DA2 /* ChatTypes.swift in Sources */, - 5CDCAD7E2818941F00503DA2 /* API.swift in Sources */, - 5CDCAD5428186F9700503DA2 /* GroupDefaults.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5CE2BA642845308900EC33A6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */, + 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */, + 5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */, + 5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */, + 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */, + 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */, + 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */, + 5CE2BA8C284533A300EC33A6 /* GroupDefaults.swift in Sources */, + 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */, + 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -806,6 +901,22 @@ target = 5CA059C9279559F40002BEB4 /* SimpleX (iOS) */; targetProxy = 5CA059D8279559F40002BEB4 /* PBXContainerItemProxy */; }; + 5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5CE2BA672845308900EC33A6 /* SimpleXChat */; + targetProxy = 5CE2BA6E2845308900EC33A6 /* PBXContainerItemProxy */; + }; + 5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5CDCAD442818589900503DA2 /* SimpleX NSE */; + targetProxy = 5CE2BA9E284555F500EC33A6 /* PBXContainerItemProxy */; + }; + 5CE2BAA92845617C00EC33A6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 5CE2BA672845308900EC33A6 /* SimpleXChat */; + targetProxy = 5CE2BAA82845617C00EC33A6 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -961,6 +1072,7 @@ 5CA059F4279559F40002BEB4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; @@ -994,7 +1106,6 @@ PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "Shared/SimpleX (iOS)-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1004,6 +1115,7 @@ 5CA059F5279559F40002BEB4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; @@ -1037,7 +1149,6 @@ PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "Shared/SimpleX (iOS)-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; @@ -1118,7 +1229,6 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "SimpleX NSE/SimpleX NSE-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1158,13 +1268,106 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "SimpleX NSE/SimpleX NSE-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; }; name = Release; }; + 5CE2BA732845308900EC33A6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 5NN7GUYB6T; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Libraries/ios", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Libraries/sim", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INCLUDE_PATHS = ""; + SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 5CE2BA742845308900EC33A6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 5NN7GUYB6T; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Libraries/ios", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Libraries/sim", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INCLUDE_PATHS = ""; + SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1204,6 +1407,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 5CE2BA752845308900EC33A6 /* Build configuration list for PBXNativeTarget "SimpleXChat" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5CE2BA732845308900EC33A6 /* Debug */, + 5CE2BA742845308900EC33A6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/apps/ios/Shared/Model/Shared/API.swift b/apps/ios/SimpleXChat/API.swift similarity index 88% rename from apps/ios/Shared/Model/Shared/API.swift rename to apps/ios/SimpleXChat/API.swift index 7208c45d99..091a5a7190 100644 --- a/apps/ios/Shared/Model/Shared/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -10,7 +10,7 @@ import Foundation private var chatController: chat_ctrl? -func getChatCtrl() -> chat_ctrl { +public func getChatCtrl() -> chat_ctrl { if let controller = chatController { return controller } let dataDir = getDocumentsDirectory().path + "/mobile_v1" logger.debug("documents directory \(dataDir)") @@ -20,12 +20,12 @@ func getChatCtrl() -> chat_ctrl { return chatController! } -func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse { +public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse { var c = cmd.cmdString.cString(using: .utf8)! return chatResponse(chat_send_cmd(getChatCtrl(), &c)) } -func chatResponse(_ cjson: UnsafeMutablePointer) -> ChatResponse { +public func chatResponse(_ cjson: UnsafeMutablePointer) -> ChatResponse { let s = String.init(cString: cjson) let d = s.data(using: .utf8)! // TODO is there a way to do it without copying the data? e.g: @@ -57,7 +57,7 @@ func prettyJSON(_ obj: NSDictionary) -> String? { return nil } -func responseError(_ err: Error) -> String { +public func responseError(_ err: Error) -> String { if let r = err as? ChatResponse { return String(describing: r) } else { diff --git a/apps/ios/Shared/Model/Shared/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift similarity index 96% rename from apps/ios/Shared/Model/Shared/APITypes.swift rename to apps/ios/SimpleXChat/APITypes.swift index c4e5f5eabe..d45ceef19e 100644 --- a/apps/ios/Shared/Model/Shared/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -11,7 +11,7 @@ import Foundation let jsonDecoder = getJSONDecoder() let jsonEncoder = getJSONEncoder() -enum ChatCommand { +public enum ChatCommand { case showActiveUser case createActiveUser(profile: Profile) case startChat @@ -50,7 +50,7 @@ enum ChatCommand { case receiveFile(fileId: Int64) case string(String) - var cmdString: String { + public var cmdString: String { get { switch self { case .showActiveUser: return "/u" @@ -95,7 +95,7 @@ enum ChatCommand { } } - var cmdType: String { + public var cmdType: String { get { switch self { case .showActiveUser: return "showActiveUser" @@ -151,7 +151,7 @@ struct APIResponse: Decodable { var resp: ChatResponse } -enum ChatResponse: Decodable, Error { +public enum ChatResponse: Decodable, Error { case response(type: String, json: String) case activeUser(user: User) case chatStarted @@ -211,7 +211,7 @@ enum ChatResponse: Decodable, Error { case chatCmdError(chatError: ChatError) case chatError(chatError: ChatError) - var responseType: String { + public var responseType: String { get { switch self { case let .response(type, _): return "* \(type)" @@ -274,7 +274,7 @@ enum ChatResponse: Decodable, Error { } } - var details: String { + public var details: String { get { switch self { case let .response(_, json): return json @@ -346,7 +346,7 @@ struct ComposedMessage: Encodable { var msgContent: MsgContent } -func decodeJSON(_ json: String) -> T? { +public func decodeJSON(_ json: String) -> T? { if let data = json.data(using: .utf8) { return try? jsonDecoder.decode(T.self, from: data) } @@ -366,7 +366,7 @@ private func getJSONObject(_ cjson: UnsafePointer) -> NSDictionary? { return try? JSONSerialization.jsonObject(with: d) as? NSDictionary } -func encodeJSON(_ value: T) -> String { +public func encodeJSON(_ value: T) -> String { let data = try! jsonEncoder.encode(value) return String(decoding: data, as: UTF8.self) } @@ -375,13 +375,13 @@ private func encodeCJSON(_ value: T) -> [CChar] { encodeJSON(value).cString(using: .utf8)! } -enum ChatError: Decodable { +public enum ChatError: Decodable { case error(errorType: ChatErrorType) case errorAgent(agentError: AgentErrorType) case errorStore(storeError: StoreError) } -enum ChatErrorType: Decodable { +public enum ChatErrorType: Decodable { case noActiveUser case activeUserExists case chatNotStarted @@ -415,7 +415,7 @@ enum ChatErrorType: Decodable { case commandError(message: String) } -enum StoreError: Decodable { +public enum StoreError: Decodable { case duplicateName case contactNotFound(contactId: Int64) case contactNotFoundByName(contactName: ContactName) @@ -448,7 +448,7 @@ enum StoreError: Decodable { case chatItemNotFoundByFileId(fileId: Int64) } -enum AgentErrorType: Decodable { +public enum AgentErrorType: Decodable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) case SMP(smpErr: ProtocolErrorType) @@ -458,7 +458,7 @@ enum AgentErrorType: Decodable { case INTERNAL(internalErr: String) } -enum CommandErrorType: Decodable { +public enum CommandErrorType: Decodable { case PROHIBITED case SYNTAX case NO_CONN @@ -466,7 +466,7 @@ enum CommandErrorType: Decodable { case LARGE } -enum ConnectionErrorType: Decodable { +public enum ConnectionErrorType: Decodable { case NOT_FOUND case DUPLICATE case SIMPLEX @@ -474,7 +474,7 @@ enum ConnectionErrorType: Decodable { case NOT_AVAILABLE } -enum BrokerErrorType: Decodable { +public enum BrokerErrorType: Decodable { case RESPONSE(smpErr: ProtocolErrorType) case UNEXPECTED case NETWORK @@ -482,7 +482,7 @@ enum BrokerErrorType: Decodable { case TIMEOUT } -enum ProtocolErrorType: Decodable { +public enum ProtocolErrorType: Decodable { case BLOCK case SESSION case CMD(cmdErr: ProtocolCommandError) @@ -493,7 +493,7 @@ enum ProtocolErrorType: Decodable { case INTERNAL } -enum ProtocolCommandError: Decodable { +public enum ProtocolCommandError: Decodable { case UNKNOWN case SYNTAX case NO_AUTH @@ -501,20 +501,20 @@ enum ProtocolCommandError: Decodable { case NO_ENTITY } -enum ProtocolTransportError: Decodable { +public enum ProtocolTransportError: Decodable { case badBlock case largeMsg case badSession case handshake(handshakeErr: SMPHandshakeError) } -enum SMPHandshakeError: Decodable { +public enum SMPHandshakeError: Decodable { case PARSE case VERSION case IDENTITY } -enum SMPAgentError: Decodable { +public enum SMPAgentError: Decodable { case A_MESSAGE case A_PROHIBITED case A_VERSION diff --git a/apps/ios/SimpleXChat/CallTypes.swift b/apps/ios/SimpleXChat/CallTypes.swift new file mode 100644 index 0000000000..01f05ac4fd --- /dev/null +++ b/apps/ios/SimpleXChat/CallTypes.swift @@ -0,0 +1,99 @@ +// +// CallTypes.swift +// SimpleX (iOS) +// +// Created by Evgeny on 05/05/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +public struct WebRTCCallOffer: Encodable { + public init(callType: CallType, rtcSession: WebRTCSession) { + self.callType = callType + self.rtcSession = rtcSession + } + + public var callType: CallType + public var rtcSession: WebRTCSession +} + +public struct WebRTCSession: Codable { + public init(rtcSession: String, rtcIceCandidates: String) { + self.rtcSession = rtcSession + self.rtcIceCandidates = rtcIceCandidates + } + + public var rtcSession: String + public var rtcIceCandidates: String +} + +public struct WebRTCExtraInfo: Codable { + public init(rtcIceCandidates: String) { + self.rtcIceCandidates = rtcIceCandidates + } + + public var rtcIceCandidates: String +} + +public struct CallInvitation { + public init(contact: Contact, callkitUUID: UUID? = nil, peerMedia: CallMediaType, sharedKey: String? = nil, callTs: Date) { + self.contact = contact + self.callkitUUID = callkitUUID + self.peerMedia = peerMedia + self.sharedKey = sharedKey + self.callTs = callTs + } + + public var contact: Contact + public var callkitUUID: UUID? + public var peerMedia: CallMediaType + public var sharedKey: String? + public var callTs: Date + public var callTypeText: LocalizedStringKey { + get { + switch peerMedia { + case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call" + case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call" + } + } + } + + public static let sampleData = CallInvitation( + contact: Contact.sampleData, + peerMedia: .audio, + callTs: .now + ) +} + +public struct CallType: Codable { + public init(media: CallMediaType, capabilities: CallCapabilities) { + self.media = media + self.capabilities = capabilities + } + + public var media: CallMediaType + public var capabilities: CallCapabilities +} + +public enum CallMediaType: String, Codable, Equatable { + case video = "video" + case audio = "audio" +} + +public enum VideoCamera: String, Codable, Equatable { + case user = "user" + case environment = "environment" +} + +public struct CallCapabilities: Codable, Equatable { + public var encryption: Bool +} + +public enum WebRTCCallStatus: String, Encodable { + case connected = "connected" + case connecting = "connecting" + case disconnected = "disconnected" + case failed = "failed" +} diff --git a/apps/ios/Shared/Model/Shared/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift similarity index 71% rename from apps/ios/Shared/Model/Shared/ChatTypes.swift rename to apps/ios/SimpleXChat/ChatTypes.swift index 69966d8a59..fd631a0cc4 100644 --- a/apps/ios/Shared/Model/Shared/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -9,18 +9,18 @@ import Foundation import SwiftUI -struct User: Decodable, NamedChat { +public struct User: Decodable, NamedChat { var userId: Int64 var userContactId: Int64 var localDisplayName: ContactName - var profile: Profile + public var profile: Profile var activeUser: Bool - var displayName: String { get { profile.displayName } } - var fullName: String { get { profile.fullName } } - var image: String? { get { profile.image } } + public var displayName: String { get { profile.displayName } } + public var fullName: String { get { profile.fullName } } + public var image: String? { get { profile.image } } - static let sampleData = User( + public static let sampleData = User( userId: 1, userContactId: 1, localDisplayName: "alice", @@ -29,14 +29,20 @@ struct User: Decodable, NamedChat { ) } -typealias ContactName = String +public typealias ContactName = String -typealias GroupName = String +public typealias GroupName = String -struct Profile: Codable, NamedChat { - var displayName: String - var fullName: String - var image: String? +public struct Profile: Codable, NamedChat { + public init(displayName: String, fullName: String, image: String? = nil) { + self.displayName = displayName + self.fullName = fullName + self.image = image + } + + public var displayName: String + public var fullName: String + public var image: String? static let sampleData = Profile( displayName: "alice", @@ -44,34 +50,34 @@ struct Profile: Codable, NamedChat { ) } -enum ChatType: String { +public enum ChatType: String { case direct = "@" case group = "#" case contactRequest = "<@" case contactConnection = ":" } -protocol NamedChat { +public protocol NamedChat { var displayName: String { get } var fullName: String { get } var image: String? { get } } extension NamedChat { - var chatViewName: String { + public var chatViewName: String { get { displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") } } } -typealias ChatId = String +public typealias ChatId = String -enum ChatInfo: Identifiable, Decodable, NamedChat { +public enum ChatInfo: Identifiable, Decodable, NamedChat { case direct(contact: Contact) case group(groupInfo: GroupInfo) case contactRequest(contactRequest: UserContactRequest) case contactConnection(contactConnection: PendingContactConnection) - var localDisplayName: String { + public var localDisplayName: String { get { switch self { case let .direct(contact): return contact.localDisplayName @@ -82,7 +88,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - var displayName: String { + public var displayName: String { get { switch self { case let .direct(contact): return contact.displayName @@ -93,7 +99,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - var fullName: String { + public var fullName: String { get { switch self { case let .direct(contact): return contact.fullName @@ -104,7 +110,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - var image: String? { + public var image: String? { get { switch self { case let .direct(contact): return contact.image @@ -115,7 +121,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - var id: ChatId { + public var id: ChatId { get { switch self { case let .direct(contact): return contact.id @@ -126,7 +132,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - var chatType: ChatType { + public var chatType: ChatType { get { switch self { case .direct: return .direct @@ -137,7 +143,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - var apiId: Int64 { + public var apiId: Int64 { get { switch self { case let .direct(contact): return contact.apiId @@ -148,7 +154,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - var ready: Bool { + public var ready: Bool { get { switch self { case let .direct(contact): return contact.ready @@ -168,7 +174,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - var updatedAt: Date { + public var updatedAt: Date { switch self { case let .direct(contact): return contact.updatedAt case let .group(groupInfo): return groupInfo.updatedAt @@ -177,49 +183,54 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } - struct SampleData { - var direct: ChatInfo - var group: ChatInfo - var contactRequest: ChatInfo + public struct SampleData { + public var direct: ChatInfo + public var group: ChatInfo + public var contactRequest: ChatInfo } - static var sampleData: ChatInfo.SampleData = SampleData( + public static var sampleData: ChatInfo.SampleData = SampleData( direct: ChatInfo.direct(contact: Contact.sampleData), group: ChatInfo.group(groupInfo: GroupInfo.sampleData), contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData) ) } -struct ChatData: Decodable, Identifiable { - var chatInfo: ChatInfo - var chatItems: [ChatItem] - var chatStats: ChatStats +public struct ChatData: Decodable, Identifiable { + public var chatInfo: ChatInfo + public var chatItems: [ChatItem] + public var chatStats: ChatStats - var id: ChatId { get { chatInfo.id } } + public var id: ChatId { get { chatInfo.id } } } -struct ChatStats: Decodable { - var unreadCount: Int = 0 - var minUnreadItemId: Int64 = 0 +public struct ChatStats: Decodable { + public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0) { + self.unreadCount = unreadCount + self.minUnreadItemId = minUnreadItemId + } + + public var unreadCount: Int = 0 + public var minUnreadItemId: Int64 = 0 } -struct Contact: Identifiable, Decodable, NamedChat { +public struct Contact: Identifiable, Decodable, NamedChat { var contactId: Int64 var localDisplayName: ContactName - var profile: Profile - var activeConn: Connection + public var profile: Profile + public var activeConn: Connection var viaGroup: Int64? var createdAt: Date var updatedAt: Date - var id: ChatId { get { "@\(contactId)" } } - var apiId: Int64 { get { contactId } } - var ready: Bool { get { activeConn.connStatus == .ready } } - var displayName: String { get { profile.displayName } } - var fullName: String { get { profile.fullName } } - var image: String? { get { profile.image } } + public var id: ChatId { get { "@\(contactId)" } } + public var apiId: Int64 { get { contactId } } + public var ready: Bool { get { activeConn.connStatus == .ready } } + public var displayName: String { get { profile.displayName } } + public var fullName: String { get { profile.fullName } } + public var image: String? { get { profile.image } } - static let sampleData = Contact( + public static let sampleData = Contact( contactId: 1, localDisplayName: "alice", profile: Profile.sampleData, @@ -229,23 +240,23 @@ struct Contact: Identifiable, Decodable, NamedChat { ) } -struct ContactRef: Decodable, Equatable { +public struct ContactRef: Decodable, Equatable { var contactId: Int64 var localDisplayName: ContactName - var id: ChatId { get { "@\(contactId)" } } + public var id: ChatId { get { "@\(contactId)" } } } -struct ContactSubStatus: Decodable { - var contact: Contact - var contactError: ChatError? +public struct ContactSubStatus: Decodable { + public var contact: Contact + public var contactError: ChatError? } -struct Connection: Decodable { +public struct Connection: Decodable { var connId: Int64 var connStatus: ConnStatus - var id: ChatId { get { ":\(connId)" } } + public var id: ChatId { get { ":\(connId)" } } static let sampleData = Connection( connId: 1, @@ -253,21 +264,21 @@ struct Connection: Decodable { ) } -struct UserContactRequest: Decodable, NamedChat { +public struct UserContactRequest: Decodable, NamedChat { var contactRequestId: Int64 var localDisplayName: ContactName var profile: Profile var createdAt: Date - var updatedAt: Date + public var updatedAt: Date - var id: ChatId { get { "<@\(contactRequestId)" } } - var apiId: Int64 { get { contactRequestId } } + public var id: ChatId { get { "<@\(contactRequestId)" } } + public var apiId: Int64 { get { contactRequestId } } var ready: Bool { get { true } } - var displayName: String { get { profile.displayName } } - var fullName: String { get { profile.fullName } } - var image: String? { get { profile.image } } + public var displayName: String { get { profile.displayName } } + public var fullName: String { get { profile.fullName } } + public var image: String? { get { profile.image } } - static let sampleData = UserContactRequest( + public static let sampleData = UserContactRequest( contactRequestId: 1, localDisplayName: "alice", profile: Profile.sampleData, @@ -276,21 +287,21 @@ struct UserContactRequest: Decodable, NamedChat { ) } -struct PendingContactConnection: Decodable, NamedChat { +public struct PendingContactConnection: Decodable, NamedChat { var pccConnId: Int64 var pccAgentConnId: String var pccConnStatus: ConnStatus - var viaContactUri: Bool + public var viaContactUri: Bool var createdAt: Date - var updatedAt: Date + public var updatedAt: Date - var id: ChatId { get { ":\(pccConnId)" } } - var apiId: Int64 { get { pccConnId } } + public var id: ChatId { get { ":\(pccConnId)" } } + public var apiId: Int64 { get { pccConnId } } var ready: Bool { get { false } } var localDisplayName: String { get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) } } - var displayName: String { + public var displayName: String { get { if let initiated = pccConnStatus.initiated { return initiated && !viaContactUri @@ -302,11 +313,11 @@ struct PendingContactConnection: Decodable, NamedChat { } } } - var fullName: String { get { "" } } - var image: String? { get { nil } } - var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } } + public var fullName: String { get { "" } } + public var image: String? { get { nil } } + public var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } } - var description: String { + public var description: String { get { if let initiated = pccConnStatus.initiated { return initiated && !viaContactUri @@ -320,7 +331,7 @@ struct PendingContactConnection: Decodable, NamedChat { } } - static func getSampleData(_ status: ConnStatus = .new, viaContactUri: Bool = false) -> PendingContactConnection { + public static func getSampleData(_ status: ConnStatus = .new, viaContactUri: Bool = false) -> PendingContactConnection { PendingContactConnection( pccConnId: 1, pccAgentConnId: "abcd", @@ -332,7 +343,7 @@ struct PendingContactConnection: Decodable, NamedChat { } } -enum ConnStatus: String, Decodable { +public enum ConnStatus: String, Decodable { case new = "new" case joined = "joined" case requested = "requested" @@ -356,19 +367,19 @@ enum ConnStatus: String, Decodable { } } -struct GroupInfo: Identifiable, Decodable, NamedChat { +public struct GroupInfo: Identifiable, Decodable, NamedChat { var groupId: Int64 var localDisplayName: GroupName var groupProfile: GroupProfile var createdAt: Date var updatedAt: Date - var id: ChatId { get { "#\(groupId)" } } + public var id: ChatId { get { "#\(groupId)" } } var apiId: Int64 { get { groupId } } - var ready: Bool { get { true } } - var displayName: String { get { groupProfile.displayName } } - var fullName: String { get { groupProfile.fullName } } - var image: String? { get { groupProfile.image } } + public var ready: Bool { get { true } } + public var displayName: String { get { groupProfile.displayName } } + public var fullName: String { get { groupProfile.fullName } } + public var image: String? { get { groupProfile.image } } static let sampleData = GroupInfo( groupId: 1, @@ -379,10 +390,10 @@ struct GroupInfo: Identifiable, Decodable, NamedChat { ) } -struct GroupProfile: Codable, NamedChat { - var displayName: String - var fullName: String - var image: String? +public struct GroupProfile: Codable, NamedChat { + public var displayName: String + public var fullName: String + public var image: String? static let sampleData = GroupProfile( displayName: "team", @@ -390,15 +401,15 @@ struct GroupProfile: Codable, NamedChat { ) } -struct GroupMember: Decodable { - var groupMemberId: Int64 +public struct GroupMember: Decodable { + public var groupMemberId: Int64 var memberId: String // var memberRole: GroupMemberRole // var memberCategory: GroupMemberCategory // var memberStatus: GroupMemberStatus // var invitedBy: InvitedBy var localDisplayName: ContactName - var memberProfile: Profile + public var memberProfile: Profile var memberContactId: Int64? // var activeConn: Connection? @@ -412,7 +423,7 @@ struct GroupMember: Decodable { } } - static let sampleData = GroupMember( + public static let sampleData = GroupMember( groupMemberId: 1, memberId: "abcd", localDisplayName: "alice", @@ -421,29 +432,38 @@ struct GroupMember: Decodable { ) } -struct MemberSubError: Decodable { +public struct MemberSubError: Decodable { var member: GroupMember var memberError: ChatError } -struct AChatItem: Decodable { - var chatInfo: ChatInfo - var chatItem: ChatItem +public struct AChatItem: Decodable { + public var chatInfo: ChatInfo + public var chatItem: ChatItem } -struct ChatItem: Identifiable, Decodable { - var chatDir: CIDirection - var meta: CIMeta - var content: CIContent - var formattedText: [FormattedText]? - var quotedItem: CIQuote? - var file: CIFile? +public struct ChatItem: Identifiable, Decodable { + public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, file: CIFile? = nil) { + self.chatDir = chatDir + self.meta = meta + self.content = content + self.formattedText = formattedText + self.quotedItem = quotedItem + self.file = file + } + + public var chatDir: CIDirection + public var meta: CIMeta + public var content: CIContent + public var formattedText: [FormattedText]? + public var quotedItem: CIQuote? + public var file: CIFile? - var id: Int64 { get { meta.itemId } } + public var id: Int64 { get { meta.itemId } } - var timestampText: Text { get { meta.timestampText } } + public var timestampText: Text { get { meta.timestampText } } - var text: String { + public var text: String { get { switch (content.text, file) { case let ("", .some(file)): return file.fileName @@ -452,12 +472,12 @@ struct ChatItem: Identifiable, Decodable { } } - func isRcvNew() -> Bool { + public func isRcvNew() -> Bool { if case .rcvNew = meta.itemStatus { return true } return false } - func isMsgContent() -> Bool { + public func isMsgContent() -> Bool { switch content { case .sndMsgContent: return true case .rcvMsgContent: return true @@ -465,7 +485,7 @@ struct ChatItem: Identifiable, Decodable { } } - func isDeletedContent() -> Bool { + public func isDeletedContent() -> Bool { switch content { case .sndDeleted: return true case .rcvDeleted: return true @@ -473,7 +493,7 @@ struct ChatItem: Identifiable, Decodable { } } - func isCall() -> Bool { + public func isCall() -> Bool { switch content { case .sndCall: return true case .rcvCall: return true @@ -481,7 +501,7 @@ struct ChatItem: Identifiable, Decodable { } } - var memberDisplayName: String? { + public var memberDisplayName: String? { get { if case let .groupRcv(groupMember) = chatDir { return groupMember.memberProfile.displayName @@ -491,7 +511,7 @@ struct ChatItem: Identifiable, Decodable { } } - static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem { + public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem { ChatItem( chatDir: dir, meta: CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable), @@ -501,7 +521,7 @@ struct ChatItem: Identifiable, Decodable { ) } - static func getFileMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "test.txt", fileSize: Int64 = 100, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem { + public static func getFileMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "test.txt", fileSize: Int64 = 100, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem { ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(id, .now, text, .rcvRead, false, false, false), @@ -511,7 +531,7 @@ struct ChatItem: Identifiable, Decodable { ) } - static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem { + public static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem { ChatItem( chatDir: dir, meta: CIMeta.getSample(id, ts, text, status, false, false, false), @@ -521,7 +541,7 @@ struct ChatItem: Identifiable, Decodable { ) } - static func getIntegrityErrorSample (_ status: CIStatus = .rcvRead, fromMsgId: Int64 = 1, toMsgId: Int64 = 2) -> ChatItem { + public static func getIntegrityErrorSample (_ status: CIStatus = .rcvRead, fromMsgId: Int64 = 1, toMsgId: Int64 = 2) -> ChatItem { ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "1 skipped message", status, false, false, false), @@ -532,13 +552,13 @@ struct ChatItem: Identifiable, Decodable { } } -enum CIDirection: Decodable { +public enum CIDirection: Decodable { case directSnd case directRcv case groupSnd case groupRcv(groupMember: GroupMember) - var sent: Bool { + public var sent: Bool { get { switch self { case .directSnd: return true @@ -550,19 +570,19 @@ enum CIDirection: Decodable { } } -struct CIMeta: Decodable { +public struct CIMeta: Decodable { var itemId: Int64 var itemTs: Date var itemText: String - var itemStatus: CIStatus + public var itemStatus: CIStatus var createdAt: Date - var itemDeleted: Bool - var itemEdited: Bool - var editable: Bool + public var itemDeleted: Bool + public var itemEdited: Bool + public var editable: Bool var timestampText: Text { get { formatTimestampText(itemTs) } } - static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta { + public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta { CIMeta( itemId: id, itemTs: ts, @@ -579,14 +599,14 @@ struct CIMeta: Decodable { let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute() let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits) -func formatTimestampText(_ date: Date) -> Text { +public func formatTimestampText(_ date: Date) -> Text { let now = Calendar.current.dateComponents([.day, .hour], from: .now) let dc = Calendar.current.dateComponents([.day, .hour], from: date) let recent = now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12) return Text(date, format: recent ? msgTimeFormat : msgDateFormat) } -enum CIStatus: Decodable { +public enum CIStatus: Decodable { case sndNew case sndSent case sndErrorAuth @@ -595,7 +615,7 @@ enum CIStatus: Decodable { case rcvRead } -enum CIDeleteMode: String, Decodable { +public enum CIDeleteMode: String, Decodable { case cidmBroadcast = "broadcast" case cidmInternal = "internal" } @@ -604,7 +624,7 @@ protocol ItemContent { var text: String { get } } -enum CIContent: Decodable, ItemContent { +public enum CIContent: Decodable, ItemContent { case sndMsgContent(msgContent: MsgContent) case rcvMsgContent(msgContent: MsgContent) case sndDeleted(deleteMode: CIDeleteMode) @@ -613,7 +633,7 @@ enum CIContent: Decodable, ItemContent { case rcvCall(status: CICallStatus, duration: Int) case rcvIntegrityError(msgError: MsgErrorType) - var text: String { + public var text: String { get { switch self { case let .sndMsgContent(mc): return mc.text @@ -627,7 +647,7 @@ enum CIContent: Decodable, ItemContent { } } - var msgContent: MsgContent? { + public var msgContent: MsgContent? { get { switch self { case let .sndMsgContent(mc): return mc @@ -638,17 +658,17 @@ enum CIContent: Decodable, ItemContent { } } -struct CIQuote: Decodable, ItemContent { +public struct CIQuote: Decodable, ItemContent { var chatDir: CIDirection? var itemId: Int64? var sharedMsgId: String? = nil var sentAt: Date - var content: MsgContent - var formattedText: [FormattedText]? + public var content: MsgContent + public var formattedText: [FormattedText]? - var text: String { get { content.text } } + public var text: String { get { content.text } } - func getSender(_ currentUser: User?) -> String? { + public func getSender(_ currentUser: User?) -> String? { switch (chatDir) { case .directSnd: return "you" case .directRcv: return nil @@ -658,7 +678,7 @@ struct CIQuote: Decodable, ItemContent { } } - static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote { + public static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote { let mc: MsgContent if let image = image { mc = .image(text: text, image: image) @@ -669,14 +689,14 @@ struct CIQuote: Decodable, ItemContent { } } -struct CIFile: Decodable { - var fileId: Int64 - var fileName: String - var fileSize: Int64 - var filePath: String? - var fileStatus: CIFileStatus +public struct CIFile: Decodable { + public var fileId: Int64 + public var fileName: String + public var fileSize: Int64 + public var filePath: String? + public var fileStatus: CIFileStatus - static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile { + public static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile { CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus) } @@ -697,7 +717,7 @@ struct CIFile: Decodable { } } -enum CIFileStatus: String, Decodable { +public enum CIFileStatus: String, Decodable { case sndStored = "snd_stored" case sndTransfer = "snd_transfer" case sndComplete = "snd_complete" @@ -709,7 +729,7 @@ enum CIFileStatus: String, Decodable { case rcvCancelled = "rcv_cancelled" } -enum MsgContent { +public enum MsgContent { case text(String) case link(text: String, preview: LinkPreview) case image(text: String, image: String) @@ -738,7 +758,7 @@ enum MsgContent { } } - func isFile() -> Bool { + public func isFile() -> Bool { switch self { case .file: return true default: return false @@ -754,7 +774,7 @@ enum MsgContent { } extension MsgContent: Decodable { - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { do { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: CodingKeys.type) @@ -784,7 +804,7 @@ extension MsgContent: Decodable { } extension MsgContent: Encodable { - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case let .text(text): @@ -809,12 +829,12 @@ extension MsgContent: Encodable { } } -struct FormattedText: Decodable { - var text: String - var format: Format? +public struct FormattedText: Decodable { + public var text: String + public var format: Format? } -enum Format: Decodable, Equatable { +public enum Format: Decodable, Equatable { case bold case italic case strikeThrough @@ -826,7 +846,7 @@ enum Format: Decodable, Equatable { case phone } -enum FormatColor: String, Decodable { +public enum FormatColor: String, Decodable { case red = "red" case green = "green" case blue = "blue" @@ -836,7 +856,7 @@ enum FormatColor: String, Decodable { case black = "black" case white = "white" - var uiColor: Color { + public var uiColor: Color { get { switch (self) { case .red: return .red @@ -853,15 +873,22 @@ enum FormatColor: String, Decodable { } // Struct to use with simplex API -struct LinkPreview: Codable { - var uri: URL - var title: String +public struct LinkPreview: Codable { + public init(uri: URL, title: String, description: String = "", image: String) { + self.uri = uri + self.title = title + self.description = description + self.image = image + } + + public var uri: URL + public var title: String // TODO remove once optional in haskell - var description: String = "" - var image: String + public var description: String = "" + public var image: String } -enum NtfTknStatus: String, Decodable { +public enum NtfTknStatus: String, Decodable { case new = "NEW" case registered = "REGISTERED" case invalid = "INVALID" @@ -870,15 +897,15 @@ enum NtfTknStatus: String, Decodable { case expired = "EXPIRED" } -struct SndFileTransfer: Decodable { +public struct SndFileTransfer: Decodable { } -struct FileTransferMeta: Decodable { +public struct FileTransferMeta: Decodable { } -enum CICallStatus: String, Decodable { +public enum CICallStatus: String, Decodable { case pending case missed case rejected @@ -901,12 +928,12 @@ enum CICallStatus: String, Decodable { } } - static func durationText(_ sec: Int) -> String { + public static func durationText(_ sec: Int) -> String { String(format: "%02d:%02d", sec / 60, sec % 60) } } -enum MsgErrorType: Decodable { +public enum MsgErrorType: Decodable { case msgSkipped(fromMsgId: Int64, toMsgId: Int64) case msgBadId(msgId: Int64) case msgBadHash diff --git a/apps/ios/Shared/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift similarity index 87% rename from apps/ios/Shared/FileUtils.swift rename to apps/ios/SimpleXChat/FileUtils.swift index abb1509653..6cf037a5c5 100644 --- a/apps/ios/Shared/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -8,17 +8,21 @@ import Foundation import SwiftUI +import OSLog + +let logger = Logger() // maximum image file size to be auto-accepted -let maxImageSize: Int64 = 236700 +public let maxImageSize: Int64 = 236700 -let maxFileSize: Int64 = 8000000 +public let maxFileSize: Int64 = 8000000 func getDocumentsDirectory() -> URL { - FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! +// FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.chat.simplex.app")! } -func getAppFilesDirectory() -> URL { +public func getAppFilesDirectory() -> URL { getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true) } @@ -26,7 +30,7 @@ func getAppFilePath(_ fileName: String) -> URL { getAppFilesDirectory().appendingPathComponent(fileName) } -func getLoadedFilePath(_ file: CIFile?) -> String? { +public func getLoadedFilePath(_ file: CIFile?) -> String? { if let file = file, file.loaded, let savedFile = file.filePath { @@ -35,14 +39,14 @@ func getLoadedFilePath(_ file: CIFile?) -> String? { return nil } -func getLoadedImage(_ file: CIFile?) -> UIImage? { +public func getLoadedImage(_ file: CIFile?) -> UIImage? { if let filePath = getLoadedFilePath(file) { return UIImage(contentsOfFile: filePath) } return nil } -func saveFileFromURL(_ url: URL) -> String? { +public func saveFileFromURL(_ url: URL) -> String? { let savedFile: String? if url.startAccessingSecurityScopedResource() { do { @@ -61,7 +65,7 @@ func saveFileFromURL(_ url: URL) -> String? { return savedFile } -func saveImage(_ uiImage: UIImage) -> String? { +public func saveImage(_ uiImage: UIImage) -> String? { if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: maxImageSize) { let timestamp = Date().getFormattedDate("yyyyMMdd_HHmmss") let fileName = uniqueCombine("IMG_\(timestamp).jpg") @@ -113,7 +117,7 @@ private extension String { } } -func removeFile(_ fileName: String) { +public func removeFile(_ fileName: String) { do { try FileManager.default.removeItem(atPath: getAppFilePath(fileName).path) } catch { @@ -123,7 +127,7 @@ func removeFile(_ fileName: String) { // image utils -func dropImagePrefix(_ s: String) -> String { +public func dropImagePrefix(_ s: String) -> String { dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,") } @@ -131,7 +135,7 @@ private func dropPrefix(_ s: String, _ prefix: String) -> String { s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s } -func cropToSquare(_ image: UIImage) -> UIImage { +public func cropToSquare(_ image: UIImage) -> UIImage { let size = image.size let side = min(size.width, size.height) let newSize = CGSize(width: side, height: side) @@ -159,7 +163,7 @@ func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64) -> Data? { return data } -func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? { +public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? { var img = image var str = compressImageStr(img) var dataSize = str?.count ?? 0 diff --git a/apps/ios/Shared/Model/Shared/GroupDefaults.swift b/apps/ios/SimpleXChat/GroupDefaults.swift similarity index 54% rename from apps/ios/Shared/Model/Shared/GroupDefaults.swift rename to apps/ios/SimpleXChat/GroupDefaults.swift index ca5aaef410..f7fc3fa26f 100644 --- a/apps/ios/Shared/Model/Shared/GroupDefaults.swift +++ b/apps/ios/SimpleXChat/GroupDefaults.swift @@ -9,20 +9,22 @@ import Foundation import SwiftUI +let GROUP_DEFAULT_APP_IN_BACKGROUND = "appInBackground" + func getGroupDefaults() -> UserDefaults? { - UserDefaults(suiteName: "5NN7GUYB6T.group.chat.simplex.app") + UserDefaults(suiteName: "group.chat.simplex.app") } -func setAppState(_ phase: ScenePhase) { +public func setAppState(_ phase: ScenePhase) { if let defaults = getGroupDefaults() { - defaults.set(phase == .background, forKey: "appInBackground") + defaults.set(phase == .background, forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) defaults.synchronize() } } -func getAppState() -> ScenePhase { +public func getAppState() -> ScenePhase { if let defaults = getGroupDefaults() { - if defaults.bool(forKey: "appInBackground") { + if defaults.bool(forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) { return .background } } diff --git a/apps/ios/Shared/Model/Shared/JSON.swift b/apps/ios/SimpleXChat/JSON.swift similarity index 92% rename from apps/ios/Shared/Model/Shared/JSON.swift rename to apps/ios/SimpleXChat/JSON.swift index 2123c54811..e9dfdef44e 100644 --- a/apps/ios/Shared/Model/Shared/JSON.swift +++ b/apps/ios/SimpleXChat/JSON.swift @@ -8,7 +8,7 @@ import Foundation -func getJSONDecoder() -> JSONDecoder { +public func getJSONDecoder() -> JSONDecoder { let jd = JSONDecoder() let fracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ") let noFracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ssZZZZZ") @@ -23,7 +23,7 @@ func getJSONDecoder() -> JSONDecoder { return jd } -func getJSONEncoder() -> JSONEncoder { +public func getJSONEncoder() -> JSONEncoder { let je = JSONEncoder() je.dateEncodingStrategy = .iso8601 return je diff --git a/apps/ios/Shared/Model/Shared/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift similarity index 76% rename from apps/ios/Shared/Model/Shared/Notifications.swift rename to apps/ios/SimpleXChat/Notifications.swift index 84bb1b5bad..9413e6381b 100644 --- a/apps/ios/Shared/Model/Shared/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -10,17 +10,17 @@ import Foundation import UserNotifications import SwiftUI -let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST" -let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED" -let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED" -let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION" -let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE" +public let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST" +public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED" +public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED" +public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION" +public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE" // TODO remove -let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES" +public let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES" -let appNotificationId = "chat.simplex.app.notification" +public let appNotificationId = "chat.simplex.app.notification" -func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent { +public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent { createNotification( categoryIdentifier: ntfCategoryContactRequest, title: String.localizedStringWithFormat(NSLocalizedString("%@ wants to connect!", comment: "notification title"), contactRequest.displayName), @@ -30,7 +30,7 @@ func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableN ) } -func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent { +public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent { createNotification( categoryIdentifier: ntfCategoryContactConnected, title: String.localizedStringWithFormat(NSLocalizedString("%@ is connected!", comment: "notification title"), contact.displayName), @@ -40,7 +40,7 @@ func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationConte ) } -func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { +public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { createNotification( categoryIdentifier: ntfCategoryMessageReceived, title: "\(cInfo.chatViewName):", @@ -50,7 +50,7 @@ func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutable ) } -func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent { +public func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent { let text = invitation.peerMedia == .video ? NSLocalizedString("Incoming video call", comment: "notification") : NSLocalizedString("Incoming audio call", comment: "notification") @@ -63,7 +63,7 @@ func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificat ) } -func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil, +public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil, targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent { let content = UNMutableNotificationContent() content.categoryIdentifier = categoryIdentifier diff --git a/apps/ios/SimpleX NSE/SimpleX NSE-Bridging-Header.h b/apps/ios/SimpleXChat/SimpleX.h similarity index 54% rename from apps/ios/SimpleX NSE/SimpleX NSE-Bridging-Header.h rename to apps/ios/SimpleXChat/SimpleX.h index bc28b42d38..dab7fa3eb6 100644 --- a/apps/ios/SimpleX NSE/SimpleX NSE-Bridging-Header.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -1,6 +1,15 @@ // -// Use this file to import your target's public headers that you would like to expose to Swift. +// SimpleX.h +// SimpleX // +// Created by Evgeny on 30/05/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +#ifndef SimpleX_h +#define SimpleX_h + +#endif /* SimpleX_h */ extern void hs_init(int argc, char **argv[]); diff --git a/apps/ios/SimpleXChat/SimpleXChat.docc/SimpleXChat.md b/apps/ios/SimpleXChat/SimpleXChat.docc/SimpleXChat.md new file mode 100755 index 0000000000..2ab4ef5f06 --- /dev/null +++ b/apps/ios/SimpleXChat/SimpleXChat.docc/SimpleXChat.md @@ -0,0 +1,13 @@ +# ``SimpleXChat`` + +Summary + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` \ No newline at end of file diff --git a/apps/ios/SimpleXChat/SimpleXChat.h b/apps/ios/SimpleXChat/SimpleXChat.h new file mode 100644 index 0000000000..c109fc9412 --- /dev/null +++ b/apps/ios/SimpleXChat/SimpleXChat.h @@ -0,0 +1,17 @@ +// +// SimpleXChat.h +// SimpleXChat +// +// Created by Evgeny on 30/05/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +#import + +//! Project version number for SimpleXChat. +FOUNDATION_EXPORT double SimpleXChatVersionNumber; + +//! Project version string for SimpleXChat. +FOUNDATION_EXPORT const unsigned char SimpleXChatVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import diff --git a/apps/ios/SimpleX NSE/dummy.m b/apps/ios/SimpleXChat/dummy.m similarity index 89% rename from apps/ios/SimpleX NSE/dummy.m rename to apps/ios/SimpleXChat/dummy.m index adef540363..64fbc32dd3 100644 --- a/apps/ios/SimpleX NSE/dummy.m +++ b/apps/ios/SimpleXChat/dummy.m @@ -1,8 +1,8 @@ // // dummy.m -// SimpleX NSE +// SimpleXChat // -// Created by Evgeny on 26/04/2022. +// Created by Evgeny on 30/05/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // From 949fb17406954ca666123272c5d057e8e6701ead Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 2 Jun 2022 13:16:22 +0100 Subject: [PATCH 03/89] ios: mach messages to coordinate database acceess between app & NSE (#717) --- apps/ios/Shared/SimpleXApp.swift | 12 ++ .../ios/SimpleX NSE/NotificationService.swift | 13 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +- apps/ios/SimpleXChat/AppGroup.swift | 177 ++++++++++++++++++ apps/ios/SimpleXChat/FileUtils.swift | 2 +- apps/ios/SimpleXChat/GroupDefaults.swift | 32 ---- 6 files changed, 206 insertions(+), 46 deletions(-) create mode 100644 apps/ios/SimpleXChat/AppGroup.swift delete mode 100644 apps/ios/SimpleXChat/GroupDefaults.swift diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 26ec3490ca..2c2540eabd 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -11,6 +11,13 @@ import SimpleXChat let logger = Logger() +let machMessenger = MachMessenger(APP_MACH_PORT, callback: receivedNSEMachMessage) + +func receivedNSEMachMessage(msgId: Int32, msg: String) -> String? { + logger.debug("MachMessenger: receivedNSEMachMessage \"\(msg)\" from NSE, replying") + return "reply from App to: \(msg)" +} + @main struct SimpleXApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @@ -27,6 +34,7 @@ struct SimpleXApp: App { UserDefaults.standard.register(defaults: appDefaults) BGManager.shared.register() NtfManager.shared.registerCategories() + machMessenger.start() } var body: some Scene { @@ -42,14 +50,18 @@ struct SimpleXApp: App { } .onChange(of: scenePhase) { phase in logger.debug("scenePhase \(String(describing: scenePhase))") + let res = machMessenger.sendMessageWithReply(NSE_MACH_PORT, msg: "App scenePhase changed to \(String(describing: scenePhase))") + logger.debug("MachMessenger \(String(describing: res), privacy: .public)") setAppState(phase) switch (phase) { case .background: BGManager.shared.schedule() doAuthenticate = false enteredBackground = ProcessInfo.processInfo.systemUptime + machMessenger.stop() case .active: doAuthenticate = true + machMessenger.start() default: break } diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index c0a06a1232..41668e9543 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -12,15 +12,20 @@ import SimpleXChat let logger = Logger() -class NotificationService: UNNotificationServiceExtension { +let machMessenger = MachMessenger(NSE_MACH_PORT, callback: receivedAppMachMessage) +class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("NotificationService.didReceive") + machMessenger.start() + let res = machMessenger.sendMessageWithReply(APP_MACH_PORT, msg: "starting NSE didReceive") + logger.debug("MachMessenger \(String(describing: res), privacy: .public)") if getAppState() != .background { contentHandler(request.content) + machMessenger.stop() return } logger.debug("NotificationService: app is in the background") @@ -29,6 +34,7 @@ class NotificationService: UNNotificationServiceExtension { if let _ = startChat() { let content = receiveMessages() contentHandler (content) + machMessenger.stop() return } @@ -38,6 +44,7 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } + machMessenger.stop() } override func serviceExtensionTimeWillExpire() { @@ -48,7 +55,11 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } } +} +func receivedAppMachMessage(msgId: Int32, msg: String) -> String? { + logger.debug("MachMessenger: receivedAppMachMessage \"\(msg)\" from App, replying") + return "reply from NSE to: \(msg)" } func startChat() -> User? { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index def572caa8..a0d4de258b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -85,7 +85,7 @@ 5CE2BA88284532AD00EC33A6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907F28376BB90076573F /* libgmp.a */; }; 5CE2BA89284532AD00EC33A6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907E28376BB90076573F /* libgmpxx.a */; }; 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; }; - 5CE2BA8C284533A300EC33A6 /* GroupDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */; }; + 5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* AppGroup.swift */; }; 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; }; 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7D2818941F00503DA2 /* API.swift */; }; 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7428188D2900503DA2 /* APITypes.swift */; }; @@ -250,7 +250,7 @@ 5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX NSE.entitlements"; sourceTree = ""; }; - 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDefaults.swift; sourceTree = ""; }; + 5CDCAD5228186F9500503DA2 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; }; 5CDCAD6028187D7900503DA2 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTypes.swift; sourceTree = ""; }; @@ -398,7 +398,6 @@ 5C764E87279CBC8E000C6508 /* Model */ = { isa = PBXGroup; children = ( - 5CDCAD7128188CEB00503DA2 /* Shared */, 5C764E88279CBCB3000C6508 /* ChatModel.swift */, 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, @@ -540,17 +539,10 @@ path = "SimpleX NSE"; sourceTree = ""; }; - 5CDCAD7128188CEB00503DA2 /* Shared */ = { - isa = PBXGroup; - children = ( - ); - path = Shared; - sourceTree = ""; - }; 5CE2BA692845308900EC33A6 /* SimpleXChat */ = { isa = PBXGroup; children = ( - 5CDCAD5228186F9500503DA2 /* GroupDefaults.swift */, + 5CDCAD5228186F9500503DA2 /* AppGroup.swift */, 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */, 5CDCAD7428188D2900503DA2 /* APITypes.swift */, 5C5E5D3C282447AB00B0488A /* CallTypes.swift */, @@ -887,7 +879,7 @@ 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */, 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */, 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */, - 5CE2BA8C284533A300EC33A6 /* GroupDefaults.swift in Sources */, + 5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */, 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */, 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */, ); diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift new file mode 100644 index 0000000000..92573e0354 --- /dev/null +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -0,0 +1,177 @@ +// +// GroupDefaults.swift +// SimpleX (iOS) +// +// Created by Evgeny on 26/04/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +let GROUP_DEFAULT_APP_IN_BACKGROUND = "appInBackground" + +let APP_GROUP_NAME = "group.chat.simplex.app" + +public let NSE_MACH_PORT = "\(APP_GROUP_NAME).nse" as CFString + +public let APP_MACH_PORT = "\(APP_GROUP_NAME).app" as CFString + +func getGroupDefaults() -> UserDefaults? { + UserDefaults(suiteName: APP_GROUP_NAME) +} + +public func setAppState(_ phase: ScenePhase) { + if let defaults = getGroupDefaults() { + defaults.set(phase == .background, forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) + defaults.synchronize() + } +} + +public func getAppState() -> ScenePhase { + if let defaults = getGroupDefaults() { + if defaults.bool(forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) { + return .background + } + } + return .active +} + +let MACH_SEND_TIMEOUT: CFTimeInterval = 1.0 +let MACH_REPLY_TIMEOUT: CFTimeInterval = 1.0 + +public class MachMessenger { + public init(_ localPortName: CFString, callback: @escaping Callback) { + self.localPortName = localPortName + self.callback = callback + self.localPort = nil + } + + var localPortName: CFString + var callback: Callback + var localPort: CFMessagePort? + + public enum SendError: Error { + case sndTimeout + case rcvTimeout + case portInvalid + case sendError(Int32) + case msgError + } + + public typealias Callback = (_ msgId: Int32, _ msg: String) -> String? + + class CallbackInfo { + internal init(callback: @escaping MachMessenger.Callback) { + self.callback = callback + } + + var callback: Callback + } + + public static func sendError(_ code: Int32) -> SendError? { + switch code { + case kCFMessagePortSuccess: return nil + case kCFMessagePortSendTimeout: return .sndTimeout + case kCFMessagePortReceiveTimeout: return .rcvTimeout + case kCFMessagePortIsInvalid: return .portInvalid + case kCFMessagePortBecameInvalidError: return .portInvalid + default: return .sendError(code) + } + } + + public func start() { + logger.debug("MachMessenger.start") + localPort = createLocalPort(localPortName, callback: callback) + } + + public func stop() { + if let port = localPort { + logger.debug("MachMessenger.stop") + CFMessagePortInvalidate(port) + localPort = nil + } + } + + public func sendMessage(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> SendError? { + logger.debug("MachMessenger.sendMessage") + if let port = createRemotePort(remotePortName) { + logger.debug("MachMessenger.sendMessage: sending...") + return sendMessage(port, msgId: msgId, msg: msg) + } else { + logger.debug("MachMessenger.sendMessage: no remote port") + return .portInvalid + } + } + + public func sendMessageWithReply(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> Result { + logger.debug("MachMessenger.sendMessageWithReply") + if let port = createRemotePort(remotePortName) { + logger.debug("MachMessenger.sendMessageWithReply: sending...") + return sendMessageWithReply(port, msgId: msgId, msg: msg) + } else { + logger.debug("MachMessenger.sendMessageWithReply: no remote port") + return .failure(.portInvalid) + } + } + + private func createLocalPort(_ portName: CFString, callback: @escaping Callback) -> CFMessagePort? { + logger.debug("MachMessenger.createLocalPort") + if let port = localPort { return port } + logger.debug("MachMessenger.createLocalPort: creating...") + var context = CFMessagePortContext() + context.version = 0 + context.info = Unmanaged.passRetained(CallbackInfo(callback: callback)).toOpaque() + let callout: CFMessagePortCallBack = { port, msgId, msgData, info in + logger.debug("MachMessenger CFMessagePortCallBack called") + if let data = msgData, + let msg = String(data: data as Data, encoding: .utf8), + let info = info, + let resp = Unmanaged.fromOpaque(info).takeUnretainedValue().callback(msgId, msg), + let respData = resp.data(using: .utf8) { + return Unmanaged.passRetained(respData as CFData) + } + return nil + } + return withUnsafeMutablePointer(to: &context) { cxt in + let port = CFMessagePortCreateLocal(kCFAllocatorDefault, portName, callout, cxt, nil) + CFMessagePortSetDispatchQueue(port, DispatchQueue.main); + localPort = port + logger.debug("MachMessenger.createLocalPort created: \(portName)") + return port + } + } + + private func createRemotePort(_ portName: CFString) -> CFMessagePort? { + CFMessagePortCreateRemote(kCFAllocatorDefault, portName) + } + + private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> SendError? { + if let data = msg.data(using: .utf8) { + logger.debug("MachMessenger sendMessage") + let msgData = data as CFData + let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, 0, nil, nil) + logger.debug("MachMessenger sendMessage \(code)") + return MachMessenger.sendError(code) + } + return .msgError + } + + private func sendMessageWithReply(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> Result { + if let data = msg.data(using: .utf8) { + let msgData = data as CFData + var respData: Unmanaged? = nil + let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, MACH_REPLY_TIMEOUT, CFRunLoopMode.defaultMode.rawValue, &respData) + if let err = MachMessenger.sendError(code) { + return .failure(err) + } else if let data = respData?.takeUnretainedValue(), + let resp = String(data: data as Data, encoding: .utf8) { + return .success(resp) + } else { + return .success(nil) + } + + } + return .failure(.msgError) + } +} diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 6cf037a5c5..0393039316 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -19,7 +19,7 @@ public let maxFileSize: Int64 = 8000000 func getDocumentsDirectory() -> URL { // FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.chat.simplex.app")! + FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)! } public func getAppFilesDirectory() -> URL { diff --git a/apps/ios/SimpleXChat/GroupDefaults.swift b/apps/ios/SimpleXChat/GroupDefaults.swift deleted file mode 100644 index f7fc3fa26f..0000000000 --- a/apps/ios/SimpleXChat/GroupDefaults.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// GroupDefaults.swift -// SimpleX (iOS) -// -// Created by Evgeny on 26/04/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import Foundation -import SwiftUI - -let GROUP_DEFAULT_APP_IN_BACKGROUND = "appInBackground" - -func getGroupDefaults() -> UserDefaults? { - UserDefaults(suiteName: "group.chat.simplex.app") -} - -public func setAppState(_ phase: ScenePhase) { - if let defaults = getGroupDefaults() { - defaults.set(phase == .background, forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) - defaults.synchronize() - } -} - -public func getAppState() -> ScenePhase { - if let defaults = getGroupDefaults() { - if defaults.bool(forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) { - return .background - } - } - return .active -} From f341e5412890dca45d20afa4ab7169d78433bc1b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 6 Jun 2022 16:23:47 +0100 Subject: [PATCH 04/89] Export & import storage archive (#726) * core: import and export of chat archive * export chat archive * import archive, support starting chat after it is stopped * test for maintenance mode * test/fix archive with files * prevent starting chat after chat database was deleted or imported * update simplexmq --- cabal.project | 2 +- docs/rfcs/2022-06-03-portable-archive.md | 25 ++++++ package.yaml | 11 +++ scripts/nix/README.md | 15 ++++ scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 16 ++++ src/Simplex/Chat.hs | 25 +++++- src/Simplex/Chat/Archive.hs | 81 +++++++++++++++++ src/Simplex/Chat/Controller.hs | 11 +++ src/Simplex/Chat/Core.hs | 14 +-- src/Simplex/Chat/Mobile.hs | 3 +- src/Simplex/Chat/Options.hs | 11 ++- src/Simplex/Chat/View.hs | 5 +- stack.yaml | 8 +- tests/ChatClient.hs | 50 ++++++----- tests/ChatTests.hs | 105 ++++++++++++++++++++--- 16 files changed, 337 insertions(+), 47 deletions(-) create mode 100644 docs/rfcs/2022-06-03-portable-archive.md create mode 100644 scripts/nix/README.md create mode 100644 src/Simplex/Chat/Archive.hs diff --git a/cabal.project b/cabal.project index d07117a669..e3ad4ef1c1 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 964daf5442e1069634762450bc28cfd69a2968a1 + tag: 628930df1fa1c3fff6fd1413e7b437148c4a83b5 source-repository-package type: git diff --git a/docs/rfcs/2022-06-03-portable-archive.md b/docs/rfcs/2022-06-03-portable-archive.md new file mode 100644 index 0000000000..7f83df170b --- /dev/null +++ b/docs/rfcs/2022-06-03-portable-archive.md @@ -0,0 +1,25 @@ +# Portable archive file format + +## Problems + +- database migration for notifications support +- export and import of the database + +The first problem could have been solved in an ad hoc way, but it may cause data loss, so the proposal is to have migration performed via export/import steps. + +Out of scope of this doc - what will be the UX for database migration. It may be fully automatic, via code, with zero user interactions, or it could be via step by step wizard - irrespective of this choice it would include export and import steps. + +# Proposal + +Implement creating archive file and restoring from the archive in Haskell, application would only provide a source and target folders, respectively + +Archive files structure: + +- simplex_v1_chat.db +- simplex_v1_agent.db +- simplex_v1_files + - ... + +Archive file name (includes UTC time): + +simplex-chat.YYYY-MM-DDTHH:MM:SSZ.zip diff --git a/package.yaml b/package.yaml index 9e097678fa..f1de3ff48b 100644 --- a/package.yaml +++ b/package.yaml @@ -39,6 +39,17 @@ dependencies: - time == 1.9.* - unliftio == 0.2.* - unliftio-core == 0.2.* + - zip == 1.7.* + +flags: + disable-bzip2: + description: removes dependency on bzip2 C library (zip package) + manual: True + default: True + disable-zstd: + description: Removes dependency on zstd C library (zip package) + manual: True + default: True library: source-dirs: src diff --git a/scripts/nix/README.md b/scripts/nix/README.md new file mode 100644 index 0000000000..5117032710 --- /dev/null +++ b/scripts/nix/README.md @@ -0,0 +1,15 @@ +# Updating nix package config + +1. Install `nix`, `gawk` and `jq`. + +2. Start nix-shell from repo root: + +```sh +nix-shell -p nix-prefetch-git +``` + +3. Run in nix shell: + +```sh +gawk -f ./scripts/nix/update-sha256.awk cabal.project > ./scripts/nix/sha256map.nix +``` diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 984e8afc7a..f56ebfa858 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."964daf5442e1069634762450bc28cfd69a2968a1" = "1vsbiawqlvi6v48ws2rmg5cmp5qphnry3ymg6458p2w8wdm2gsng"; + "https://github.com/simplex-chat/simplexmq.git"."628930df1fa1c3fff6fd1413e7b437148c4a83b5" = "03h063yahq6b5m1lng7as70a59lklhzsxg0ykmr9wldy8768dlvd"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 588beb8ca0..eb91672b1a 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -17,9 +17,20 @@ build-type: Simple extra-source-files: README.md +flag disable-bzip2 + description: removes dependency on bzip2 C library (zip package) + manual: True + default: True + +flag disable-zstd + description: Removes dependency on zstd C library (zip package) + manual: True + default: True + library exposed-modules: Simplex.Chat + Simplex.Chat.Archive Simplex.Chat.Bot Simplex.Chat.Call Simplex.Chat.Controller @@ -83,6 +94,7 @@ library , time ==1.9.* , unliftio ==0.2.* , unliftio-core ==0.2.* + , zip ==1.7.* default-language: Haskell2010 executable simplex-bot @@ -121,6 +133,7 @@ executable simplex-bot , time ==1.9.* , unliftio ==0.2.* , unliftio-core ==0.2.* + , zip ==1.7.* default-language: Haskell2010 executable simplex-bot-advanced @@ -159,6 +172,7 @@ executable simplex-bot-advanced , time ==1.9.* , unliftio ==0.2.* , unliftio-core ==0.2.* + , zip ==1.7.* default-language: Haskell2010 executable simplex-chat @@ -200,6 +214,7 @@ executable simplex-chat , unliftio ==0.2.* , unliftio-core ==0.2.* , websockets ==0.12.* + , zip ==1.7.* default-language: Haskell2010 test-suite simplex-chat-test @@ -247,4 +262,5 @@ test-suite simplex-chat-test , time ==1.9.* , unliftio ==0.2.* , unliftio-core ==0.2.* + , zip ==1.7.* default-language: Haskell2010 diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e91a4c4180..da5f7116c7 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -41,6 +41,7 @@ import qualified Data.Text as T import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds) import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime) import Data.Word (Word32) +import Simplex.Chat.Archive import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Markdown @@ -134,7 +135,8 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de rcvFiles <- newTVarIO M.empty currentCalls <- atomically TM.empty filesFolder <- newTVarIO Nothing - pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder} + chatStoreChanged <- newTVarIO False + pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder} where resolveServers :: InitialAgentServers -> IO InitialAgentServers resolveServers ss@InitialAgentServers {smp = defaultSMPServers} = case nonEmpty smpServers of @@ -150,6 +152,7 @@ runChatController = race_ notificationSubscriber . agentSubscriber startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m (Async ()) startChatController user = do + asks smpAgent >>= resumeAgentClient s <- asks agentAsync readTVarIO s >>= maybe (start s) pure where @@ -194,13 +197,23 @@ processChatCommand = \case StartChat -> withUser' $ \user -> asks agentAsync >>= readTVarIO >>= \case Just _ -> pure CRChatRunning - _ -> startChatController user $> CRChatStarted + _ -> + ifM + (asks chatStoreChanged >>= readTVarIO) + (throwChatError CEChatStoreChanged) + (startChatController user $> CRChatStarted) + APIStopChat -> do + ask >>= stopChatController + pure CRChatStopped ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk SetFilesFolder filesFolder' -> withUser $ \_ -> do createDirectoryIfMissing True filesFolder' ff <- asks filesFolder atomically . writeTVar ff $ Just filesFolder' pure CRCmdOk + APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk + APIImportArchive cfg -> checkChatStopped $ importArchive cfg >> setStoreChanged $> CRCmdOk + APIDeleteStorage -> checkChatStopped $ deleteStorage >> setStoreChanged $> CRCmdOk APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore $ \st -> getChatPreviews st user withPCC) APIGetChat (ChatRef cType cId) pagination -> withUser $ \user -> case cType of CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\st -> getDirectChat st user cId pagination) @@ -770,6 +783,10 @@ processChatCommand = \case CTDirect -> withStore $ \st -> getContactIdByName st userId name CTGroup -> withStore $ \st -> getGroupIdByName st user name _ -> throwChatError $ CECommandError "not supported" + checkChatStopped :: m ChatResponse -> m ChatResponse + checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped) + setStoreChanged :: m () + setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True) getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64 getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of CTDirect -> withStore $ \st -> getDirectChatItemIdByText st userId cId SMDSnd (safeDecodeUtf8 msg) @@ -2212,8 +2229,12 @@ chatCommandP = ("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile) <|> ("/user" <|> "/u") $> ShowActiveUser <|> "/_start" $> StartChat + <|> "/_stop" $> APIStopChat <|> "/_resubscribe all" $> ResubscribeAllConnections <|> "/_files_folder " *> (SetFilesFolder <$> filePath) + <|> "/_db export " *> (APIExportArchive <$> jsonP) + <|> "/_db import " *> (APIImportArchive <$> jsonP) + <|> "/_db delete" $> APIDeleteStorage <|> "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)) <|> "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP) <|> "/_get items count=" *> (APIGetChatItems <$> A.decimal) diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs new file mode 100644 index 0000000000..31a5714eae --- /dev/null +++ b/src/Simplex/Chat/Archive.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Simplex.Chat.Archive where + +import qualified Codec.Archive.Zip as Z +import Control.Monad.Reader +import Simplex.Chat.Controller +import Simplex.Chat.Util (whenM) +import Simplex.Messaging.Agent.Client (agentDbPath) +import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..)) +import System.FilePath +import UnliftIO.Directory +import UnliftIO.STM +import UnliftIO.Temporary + +archiveAgentDbFile :: String +archiveAgentDbFile = "simplex_v1_agent.db" + +archiveChatDbFile :: String +archiveChatDbFile = "simplex_v1_chat.db" + +archiveFilesFolder :: String +archiveFilesFolder = "simplex_v1_files" + +exportArchive :: ChatMonad m => ArchiveConfig -> m () +exportArchive ArchiveConfig {archivePath, disableCompression} = + withSystemTempDirectory "simplex-chat." $ \dir -> do + StorageFiles {chatDb, agentDb, filesPath} <- storageFiles + copyFile chatDb $ dir archiveChatDbFile + copyFile agentDb $ dir archiveAgentDbFile + forM_ filesPath $ \fp -> + copyDirectoryFiles fp $ dir archiveFilesFolder + let method = if disableCompression == Just True then Z.Store else Z.Deflate + Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir + +importArchive :: ChatMonad m => ArchiveConfig -> m () +importArchive ArchiveConfig {archivePath} = + withSystemTempDirectory "simplex-chat." $ \dir -> do + Z.withArchive archivePath $ Z.unpackInto dir + StorageFiles {chatDb, agentDb, filesPath} <- storageFiles + backup chatDb + backup agentDb + copyFile (dir archiveChatDbFile) chatDb + copyFile (dir archiveAgentDbFile) agentDb + let filesDir = dir archiveFilesFolder + forM_ filesPath $ \fp -> + whenM (doesDirectoryExist filesDir) $ + copyDirectoryFiles filesDir fp + where + backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak" + +copyDirectoryFiles :: MonadIO m => FilePath -> FilePath -> m () +copyDirectoryFiles fromDir toDir = do + createDirectoryIfMissing False toDir + fs <- listDirectory fromDir + forM_ fs $ \f -> do + let fn = takeFileName f + f' = fromDir fn + whenM (doesFileExist f') $ copyFile f' $ toDir fn + +deleteStorage :: ChatMonad m => m () +deleteStorage = do + StorageFiles {chatDb, agentDb, filesPath} <- storageFiles + removeFile chatDb + removeFile agentDb + mapM_ removePathForcibly filesPath + +data StorageFiles = StorageFiles + { chatDb :: FilePath, + agentDb :: FilePath, + filesPath :: Maybe FilePath + } + +storageFiles :: ChatMonad m => m StorageFiles +storageFiles = do + ChatController {chatStore, filesFolder, smpAgent} <- ask + let SQLiteStore {dbFilePath = chatDb} = chatStore + agentDb = agentDbPath smpAgent + filesPath <- readTVarIO filesFolder + pure StorageFiles {chatDb, agentDb, filesPath} diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 9abaa4cdda..321ff0e7da 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -76,6 +76,7 @@ data ChatController = ChatController smpAgent :: AgentClient, agentAsync :: TVar (Maybe (Async ())), chatStore :: SQLiteStore, + chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted idsDrg :: TVar ChaChaDRG, inputQ :: TBQueue String, outputQ :: TBQueue (Maybe CorrId, ChatResponse), @@ -100,8 +101,12 @@ data ChatCommand = ShowActiveUser | CreateActiveUser Profile | StartChat + | APIStopChat | ResubscribeAllConnections | SetFilesFolder FilePath + | APIExportArchive ArchiveConfig + | APIImportArchive ArchiveConfig + | APIDeleteStorage | APIGetChats {pendingConnections :: Bool} | APIGetChat ChatRef ChatPagination | APIGetChatItems Int @@ -178,6 +183,7 @@ data ChatResponse = CRActiveUser {user :: User} | CRChatStarted | CRChatRunning + | CRChatStopped | CRApiChats {chats :: [AChat]} | CRApiChat {chat :: AChat} | CRLastMessages {chatItems :: [AChatItem]} @@ -279,6 +285,9 @@ instance ToJSON ChatResponse where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR" +data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool} + deriving (Show, Generic, FromJSON) + data ContactSubStatus = ContactSubStatus { contact :: Contact, contactError :: Maybe ChatError @@ -329,6 +338,8 @@ data ChatErrorType = CENoActiveUser | CEActiveUserExists | CEChatNotStarted + | CEChatNotStopped + | CEChatStoreChanged | CEInvalidConnReq | CEInvalidChatMessage {message :: String} | CEContactNotReady {contact :: Contact} diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index a4faaf876f..6290e7f02f 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -26,13 +26,15 @@ simplexChatCore cfg@ChatConfig {dbPoolSize, yesToMigrations} opts sendToast chat st <- createStore f dbPoolSize yesToMigrations u <- getCreateActiveUser st cc <- newChatController st (Just u) cfg opts sendToast - runSimplexChat u cc chat + runSimplexChat opts u cc chat -runSimplexChat :: User -> ChatController -> (User -> ChatController -> IO ()) -> IO () -runSimplexChat u cc chat = do - a1 <- async $ chat u cc - a2 <- runReaderT (startChatController u) cc - waitEither_ a1 a2 +runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO () +runSimplexChat ChatOpts {maintenance} u cc chat + | maintenance = wait =<< async (chat u cc) + | otherwise = do + a1 <- async $ chat u cc + a2 <- runReaderT (startChatController u) cc + waitEither_ a1 a2 sendChatCmd :: ChatController -> String -> IO ChatResponse sendChatCmd cc s = runReaderT (execChatCommand . encodeUtf8 $ T.pack s) cc diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index a83fc67f0d..74d06f2835 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -54,7 +54,8 @@ mobileChatOpts = logAgent = False, chatCmd = "", chatCmdDelay = 3, - chatServerPort = Nothing + chatServerPort = Nothing, + maintenance = True } defaultMobileConfig :: ChatConfig diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index ad7cb97e12..72211bc018 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -25,7 +25,8 @@ data ChatOpts = ChatOpts logAgent :: Bool, chatCmd :: String, chatCmdDelay :: Int, - chatServerPort :: Maybe String + chatServerPort :: Maybe String, + maintenance :: Bool } chatOpts :: FilePath -> FilePath -> Parser ChatOpts @@ -88,7 +89,13 @@ chatOpts appDir defaultDbFileName = do <> help "Run chat server on specified port" <> value Nothing ) - pure ChatOpts {dbFilePrefix, smpServers, logConnections, logAgent, chatCmd, chatCmdDelay, chatServerPort} + maintenance <- + switch + ( long "maintenance" + <> short 'm' + <> help "Run in maintenance mode (/_start to start chat)" + ) + pure ChatOpts {dbFilePrefix, smpServers, logConnections, logAgent, chatCmd, chatCmdDelay, chatServerPort, maintenance} where defaultDbFilePath = combine appDir defaultDbFileName diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 6f7f31a66c..6ac5b0faa0 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -53,7 +53,8 @@ responseToView :: Bool -> ChatResponse -> [StyledString] responseToView testView = \case CRActiveUser User {profile} -> viewUserProfile profile CRChatStarted -> ["chat started"] - CRChatRunning -> [] + CRChatRunning -> ["chat is running"] + CRChatStopped -> ["chat stopped"] CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] @@ -721,6 +722,8 @@ viewChatError = \case CENoActiveUser -> ["error: active user is required"] CEActiveUserExists -> ["error: active user already exists"] CEChatNotStarted -> ["error: chat not started"] + CEChatNotStopped -> ["error: chat not stopped"] + CEChatStoreChanged -> ["error: chat store changed"] CEInvalidConnReq -> viewInvalidConnReq CEInvalidChatMessage e -> ["chat message error: " <> sShow e] CEContactNotReady c -> [ttyContact' c <> ": not ready"] diff --git a/stack.yaml b/stack.yaml index 5b6b00b395..eac63e18ad 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 964daf5442e1069634762450bc28cfd69a2968a1 + commit: 628930df1fa1c3fff6fd1413e7b437148c4a83b5 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 @@ -59,8 +59,10 @@ extra-deps: # extra-deps: [] # Override default flag values for local packages and extra-deps -# flags: {} - +flags: + zip: + disable-bzip2: true + disable-zstd: true # Extra package databases containing global packages # extra-package-dbs: [] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 2aba3feb20..7c5ebc6c1b 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -42,8 +42,8 @@ testDBPrefix = "tests/tmp/test" serverPort :: ServiceName serverPort = "5001" -opts :: ChatOpts -opts = +testOpts :: ChatOpts +testOpts = ChatOpts { dbFilePrefix = undefined, smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"], @@ -51,7 +51,8 @@ opts = logAgent = False, chatCmd = "", chatCmdDelay = 3, - chatServerPort = Nothing + chatServerPort = Nothing, + maintenance = False } termSettings :: VirtualTerminalSettings @@ -82,26 +83,26 @@ cfg = testView = True } -createTestChat :: String -> Profile -> IO TestCC -createTestChat dbPrefix profile = do +createTestChat :: ChatOpts -> String -> Profile -> IO TestCC +createTestChat opts dbPrefix profile = do let dbFilePrefix = testDBPrefix <> dbPrefix st <- createStore (dbFilePrefix <> "_chat.db") 1 False Right user <- runExceptT $ createUser st profile True - startTestChat_ st dbFilePrefix user + startTestChat_ st opts dbFilePrefix user -startTestChat :: String -> IO TestCC -startTestChat dbPrefix = do +startTestChat :: ChatOpts -> String -> IO TestCC +startTestChat opts dbPrefix = do let dbFilePrefix = testDBPrefix <> dbPrefix st <- createStore (dbFilePrefix <> "_chat.db") 1 False Just user <- find activeUser <$> getUsers st - startTestChat_ st dbFilePrefix user + startTestChat_ st opts dbFilePrefix user -startTestChat_ :: SQLiteStore -> FilePath -> User -> IO TestCC -startTestChat_ st dbFilePrefix user = do +startTestChat_ :: SQLiteStore -> ChatOpts -> FilePath -> User -> IO TestCC +startTestChat_ st opts dbFilePrefix user = do t <- withVirtualTerminal termSettings pure ct <- newChatTerminal t cc <- newChatController st (Just user) cfg opts {dbFilePrefix} Nothing -- no notifications - chatAsync <- async . runSimplexChat user cc . const $ runChatTerminal ct + chatAsync <- async . runSimplexChat opts user cc . const $ runChatTerminal ct termQ <- newTQueueIO termAsync <- async $ readTerminalOutput t termQ pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ} @@ -113,10 +114,16 @@ stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do uninterruptibleCancel chatAsync withNewTestChat :: String -> Profile -> (TestCC -> IO a) -> IO a -withNewTestChat dbPrefix profile = bracket (createTestChat dbPrefix profile) (\cc -> cc > stopTestChat cc) +withNewTestChat = withNewTestChatOpts testOpts + +withNewTestChatOpts :: ChatOpts -> String -> Profile -> (TestCC -> IO a) -> IO a +withNewTestChatOpts opts dbPrefix profile = bracket (createTestChat opts dbPrefix profile) (\cc -> cc > stopTestChat cc) withTestChat :: String -> (TestCC -> IO a) -> IO a -withTestChat dbPrefix = bracket (startTestChat dbPrefix) (\cc -> cc > stopTestChat cc) +withTestChat = withTestChatOpts testOpts + +withTestChatOpts :: ChatOpts -> String -> (TestCC -> IO a) -> IO a +withTestChatOpts opts dbPrefix = bracket (startTestChat opts dbPrefix) (\cc -> cc > stopTestChat cc) readTerminalOutput :: VirtualTerminal -> TQueue String -> IO () readTerminalOutput t termQ = do @@ -147,8 +154,8 @@ withTmpFiles = (createDirectoryIfMissing False "tests/tmp") (removePathForcibly "tests/tmp") -testChatN :: [Profile] -> ([TestCC] -> IO ()) -> IO () -testChatN ps test = withTmpFiles $ do +testChatN :: ChatOpts -> [Profile] -> ([TestCC] -> IO ()) -> IO () +testChatN opts ps test = withTmpFiles $ do tcs <- getTestCCs (zip ps [1 ..]) [] test tcs concurrentlyN_ $ map ( [TestCC] -> IO [TestCC] getTestCCs [] tcs = pure tcs - getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat (show db) p <*> getTestCCs envs' tcs + getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat opts (show db) p <*> getTestCCs envs' tcs ( Int -> Expectation ( IO [Char] userName (TestCC ChatController {currentUser} _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser testChat2 :: Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO () -testChat2 p1 p2 test = testChatN [p1, p2] test_ +testChat2 = testChatOpts2 testOpts + +testChatOpts2 :: ChatOpts -> Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO () +testChatOpts2 opts p1 p2 test = testChatN opts [p1, p2] test_ where test_ :: [TestCC] -> IO () test_ [tc1, tc2] = test tc1 tc2 test_ _ = error "expected 2 chat clients" testChat3 :: Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO () -testChat3 p1 p2 p3 test = testChatN [p1, p2, p3] test_ +testChat3 p1 p2 p3 test = testChatN testOpts [p1, p2, p3] test_ where test_ :: [TestCC] -> IO () test_ [tc1, tc2, tc3] = test tc1 tc2 tc3 test_ _ = error "expected 3 chat clients" testChat4 :: Profile -> Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> IO () -testChat4 p1 p2 p3 p4 test = testChatN [p1, p2, p3, p4] test_ +testChat4 p1 p2 p3 p4 test = testChatN testOpts [p1, p2, p3, p4] test_ where test_ :: [TestCC] -> IO () test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4 diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 4d60fe2154..03c7991c85 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -18,9 +18,11 @@ import Data.Char (isDigit) import qualified Data.Text as T import Simplex.Chat.Call import Simplex.Chat.Controller (ChatController (..)) +import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Types (ConnStatus (..), ImageData (..), Profile (..), User (..)) import Simplex.Chat.Util (unlessM) -import System.Directory (copyFile, doesFileExist) +import System.Directory (copyFile, doesDirectoryExist, doesFileExist) +import System.FilePath (()) import Test.Hspec aliceProfile :: Profile @@ -91,6 +93,9 @@ chatTests = do it "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer describe "webrtc calls api" $ do it "negotiate call" testNegotiateCall + describe "maintenance mode" $ do + it "start/stop/export/import chat" testMaintenanceMode + it "export/import chat with files" testMaintenanceModeWithFiles testAddContact :: IO () testAddContact = @@ -1963,6 +1968,81 @@ testNegotiateCall = alice <## "message updated" alice #$> ("/_get chat @2 count=100", chat, [(1, "outgoing call: ended (00:00)")]) +testMaintenanceMode :: IO () +testMaintenanceMode = withTmpFiles $ do + withNewTestChat "bob" bobProfile $ \bob -> do + withNewTestChatOpts testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do + alice ##> "/c" + alice <## "error: chat not started" + alice ##> "/_start" + alice <## "chat started" + connectUsers alice bob + alice #> "@bob hi" + bob <# "alice> hi" + alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" + alice <## "error: chat not stopped" + alice ##> "/_stop" + alice <## "chat stopped" + alice ##> "/_start" + alice <## "chat started" + -- chat works after start + alice <## "1 contacts connected (use /cs for the list)" + alice #> "@bob hi again" + bob <# "alice> hi again" + bob #> "@alice hello" + alice <# "bob> hello" + -- export / delete / import + alice ##> "/_stop" + alice <## "chat stopped" + alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" + alice <## "ok" + doesFileExist "./tests/tmp/alice-chat.zip" `shouldReturn` True + alice ##> "/_db import {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" + alice <## "ok" + -- cannot start chat after import + alice ##> "/_start" + alice <## "error: chat store changed" + -- works after full restart + withTestChat "alice" $ \alice -> testChatWorking alice bob + +testChatWorking :: TestCC -> TestCC -> IO () +testChatWorking alice bob = do + alice <## "1 contacts connected (use /cs for the list)" + alice #> "@bob hello again" + bob <# "alice> hello again" + bob #> "@alice hello too" + alice <# "bob> hello too" + +testMaintenanceModeWithFiles :: IO () +testMaintenanceModeWithFiles = withTmpFiles $ do + withNewTestChat "bob" bobProfile $ \bob -> do + withNewTestChatOpts testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do + alice ##> "/_start" + alice <## "chat started" + alice ##> "/_files_folder ./tests/tmp/alice_files" + alice <## "ok" + connectUsers alice bob + startFileTransferWithDest' bob alice "test.jpg" "136.5 KiB / 139737 bytes" Nothing + bob <## "completed sending file 1 (test.jpg) to alice" + alice <## "completed receiving file 1 (test.jpg) from bob" + src <- B.readFile "./tests/fixtures/test.jpg" + B.readFile "./tests/tmp/alice_files/test.jpg" `shouldReturn` src + alice ##> "/_stop" + alice <## "chat stopped" + alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" + alice <## "ok" + alice ##> "/_db delete" + alice <## "ok" + -- cannot start chat after delete + alice ##> "/_start" + alice <## "error: chat store changed" + doesDirectoryExist "./tests/tmp/alice_files" `shouldReturn` False + alice ##> "/_db import {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" + alice <## "ok" + B.readFile "./tests/tmp/alice_files/test.jpg" `shouldReturn` src + -- works after full restart + withTestChat "alice" $ \alice -> testChatWorking alice bob + withTestChatContactConnected :: String -> (TestCC -> IO a) -> IO a withTestChatContactConnected dbPrefix action = withTestChat dbPrefix $ \cc -> do @@ -1987,16 +2067,21 @@ startFileTransfer alice bob = startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes" startFileTransfer' :: TestCC -> TestCC -> String -> String -> IO () -startFileTransfer' alice bob fileName fileSize = do - alice #> ("/f @bob ./tests/fixtures/" <> fileName) - alice <## "use /fc 1 to cancel sending" - bob <# ("alice> sends file " <> fileName <> " (" <> fileSize <> ")") - bob <## "use /fr 1 [/ | ] to receive it" - bob ##> "/fr 1 ./tests/tmp" - bob <## ("saving file 1 from alice to ./tests/tmp/" <> fileName) +startFileTransfer' cc1 cc2 fileName fileSize = startFileTransferWithDest' cc1 cc2 fileName fileSize $ Just "./tests/tmp" + +startFileTransferWithDest' :: TestCC -> TestCC -> String -> String -> Maybe String -> IO () +startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do + name1 <- userName cc1 + name2 <- userName cc2 + cc1 #> ("/f @" <> name2 <> " ./tests/fixtures/" <> fileName) + cc1 <## "use /fc 1 to cancel sending" + cc2 <# (name1 <> "> sends file " <> fileName <> " (" <> fileSize <> ")") + cc2 <## "use /fr 1 [/ | ] to receive it" + cc2 ##> ("/fr 1" <> maybe "" (" " <>) fileDest_) + cc2 <## ("saving file 1 from " <> name1 <> " to " <> maybe id () fileDest_ fileName) concurrently_ - (bob <## ("started receiving file 1 (" <> fileName <> ") from alice")) - (alice <## ("started sending file 1 (" <> fileName <> ") to bob")) + (cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1)) + (cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2)) checkPartialTransfer :: String -> IO () checkPartialTransfer fileName = do From 883bf768af595cdbe7f1c2232fa34a53f9f3751c Mon Sep 17 00:00:00 2001 From: Milton Date: Mon, 6 Jun 2022 17:24:33 +0200 Subject: [PATCH 05/89] fix typo (#732) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 1e53c00bfb..4518967e52 100755 --- a/install.sh +++ b/install.sh @@ -77,7 +77,7 @@ fi chmod +x $BIN_PATH -echo "$APP_NAME installed sucesfully!" +echo "$APP_NAME installed successfully!" if [ -z "$(command -v $APP_NAME)" ]; then if [ -n "$($SHELL -c 'echo $ZSH_VERSION')" ]; then From 16bd9ccc4f4bcb16dda8ee59c5d8a7b6ec7c4361 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 7 Jun 2022 14:14:54 +0100 Subject: [PATCH 06/89] core: send SMP notification msg flag based on chat message (#733) * core: send SMP notification msg flag based on chat message * update simplexmq * remove unnecessary condition Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- cabal.project | 4 ++-- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 41 +++++++++++++++++++----------------- src/Simplex/Chat/Archive.hs | 2 +- src/Simplex/Chat/Messages.hs | 4 ++-- src/Simplex/Chat/Protocol.hs | 14 ++++++++++-- src/Simplex/Chat/Store.hs | 3 +-- src/Simplex/Chat/Util.hs | 13 ------------ stack.yaml | 2 +- tests/ChatTests.hs | 2 +- 10 files changed, 43 insertions(+), 44 deletions(-) diff --git a/cabal.project b/cabal.project index e3ad4ef1c1..b12f8d3987 100644 --- a/cabal.project +++ b/cabal.project @@ -1,9 +1,9 @@ -packages: . +packages: . ../simplexmq source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 628930df1fa1c3fff6fd1413e7b437148c4a83b5 + tag: 60294521f4e7a8faa576872eba140de1a3ffd21c source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index f56ebfa858..a200b8857c 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."628930df1fa1c3fff6fd1413e7b437148c4a83b5" = "03h063yahq6b5m1lng7as70a59lklhzsxg0ykmr9wldy8768dlvd"; + "https://github.com/simplex-chat/simplexmq.git"."60294521f4e7a8faa576872eba140de1a3ffd21c" = "1g99q2ds8g5jz14xs3h4xjnh0w0j2bf40adaa5cb6fpiv67fsv7y"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index da5f7116c7..e6a9276e17 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -50,7 +50,7 @@ import Simplex.Chat.Options (ChatOpts (..), smpServersP) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Types -import Simplex.Chat.Util (ifM, safeDecodeUtf8, unlessM, whenM) +import Simplex.Chat.Util (safeDecodeUtf8) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), defaultAgentConfig) import Simplex.Messaging.Agent.Protocol @@ -60,10 +60,10 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Client (NtfServer) import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), PushProvider (..)) import Simplex.Messaging.Parsers (base64P, parseAll) -import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) +import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..)) import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Util (tryError, (<$?>)) +import Simplex.Messaging.Util (ifM, tryError, unlessM, whenM, (<$?>)) import System.Exit (exitFailure, exitSuccess) import System.FilePath (combine, splitExtensions, takeFileName) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout) @@ -1132,7 +1132,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage allowAgentConnection conn confId $ XInfo profile INFO connInfo -> saveConnInfo conn connInfo - MSG meta msgBody -> do + MSG meta _msgFlags msgBody -> do _ <- saveRcvMSG conn (ConnectionId connId) meta msgBody withAckMessage agentConnId meta $ pure () ackMsgDeliveryEvent conn meta @@ -1145,7 +1145,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage -- TODO add debugging output _ -> pure () Just ct@Contact {localDisplayName = c, contactId} -> case agentMsg of - MSG msgMeta msgBody -> do + MSG msgMeta _msgFlags msgBody -> do msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody withAckMessage agentConnId msgMeta $ case chatMsgEvent of @@ -1285,7 +1285,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage when (connStatus == ConnReady) $ do notifyMemberConnected gInfo m when (memberCategory m == GCPreMember) $ probeMatchingContacts ct - MSG msgMeta msgBody -> do + MSG msgMeta _msgFlags msgBody -> do msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody withAckMessage agentConnId msgMeta $ case chatMsgEvent of @@ -1344,7 +1344,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage ci <- withStore $ \st -> getChatItemByFileId st user fileId toView $ CRSndFileRcvCancelled ci ft _ -> throwChatError $ CEFileSend fileId err - MSG meta _ -> + MSG meta _ _ -> withAckMessage agentConnId meta $ pure () -- TODO print errors ERR _ -> pure () @@ -1368,7 +1368,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage updateCIFileStatus st user fileId CIFSRcvTransfer getChatItemByFileId st user fileId toView $ CRRcvFileStart ci - MSG meta@MsgMeta {recipient = (msgId, _), integrity} msgBody -> withAckMessage agentConnId meta $ do + MSG meta@MsgMeta {recipient = (msgId, _), integrity} _ msgBody -> withAckMessage agentConnId meta $ do parseFileChunk msgBody >>= \case FileChunkCancel -> unless cancelled $ do @@ -1911,7 +1911,7 @@ sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentCo sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m () sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do chunkBytes <- readFileChunk ft chunkNo - msgId <- withAgent $ \a -> sendMessage a acId $ smpEncode FileChunk {chunkNo, chunkBytes} + msgId <- withAgent $ \a -> sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunk {chunkNo, chunkBytes} withStore $ \st -> updateSndFileChunkMsg st ft chunkNo msgId readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString @@ -2005,7 +2005,7 @@ cancelSndFileTransfer ft@SndFileTransfer {agentConnId = AgentConnId acId, fileSt updateSndFileStatus st ft FSCancelled deleteSndFileChunks st ft withAgent $ \a -> do - void (sendMessage a acId $ smpEncode FileChunkCancel) `catchError` \_ -> pure () + void (sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunkCancel) `catchError` \_ -> pure () deleteConnection a acId closeFileHandle :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m () @@ -2033,7 +2033,7 @@ sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connS sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> ConnOrGroupId -> m SndMessage sendDirectMessage conn chatMsgEvent connOrGroupId = do msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId - deliverMessage conn msgBody msgId + deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId pure msg createSndMessage :: ChatMonad m => ChatMsgEvent -> ConnOrGroupId -> m SndMessage @@ -2046,9 +2046,10 @@ createSndMessage chatMsgEvent connOrGroupId = do directMessage :: ChatMsgEvent -> ByteString directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent} -deliverMessage :: ChatMonad m => Connection -> MsgBody -> MessageId -> m () -deliverMessage conn@Connection {connId} msgBody msgId = do - agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgBody +deliverMessage :: ChatMonad m => Connection -> CMEventTag -> MsgBody -> MessageId -> m () +deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do + let msgFlags = MsgFlags {notification = hasNotification cmEventTag} + agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} withStore $ \st -> createSndMsgDelivery st sndMsgDelivery msgId @@ -2068,10 +2069,12 @@ sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do forM_ (filter memberCurrent members) $ \m@GroupMember {groupMemberId} -> case memberConn m of Nothing -> withStore $ \st -> createPendingGroupMessage st groupMemberId msgId introId_ - Just conn@Connection {connStatus} -> - if not (connStatus == ConnSndReady || connStatus == ConnReady) - then unless (connStatus == ConnDeleted) $ withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_) - else (deliverMessage conn msgBody msgId >> postDeliver) `catchError` const (pure ()) + Just conn@Connection {connStatus} + | connStatus == ConnSndReady || connStatus == ConnReady -> do + let tag = toCMEventTag chatMsgEvent + (deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ()) + | connStatus == ConnDeleted -> pure () + | otherwise -> withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_) pure msg sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m () @@ -2079,7 +2082,7 @@ sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do pendingMessages <- withStore $ \st -> getPendingGroupMessages st groupMemberId -- TODO ensure order - pending messages interleave with user input messages forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} -> do - deliverMessage conn msgBody msgId + deliverMessage conn cmEventTag msgBody msgId withStore (\st -> deletePendingGroupMessage st groupMemberId msgId) when (cmEventTag == XGrpMemFwd_) $ case introId_ of Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index 31a5714eae..a15ca1bb2d 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -6,9 +6,9 @@ module Simplex.Chat.Archive where import qualified Codec.Archive.Zip as Z import Control.Monad.Reader import Simplex.Chat.Controller -import Simplex.Chat.Util (whenM) import Simplex.Messaging.Agent.Client (agentDbPath) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..)) +import Simplex.Messaging.Util (whenM) import System.FilePath import UnliftIO.Directory import UnliftIO.STM diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 48c1cb337a..e3bad56595 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -32,12 +32,12 @@ import GHC.Generics (Generic) import Simplex.Chat.Markdown import Simplex.Chat.Protocol import Simplex.Chat.Types -import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8) +import Simplex.Chat.Util (safeDecodeUtf8) import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgErrorType (..), MsgMeta (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, singleFieldJSON, sumTypeJSON) import Simplex.Messaging.Protocol (MsgBody) -import Simplex.Messaging.Util ((<$?>)) +import Simplex.Messaging.Util (eitherToMaybe, (<$?>)) data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection deriving (Show, Generic) diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 83b0d8c204..9418d26223 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -31,10 +31,10 @@ import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Generics (Generic) import Simplex.Chat.Call import Simplex.Chat.Types -import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8) +import Simplex.Chat.Util (safeDecodeUtf8) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (fromTextField_) -import Simplex.Messaging.Util ((<$?>)) +import Simplex.Messaging.Util (eitherToMaybe, (<$?>)) data ConnectionEntity = RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact} @@ -437,6 +437,16 @@ instance FromField CMEventTag where fromField = fromTextField_ cmEventTagT instance ToField CMEventTag where toField = toField . serializeCMEventTag +hasNotification :: CMEventTag -> Bool +hasNotification = \case + XMsgNew_ -> True + XFile_ -> True + XContact_ -> True + XGrpInv_ -> True + XGrpDel_ -> True + XCallInv_ -> True + _ -> False + appToChatMessage :: AppMessage -> Either String ChatMessage appToChatMessage AppMessage {msgId, event, params} = do eventTag <- strDecode $ encodeUtf8 event diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index aadac15c92..f6b4c7a1ec 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -206,7 +206,6 @@ import Simplex.Chat.Migrations.M20220404_files_status_fields import Simplex.Chat.Migrations.M20220514_profiles_user_id import Simplex.Chat.Protocol import Simplex.Chat.Types -import Simplex.Chat.Util (eitherToMaybe) import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, withTransaction) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) @@ -214,7 +213,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String (StrEncoding (strEncode)) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer) -import Simplex.Messaging.Util (liftIOEither, (<$$>)) +import Simplex.Messaging.Util (eitherToMaybe, liftIOEither, (<$$>)) import UnliftIO.STM schemaMigrations :: [(String, Query)] diff --git a/src/Simplex/Chat/Util.hs b/src/Simplex/Chat/Util.hs index a5e4c8b1e0..d2fe0c3d40 100644 --- a/src/Simplex/Chat/Util.hs +++ b/src/Simplex/Chat/Util.hs @@ -1,6 +1,5 @@ module Simplex.Chat.Util where -import Control.Monad (when) import Data.ByteString.Char8 (ByteString) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8With) @@ -9,15 +8,3 @@ safeDecodeUtf8 :: ByteString -> Text safeDecodeUtf8 = decodeUtf8With onError where onError _ _ = Just '?' - -ifM :: Monad m => m Bool -> m a -> m a -> m a -ifM ba t f = ba >>= \b -> if b then t else f - -whenM :: Monad m => m Bool -> m () -> m () -whenM ba a = ba >>= (`when` a) - -unlessM :: Monad m => m Bool -> m () -> m () -unlessM b = ifM b $ pure () - -eitherToMaybe :: Either a b -> Maybe b -eitherToMaybe = either (const Nothing) Just diff --git a/stack.yaml b/stack.yaml index eac63e18ad..e6e2417f55 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 628930df1fa1c3fff6fd1413e7b437148c4a83b5 + commit: 60294521f4e7a8faa576872eba140de1a3ffd21c # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 03c7991c85..23f4d1bb45 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -20,7 +20,7 @@ import Simplex.Chat.Call import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Types (ConnStatus (..), ImageData (..), Profile (..), User (..)) -import Simplex.Chat.Util (unlessM) +import Simplex.Messaging.Util (unlessM) import System.Directory (copyFile, doesDirectoryExist, doesFileExist) import System.FilePath (()) import Test.Hspec From 716a941dc671b6605df17d316dbc4cccb3d2b443 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 9 Jun 2022 14:52:12 +0100 Subject: [PATCH 07/89] core: use duplex handshake (agent v2) (#735) * core: use duplex handshake (agent v2) * version test matrix * update simplexmq --- cabal.project | 4 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- tests/ChatClient.hs | 92 +++++++++---- tests/ChatTests.hs | 277 +++++++++++++++++++++++++++++--------- tests/ProtocolTests.hs | 3 +- 6 files changed, 289 insertions(+), 91 deletions(-) diff --git a/cabal.project b/cabal.project index b12f8d3987..b7a510848c 100644 --- a/cabal.project +++ b/cabal.project @@ -1,9 +1,9 @@ -packages: . ../simplexmq +packages: . source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 60294521f4e7a8faa576872eba140de1a3ffd21c + tag: c1348aa54fba292d34339d6b111572cb1c74b546 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index a200b8857c..b8da2b3028 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."60294521f4e7a8faa576872eba140de1a3ffd21c" = "1g99q2ds8g5jz14xs3h4xjnh0w0j2bf40adaa5cb6fpiv67fsv7y"; + "https://github.com/simplex-chat/simplexmq.git"."c1348aa54fba292d34339d6b111572cb1c74b546" = "103hw1h1agy42krf11d98bv3c1w0q0wi2z7r2ll0gmp5xv1r4rf0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index e6e2417f55..009891d42d 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 60294521f4e7a8faa576872eba140de1a3ffd21c + commit: c1348aa54fba292d34339d6b111572cb1c74b546 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 7c5ebc6c1b..d5a44b4486 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -30,6 +30,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM import Simplex.Messaging.Transport +import Simplex.Messaging.Version import System.Directory (createDirectoryIfMissing, removePathForcibly) import qualified System.Terminal as C import System.Terminal.Internal (VirtualTerminal (..), VirtualTerminalSettings (..), withVirtualTerminal) @@ -75,30 +76,42 @@ data TestCC = TestCC aCfg :: AgentConfig aCfg = agentConfig defaultChatConfig -cfg :: ChatConfig -cfg = +testAgentCfg :: AgentConfig +testAgentCfg = aCfg {reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}} + +testCfg :: ChatConfig +testCfg = defaultChatConfig - { agentConfig = - aCfg {reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}}, + { agentConfig = testAgentCfg, testView = True } -createTestChat :: ChatOpts -> String -> Profile -> IO TestCC -createTestChat opts dbPrefix profile = do +testAgentCfgV1 :: AgentConfig +testAgentCfgV1 = + testAgentCfg + { smpAgentVersion = 1, + smpAgentVRange = mkVersionRange 1 1 + } + +testCfgV1 :: ChatConfig +testCfgV1 = testCfg {agentConfig = testAgentCfgV1} + +createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC +createTestChat cfg opts dbPrefix profile = do let dbFilePrefix = testDBPrefix <> dbPrefix st <- createStore (dbFilePrefix <> "_chat.db") 1 False Right user <- runExceptT $ createUser st profile True - startTestChat_ st opts dbFilePrefix user + startTestChat_ st cfg opts dbFilePrefix user -startTestChat :: ChatOpts -> String -> IO TestCC -startTestChat opts dbPrefix = do +startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC +startTestChat cfg opts dbPrefix = do let dbFilePrefix = testDBPrefix <> dbPrefix st <- createStore (dbFilePrefix <> "_chat.db") 1 False Just user <- find activeUser <$> getUsers st - startTestChat_ st opts dbFilePrefix user + startTestChat_ st cfg opts dbFilePrefix user -startTestChat_ :: SQLiteStore -> ChatOpts -> FilePath -> User -> IO TestCC -startTestChat_ st opts dbFilePrefix user = do +startTestChat_ :: SQLiteStore -> ChatConfig -> ChatOpts -> FilePath -> User -> IO TestCC +startTestChat_ st cfg opts dbFilePrefix user = do t <- withVirtualTerminal termSettings pure ct <- newChatTerminal t cc <- newChatController st (Just user) cfg opts {dbFilePrefix} Nothing -- no notifications @@ -114,16 +127,34 @@ stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do uninterruptibleCancel chatAsync withNewTestChat :: String -> Profile -> (TestCC -> IO a) -> IO a -withNewTestChat = withNewTestChatOpts testOpts +withNewTestChat = withNewTestChatCfgOpts testCfg testOpts + +withNewTestChatV1 :: String -> Profile -> (TestCC -> IO a) -> IO a +withNewTestChatV1 = withNewTestChatCfg testCfgV1 + +withNewTestChatCfg :: ChatConfig -> String -> Profile -> (TestCC -> IO a) -> IO a +withNewTestChatCfg cfg = withNewTestChatCfgOpts cfg testOpts withNewTestChatOpts :: ChatOpts -> String -> Profile -> (TestCC -> IO a) -> IO a -withNewTestChatOpts opts dbPrefix profile = bracket (createTestChat opts dbPrefix profile) (\cc -> cc > stopTestChat cc) +withNewTestChatOpts = withNewTestChatCfgOpts testCfg + +withNewTestChatCfgOpts :: ChatConfig -> ChatOpts -> String -> Profile -> (TestCC -> IO a) -> IO a +withNewTestChatCfgOpts cfg opts dbPrefix profile = bracket (createTestChat cfg opts dbPrefix profile) (\cc -> cc > stopTestChat cc) + +withTestChatV1 :: String -> (TestCC -> IO a) -> IO a +withTestChatV1 = withTestChatCfg testCfgV1 withTestChat :: String -> (TestCC -> IO a) -> IO a -withTestChat = withTestChatOpts testOpts +withTestChat = withTestChatCfgOpts testCfg testOpts + +withTestChatCfg :: ChatConfig -> String -> (TestCC -> IO a) -> IO a +withTestChatCfg cfg = withTestChatCfgOpts cfg testOpts withTestChatOpts :: ChatOpts -> String -> (TestCC -> IO a) -> IO a -withTestChatOpts opts dbPrefix = bracket (startTestChat opts dbPrefix) (\cc -> cc > stopTestChat cc) +withTestChatOpts = withTestChatCfgOpts testCfg + +withTestChatCfgOpts :: ChatConfig -> ChatOpts -> String -> (TestCC -> IO a) -> IO a +withTestChatCfgOpts cfg opts dbPrefix = bracket (startTestChat cfg opts dbPrefix) (\cc -> cc > stopTestChat cc) readTerminalOutput :: VirtualTerminal -> TQueue String -> IO () readTerminalOutput t termQ = do @@ -154,8 +185,8 @@ withTmpFiles = (createDirectoryIfMissing False "tests/tmp") (removePathForcibly "tests/tmp") -testChatN :: ChatOpts -> [Profile] -> ([TestCC] -> IO ()) -> IO () -testChatN opts ps test = withTmpFiles $ do +testChatN :: ChatConfig -> ChatOpts -> [Profile] -> ([TestCC] -> IO ()) -> IO () +testChatN cfg opts ps test = withTmpFiles $ do tcs <- getTestCCs (zip ps [1 ..]) [] test tcs concurrentlyN_ $ map ( [TestCC] -> IO [TestCC] getTestCCs [] tcs = pure tcs - getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat opts (show db) p <*> getTestCCs envs' tcs + getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat cfg opts (show db) p <*> getTestCCs envs' tcs ( Int -> Expectation ( IO [Char] userName (TestCC ChatController {currentUser} _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser testChat2 :: Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO () -testChat2 = testChatOpts2 testOpts +testChat2 = testChatCfgOpts2 testCfg testOpts + +testChatCfg2 :: ChatConfig -> Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO () +testChatCfg2 cfg = testChatCfgOpts2 cfg testOpts testChatOpts2 :: ChatOpts -> Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO () -testChatOpts2 opts p1 p2 test = testChatN opts [p1, p2] test_ +testChatOpts2 = testChatCfgOpts2 testCfg + +testChatCfgOpts2 :: ChatConfig -> ChatOpts -> Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO () +testChatCfgOpts2 cfg opts p1 p2 test = testChatN cfg opts [p1, p2] test_ where test_ :: [TestCC] -> IO () test_ [tc1, tc2] = test tc1 tc2 test_ _ = error "expected 2 chat clients" testChat3 :: Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO () -testChat3 p1 p2 p3 test = testChatN testOpts [p1, p2, p3] test_ +testChat3 = testChatCfgOpts3 testCfg testOpts + +testChatCfg3 :: ChatConfig -> Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO () +testChatCfg3 cfg = testChatCfgOpts3 cfg testOpts + +testChatCfgOpts3 :: ChatConfig -> ChatOpts -> Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO () +testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_ where test_ :: [TestCC] -> IO () test_ [tc1, tc2, tc3] = test tc1 tc2 tc3 test_ _ = error "expected 3 chat clients" testChat4 :: Profile -> Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> IO () -testChat4 p1 p2 p3 p4 test = testChatN testOpts [p1, p2, p3, p4] test_ +testChat4 p1 p2 p3 p4 test = testChatN testCfg testOpts [p1, p2, p3, p4] test_ where test_ :: [TestCC] -> IO () test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4 @@ -226,7 +269,8 @@ serverCfg = privateKeyFile = "tests/fixtures/tls/server.key", certificateFile = "tests/fixtures/tls/server.crt", logStatsInterval = Just 86400, - logStatsStartTime = 0 + logStatsStartTime = 0, + smpServerVRange = supportedSMPServerVRange } withSmpServer :: IO a -> IO a diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 23f4d1bb45..124f866e99 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -40,12 +40,12 @@ danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing} chatTests :: Spec chatTests = do describe "direct messages" $ do - it "add contact and send/receive message" testAddContact + describe "add contact and send/receive message" testAddContact it "direct message quoted replies" testDirectMessageQuotedReply it "direct message update" testDirectMessageUpdate it "direct message delete" testDirectMessageDelete describe "chat groups" $ do - it "add contacts, create group and send/receive messages" testGroup + describe "add contacts, create group and send/receive messages" testGroup it "create and join group with 4 members" testGroup2 it "create and delete group" testGroupDelete it "invitee delete group when in status invited" testGroupDeleteWhenInvited @@ -67,16 +67,16 @@ chatTests = do it "send and receive file to group" testGroupFileTransfer it "sender cancelled group file transfer before transfer" testGroupFileSndCancelBeforeTransfer describe "messages with files" $ do - it "send and receive message with file" testMessageWithFile + describe "send and receive message with file" testMessageWithFile it "send and receive image" testSendImage it "files folder: send and receive image" testFilesFoldersSendImage it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete it "send and receive image with text and quote" testSendImageWithTextAndQuote - it "send and receive image to group" testGroupSendImage + describe "send and receive image to group" testGroupSendImage it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote describe "user contact link" $ do - it "create and connect via contact link" testUserContactLink + describe "create and connect via contact link" testUserContactLink it "auto accept contact requests" testUserContactLinkAutoAccept it "deduplicate contact requests" testDeduplicateContactRequests it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange @@ -87,20 +87,64 @@ chatTests = do describe "async connection handshake" $ do it "connect when initiating client goes offline" testAsyncInitiatingOffline it "connect when accepting client goes offline" testAsyncAcceptingOffline - it "connect, fully asynchronous (when clients are never simultaneously online)" testFullAsync - xdescribe "async sending and receiving files" $ do - it "send and receive file, fully asynchronous" testAsyncFileTransfer - it "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer + describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do + it "v2" testFullAsync + it "v1" testFullAsyncV1 + it "v1 to v2" testFullAsyncV1toV2 + it "v2 to v1" testFullAsyncV2toV1 + describe "async sending and receiving files" $ do + xdescribe "send and receive file, fully asynchronous" $ do + it "v2" testAsyncFileTransfer + it "v1" testAsyncFileTransferV1 + xit "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer describe "webrtc calls api" $ do it "negotiate call" testNegotiateCall describe "maintenance mode" $ do it "start/stop/export/import chat" testMaintenanceMode it "export/import chat with files" testMaintenanceModeWithFiles -testAddContact :: IO () -testAddContact = - testChat2 aliceProfile bobProfile $ - \alice bob -> do +versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec +versionTestMatrix2 runTest = do + it "v2" $ testChat2 aliceProfile bobProfile $ runTest + it "v1" $ testChatCfg2 testCfgV1 aliceProfile bobProfile $ runTest + it "v1 to v2" . withTmpFiles $ + withNewTestChat "alice" aliceProfile $ \alice -> + withNewTestChatV1 "bob" bobProfile $ \bob -> + runTest alice bob + it "v2 to v1" . withTmpFiles $ + withNewTestChatV1 "alice" aliceProfile $ \alice -> + withNewTestChat "bob" bobProfile $ \bob -> + runTest alice bob + +versionTestMatrix3 :: (TestCC -> TestCC -> TestCC -> IO ()) -> Spec +versionTestMatrix3 runTest = do + it "v2" $ testChat3 aliceProfile bobProfile cathProfile $ runTest + it "v1" $ testChatCfg3 testCfgV1 aliceProfile bobProfile cathProfile $ runTest + it "v1 to v2" . withTmpFiles $ + withNewTestChat "alice" aliceProfile $ \alice -> + withNewTestChatV1 "bob" bobProfile $ \bob -> + withNewTestChatV1 "cath" cathProfile $ \cath -> + runTest alice bob cath + it "v2+v1 to v2" . withTmpFiles $ + withNewTestChat "alice" aliceProfile $ \alice -> + withNewTestChat "bob" bobProfile $ \bob -> + withNewTestChatV1 "cath" cathProfile $ \cath -> + runTest alice bob cath + it "v2 to v1" . withTmpFiles $ + withNewTestChatV1 "alice" aliceProfile $ \alice -> + withNewTestChat "bob" bobProfile $ \bob -> + withNewTestChat "cath" cathProfile $ \cath -> + runTest alice bob cath + it "v2+v1 to v1" . withTmpFiles $ + withNewTestChatV1 "alice" aliceProfile $ \alice -> + withNewTestChat "bob" bobProfile $ \bob -> + withNewTestChatV1 "cath" cathProfile $ \cath -> + runTest alice bob cath + +testAddContact :: Spec +testAddContact = versionTestMatrix2 runTestAddContact + where + runTestAddContact alice bob = do alice ##> "/c" inv <- getInvitation alice bob ##> ("/c " <> inv) @@ -141,7 +185,6 @@ testAddContact = alice #$> ("/_get chat @2 count=100", chat, []) bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY") bob #$> ("/_get chat @2 count=100", chat, []) - where chatsEmpty alice bob = do alice @@@ [("@bob", "")] alice #$> ("/_get chat @2 count=100", chat, []) @@ -313,10 +356,10 @@ testDirectMessageDelete = bob @@@ [("@alice", "do you receive my messages?")] bob #$> ("/_get chat @2 count=100", chat', [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))]) -testGroup :: IO () -testGroup = - testChat3 aliceProfile bobProfile cathProfile $ - \alice bob cath -> do +testGroup :: Spec +testGroup = versionTestMatrix3 runTestGroup + where + runTestGroup alice bob cath = do connectUsers alice bob connectUsers alice cath alice ##> "/g team" @@ -407,7 +450,7 @@ testGroup = bob #$> ("/_get chat #1 count=100", chat, []) cath #$> ("/clear #team", id, "#team: all messages are removed locally ONLY") cath #$> ("/_get chat #1 count=100", chat, []) - where + getReadChats :: TestCC -> TestCC -> TestCC -> IO () getReadChats alice bob cath = do alice @@@ [("#team", "hey team"), ("@cath", ""), ("@bob", "")] alice #$> ("/_get chat #1 count=100", chat, [(1, "hello"), (0, "hi there"), (0, "hey team")]) @@ -1204,10 +1247,10 @@ testGroupFileSndCancelBeforeTransfer = bob ##> "/fr 1 ./tests/tmp" bob <## "file cancelled: test.txt" -testMessageWithFile :: IO () -testMessageWithFile = - testChat2 aliceProfile bobProfile $ - \alice bob -> do +testMessageWithFile :: Spec +testMessageWithFile = versionTestMatrix2 runTestMessageWithFile + where + runTestMessageWithFile alice bob = do connectUsers alice bob alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}" alice <# "@bob hi, sending a file" @@ -1415,10 +1458,10 @@ testSendImageWithTextAndQuote = (alice <## "completed sending file 3 (test.jpg) to bob") B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src -testGroupSendImage :: IO () -testGroupSendImage = - testChat3 aliceProfile bobProfile cathProfile $ - \alice bob cath -> do +testGroupSendImage :: Spec +testGroupSendImage = versionTestMatrix3 runTestGroupSendImage + where + runTestGroupSendImage alice bob cath = do createGroup3 "team" alice bob cath alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" alice <# "/f #team ./tests/fixtures/test.jpg" @@ -1519,32 +1562,31 @@ testGroupSendImageWithTextAndQuote = cath #$> ("/_get chat #1 count=100", chat'', [((0, "hi team"), Nothing, Nothing), ((0, "hey bob"), Just (0, "hi team"), Just "./tests/tmp/test_1.jpg")]) cath @@@ [("#team", "hey bob"), ("@alice", ""), ("@bob", "")] -testUserContactLink :: IO () -testUserContactLink = testChat3 aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - alice ##> "/ad" - cLink <- getContactLink alice True - bob ##> ("/c " <> cLink) - alice <#? bob - alice @@@ [("<@bob", "")] - alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." - concurrently_ - (bob <## "alice (Alice): contact is connected") - (alice <## "bob (Bob): contact is connected") - alice @@@ [("@bob", "")] - alice <##> bob +testUserContactLink :: Spec +testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do + alice ##> "/ad" + cLink <- getContactLink alice True + bob ##> ("/c " <> cLink) + alice <#? bob + alice @@@ [("<@bob", "")] + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice @@@ [("@bob", "")] + alice <##> bob - cath ##> ("/c " <> cLink) - alice <#? cath - alice @@@ [("<@cath", ""), ("@bob", "hey")] - alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." - concurrently_ - (cath <## "alice (Alice): contact is connected") - (alice <## "cath (Catherine): contact is connected") - alice @@@ [("@cath", ""), ("@bob", "hey")] - alice <##> cath + cath ##> ("/c " <> cLink) + alice <#? cath + alice @@@ [("<@cath", ""), ("@bob", "hey")] + alice ##> "/ac cath" + alice <## "cath (Catherine): accepting contact request..." + concurrently_ + (cath <## "alice (Alice): contact is connected") + (alice <## "cath (Catherine): contact is connected") + alice @@@ [("@cath", ""), ("@bob", "hey")] + alice <##> cath testUserContactLinkAutoAccept :: IO () testUserContactLinkAutoAccept = @@ -1808,11 +1850,8 @@ testFullAsync = withTmpFiles $ do withNewTestChat "bob" bobProfile $ \bob -> do bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChat "alice" $ \_ -> pure () - withTestChat "bob" $ \_ -> pure () - withTestChat "alice" $ \alice -> - alice <## "1 contacts connected (use /cs for the list)" - withTestChat "bob" $ \_ -> pure () + withTestChat "alice" $ \_ -> pure () -- connecting... notification in UI + withTestChat "bob" $ \_ -> pure () -- connecting... notification in UI withTestChat "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "bob (Bob): contact is connected" @@ -1820,6 +1859,81 @@ testFullAsync = withTmpFiles $ do bob <## "1 contacts connected (use /cs for the list)" bob <## "alice (Alice): contact is connected" +testFullAsyncV1 :: IO () +testFullAsyncV1 = withTmpFiles $ do + inv <- withNewAlice $ \alice -> do + alice ##> "/c" + getInvitation alice + withNewBob $ \bob -> do + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + withAlice $ \_ -> pure () + withBob $ \_ -> pure () + withAlice $ \alice -> + alice <## "1 contacts connected (use /cs for the list)" + withBob $ \_ -> pure () + withAlice $ \alice -> do + alice <## "1 contacts connected (use /cs for the list)" + alice <## "bob (Bob): contact is connected" + withBob $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "alice (Alice): contact is connected" + where + withNewAlice = withNewTestChatV1 "alice" aliceProfile + withAlice = withTestChatV1 "alice" + withNewBob = withNewTestChatV1 "bob" bobProfile + withBob = withTestChatV1 "bob" + +testFullAsyncV1toV2 :: IO () +testFullAsyncV1toV2 = withTmpFiles $ do + inv <- withNewAlice $ \alice -> do + alice ##> "/c" + getInvitation alice + withNewBob $ \bob -> do + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + withAlice $ \_ -> pure () + withBob $ \_ -> pure () + withAlice $ \alice -> + alice <## "1 contacts connected (use /cs for the list)" + withBob $ \_ -> pure () + withAlice $ \alice -> do + alice <## "1 contacts connected (use /cs for the list)" + alice <## "bob (Bob): contact is connected" + withBob $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "alice (Alice): contact is connected" + where + withNewAlice = withNewTestChat "alice" aliceProfile + withAlice = withTestChat "alice" + withNewBob = withNewTestChatV1 "bob" bobProfile + withBob = withTestChatV1 "bob" + +testFullAsyncV2toV1 :: IO () +testFullAsyncV2toV1 = withTmpFiles $ do + inv <- withNewAlice $ \alice -> do + alice ##> "/c" + getInvitation alice + withNewBob $ \bob -> do + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + withAlice $ \_ -> pure () + withBob $ \_ -> pure () + withAlice $ \alice -> + alice <## "1 contacts connected (use /cs for the list)" + withBob $ \_ -> pure () + withAlice $ \alice -> do + alice <## "1 contacts connected (use /cs for the list)" + alice <## "bob (Bob): contact is connected" + withBob $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "alice (Alice): contact is connected" + where + withNewAlice = withNewTestChatV1 "alice" aliceProfile + withAlice = withTestChatV1 "alice" + withNewBob = withNewTestChat "bob" bobProfile + withBob = withTestChat "bob" + testAsyncFileTransfer :: IO () testAsyncFileTransfer = withTmpFiles $ do withNewTestChat "alice" aliceProfile $ \alice -> @@ -1836,8 +1950,8 @@ testAsyncFileTransfer = withTmpFiles $ do bob <## "use /fr 1 [/ | ] to receive it" bob ##> "/fr 1 ./tests/tmp" bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - withTestChatContactConnected' "alice" - withTestChatContactConnected' "bob" + -- withTestChatContactConnected' "alice" -- TODO not needed in v2 + -- withTestChatContactConnected' "bob" -- TODO not needed in v2 withTestChatContactConnected' "alice" withTestChatContactConnected' "bob" withTestChatContactConnected "alice" $ \alice -> do @@ -1850,6 +1964,36 @@ testAsyncFileTransfer = withTmpFiles $ do dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src +testAsyncFileTransferV1 :: IO () +testAsyncFileTransferV1 = withTmpFiles $ do + withNewTestChatV1 "alice" aliceProfile $ \alice -> + withNewTestChatV1 "bob" bobProfile $ \bob -> + connectUsers alice bob + withTestChatContactConnectedV1 "alice" $ \alice -> do + alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\":\"text\", \"text\": \"hi, sending a file\"}}" + alice <# "@bob hi, sending a file" + alice <# "/f @bob ./tests/fixtures/test.jpg" + alice <## "use /fc 1 to cancel sending" + withTestChatContactConnectedV1 "bob" $ \bob -> do + bob <# "alice> hi, sending a file" + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + bob ##> "/fr 1 ./tests/tmp" + bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" + withTestChatContactConnectedV1' "alice" -- TODO not needed in v2 + withTestChatContactConnectedV1' "bob" -- TODO not needed in v2 + withTestChatContactConnectedV1' "alice" + withTestChatContactConnectedV1' "bob" + withTestChatContactConnectedV1 "alice" $ \alice -> do + alice <## "started sending file 1 (test.jpg) to bob" + alice <## "completed sending file 1 (test.jpg) to bob" + withTestChatContactConnectedV1 "bob" $ \bob -> do + bob <## "started receiving file 1 (test.jpg) from alice" + bob <## "completed receiving file 1 (test.jpg) from alice" + src <- B.readFile "./tests/fixtures/test.jpg" + dest <- B.readFile "./tests/tmp/test.jpg" + dest `shouldBe` src + testAsyncGroupFileTransfer :: IO () testAsyncGroupFileTransfer = withTmpFiles $ do withNewTestChat "alice" aliceProfile $ \alice -> @@ -1873,9 +2017,9 @@ testAsyncGroupFileTransfer = withTmpFiles $ do withTestChatGroup3Connected' "alice" withTestChatGroup3Connected' "bob" withTestChatGroup3Connected' "cath" - withTestChatGroup3Connected' "alice" - withTestChatGroup3Connected' "bob" - withTestChatGroup3Connected' "cath" + -- withTestChatGroup3Connected' "alice" -- TODO not needed in v2 + -- withTestChatGroup3Connected' "bob" -- TODO not needed in v2 + -- withTestChatGroup3Connected' "cath" -- TODO not needed in v2 withTestChatGroup3Connected' "alice" withTestChatGroup3Connected "bob" $ \bob -> do bob <## "started receiving file 1 (test.jpg) from alice" @@ -2052,6 +2196,15 @@ withTestChatContactConnected dbPrefix action = withTestChatContactConnected' :: String -> IO () withTestChatContactConnected' dbPrefix = withTestChatContactConnected dbPrefix $ \_ -> pure () +withTestChatContactConnectedV1 :: String -> (TestCC -> IO a) -> IO a +withTestChatContactConnectedV1 dbPrefix action = + withTestChatV1 dbPrefix $ \cc -> do + cc <## "1 contacts connected (use /cs for the list)" + action cc + +withTestChatContactConnectedV1' :: String -> IO () +withTestChatContactConnectedV1' dbPrefix = withTestChatContactConnectedV1 dbPrefix $ \_ -> pure () + withTestChatGroup3Connected :: String -> (TestCC -> IO a) -> IO a withTestChatGroup3Connected dbPrefix action = do withTestChat dbPrefix $ \cc -> do diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 3b615ddffd..1be897fec0 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -16,6 +16,7 @@ import Simplex.Messaging.Crypto.Ratchet import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (ProtocolServer (..), smpClientVRange) +import Simplex.Messaging.Version import Test.Hspec protocolTests :: Spec @@ -42,7 +43,7 @@ connReqData :: ConnReqUriData connReqData = ConnReqUriData { crScheme = simplexChat, - crAgentVRange = smpAgentVRange, + crAgentVRange = mkVersionRange 1 1, crSmpQueues = [queue] } From 121541759ba498f67b84142cad8536976c980c86 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 9 Jun 2022 14:52:55 +0100 Subject: [PATCH 08/89] ios: update library --- apps/ios/SimpleX--iOS--Info.plist | 2 + apps/ios/SimpleX.xcodeproj/project.pbxproj | 60 +++++++++++----------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/apps/ios/SimpleX--iOS--Info.plist b/apps/ios/SimpleX--iOS--Info.plist index 1287772e17..01f16a5a1c 100644 --- a/apps/ios/SimpleX--iOS--Info.plist +++ b/apps/ios/SimpleX--iOS--Info.plist @@ -23,8 +23,10 @@ UIBackgroundModes + audio fetch remote-notification + voip diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 27c9e9c79f..e592160690 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -43,6 +43,16 @@ 5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; }; 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; }; 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; + 5C6F2A3C28522C9A00103588 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3728522C9A00103588 /* libgmp.a */; }; + 5C6F2A3D28522C9A00103588 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3728522C9A00103588 /* libgmp.a */; }; + 5C6F2A3E28522C9A00103588 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3828522C9A00103588 /* libffi.a */; }; + 5C6F2A3F28522C9A00103588 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3828522C9A00103588 /* libffi.a */; }; + 5C6F2A4028522C9A00103588 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3928522C9A00103588 /* libgmpxx.a */; }; + 5C6F2A4128522C9A00103588 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3928522C9A00103588 /* libgmpxx.a */; }; + 5C6F2A4228522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */; }; + 5C6F2A4328522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */; }; + 5C6F2A4428522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */; }; + 5C6F2A4528522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */; }; 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; }; @@ -109,16 +119,6 @@ 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; - 64A6908128376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */; }; - 64A6908228376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */; }; - 64A6908328376BBA0076573F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907D28376BB90076573F /* libffi.a */; }; - 64A6908428376BBA0076573F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907D28376BB90076573F /* libffi.a */; }; - 64A6908528376BBA0076573F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907E28376BB90076573F /* libgmpxx.a */; }; - 64A6908628376BBA0076573F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907E28376BB90076573F /* libgmpxx.a */; }; - 64A6908728376BBA0076573F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907F28376BB90076573F /* libgmp.a */; }; - 64A6908828376BBA0076573F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907F28376BB90076573F /* libgmp.a */; }; - 64A6908928376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */; }; - 64A6908A28376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; 64DAE1512809D9F5000DA960 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DAE1502809D9F5000DA960 /* FileUtils.swift */; }; @@ -172,6 +172,11 @@ 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = ""; }; 5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = ""; }; + 5C6F2A3728522C9A00103588 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C6F2A3828522C9A00103588 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C6F2A3928522C9A00103588 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a"; sourceTree = ""; }; + 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a"; sourceTree = ""; }; 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = ""; }; 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = ""; }; 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = ""; }; @@ -237,11 +242,6 @@ 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; - 64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a"; sourceTree = ""; }; - 64A6907D28376BB90076573F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64A6907E28376BB90076573F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 64A6907F28376BB90076573F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a"; sourceTree = ""; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; }; 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; }; 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; @@ -252,15 +252,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 64A6908928376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */, + 5C6F2A4228522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */, 646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */, - 64A6908528376BBA0076573F /* libgmpxx.a in Frameworks */, - 64A6908728376BBA0076573F /* libgmp.a in Frameworks */, + 5C6F2A4428522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */, + 5C6F2A3E28522C9A00103588 /* libffi.a in Frameworks */, + 5C6F2A3C28522C9A00103588 /* libgmp.a in Frameworks */, 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, 5C764E83279C748B000C6508 /* libz.tbd in Frameworks */, 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */, - 64A6908328376BBA0076573F /* libffi.a in Frameworks */, - 64A6908128376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */, + 5C6F2A4028522C9A00103588 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -275,13 +275,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5C6F2A4528522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */, + 5C6F2A4328522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */, 5CDCAD5F28187D6900503DA2 /* libiconv.tbd in Frameworks */, - 64A6908628376BBA0076573F /* libgmpxx.a in Frameworks */, - 64A6908A28376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */, + 5C6F2A4128522C9A00103588 /* libgmpxx.a in Frameworks */, + 5C6F2A3D28522C9A00103588 /* libgmp.a in Frameworks */, 5CDCAD6128187D8000503DA2 /* libz.tbd in Frameworks */, - 64A6908228376BBA0076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */, - 64A6908828376BBA0076573F /* libgmp.a in Frameworks */, - 64A6908428376BBA0076573F /* libffi.a in Frameworks */, + 5C6F2A3F28522C9A00103588 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -334,11 +334,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 64A6907D28376BB90076573F /* libffi.a */, - 64A6907F28376BB90076573F /* libgmp.a */, - 64A6907E28376BB90076573F /* libgmpxx.a */, - 64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */, - 64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */, + 5C6F2A3828522C9A00103588 /* libffi.a */, + 5C6F2A3728522C9A00103588 /* libgmp.a */, + 5C6F2A3928522C9A00103588 /* libgmpxx.a */, + 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */, + 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */, ); path = Libraries; sourceTree = ""; From 235fb8dc0c87dae5274e56b34c576e718a0e8cd5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 9 Jun 2022 15:18:12 +0100 Subject: [PATCH 09/89] remove missing libs --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8aa6904b58..cea0f83938 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -217,8 +217,6 @@ 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = ""; }; 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = ""; }; 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = ""; }; - 5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; }; - 5C764E7C279C71DB000C6508 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = ""; }; 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = ""; }; 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = ""; }; @@ -389,8 +387,6 @@ 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */, 5CDCAD6028187D7900503DA2 /* libz.tbd */, 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */, - 5C764E7C279C71DB000C6508 /* libz.tbd */, - 5C764E7B279C71D4000C6508 /* libiconv.tbd */, ); name = Frameworks; sourceTree = ""; From 6f195c41678c2214e212c0138db01e992d989376 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 11 Jun 2022 11:52:55 +0100 Subject: [PATCH 10/89] core add chat_recv_msg_wait and chat_parse_markdown to published API (#736) --- src/Simplex/Chat/Markdown.hs | 9 ++++++++- src/Simplex/Chat/Mobile.hs | 23 +++++++++++++++++++++++ tests/MobileTests.hs | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 8242d46510..1708659f48 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -89,6 +89,13 @@ instance IsString FormattedText where type MarkdownList = [FormattedText] +data ParsedMarkdown = ParsedMarkdown {formattedText :: Maybe MarkdownList} + deriving (Generic) + +instance ToJSON ParsedMarkdown where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + unmarked :: Text -> Markdown unmarked = Markdown Nothing @@ -183,6 +190,6 @@ markdownP = mconcat <$> A.many' fragmentP | isUri s = markdown Uri s | isEmail s = markdown Email s | otherwise = unmarked s - isUri s = "http://" `T.isPrefixOf` s || "https://" `T.isPrefixOf` s || "simplex:/" `T.isPrefixOf` s + isUri s = T.length s >= 10 && any (`T.isPrefixOf` s) ["http://", "https://", "simplex:/"] isEmail s = T.any (== '@') s && Email.isValid (encodeUtf8 s) noFormat = pure . unmarked diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 74d06f2835..c089666439 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -12,16 +12,21 @@ import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.List (find) +import Data.Maybe (fromMaybe) import Foreign.C.String +import Foreign.C.Types (CInt (..)) import Foreign.StablePtr import GHC.Generics (Generic) import Simplex.Chat import Simplex.Chat.Controller +import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList) import Simplex.Chat.Options import Simplex.Chat.Store import Simplex.Chat.Types +import Simplex.Chat.Util (safeDecodeUtf8) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations)) import Simplex.Messaging.Protocol (CorrId (..)) +import System.Timeout (timeout) foreign export ccall "chat_init" cChatInit :: CString -> IO (StablePtr ChatController) @@ -29,6 +34,10 @@ foreign export ccall "chat_send_cmd" cChatSendCmd :: StablePtr ChatController -> foreign export ccall "chat_recv_msg" cChatRecvMsg :: StablePtr ChatController -> IO CJSONString +foreign export ccall "chat_recv_msg_wait" cChatRecvMsgWait :: StablePtr ChatController -> CInt -> IO CJSONString + +foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO CJSONString + -- | initialize chat controller -- The active user has to be created and the chat has to be started before most commands can be used. cChatInit :: CString -> IO (StablePtr ChatController) @@ -45,6 +54,14 @@ cChatSendCmd cPtr cCmd = do cChatRecvMsg :: StablePtr ChatController -> IO CJSONString cChatRecvMsg cc = deRefStablePtr cc >>= chatRecvMsg >>= newCAString +-- | receive message from chat (blocking up to `t` microseconds (1/10^6 sec), returns empty string if times out) +cChatRecvMsgWait :: StablePtr ChatController -> CInt -> IO CJSONString +cChatRecvMsgWait cc t = deRefStablePtr cc >>= (`chatRecvMsgWait` fromIntegral t) >>= newCAString + +-- | parse markdown - returns ParsedMarkdown type JSON +cChatParseMarkdown :: CString -> IO CJSONString +cChatParseMarkdown s = newCAString . chatParseMarkdown =<< peekCAString s + mobileChatOpts :: ChatOpts mobileChatOpts = ChatOpts @@ -85,6 +102,12 @@ chatRecvMsg ChatController {outputQ} = json <$> atomically (readTBQueue outputQ) where json (corr, resp) = LB.unpack $ J.encode APIResponse {corr, resp} +chatRecvMsgWait :: ChatController -> Int -> IO JSONString +chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc) + +chatParseMarkdown :: String -> JSONString +chatParseMarkdown = LB.unpack . J.encode . ParsedMarkdown . parseMaybeMarkdownList . safeDecodeUtf8 . B.pack + data APIResponse = APIResponse {corr :: Maybe CorrId, resp :: ChatResponse} deriving (Generic) diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 9e73060f9e..3996365210 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -43,6 +43,34 @@ chatStarted = "{\"resp\":{\"chatStarted\":{}}}" chatStarted = "{\"resp\":{\"type\":\"chatStarted\"}}" #endif +contactSubSummary :: String +#if defined(darwin_HOST_OS) && defined(swiftJSON) +contactSubSummary = "{\"resp\":{\"contactSubSummary\":{\"contactSubscriptions\":[]}}}" +#else +contactSubSummary = "{\"resp\":{\"type\":\"contactSubSummary\",\"contactSubscriptions\":[]}}" +#endif + +memberSubErrors :: String +#if defined(darwin_HOST_OS) && defined(swiftJSON) +memberSubErrors = "{\"resp\":{\"memberSubErrors\":{\"memberSubErrors\":[]}}}" +#else +memberSubErrors = "{\"resp\":{\"type\":\"memberSubErrors\",\"memberSubErrors\":[]}}" +#endif + +pendingSubSummary :: String +#if defined(darwin_HOST_OS) && defined(swiftJSON) +pendingSubSummary = "{\"resp\":{\"pendingSubSummary\":{\"pendingSubStatus\":[]}}}" +#else +pendingSubSummary = "{\"resp\":{\"type\":\"pendingSubSummary\",\"pendingSubStatus\":[]}}" +#endif + +parsedMarkdown :: String +#if defined(darwin_HOST_OS) && defined(swiftJSON) +parsedMarkdown = "{\"formattedText\":[{\"format\":{\"bold\":{}},\"text\":\"hello\"}]}" +#else +parsedMarkdown = "{\"formattedText\":[{\"format\":{\"type\":\"bold\"},\"text\":\"hello\"}]}" +#endif + testChatApiNoUser :: IO () testChatApiNoUser = withTmpFiles $ do cc <- chatInit testDBPrefix @@ -60,3 +88,9 @@ testChatApi = withTmpFiles $ do chatSendCmd cc "/u" `shouldReturn` activeUser chatSendCmd cc "/u alice Alice" `shouldReturn` activeUserExists chatSendCmd cc "/_start" `shouldReturn` chatStarted + chatRecvMsg cc `shouldReturn` contactSubSummary + chatRecvMsg cc `shouldReturn` memberSubErrors + chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary + chatRecvMsgWait cc 10000 `shouldReturn` "" + chatParseMarkdown "hello" `shouldBe` "{}" + chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown From 1f1ed3f3dd319a759fd3a006cf247621476f2f67 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 12 Jun 2022 11:28:30 +0100 Subject: [PATCH 11/89] core: remove dependency on zip algorithms bzip2/zstd (#737) * core: remove dependency on zip algorithms bzip2/zstd * enable test log * disable test log --- cabal.project | 2 ++ package.yaml | 10 ---------- simplex-chat.cabal | 10 ---------- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/cabal.project b/cabal.project index b7a510848c..5b76a94689 100644 --- a/cabal.project +++ b/cabal.project @@ -1,5 +1,7 @@ packages: . +constraints: zip +disable-bzip2 +disable-zstd + source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git diff --git a/package.yaml b/package.yaml index f1de3ff48b..b4a4d3bb83 100644 --- a/package.yaml +++ b/package.yaml @@ -41,16 +41,6 @@ dependencies: - unliftio-core == 0.2.* - zip == 1.7.* -flags: - disable-bzip2: - description: removes dependency on bzip2 C library (zip package) - manual: True - default: True - disable-zstd: - description: Removes dependency on zstd C library (zip package) - manual: True - default: True - library: source-dirs: src diff --git a/simplex-chat.cabal b/simplex-chat.cabal index eb91672b1a..61b0e1f5ae 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -17,16 +17,6 @@ build-type: Simple extra-source-files: README.md -flag disable-bzip2 - description: removes dependency on bzip2 C library (zip package) - manual: True - default: True - -flag disable-zstd - description: Removes dependency on zstd C library (zip package) - manual: True - default: True - library exposed-modules: Simplex.Chat From c62d99ab97d0fc3a6484cb60b520871a1d7a2e71 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 16 Jun 2022 20:00:51 +0100 Subject: [PATCH 12/89] core: remove connection pool (#738) * core: remove connection pool * remove local ref from cabal.project * update simplexmq * log test * fix test --- .gitignore | 1 + cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 2 -- src/Simplex/Chat/Controller.hs | 1 - src/Simplex/Chat/Core.hs | 4 ++-- src/Simplex/Chat/Mobile.hs | 2 +- src/Simplex/Chat/Store.hs | 4 ++-- stack.yaml | 2 +- tests/ChatClient.hs | 5 +++-- tests/ChatTests.hs | 1 + tests/MobileTests.hs | 2 +- tests/SchemaDump.hs | 2 +- 13 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index e645225e93..42477be207 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ stack.yaml.lock # Temporary test files tests/tmp +logs/ diff --git a/cabal.project b/cabal.project index 5b76a94689..b3004cca11 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: c1348aa54fba292d34339d6b111572cb1c74b546 + tag: 5f94240ed420c42c725810b72b7f512ab320a98f source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b8da2b3028..25aee86303 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."c1348aa54fba292d34339d6b111572cb1c74b546" = "103hw1h1agy42krf11d98bv3c1w0q0wi2z7r2ll0gmp5xv1r4rf0"; + "https://github.com/simplex-chat/simplexmq.git"."5f94240ed420c42c725810b72b7f512ab320a98f" = "194zdljlm85w97zql35ikswwpv0r5j6651bn6xqa6z7wmb5dvsxp"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e6a9276e17..232e600575 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -82,10 +82,8 @@ defaultChatConfig = defaultAgentConfig { tcpPort = undefined, -- agent does not listen to TCP dbFile = "simplex_v1", - dbPoolSize = 1, yesToMigrations = False }, - dbPoolSize = 1, yesToMigrations = False, defaultServers = InitialAgentServers {smp = _defaultSMPServers, ntf = _defaultNtfServers}, tbqSize = 64, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 321ff0e7da..dd58d5e71a 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -56,7 +56,6 @@ updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-c data ChatConfig = ChatConfig { agentConfig :: AgentConfig, - dbPoolSize :: Int, yesToMigrations :: Bool, defaultServers :: InitialAgentServers, tbqSize :: Natural, diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 6290e7f02f..07bcff63e5 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -15,7 +15,7 @@ import Simplex.Chat.Types import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {dbPoolSize, yesToMigrations} opts sendToast chat +simplexChatCore cfg@ChatConfig {yesToMigrations} opts sendToast chat | logAgent opts = do setLogLevel LogInfo -- LogError withGlobalLogging logCfg initRun @@ -23,7 +23,7 @@ simplexChatCore cfg@ChatConfig {dbPoolSize, yesToMigrations} opts sendToast chat where initRun = do let f = chatStoreFile $ dbFilePrefix opts - st <- createStore f dbPoolSize yesToMigrations + st <- createStore f yesToMigrations u <- getCreateActiveUser st cc <- newChatController st (Just u) cfg opts sendToast runSimplexChat opts u cc chat diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index c089666439..c8f05c4bec 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -90,7 +90,7 @@ getActiveUser_ st = find activeUser <$> getUsers st chatInit :: String -> IO ChatController chatInit dbFilePrefix = do let f = chatStoreFile dbFilePrefix - chatStore <- createStore f (dbPoolSize defaultMobileConfig) (yesToMigrations (defaultMobileConfig :: ChatConfig)) + chatStore <- createStore f (yesToMigrations (defaultMobileConfig :: ChatConfig)) user_ <- getActiveUser_ chatStore newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix} Nothing diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index f6b4c7a1ec..14a4ab61b1 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -237,8 +237,8 @@ migrations = sortBy (compare `on` name) $ map migration schemaMigrations where migration (name, query) = Migration {name = name, up = fromQuery query} -createStore :: FilePath -> Int -> Bool -> IO SQLiteStore -createStore dbFilePath poolSize = createSQLiteStore dbFilePath poolSize migrations +createStore :: FilePath -> Bool -> IO SQLiteStore +createStore dbFilePath = createSQLiteStore dbFilePath migrations chatStoreFile :: FilePath -> FilePath chatStoreFile = (<> "_chat.db") diff --git a/stack.yaml b/stack.yaml index 009891d42d..d51339fd89 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: c1348aa54fba292d34339d6b111572cb1c74b546 + commit: 5f94240ed420c42c725810b72b7f512ab320a98f # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index d5a44b4486..fc98aff18a 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -99,14 +99,14 @@ testCfgV1 = testCfg {agentConfig = testAgentCfgV1} createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC createTestChat cfg opts dbPrefix profile = do let dbFilePrefix = testDBPrefix <> dbPrefix - st <- createStore (dbFilePrefix <> "_chat.db") 1 False + st <- createStore (dbFilePrefix <> "_chat.db") False Right user <- runExceptT $ createUser st profile True startTestChat_ st cfg opts dbFilePrefix user startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC startTestChat cfg opts dbPrefix = do let dbFilePrefix = testDBPrefix <> dbPrefix - st <- createStore (dbFilePrefix <> "_chat.db") 1 False + st <- createStore (dbFilePrefix <> "_chat.db") False Just user <- find activeUser <$> getUsers st startTestChat_ st cfg opts dbFilePrefix user @@ -262,6 +262,7 @@ serverCfg = queueIdBytes = 12, msgIdBytes = 6, storeLogFile = Nothing, + storeMsgsFile = Nothing, allowNewQueues = True, messageExpiration = Just defaultMessageExpiration, inactiveClientExpiration = Just defaultInactiveClientExpiration, diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 124f866e99..6a01223d12 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -2171,6 +2171,7 @@ testMaintenanceModeWithFiles = withTmpFiles $ do alice <## "completed receiving file 1 (test.jpg) from bob" src <- B.readFile "./tests/fixtures/test.jpg" B.readFile "./tests/tmp/alice_files/test.jpg" `shouldReturn` src + threadDelay 500000 alice ##> "/_stop" alice <## "chat stopped" alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 3996365210..8573d00496 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -82,7 +82,7 @@ testChatApiNoUser = withTmpFiles $ do testChatApi :: IO () testChatApi = withTmpFiles $ do let f = chatStoreFile $ testDBPrefix <> "1" - st <- createStore f 1 True + st <- createStore f True Right _ <- runExceptT $ createUser st aliceProfile True cc <- chatInit $ testDBPrefix <> "1" chatSendCmd cc "/u" `shouldReturn` activeUser diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 03ef2a47d9..a876b10afc 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -21,7 +21,7 @@ schemaDumpTest = testVerifySchemaDump :: IO () testVerifySchemaDump = withTmpFiles $ do - void $ createStore testDB 1 False + void $ createStore testDB False void $ readCreateProcess (shell $ "touch " <> schema) "" savedSchema <- readFile schema savedSchema `seq` pure () From 2ae621792e84520ea187e45e44b6a4aaa815f668 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 16 Jun 2022 20:04:21 +0100 Subject: [PATCH 13/89] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index b3004cca11..2b006bd936 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 5f94240ed420c42c725810b72b7f512ab320a98f + tag: b58523d4a29235c29c56a461edb686ed93bc1e89 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 25aee86303..4002a3b2fd 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."5f94240ed420c42c725810b72b7f512ab320a98f" = "194zdljlm85w97zql35ikswwpv0r5j6651bn6xqa6z7wmb5dvsxp"; + "https://github.com/simplex-chat/simplexmq.git"."b58523d4a29235c29c56a461edb686ed93bc1e89" = "0k8kmk5dil8dny4xzw6k0c97g0m9ql105x870bvy4sdd2ix23var"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index d51339fd89..de3765dba7 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 5f94240ed420c42c725810b72b7f512ab320a98f + commit: b58523d4a29235c29c56a461edb686ed93bc1e89 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From b2a523c3fe40e92e838175d9226605d7667eb999 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 16 Jun 2022 20:55:45 +0100 Subject: [PATCH 14/89] core: fix dependencies, update nix (#739) * core: fix dependencies, update nix * update --- flake.nix | 5 +++++ stack.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 6e13c24a64..5001484ab8 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,11 @@ inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; #angerman/nixpkgs/patch-1"; # based on 21.11, still need this, until everything is merged into 21.11. inputs.haskellNix.url = "github:input-output-hk/haskell.nix?ref=angerman/try-no-libcharset"; inputs.haskellNix.inputs.nixpkgs.follows = "nixpkgs"; + inputs.hackage = { + url = "github:input-output-hk/hackage.nix"; + flake = false; + }; + inputs.haskellNix.inputs.hackage.follows = "hackage"; inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, haskellNix, nixpkgs, flake-utils }: let systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; in diff --git a/stack.yaml b/stack.yaml index de3765dba7..fec37b2e53 100644 --- a/stack.yaml +++ b/stack.yaml @@ -38,7 +38,7 @@ extra-deps: - cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881 - network-3.1.2.7@sha256:e3d78b13db9512aeb106e44a334ab42b7aa48d26c097299084084cb8be5c5568,4888 - simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079 - - tls-1.5.7@sha256:1cc30253a9696b65a9cafc0317fbf09f7dcea15e3a145ed6c9c0e28c632fa23a,6991 + - tls-1.6.0@sha256:7ae39373fd2de27fb80e90f76d22aeeb9a074a0ddd120cbd02c9c52f516a9e55,6987 # below hackage dependencies are to update Aeson to 2.0.3 - OneTuple-0.3.1@sha256:a848c096c9d29e82ffdd30a9998aa2931cbccb3a1bc137539d80f6174d31603e,2262 - attoparsec-0.14.4@sha256:79584bdada8b730cb5138fca8c35c76fbef75fc1d1e01e6b1d815a5ee9843191,5810 From 4bf5125c5167be175a175293d855c31aed56a743 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 18 Jun 2022 20:06:13 +0100 Subject: [PATCH 15/89] core: support combining store functions in one transaction (#740) * refactor store functions (WIP - does not compile yet) * update chat * update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 499 +++--- src/Simplex/Chat/Mobile.hs | 2 +- src/Simplex/Chat/Store.hs | 3113 +++++++++++++++++------------------- stack.yaml | 2 +- tests/ChatClient.hs | 4 +- tests/MobileTests.hs | 2 +- 8 files changed, 1712 insertions(+), 1914 deletions(-) diff --git a/cabal.project b/cabal.project index 2b006bd936..c13077743a 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: b58523d4a29235c29c56a461edb686ed93bc1e89 + tag: d1db7d6f79b527a689af1807e7d91a92076d2d8c source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 4002a3b2fd..5388d80e6a 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."b58523d4a29235c29c56a461edb686ed93bc1e89" = "0k8kmk5dil8dny4xzw6k0c97g0m9ql105x870bvy4sdd2ix23var"; + "https://github.com/simplex-chat/simplexmq.git"."d1db7d6f79b527a689af1807e7d91a92076d2d8c" = "1zlp925qq8akv8v846g03czn0yb293vdsgd7vs6g0axs4yx6c9ap"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 232e600575..4a6213f904 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -41,6 +41,7 @@ import qualified Data.Text as T import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds) import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime) import Data.Word (Word32) +import qualified Database.SQLite.Simple as DB import Simplex.Chat.Archive import Simplex.Chat.Call import Simplex.Chat.Controller @@ -63,7 +64,7 @@ import Simplex.Messaging.Parsers (base64P, parseAll) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..)) import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Util (ifM, tryError, unlessM, whenM, (<$?>)) +import Simplex.Messaging.Util (ifM, liftEitherError, tryError, unlessM, whenM, (<$?>)) import System.Exit (exitFailure, exitSuccess) import System.FilePath (combine, splitExtensions, takeFileName) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout) @@ -141,7 +142,7 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de Just smpServers' -> pure ss {smp = smpServers'} _ -> case user of Just usr -> do - userSmpServers <- getSMPServers chatStore usr + userSmpServers <- withTransaction chatStore (`getSMPServers` usr) pure ss {smp = fromMaybe defaultSMPServers $ nonEmpty userSmpServers} _ -> pure ss @@ -189,7 +190,7 @@ processChatCommand = \case CreateActiveUser p -> do u <- asks currentUser whenM (isJust <$> readTVarIO u) $ throwChatError CEActiveUserExists - user <- withStore $ \st -> createUser st p True + user <- withStore $ \db -> createUser db p True atomically . writeTVar u $ Just user pure $ CRActiveUser user StartChat -> withUser' $ \user -> @@ -212,16 +213,16 @@ processChatCommand = \case APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk APIImportArchive cfg -> checkChatStopped $ importArchive cfg >> setStoreChanged $> CRCmdOk APIDeleteStorage -> checkChatStopped $ deleteStorage >> setStoreChanged $> CRCmdOk - APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore $ \st -> getChatPreviews st user withPCC) + APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore' $ \db -> getChatPreviews db user withPCC) APIGetChat (ChatRef cType cId) pagination -> withUser $ \user -> case cType of - CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\st -> getDirectChat st user cId pagination) - CTGroup -> CRApiChat . AChat SCTGroup <$> withStore (\st -> getGroupChat st user cId pagination) + CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\db -> getDirectChat db user cId pagination) + CTGroup -> CRApiChat . AChat SCTGroup <$> withStore (\db -> getGroupChat db user cId pagination) CTContactRequest -> pure $ chatCmdError "not implemented" CTContactConnection -> pure $ chatCmdError "not supported" APIGetChatItems _pagination -> pure $ chatCmdError "not implemented" APISendMessage (ChatRef cType chatId) (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock $ case cType of CTDirect -> do - ct@Contact {localDisplayName = c} <- withStore $ \st -> getContact st userId chatId + ct@Contact {localDisplayName = c} <- withStore $ \db -> getContact db userId chatId (fileInvitation_, ciFile_) <- unzipMaybe <$> setupSndFileTransfer ct (msgContainer, quotedItem_) <- prepareMsg fileInvitation_ msg <- sendDirectContactMessage ct (XMsgNew msgContainer) @@ -237,7 +238,7 @@ processChatCommand = \case (agentConnId, fileConnReq) <- withAgent (`createConnection` SCMInvitation) let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Just fileConnReq} - fileId <- withStore $ \st -> createSndFileTransfer st userId ct file fileInvitation agentConnId chSize + fileId <- withStore' $ \db -> createSndFileTransfer db userId ct file fileInvitation agentConnId chSize let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} pure $ Just (fileInvitation, ciFile) prepareMsg :: Maybe FileInvitation -> m (MsgContainer, Maybe (CIQuote 'CTDirect)) @@ -245,7 +246,7 @@ processChatCommand = \case Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_), Nothing) Just quotedItemId -> do CChatItem _ ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, content = ciContent, formattedText, file} <- - withStore $ \st -> getDirectChatItem st userId chatId quotedItemId + withStore $ \db -> getDirectChatItem db userId chatId quotedItemId (origQmc, qd, sent) <- quoteData ciContent let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} qmc = quoteContent origQmc file @@ -257,7 +258,7 @@ processChatCommand = \case quoteData (CIRcvMsgContent qmc) = pure (qmc, CIQDirectRcv, False) quoteData _ = throwChatError CEInvalidQuote CTGroup -> do - Group gInfo@GroupInfo {membership, localDisplayName = gName} ms <- withStore $ \st -> getGroup st user chatId + Group gInfo@GroupInfo {membership, localDisplayName = gName} ms <- withStore $ \db -> getGroup db user chatId unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved (fileInvitation_, ciFile_) <- unzipMaybe <$> setupSndFileTransfer gInfo (msgContainer, quotedItem_) <- prepareMsg fileInvitation_ membership @@ -273,7 +274,7 @@ processChatCommand = \case (fileSize, chSize) <- checkSndFile file let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Nothing} - fileId <- withStore $ \st -> createSndGroupFileTransfer st userId gInfo file fileInvitation chSize + fileId <- withStore' $ \db -> createSndGroupFileTransfer db userId gInfo file fileInvitation chSize let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} pure $ Just (fileInvitation, ciFile) prepareMsg :: Maybe FileInvitation -> GroupMember -> m (MsgContainer, Maybe (CIQuote 'CTGroup)) @@ -281,7 +282,7 @@ processChatCommand = \case Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_), Nothing) Just quotedItemId -> do CChatItem _ ChatItem {chatDir, meta = CIMeta {itemTs, itemSharedMsgId}, content = ciContent, formattedText, file} <- - withStore $ \st -> getGroupChatItem st user chatId quotedItemId + withStore $ \db -> getGroupChatItem db user chatId quotedItemId (origQmc, qd, sent, GroupMember {memberId}) <- quoteData ciContent chatDir membership let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId} qmc = quoteContent origQmc file @@ -318,27 +319,27 @@ processChatCommand = \case unzipMaybe t = (fst <$> t, snd <$> t) APIUpdateChatItem (ChatRef cType chatId) itemId mc -> withUser $ \user@User {userId} -> withChatLock $ case cType of CTDirect -> do - (ct@Contact {contactId, localDisplayName = c}, ci) <- withStore $ \st -> (,) <$> getContact st userId chatId <*> getDirectChatItem st userId chatId itemId + (ct@Contact {contactId, localDisplayName = c}, ci) <- withStore $ \db -> (,) <$> getContact db userId chatId <*> getDirectChatItem db userId chatId itemId case ci of CChatItem SMDSnd ChatItem {meta = CIMeta {itemSharedMsgId}, content = ciContent} -> do case (ciContent, itemSharedMsgId) of (CISndMsgContent _, Just itemSharedMId) -> do SndMessage {msgId} <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc) - updCi <- withStore $ \st -> updateDirectChatItem st userId contactId itemId (CISndMsgContent mc) $ Just msgId + updCi <- withStore $ \db -> updateDirectChatItem db userId contactId itemId (CISndMsgContent mc) $ Just msgId setActive $ ActiveC c pure . CRChatItemUpdated $ AChatItem SCTDirect SMDSnd (DirectChat ct) updCi _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTGroup -> do - Group gInfo@GroupInfo {groupId, localDisplayName = gName, membership} ms <- withStore $ \st -> getGroup st user chatId + Group gInfo@GroupInfo {groupId, localDisplayName = gName, membership} ms <- withStore $ \db -> getGroup db user chatId unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved - ci <- withStore $ \st -> getGroupChatItem st user chatId itemId + ci <- withStore $ \db -> getGroupChatItem db user chatId itemId case ci of CChatItem SMDSnd ChatItem {meta = CIMeta {itemSharedMsgId}, content = ciContent} -> do case (ciContent, itemSharedMsgId) of (CISndMsgContent _, Just itemSharedMId) -> do SndMessage {msgId} <- sendGroupMessage gInfo ms (XMsgUpdate itemSharedMId mc) - updCi <- withStore $ \st -> updateGroupChatItem st user groupId itemId (CISndMsgContent mc) msgId + updCi <- withStore $ \db -> updateGroupChatItem db user groupId itemId (CISndMsgContent mc) msgId setActive $ ActiveG gName pure . CRChatItemUpdated $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) updCi _ -> throwChatError CEInvalidChatItemUpdate @@ -347,33 +348,33 @@ processChatCommand = \case CTContactConnection -> pure $ chatCmdError "not supported" APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user@User {userId} -> withChatLock $ case cType of CTDirect -> do - (ct@Contact {localDisplayName = c}, CChatItem msgDir deletedItem@ChatItem {meta = CIMeta {itemSharedMsgId}, file}) <- withStore $ \st -> (,) <$> getContact st userId chatId <*> getDirectChatItem st userId chatId itemId + (ct@Contact {localDisplayName = c}, CChatItem msgDir deletedItem@ChatItem {meta = CIMeta {itemSharedMsgId}, file}) <- withStore $ \db -> (,) <$> getContact db userId chatId <*> getDirectChatItem db userId chatId itemId case (mode, msgDir, itemSharedMsgId) of (CIDMInternal, _, _) -> do deleteCIFile user file - toCi <- withStore $ \st -> deleteDirectChatItemLocal st userId ct itemId CIDMInternal + toCi <- withStore $ \db -> deleteDirectChatItemLocal db userId ct itemId CIDMInternal pure $ CRChatItemDeleted (AChatItem SCTDirect msgDir (DirectChat ct) deletedItem) toCi (CIDMBroadcast, SMDSnd, Just itemSharedMId) -> do void $ sendDirectContactMessage ct (XMsgDel itemSharedMId) deleteCIFile user file - toCi <- withStore $ \st -> deleteDirectChatItemLocal st userId ct itemId CIDMBroadcast + toCi <- withStore $ \db -> deleteDirectChatItemLocal db userId ct itemId CIDMBroadcast setActive $ ActiveC c pure $ CRChatItemDeleted (AChatItem SCTDirect msgDir (DirectChat ct) deletedItem) toCi (CIDMBroadcast, _, _) -> throwChatError CEInvalidChatItemDelete -- TODO for group integrity and pending messages, group items and messages are set to "deleted"; maybe a different workaround is needed CTGroup -> do - Group gInfo@GroupInfo {localDisplayName = gName, membership} ms <- withStore $ \st -> getGroup st user chatId + Group gInfo@GroupInfo {localDisplayName = gName, membership} ms <- withStore $ \db -> getGroup db user chatId unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved - CChatItem msgDir deletedItem@ChatItem {meta = CIMeta {itemSharedMsgId}, file} <- withStore $ \st -> getGroupChatItem st user chatId itemId + CChatItem msgDir deletedItem@ChatItem {meta = CIMeta {itemSharedMsgId}, file} <- withStore $ \db -> getGroupChatItem db user chatId itemId case (mode, msgDir, itemSharedMsgId) of (CIDMInternal, _, _) -> do deleteCIFile user file - toCi <- withStore $ \st -> deleteGroupChatItemInternal st user gInfo itemId + toCi <- withStore $ \db -> deleteGroupChatItemInternal db user gInfo itemId pure $ CRChatItemDeleted (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) toCi (CIDMBroadcast, SMDSnd, Just itemSharedMId) -> do SndMessage {msgId} <- sendGroupMessage gInfo ms (XMsgDel itemSharedMId) deleteCIFile user file - toCi <- withStore $ \st -> deleteGroupChatItemSndBroadcast st user gInfo itemId msgId + toCi <- withStore $ \db -> deleteGroupChatItemSndBroadcast db user gInfo itemId msgId setActive $ ActiveG gName pure $ CRChatItemDeleted (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) toCi (CIDMBroadcast, _, _) -> throwChatError CEInvalidChatItemDelete @@ -387,81 +388,84 @@ processChatCommand = \case cancelFile user fileInfo withFilesFolder $ \filesFolder -> deleteFile filesFolder fileInfo APIChatRead (ChatRef cType chatId) fromToIds -> withChatLock $ case cType of - CTDirect -> withStore (\st -> updateDirectChatItemsRead st chatId fromToIds) $> CRCmdOk - CTGroup -> withStore (\st -> updateGroupChatItemsRead st chatId fromToIds) $> CRCmdOk + CTDirect -> withStore' (\db -> updateDirectChatItemsRead db chatId fromToIds) $> CRCmdOk + CTGroup -> withStore' (\db -> updateGroupChatItemsRead db chatId fromToIds) $> CRCmdOk CTContactRequest -> pure $ chatCmdError "not supported" CTContactConnection -> pure $ chatCmdError "not supported" APIDeleteChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of CTDirect -> do - ct@Contact {localDisplayName} <- withStore $ \st -> getContact st userId chatId - withStore (\st -> getContactGroupNames st userId ct) >>= \case + ct@Contact {localDisplayName} <- withStore $ \db -> getContact db userId chatId + withStore' (\db -> getContactGroupNames db userId ct) >>= \case [] -> do - filesInfo <- withStore $ \st -> getContactFileInfo st userId ct - conns <- withStore $ \st -> getContactConnections st userId ct + filesInfo <- withStore' $ \db -> getContactFileInfo db userId ct + conns <- withStore $ \db -> getContactConnections db userId ct withChatLock . procCmd $ do forM_ filesInfo $ \fileInfo -> do cancelFile user fileInfo withFilesFolder $ \filesFolder -> deleteFile filesFolder fileInfo withAgent $ \a -> forM_ conns $ \conn -> deleteConnection a (aConnId conn) `catchError` \(_ :: AgentErrorType) -> pure () - withStore $ \st -> deleteContact st userId ct + -- two functions below are called in separate transactions to prevent crashes on android + -- (possibly, race condition on integrity check?) + withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct + withStore' $ \db -> deleteContact db userId ct unsetActive $ ActiveC localDisplayName pure $ CRContactDeleted ct gs -> throwChatError $ CEContactGroups ct gs CTContactConnection -> withChatLock . procCmd $ do - conn <- withStore $ \st -> getPendingContactConnection st userId chatId + conn <- withStore $ \db -> getPendingContactConnection db userId chatId withAgent $ \a -> deleteConnection a $ aConnId' conn - withStore $ \st -> deletePendingContactConnection st userId chatId + withStore' $ \db -> deletePendingContactConnection db userId chatId pure $ CRContactConnectionDeleted conn CTGroup -> pure $ chatCmdError "not implemented" CTContactRequest -> pure $ chatCmdError "not supported" APIClearChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of CTDirect -> do - ct <- withStore $ \st -> getContact st userId chatId - ciIdsAndFileInfo <- withStore $ \st -> getContactChatItemIdsAndFileInfo st user chatId + ct <- withStore $ \db -> getContact db userId chatId + ciIdsAndFileInfo <- withStore' $ \db -> getContactChatItemIdsAndFileInfo db user chatId forM_ ciIdsAndFileInfo $ \(itemId, _, fileInfo_) -> do forM_ fileInfo_ $ \fileInfo -> do cancelFile user fileInfo withFilesFolder $ \filesFolder -> deleteFile filesFolder fileInfo - void $ withStore $ \st -> deleteDirectChatItemLocal st userId ct itemId CIDMInternal + void $ withStore $ \db -> deleteDirectChatItemLocal db userId ct itemId CIDMInternal ct' <- case ciIdsAndFileInfo of [] -> pure ct _ -> do let (_, lastItemTs, _) = last ciIdsAndFileInfo - withStore (\st -> updateContactTs st user ct lastItemTs) + withStore' $ \db -> updateContactTs db user ct lastItemTs pure (ct :: Contact) {updatedAt = lastItemTs} pure $ CRChatCleared (AChatInfo SCTDirect (DirectChat ct')) CTGroup -> do - gInfo <- withStore $ \st -> getGroupInfo st user chatId - ciIdsAndFileInfo <- withStore $ \st -> getGroupChatItemIdsAndFileInfo st user chatId + gInfo <- withStore $ \db -> getGroupInfo db user chatId + ciIdsAndFileInfo <- withStore' $ \db -> getGroupChatItemIdsAndFileInfo db user chatId forM_ ciIdsAndFileInfo $ \(itemId, _, itemDeleted, fileInfo_) -> unless itemDeleted $ do forM_ fileInfo_ $ \fileInfo -> do cancelFile user fileInfo withFilesFolder $ \filesFolder -> deleteFile filesFolder fileInfo - void $ withStore $ \st -> deleteGroupChatItemInternal st user gInfo itemId + void $ withStore $ \db -> deleteGroupChatItemInternal db user gInfo itemId gInfo' <- case ciIdsAndFileInfo of [] -> pure gInfo _ -> do let (_, lastItemTs, _, _) = last ciIdsAndFileInfo - withStore (\st -> updateGroupTs st user gInfo lastItemTs) + withStore' $ \db -> updateGroupTs db user gInfo lastItemTs pure (gInfo :: GroupInfo) {updatedAt = lastItemTs} pure $ CRChatCleared (AChatInfo SCTGroup (GroupChat gInfo')) CTContactConnection -> pure $ chatCmdError "not supported" CTContactRequest -> pure $ chatCmdError "not supported" APIAcceptContact connReqId -> withUser $ \user@User {userId} -> withChatLock $ do - cReq <- withStore $ \st -> getContactRequest st userId connReqId + cReq <- withStore $ \db -> getContactRequest db userId connReqId procCmd $ CRAcceptingContactRequest <$> acceptContactRequest user cReq APIRejectContact connReqId -> withUser $ \User {userId} -> withChatLock $ do cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- - withStore $ \st -> - getContactRequest st userId connReqId - `E.finally` deleteContactRequest st userId connReqId + withStore $ \db -> + getContactRequest db userId connReqId + `E.finally` liftIO (deleteContactRequest db userId connReqId) withAgent $ \a -> rejectContact a connId invId pure $ CRContactRequestRejected cReq APISendCallInvitation contactId callType -> withUser $ \user@User {userId} -> do -- party initiating call - ct <- withStore $ \st -> getContact st userId contactId + ct <- withStore $ \db -> getContact db userId contactId calls <- asks currentCalls withChatLock $ do callId <- CallId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16)) @@ -476,14 +480,14 @@ processChatCommand = \case toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci pure CRCmdOk SendCallInvitation cName callType -> withUser $ \User {userId} -> do - contactId <- withStore $ \st -> getContactIdByName st userId cName + contactId <- withStore $ \db -> getContactIdByName db userId cName processChatCommand $ APISendCallInvitation contactId callType APIRejectCall contactId -> -- party accepting call withCurrentCall contactId $ \userId ct Call {chatItemId, callState} -> case callState of CallInvitationReceived {} -> do let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0 - withStore $ \st -> updateDirectChatItemsRead st contactId $ Just (chatItemId, chatItemId) + withStore' $ \db -> updateDirectChatItemsRead db contactId $ Just (chatItemId, chatItemId) updateDirectChatItemView userId ct chatItemId aciContent Nothing $> Nothing _ -> throwChatError . CECallState $ callStateTag callState APISendCallOffer contactId WebRTCCallOffer {callType, rtcSession} -> @@ -495,7 +499,7 @@ processChatCommand = \case callState' = CallOfferSent {localCallType = callType, peerCallType, localCallSession = rtcSession, sharedKey} aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0 SndMessage {msgId} <- sendDirectContactMessage ct (XCallOffer callId offer) - withStore $ \st -> updateDirectChatItemsRead st contactId $ Just (chatItemId, chatItemId) + withStore' $ \db -> updateDirectChatItemsRead db contactId $ Just (chatItemId, chatItemId) updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId pure $ Just call {callState = callState'} _ -> throwChatError . CECallState $ callStateTag callState @@ -538,9 +542,9 @@ processChatCommand = \case APIVerifyToken token code nonce -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token code nonce) $> CRCmdOk APIIntervalNofication token interval -> withUser $ \_ -> withAgent (\a -> enableNtfCron a token interval) $> CRCmdOk APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) $> CRCmdOk - GetUserSMPServers -> CRUserSMPServers <$> withUser (\user -> withStore (`getSMPServers` user)) + GetUserSMPServers -> CRUserSMPServers <$> withUser (\user -> withStore' (`getSMPServers` user)) SetUserSMPServers smpServers -> withUser $ \user -> withChatLock $ do - withStore $ \st -> overwriteSMPServers st user smpServers + withStore $ \db -> overwriteSMPServers db user smpServers ChatConfig {defaultServers = InitialAgentServers {smp = defaultSMPServers}} <- asks config withAgent $ \a -> setSMPServers a (fromMaybe defaultSMPServers (nonEmpty smpServers)) pure CRCmdOk @@ -548,12 +552,12 @@ processChatCommand = \case Welcome -> withUser $ pure . CRWelcome AddContact -> withUser $ \User {userId} -> withChatLock . procCmd $ do (connId, cReq) <- withAgent (`createConnection` SCMInvitation) - conn <- withStore $ \st -> createDirectConnection st userId connId ConnNew + conn <- withStore' $ \db -> createDirectConnection db userId connId ConnNew toView $ CRNewContactConnection conn pure $ CRInvitation cReq Connect (Just (ACR SCMInvitation cReq)) -> withUser $ \User {userId, profile} -> withChatLock . procCmd $ do connId <- withAgent $ \a -> joinConnection a cReq . directMessage $ XInfo profile - conn <- withStore $ \st -> createDirectConnection st userId connId ConnJoined + conn <- withStore' $ \db -> createDirectConnection db userId connId ConnJoined toView $ CRNewContactConnection conn pure CRSentConfirmation Connect (Just (ACR SCMContact cReq)) -> withUser $ \User {userId, profile} -> @@ -562,39 +566,39 @@ processChatCommand = \case ConnectSimplex -> withUser $ \User {userId, profile} -> connectViaContact userId adminContactReq profile DeleteContact cName -> withUser $ \User {userId} -> do - contactId <- withStore $ \st -> getContactIdByName st userId cName + contactId <- withStore $ \db -> getContactIdByName db userId cName processChatCommand $ APIDeleteChat (ChatRef CTDirect contactId) ClearContact cName -> withUser $ \User {userId} -> do - contactId <- withStore $ \st -> getContactIdByName st userId cName + contactId <- withStore $ \db -> getContactIdByName db userId cName processChatCommand $ APIClearChat (ChatRef CTDirect contactId) - ListContacts -> withUser $ \user -> CRContactsList <$> withStore (`getUserContacts` user) + ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user) CreateMyAddress -> withUser $ \User {userId} -> withChatLock . procCmd $ do (connId, cReq) <- withAgent (`createConnection` SCMContact) - withStore $ \st -> createUserContactLink st userId connId cReq + withStore $ \db -> createUserContactLink db userId connId cReq pure $ CRUserContactLinkCreated cReq DeleteMyAddress -> withUser $ \User {userId} -> withChatLock $ do - conns <- withStore $ \st -> getUserContactLinkConnections st userId + conns <- withStore (`getUserContactLinkConnections` userId) procCmd $ do withAgent $ \a -> forM_ conns $ \conn -> deleteConnection a (aConnId conn) `catchError` \(_ :: AgentErrorType) -> pure () - withStore $ \st -> deleteUserContactLink st userId + withStore' (`deleteUserContactLink` userId) pure CRUserContactLinkDeleted ShowMyAddress -> withUser $ \User {userId} -> uncurry CRUserContactLink <$> withStore (`getUserContactLink` userId) AddressAutoAccept onOff -> withUser $ \User {userId} -> do - uncurry CRUserContactLinkUpdated <$> withStore (\st -> updateUserContactLinkAutoAccept st userId onOff) + uncurry CRUserContactLinkUpdated <$> withStore (\db -> updateUserContactLinkAutoAccept db userId onOff) AcceptContact cName -> withUser $ \User {userId} -> do - connReqId <- withStore $ \st -> getContactRequestIdByName st userId cName + connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIAcceptContact connReqId RejectContact cName -> withUser $ \User {userId} -> do - connReqId <- withStore $ \st -> getContactRequestIdByName st userId cName + connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIRejectContact connReqId SendMessage chatName msg -> withUser $ \user -> do chatRef <- getChatRef user chatName let mc = MCText $ safeDecodeUtf8 msg processChatCommand . APISendMessage chatRef $ ComposedMessage Nothing Nothing mc SendMessageBroadcast msg -> withUser $ \user -> do - contacts <- withStore (`getUserContacts` user) + contacts <- withStore' (`getUserContacts` user) withChatLock . procCmd $ do let mc = MCText $ safeDecodeUtf8 msg cts = filter isReady contacts @@ -607,8 +611,8 @@ processChatCommand = \case `catchError` (toView . CRChatError) CRBroadcastSent mc (length cts) <$> liftIO getZonedTime SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \User {userId} -> do - contactId <- withStore $ \st -> getContactIdByName st userId cName - quotedItemId <- withStore $ \st -> getDirectChatItemIdByText st userId contactId msgDir (safeDecodeUtf8 quotedMsg) + contactId <- withStore $ \db -> getContactIdByName db userId cName + quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir (safeDecodeUtf8 quotedMsg) let mc = MCText $ safeDecodeUtf8 msg processChatCommand . APISendMessage (ChatRef CTDirect contactId) $ ComposedMessage Nothing (Just quotedItemId) mc DeleteMessage chatName deletedMsg -> withUser $ \user -> do @@ -622,10 +626,10 @@ processChatCommand = \case processChatCommand $ APIUpdateChatItem chatRef editedItemId mc NewGroup gProfile -> withUser $ \user -> do gVar <- asks idsDrg - CRGroupCreated <$> withStore (\st -> createNewGroup st gVar user gProfile) + CRGroupCreated <$> withStore (\db -> createNewGroup db gVar user gProfile) AddMember gName cName memRole -> withUser $ \user@User {userId} -> withChatLock $ do -- TODO for large groups: no need to load all members to determine if contact is a member - (group, contact) <- withStore $ \st -> (,) <$> getGroupByName st user gName <*> getContactByName st userId cName + (group, contact) <- withStore $ \db -> (,) <$> getGroupByName db user gName <*> getContactByName db userId cName let Group gInfo@GroupInfo {groupId, groupProfile, membership} members = group GroupMember {memberRole = userRole, memberId = userMemberId} = membership when (userRole < GRAdmin || userRole < memRole) $ throwChatError CEGroupUserRole @@ -640,26 +644,26 @@ processChatCommand = \case Nothing -> do gVar <- asks idsDrg (agentConnId, cReq) <- withAgent (`createConnection` SCMInvitation) - GroupMember {memberId} <- withStore $ \st -> createContactMember st gVar user groupId contact memRole agentConnId cReq + GroupMember {memberId} <- withStore $ \db -> createContactMember db gVar user groupId contact memRole agentConnId cReq sendInvitation memberId cReq Just GroupMember {groupMemberId, memberId, memberStatus} | memberStatus == GSMemInvited -> - withStore (\st -> getMemberInvitation st user groupMemberId) >>= \case + withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case Just cReq -> sendInvitation memberId cReq Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName JoinGroup gName -> withUser $ \user@User {userId} -> do - ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g} <- withStore $ \st -> getGroupInvitation st user gName + ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g} <- withStore $ \db -> getGroupInvitation db user gName withChatLock . procCmd $ do agentConnId <- withAgent $ \a -> joinConnection a connRequest . directMessage . XGrpAcpt $ memberId (membership g :: GroupMember) - withStore $ \st -> do - createMemberConnection st userId fromMember agentConnId - updateGroupMemberStatus st userId fromMember GSMemAccepted - updateGroupMemberStatus st userId (membership g) GSMemAccepted + withStore' $ \db -> do + createMemberConnection db userId fromMember agentConnId + updateGroupMemberStatus db userId fromMember GSMemAccepted + updateGroupMemberStatus db userId (membership g) GSMemAccepted pure $ CRUserAcceptedGroupSent g MemberRole _gName _cName _mRole -> throwChatError $ CECommandError "unsupported" RemoveMember gName cName -> withUser $ \user@User {userId} -> do - Group gInfo@GroupInfo {membership} members <- withStore $ \st -> getGroupByName st user gName + Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroupByName db user gName case find ((== cName) . (localDisplayName :: GroupMember -> ContactName)) members of Nothing -> throwChatError $ CEGroupMemberNotFound cName Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus} -> do @@ -668,17 +672,17 @@ processChatCommand = \case withChatLock . procCmd $ do when (mStatus /= GSMemInvited) . void . sendGroupMessage gInfo members $ XGrpMemDel mId deleteMemberConnection m - withStore $ \st -> updateGroupMemberStatus st userId m GSMemRemoved + withStore' $ \db -> updateGroupMemberStatus db userId m GSMemRemoved pure $ CRUserDeletedMember gInfo m LeaveGroup gName -> withUser $ \user@User {userId} -> do - Group gInfo@GroupInfo {membership} members <- withStore $ \st -> getGroupByName st user gName + Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroupByName db user gName withChatLock . procCmd $ do void $ sendGroupMessage gInfo members XGrpLeave mapM_ deleteMemberConnection members - withStore $ \st -> updateGroupMemberStatus st userId membership GSMemLeft + withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft pure $ CRLeftMemberUser gInfo DeleteGroup gName -> withUser $ \user -> do - g@(Group gInfo@GroupInfo {membership} members) <- withStore $ \st -> getGroupByName st user gName + g@(Group gInfo@GroupInfo {membership} members) <- withStore $ \db -> getGroupByName db user gName let s = memberStatus membership canDelete = memberRole (membership :: GroupMember) == GROwner @@ -687,23 +691,23 @@ processChatCommand = \case withChatLock . procCmd $ do when (memberActive membership) . void $ sendGroupMessage gInfo members XGrpDel mapM_ deleteMemberConnection members - withStore $ \st -> deleteGroup st user g + withStore' $ \db -> deleteGroup db user g pure $ CRGroupDeletedUser gInfo ClearGroup gName -> withUser $ \user -> do - groupId <- withStore $ \st -> getGroupIdByName st user gName + groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIClearChat (ChatRef CTGroup groupId) - ListMembers gName -> CRGroupMembers <$> withUser (\user -> withStore (\st -> getGroupByName st user gName)) - ListGroups -> CRGroupsList <$> withUser (\user -> withStore (`getUserGroupDetails` user)) + ListMembers gName -> CRGroupMembers <$> withUser (\user -> withStore (\db -> getGroupByName db user gName)) + ListGroups -> CRGroupsList <$> withUser (\user -> withStore' (`getUserGroupDetails` user)) SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do - groupId <- withStore $ \st -> getGroupIdByName st user gName - quotedItemId <- withStore $ \st -> getGroupChatItemIdByText st user groupId cName (safeDecodeUtf8 quotedMsg) + groupId <- withStore $ \db -> getGroupIdByName db user gName + quotedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId cName (safeDecodeUtf8 quotedMsg) let mc = MCText $ safeDecodeUtf8 msg processChatCommand . APISendMessage (ChatRef CTGroup groupId) $ ComposedMessage Nothing (Just quotedItemId) mc LastMessages (Just chatName) count -> withUser $ \user -> do chatRef <- getChatRef user chatName CRLastMessages . aChatItems . chat <$> (processChatCommand . APIGetChat chatRef $ CPLast count) - LastMessages Nothing count -> withUser $ \user -> withStore $ \st -> - CRLastMessages <$> getAllChatItems st user (CPLast count) + LastMessages Nothing count -> withUser $ \user -> withStore $ \db -> + CRLastMessages <$> getAllChatItems db user (CPLast count) SendFile chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName processChatCommand . APISendMessage chatRef $ ComposedMessage (Just f) Nothing (MCFile "") @@ -718,7 +722,7 @@ processChatCommand = \case ForwardImage chatName fileId -> forwardFile chatName fileId SendImage ReceiveFile fileId filePath_ -> withUser $ \user -> withChatLock . procCmd $ do - ft <- withStore $ \st -> getRcvFileTransfer st user fileId + ft <- withStore $ \db -> getRcvFileTransfer db user fileId (CRRcvFileAccepted <$> acceptFileReceive user ft filePath_) `catchError` processError ft where processError ft = \case @@ -728,27 +732,27 @@ processChatCommand = \case e -> throwError e CancelFile fileId -> withUser $ \user@User {userId} -> withChatLock . procCmd $ - withStore (\st -> getFileTransfer st user fileId) >>= \case + withStore (\db -> getFileTransfer db user fileId) >>= \case FTSnd ftm@FileTransferMeta {cancelled} fts -> do unless cancelled $ do cancelSndFile user ftm fts - sharedMsgId <- withStore $ \st -> getSharedMsgIdByFileId st userId fileId + sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId void $ - withStore (\st -> getChatRefByFileId st user fileId) >>= \case + withStore (\db -> getChatRefByFileId db user fileId) >>= \case ChatRef CTDirect contactId -> do - contact <- withStore $ \st -> getContact st userId contactId + contact <- withStore $ \db -> getContact db userId contactId sendDirectContactMessage contact $ XFileCancel sharedMsgId ChatRef CTGroup groupId -> do - Group gInfo ms <- withStore $ \st -> getGroup st user groupId + Group gInfo ms <- withStore $ \db -> getGroup db user groupId sendGroupMessage gInfo ms $ XFileCancel sharedMsgId _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" - ci <- withStore $ \st -> getChatItemByFileId st user fileId + ci <- withStore $ \db -> getChatItemByFileId db user fileId pure $ CRSndGroupFileCancelled ci ftm fts FTRcv ftr@RcvFileTransfer {cancelled} -> do unless cancelled $ cancelRcvFileTransfer user ftr pure $ CRRcvFileCancelled ftr FileStatus fileId -> - CRFileTransferStatus <$> withUser (\user -> withStore $ \st -> getFileTransferProgress st user fileId) + CRFileTransferStatus <$> withUser (\user -> withStore $ \db -> getFileTransferProgress db user fileId) ShowProfile -> withUser $ \User {profile} -> pure $ CRUserProfile profile UpdateProfile displayName fullName -> withUser $ \user@User {profile} -> do let p = (profile :: Profile) {displayName = displayName, fullName = fullName} @@ -778,8 +782,8 @@ processChatCommand = \case getChatRef :: User -> ChatName -> m ChatRef getChatRef user@User {userId} (ChatName cType name) = ChatRef cType <$> case cType of - CTDirect -> withStore $ \st -> getContactIdByName st userId name - CTGroup -> withStore $ \st -> getGroupIdByName st user name + CTDirect -> withStore $ \db -> getContactIdByName db userId name + CTGroup -> withStore $ \db -> getGroupIdByName db user name _ -> throwChatError $ CECommandError "not supported" checkChatStopped :: m ChatResponse -> m ChatResponse checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped) @@ -787,19 +791,19 @@ processChatCommand = \case setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True) getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64 getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of - CTDirect -> withStore $ \st -> getDirectChatItemIdByText st userId cId SMDSnd (safeDecodeUtf8 msg) - CTGroup -> withStore $ \st -> getGroupChatItemIdByText st user cId (Just localDisplayName) (safeDecodeUtf8 msg) + CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg) + CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) (safeDecodeUtf8 msg) _ -> throwChatError $ CECommandError "not supported" connectViaContact :: UserId -> ConnectionRequestUri 'CMContact -> Profile -> m ChatResponse connectViaContact userId cReq profile = withChatLock $ do let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - withStore (\st -> getConnReqContactXContactId st userId cReqHash) >>= \case + withStore' (\db -> getConnReqContactXContactId db userId cReqHash) >>= \case (Just contact, _) -> pure $ CRContactAlreadyExists contact (_, xContactId_) -> procCmd $ do let randomXContactId = XContactId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16)) xContactId <- maybe randomXContactId pure xContactId_ connId <- withAgent $ \a -> joinConnection a cReq $ directMessage (XContact profile $ Just xContactId) - conn <- withStore $ \st -> createConnReqConnection st userId connId cReqHash xContactId + conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId toView $ CRNewContactConnection conn pure CRSentInvitation contactMember :: Contact -> [GroupMember] -> Maybe GroupMember @@ -815,10 +819,10 @@ processChatCommand = \case updateProfile user@User {profile = p} p'@Profile {displayName} | p' == p = pure CRUserProfileNoChange | otherwise = do - withStore $ \st -> updateUserProfile st user p' + withStore $ \db -> updateUserProfile db user p' let user' = (user :: User) {localDisplayName = displayName, profile = p'} asks currentUser >>= atomically . (`writeTVar` Just user') - contacts <- filter isReady <$> withStore (`getUserContacts` user) + contacts <- filter isReady <$> withStore' (`getUserContacts` user) withChatLock . procCmd $ do forM_ contacts $ \ct -> void (sendDirectContactMessage ct $ XInfo p') `catchError` (toView . CRChatError) @@ -841,14 +845,14 @@ processChatCommand = \case unless (ciFileEnded status) $ case dir of SMDSnd -> do - (ftm@FileTransferMeta {cancelled}, fts) <- withStore (\st -> getSndFileTransfer st user fileId) + (ftm@FileTransferMeta {cancelled}, fts) <- withStore (\db -> getSndFileTransfer db user fileId) unless cancelled $ cancelSndFile user ftm fts SMDRcv -> do - ft@RcvFileTransfer {cancelled} <- withStore (\st -> getRcvFileTransfer st user fileId) + ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) unless cancelled $ cancelRcvFileTransfer user ft withCurrentCall :: ContactId -> (UserId -> Contact -> Call -> m (Maybe Call)) -> m ChatResponse withCurrentCall ctId action = withUser $ \User {userId} -> do - ct <- withStore $ \st -> getContact st userId ctId + ct <- withStore $ \db -> getContact db userId ctId calls <- asks currentCalls withChatLock $ atomically (TM.lookup ctId calls) >>= \case @@ -863,7 +867,7 @@ processChatCommand = \case | otherwise -> throwChatError $ CECallContact contactId forwardFile :: ChatName -> FileTransferId -> (ChatName -> FilePath -> ChatCommand) -> m ChatResponse forwardFile chatName fileId sendCommand = withUser $ \user -> do - withStore (\st -> getFileTransfer st user fileId) >>= \case + withStore (\db -> getFileTransfer db user fileId) >>= \case FTRcv RcvFileTransfer {fileStatus = RFSComplete RcvFileInfo {filePath}} -> forward filePath FTSnd {fileTransferMeta = FileTransferMeta {filePath}} -> forward filePath _ -> throwChatError CEFileNotReceived {fileId} @@ -877,13 +881,13 @@ updateCallItemStatus userId ct Call {chatItemId} receivedStatus msgId_ = do updateDirectChatItemView :: ChatMonad m => UserId -> Contact -> ChatItemId -> ACIContent -> Maybe MessageId -> m () updateDirectChatItemView userId ct@Contact {contactId} chatItemId (ACIContent msgDir ciContent) msgId_ = do - updCi <- withStore $ \st -> updateDirectChatItem st userId contactId chatItemId ciContent msgId_ + updCi <- withStore $ \db -> updateDirectChatItem db userId contactId chatItemId ciContent msgId_ toView . CRChatItemUpdated $ AChatItem SCTDirect msgDir (DirectChat ct) updCi callStatusItemContent :: ChatMonad m => UserId -> Contact -> ChatItemId -> WebRTCCallStatus -> m (Maybe ACIContent) callStatusItemContent userId Contact {contactId} chatItemId receivedStatus = do CChatItem msgDir ChatItem {meta = CIMeta {updatedAt}, content} <- - withStore $ \st -> getDirectChatItem st userId contactId chatItemId + withStore $ \db -> getDirectChatItem db userId contactId chatItemId ts <- liftIO getCurrentTime let callDuration :: Int = nominalDiffTimeToSeconds (ts `diffUTCTime` updatedAt) `div'` 1 callStatus = case content of @@ -927,20 +931,20 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F tryError (withAgent $ \a -> joinConnection a connReq . directMessage $ XFileAcpt fName) >>= \case Right agentConnId -> do filePath <- getRcvFilePath filePath_ fName - withStore $ \st -> acceptRcvFileTransfer st user fileId agentConnId ConnJoined filePath + withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnJoined filePath Left e -> throwError e -- group file protocol Nothing -> case grpMemberId of Nothing -> throwChatError $ CEFileInternal "group member not found for file transfer" Just memId -> do - (GroupInfo {groupId}, GroupMember {activeConn}) <- withStore $ \st -> getGroupAndMember st user memId + (GroupInfo {groupId}, GroupMember {activeConn}) <- withStore $ \db -> getGroupAndMember db user memId case activeConn of Just conn -> do - sharedMsgId <- withStore $ \st -> getSharedMsgIdByFileId st userId fileId + sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId (agentConnId, fileInvConnReq) <- withAgent (`createConnection` SCMInvitation) filePath <- getRcvFilePath filePath_ fName - ci <- withStore $ \st -> acceptRcvFileTransfer st user fileId agentConnId ConnNew filePath + ci <- withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnNew filePath void $ sendDirectMessage conn (XFileAcptInv sharedMsgId fileInvConnReq fName) (GroupId groupId) pure ci _ -> throwChatError $ CEFileInternal "member connection not active" @@ -986,7 +990,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> m Contact acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, xContactId} = do connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profile - withStore $ \st -> createAcceptedContact st userId connId cName profileId p xContactId + withStore' $ \db -> createAcceptedContact db userId connId cName profileId p xContactId agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m () agentSubscriber user = do @@ -1016,13 +1020,13 @@ subscribeUserConnections agentSubscribe user@User {userId} = do where catchErr a = a `catchError` \_ -> pure () subscribeContacts n ce = do - contacts <- withStore (`getUserContacts` user) + contacts <- withStore' (`getUserContacts` user) toView . CRContactSubSummary =<< pooledForConcurrentlyN n contacts (\ct -> ContactSubStatus ct <$> subscribeContact ce ct) subscribeContact ce ct = (subscribe (contactConnId ct) $> Nothing) `catchError` (\e -> when ce (toView $ CRContactSubError ct e) $> Just e) subscribeGroups n ce = do - groups <- withStore (`getUserGroups` user) + groups <- withStore' (`getUserGroups` user) toView . CRMemberSubErrors . mconcat =<< forM groups (subscribeGroup n ce) subscribeGroup n ce (Group g@GroupInfo {membership} members) = do let connectedMembers = mapMaybe (\m -> (m,) <$> memberConnId m) members @@ -1043,9 +1047,9 @@ subscribeUserConnections agentSubscribe user@User {userId} = do toView $ CRGroupSubscribed g pure $ mapMaybe (\(m, e) -> (Just . MemberSubError m) =<< e) ms subscribeFiles n = do - sndFileTransfers <- withStore (`getLiveSndFileTransfers` user) + sndFileTransfers <- withStore' (`getLiveSndFileTransfers` user) pooledForConcurrentlyN_ n sndFileTransfers $ \sft -> subscribeSndFile sft - rcvFileTransfers <- withStore (`getLiveRcvFileTransfers` user) + rcvFileTransfers <- withStore' (`getLiveRcvFileTransfers` user) pooledForConcurrentlyN_ n rcvFileTransfers $ \rft -> subscribeRcvFile rft where subscribeSndFile ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId cId} = do @@ -1066,7 +1070,7 @@ subscribeUserConnections agentSubscribe user@User {userId} = do resume RcvFileInfo {agentConnId = AgentConnId cId} = subscribe cId `catchError` (toView . CRRcvFileSubError ft) subscribePendingConnections n = do - cs <- withStore (`getPendingConnections` user) + cs <- withStore' (`getPendingConnections` user) summary <- pooledForConcurrentlyN n cs $ \Connection {agentConnId = acId@(AgentConnId cId)} -> PendingSubStatus acId <$> ((subscribe cId $> Nothing) `catchError` (pure . Just)) toView $ CRPendingSubSummary summary @@ -1087,11 +1091,11 @@ processAgentMessage (Just User {userId}) "" agentMessage = case agentMessage of _ -> pure () where serverEvent srv@SMP.ProtocolServer {host, port} conns event str = do - cs <- withStore $ \st -> getConnectionsContacts st userId conns + cs <- withStore' $ \db -> getConnectionsContacts db userId conns toView $ event srv cs showToast ("server " <> str) (safeDecodeUtf8 . strEncode $ SrvLoc host port) processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage = - (withStore (\st -> getConnectionEntity st user agentConnId) >>= updateConnStatus) >>= \case + (withStore (\db -> getConnectionEntity db user agentConnId) >>= updateConnStatus) >>= \case RcvDirectMsgConnection conn contact_ -> processDirectMessage agentMessage conn contact_ RcvGroupMsgConnection conn gInfo m -> @@ -1107,7 +1111,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage updateConnStatus acEntity = case agentMsgConnStatus agentMessage of Just connStatus -> do let conn = (entityConnection acEntity) {connStatus} - withStore $ \st -> updateConnectionStatus st conn connStatus + withStore' $ \db -> updateConnectionStatus db conn connStatus pure $ updateEntityConnStatus acEntity connStatus Nothing -> pure acEntity @@ -1187,7 +1191,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage XOk -> pure () _ -> messageError "INFO for existing contact must have x.grp.mem.info, x.info or x.ok" CON -> - withStore (\st -> getViaGroupMember st user ct) >>= \case + withStore' (\db -> getViaGroupMember db user ct) >>= \case Nothing -> do toView $ CRContactConnected ct setActive $ ActiveC c @@ -1198,9 +1202,9 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage when (memberCategory m == GCPreMember) $ probeMatchingContacts ct SENT msgId -> do sentMsgDeliveryEvent conn msgId - withStore (\st -> getDirectChatItemByAgentMsgId st userId contactId connId msgId) >>= \case + withStore' (\db -> getDirectChatItemByAgentMsgId db userId contactId connId msgId) >>= \case Just (CChatItem SMDSnd ci) -> do - chatItem <- withStore $ \st -> updateDirectChatItemStatus st userId contactId (chatItemId' ci) CISSndSent + chatItem <- withStore $ \db -> updateDirectChatItemStatus db userId contactId (chatItemId' ci) CISSndSent toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) _ -> pure () END -> do @@ -1209,11 +1213,11 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage unsetActive $ ActiveC c -- TODO print errors MERR msgId err -> do - chatItemId_ <- withStore $ \st -> getChatItemIdByAgentMsgId st connId msgId + chatItemId_ <- withStore' $ \db -> getChatItemIdByAgentMsgId db connId msgId case chatItemId_ of Nothing -> pure () Just chatItemId -> do - chatItem <- withStore $ \st -> updateDirectChatItemStatus st userId contactId chatItemId (agentErrToItemStatus err) + chatItem <- withStore $ \db -> updateDirectChatItemStatus db userId contactId chatItemId (agentErrToItemStatus err) toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) ERR _ -> pure () -- TODO add debugging output @@ -1228,7 +1232,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage case chatMsgEvent of XGrpAcpt memId | sameMemberId memId m -> do - withStore $ \st -> updateGroupMemberStatus st userId m GSMemAccepted + withStore' $ \db -> updateGroupMemberStatus db userId m GSMemAccepted allowAgentConnection conn confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" _ -> messageError "CONF from invited member must have x.grp.acpt" @@ -1252,11 +1256,11 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> messageError "INFO from member must have x.grp.mem.info" pure () CON -> do - members <- withStore $ \st -> getGroupMembers st user gInfo - withStore $ \st -> do - updateGroupMemberStatus st userId m GSMemConnected + members <- withStore' $ \db -> getGroupMembers db user gInfo + withStore' $ \db -> do + updateGroupMemberStatus db userId m GSMemConnected unless (memberActive membership) $ - updateGroupMemberStatus st userId membership GSMemConnected + updateGroupMemberStatus db userId membership GSMemConnected sendPendingGroupMessages m conn case memberCategory m of GCHostMember -> do @@ -1267,15 +1271,15 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage toView $ CRJoinedGroupMember gInfo m setActive $ ActiveG gName showToast ("#" <> gName) $ "member " <> localDisplayName (m :: GroupMember) <> " is connected" - intros <- withStore $ \st -> createIntroductions st members m + intros <- withStore' $ \db -> createIntroductions db members m void . sendGroupMessage gInfo members . XGrpMemNew $ memberInfo m forM_ intros $ \intro@GroupMemberIntro {introId} -> do void $ sendDirectMessage conn (XGrpMemIntro . memberInfo $ reMember intro) (GroupId groupId) - withStore $ \st -> updateIntroStatus st introId GMIntroSent + withStore' $ \db -> updateIntroStatus db introId GMIntroSent _ -> do -- TODO send probe and decide whether to use existing contact connection or the new contact connection -- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table - withStore (\st -> getViaGroupContact st user m) >>= \case + withStore' (\db -> getViaGroupContact db user m) >>= \case Nothing -> do notifyMemberConnected gInfo m messageError "implementation error: connected member does not have contact" @@ -1322,24 +1326,24 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage -- TODO save XFileAcpt message XFileAcpt name | name == fileName -> do - withStore $ \st -> updateSndFileStatus st ft FSAccepted + withStore' $ \db -> updateSndFileStatus db ft FSAccepted allowAgentConnection conn confId XOk | otherwise -> messageError "x.file.acpt: fileName is different from expected" _ -> messageError "CONF from file connection must have x.file.acpt" CON -> do - ci <- withStore $ \st -> do - updateSndFileStatus st ft FSConnected - updateDirectCIFileStatus st user fileId CIFSSndTransfer + ci <- withStore $ \db -> do + liftIO $ updateSndFileStatus db ft FSConnected + updateDirectCIFileStatus db user fileId CIFSSndTransfer toView $ CRSndFileStart ci ft sendFileChunk user ft SENT msgId -> do - withStore $ \st -> updateSndFileChunkSent st ft msgId + withStore' $ \db -> updateSndFileChunkSent db ft msgId unless (fileStatus == FSCancelled) $ sendFileChunk user ft MERR _ err -> do cancelSndFileTransfer ft case err of SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ do - ci <- withStore $ \st -> getChatItemByFileId st user fileId + ci <- withStore $ \db -> getChatItemByFileId db user fileId toView $ CRSndFileRcvCancelled ci ft _ -> throwChatError $ CEFileSend fileId err MSG meta _ _ -> @@ -1361,10 +1365,10 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage XOk -> allowAgentConnection conn confId XOk _ -> pure () CON -> do - ci <- withStore $ \st -> do - updateRcvFileStatus st ft FSConnected - updateCIFileStatus st user fileId CIFSRcvTransfer - getChatItemByFileId st user fileId + ci <- withStore $ \db -> do + liftIO $ updateRcvFileStatus db ft FSConnected + liftIO $ updateCIFileStatus db user fileId CIFSRcvTransfer + getChatItemByFileId db user fileId toView $ CRRcvFileStart ci MSG meta@MsgMeta {recipient = (msgId, _), integrity} _ msgBody -> withAckMessage agentConnId meta $ do parseFileChunk msgBody >>= \case @@ -1378,7 +1382,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage MsgError MsgDuplicate -> pure () -- TODO remove once agent removes duplicates MsgError e -> badRcvFileChunk ft $ "invalid file chunk number " <> show chunkNo <> ": " <> show e - withStore (\st -> createRcvFileChunk st ft chunkNo msgId) >>= \case + withStore' (\db -> createRcvFileChunk db ft chunkNo msgId) >>= \case RcvChunkOk -> if B.length chunk /= fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" @@ -1388,11 +1392,12 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage then badRcvFileChunk ft "incorrect chunk size" else do appendFileChunk ft chunkNo chunk - ci <- withStore $ \st -> do - updateRcvFileStatus st ft FSComplete - updateCIFileStatus st user fileId CIFSRcvComplete - deleteRcvFileChunks st ft - getChatItemByFileId st user fileId + ci <- withStore $ \db -> do + liftIO $ do + updateRcvFileStatus db ft FSComplete + updateCIFileStatus db user fileId CIFSRcvComplete + deleteRcvFileChunks db ft + getChatItemByFileId db user fileId toView $ CRRcvFileComplete ci closeFileHandle fileId rcvFiles withAgent (`deleteConnection` agentConnId) @@ -1421,10 +1426,10 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage where profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m () profileContactRequest invId p xContactId_ = do - withStore (\st -> createOrUpdateContactRequest st userId userContactLinkId invId p xContactId_) >>= \case + withStore (\db -> createOrUpdateContactRequest db userId userContactLinkId invId p xContactId_) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted contact CORRequest cReq@UserContactRequest {localDisplayName} -> do - (_, autoAccept) <- withStore $ \st -> getUserContactLink st userId + (_, autoAccept) <- withStore $ \db -> getUserContactLink db userId if autoAccept then acceptContactRequest user cReq >>= toView . CRAcceptingContactRequest else do @@ -1437,11 +1442,11 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage ackMsgDeliveryEvent :: Connection -> MsgMeta -> m () ackMsgDeliveryEvent Connection {connId} MsgMeta {recipient = (msgId, _)} = - withStore $ \st -> createRcvMsgDeliveryEvent st connId msgId MDSRcvAcknowledged + withStore $ \db -> createRcvMsgDeliveryEvent db connId msgId MDSRcvAcknowledged sentMsgDeliveryEvent :: Connection -> AgentMsgId -> m () sentMsgDeliveryEvent Connection {connId} msgId = - withStore $ \st -> createSndMsgDeliveryEvent st connId msgId MDSSndSent + withStore $ \db -> createSndMsgDeliveryEvent db connId msgId MDSSndSent agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd agentErrToItemStatus (SMP AUTH) = CISSndErrorAuth @@ -1463,16 +1468,16 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage probeMatchingContacts :: Contact -> m () probeMatchingContacts ct = do gVar <- asks idsDrg - (probe, probeId) <- withStore $ \st -> createSentProbe st gVar userId ct + (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId ct void . sendDirectContactMessage ct $ XInfoProbe probe - cs <- withStore (\st -> getMatchingContacts st userId ct) + cs <- withStore' $ \db -> getMatchingContacts db userId ct let probeHash = ProbeHash $ C.sha256Hash (unProbe probe) forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchError` const (pure ()) where sendProbeHash :: Contact -> ProbeHash -> Int64 -> m () sendProbeHash c probeHash probeId = do void . sendDirectContactMessage c $ XInfoProbeCheck probeHash - withStore $ \st -> createSentProbeHash st userId probeId c + withStore' $ \db -> createSentProbeHash db userId probeId c messageWarning :: Text -> m () messageWarning = toView . CRMessageError "warning" @@ -1485,7 +1490,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage checkIntegrityCreateItem (CDDirectRcv ct) msgMeta let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc ciFile_ <- processFileInvitation fileInvitation_ $ - \fi chSize -> withStore $ \st -> createRcvFileTransfer st userId ct fi chSize + \fi chSize -> withStore' $ \db -> createRcvFileTransfer db userId ct fi chSize ci@ChatItem {formattedText} <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvMsgContent content) ciFile_ toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci showMsgToast (c <> "> ") content formattedText @@ -1516,7 +1521,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> throwError e where updateRcvChatItem = do - CChatItem msgDir ChatItem {meta = CIMeta {itemId}} <- withStore $ \st -> getDirectChatItemBySharedMsgId st userId contactId sharedMsgId + CChatItem msgDir ChatItem {meta = CIMeta {itemId}} <- withStore $ \db -> getDirectChatItemBySharedMsgId db userId contactId sharedMsgId case msgDir of SMDRcv -> updateDirectChatItemView userId ct itemId (ACIContent SMDRcv $ CIRcvMsgContent mc) $ Just msgId SMDSnd -> messageError "x.msg.update: contact attempted invalid message update" @@ -1530,10 +1535,10 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> throwError e where deleteRcvChatItem = do - CChatItem msgDir deletedItem@ChatItem {meta = CIMeta {itemId}} <- withStore $ \st -> getDirectChatItemBySharedMsgId st userId contactId sharedMsgId + CChatItem msgDir deletedItem@ChatItem {meta = CIMeta {itemId}} <- withStore $ \db -> getDirectChatItemBySharedMsgId db userId contactId sharedMsgId case msgDir of SMDRcv -> do - toCi <- withStore $ \st -> deleteDirectChatItemRcvBroadcast st userId ct itemId msgId + toCi <- withStore $ \db -> deleteDirectChatItemRcvBroadcast db userId ct itemId msgId toView $ CRChatItemDeleted (AChatItem SCTDirect SMDRcv (DirectChat ct) deletedItem) toCi SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" @@ -1541,7 +1546,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage newGroupContentMessage gInfo m@GroupMember {localDisplayName = c} mc msg msgMeta = do let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc ciFile_ <- processFileInvitation fileInvitation_ $ - \fi chSize -> withStore $ \st -> createRcvGroupFileTransfer st userId m fi chSize + \fi chSize -> withStore' $ \db -> createRcvGroupFileTransfer db userId m fi chSize ci@ChatItem {formattedText} <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvMsgContent content) ciFile_ groupMsgToView gInfo m ci msgMeta let g = groupName' gInfo @@ -1550,24 +1555,24 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> m () groupMessageUpdate gInfo@GroupInfo {groupId} GroupMember {memberId} sharedMsgId mc RcvMessage {msgId} = do - CChatItem msgDir ChatItem {chatDir, meta = CIMeta {itemId}} <- withStore $ \st -> getGroupChatItemBySharedMsgId st user groupId sharedMsgId + CChatItem msgDir ChatItem {chatDir, meta = CIMeta {itemId}} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId sharedMsgId case (msgDir, chatDir) of (SMDRcv, CIGroupRcv m) -> if sameMemberId memberId m then do - updCi <- withStore $ \st -> updateGroupChatItem st user groupId itemId (CIRcvMsgContent mc) msgId + updCi <- withStore $ \db -> updateGroupChatItem db user groupId itemId (CIRcvMsgContent mc) msgId toView . CRChatItemUpdated $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) updCi else messageError "x.msg.update: group member attempted to update a message of another member" (SMDSnd, _) -> messageError "x.msg.update: group member attempted invalid message update" groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> RcvMessage -> m () groupMessageDelete gInfo@GroupInfo {groupId} GroupMember {memberId} sharedMsgId RcvMessage {msgId} = do - CChatItem msgDir deletedItem@ChatItem {chatDir, meta = CIMeta {itemId}} <- withStore $ \st -> getGroupChatItemBySharedMsgId st user groupId sharedMsgId + CChatItem msgDir deletedItem@ChatItem {chatDir, meta = CIMeta {itemId}} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId sharedMsgId case (msgDir, chatDir) of (SMDRcv, CIGroupRcv m) -> if sameMemberId memberId m then do - toCi <- withStore $ \st -> deleteGroupChatItemRcvBroadcast st user gInfo itemId msgId + toCi <- withStore $ \db -> deleteGroupChatItemRcvBroadcast db user gInfo itemId msgId toView $ CRChatItemDeleted (AChatItem SCTGroup SMDRcv (GroupChat gInfo) deletedItem) toCi else messageError "x.msg.del: group member attempted to delete a message of another member" (SMDSnd, _) -> messageError "x.msg.del: group member attempted invalid message delete" @@ -1577,7 +1582,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage processFileInvitation' ct@Contact {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta chSize <- asks $ fileChunkSize . config - RcvFileTransfer {fileId} <- withStore $ \st -> createRcvFileTransfer st userId ct fInv chSize + RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFileTransfer db userId ct fInv chSize let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation} ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvMsgContent $ MCFile "") ciFile toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci @@ -1588,7 +1593,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> MsgMeta -> m () processGroupFileInvitation' gInfo m@GroupMember {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg msgMeta = do chSize <- asks $ fileChunkSize . config - RcvFileTransfer {fileId} <- withStore $ \st -> createRcvGroupFileTransfer st userId m fInv chSize + RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvGroupFileTransfer db userId m fInv chSize let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation} ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvMsgContent $ MCFile "") ciFile groupMsgToView gInfo m ci msgMeta @@ -1599,8 +1604,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m () xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - fileId <- withStore $ \st -> getFileIdBySharedMsgId st userId contactId sharedMsgId - ft@RcvFileTransfer {cancelled} <- withStore (\st -> getRcvFileTransfer st user fileId) + fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId + ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) unless cancelled $ do cancelRcvFileTransfer user ft toView $ CRRcvFileSndCancelled ft @@ -1608,13 +1613,13 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> MsgMeta -> m () xFileCancelGroup g@GroupInfo {groupId} mem@GroupMember {memberId} sharedMsgId msgMeta = do checkIntegrityCreateItem (CDGroupRcv g mem) msgMeta - fileId <- withStore $ \st -> getGroupFileIdBySharedMsgId st userId groupId sharedMsgId - CChatItem msgDir ChatItem {chatDir} <- withStore $ \st -> getGroupChatItemBySharedMsgId st user groupId sharedMsgId + fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId + CChatItem msgDir ChatItem {chatDir} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId sharedMsgId case (msgDir, chatDir) of (SMDRcv, CIGroupRcv m) -> do if sameMemberId memberId m then do - ft@RcvFileTransfer {cancelled} <- withStore (\st -> getRcvFileTransfer st user fileId) + ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) unless cancelled $ do cancelRcvFileTransfer user ft toView $ CRRcvFileSndCancelled ft @@ -1624,14 +1629,14 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> ConnReqInvitation -> String -> MsgMeta -> m () xFileAcptInvGroup g@GroupInfo {groupId} m sharedMsgId fileConnReq fName msgMeta = do checkIntegrityCreateItem (CDGroupRcv g m) msgMeta - fileId <- withStore $ \st -> getGroupFileIdBySharedMsgId st userId groupId sharedMsgId - (FileTransferMeta {fileName, cancelled}, _) <- withStore (\st -> getSndFileTransfer st user fileId) + fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId + (FileTransferMeta {fileName, cancelled}, _) <- withStore (\db -> getSndFileTransfer db user fileId) unless cancelled $ if fName == fileName then tryError (withAgent $ \a -> joinConnection a fileConnReq . directMessage $ XOk) >>= \case Right acId -> - withStore $ \st -> createSndGroupFileTransferConnection st userId fileId acId m + withStore' $ \db -> createSndGroupFileTransferConnection db userId fileId acId m Left e -> throwError e else messageError "x.file.acpt.inv: fileName is different from expected" @@ -1644,7 +1649,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage processGroupInvitation ct@Contact {localDisplayName = c} inv@(GroupInvitation (MemberIdRole fromMemId fromRole) (MemberIdRole memId memRole) _ _) = do when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId - gInfo@GroupInfo {localDisplayName = gName} <- withStore $ \st -> createGroupInvitation st user ct inv + gInfo@GroupInfo {localDisplayName = gName} <- withStore $ \db -> createGroupInvitation db user ct inv toView $ CRReceivedGroupInvitation gInfo ct memRole showToast ("#" <> gName <> " " <> c <> "> ") "invited you to join the group" @@ -1658,23 +1663,23 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage createIntegrityErrorItem e = do createdAt <- liftIO getCurrentTime let content = CIRcvIntegrityError e - ciId <- withStore $ \st -> createNewChatItemNoMsg st user cd content brokerTs createdAt + ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content brokerTs createdAt ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing brokerTs createdAt toView $ CRNewChatItem $ AChatItem (chatTypeI @c) SMDRcv (toChatInfo cd) ci xInfo :: Contact -> Profile -> m () xInfo c@Contact {profile = p} p' = unless (p == p') $ do - c' <- withStore $ \st -> updateContactProfile st userId c p' + c' <- withStore $ \db -> updateContactProfile db userId c p' toView $ CRContactUpdated c c' xInfoProbe :: Contact -> Probe -> m () xInfoProbe c2 probe = do - r <- withStore $ \st -> matchReceivedProbe st userId c2 probe + r <- withStore' $ \db -> matchReceivedProbe db userId c2 probe forM_ r $ \c1 -> probeMatch c1 c2 probe xInfoProbeCheck :: Contact -> ProbeHash -> m () xInfoProbeCheck c1 probeHash = do - r <- withStore $ \st -> matchReceivedProbeHash st userId c1 probeHash + r <- withStore' $ \db -> matchReceivedProbeHash db userId c1 probeHash forM_ r . uncurry $ probeMatch c1 probeMatch :: Contact -> Contact -> Probe -> m () @@ -1685,7 +1690,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage xInfoProbeOk :: Contact -> Probe -> m () xInfoProbeOk c1 probe = do - r <- withStore $ \st -> matchSentProbe st userId c1 probe + r <- withStore' $ \db -> matchSentProbe db userId c1 probe forM_ r $ \c2 -> mergeContacts c1 c2 -- to party accepting call @@ -1784,7 +1789,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage mergeContacts :: Contact -> Contact -> m () mergeContacts to from = do - withStore $ \st -> mergeContactRecords st userId to from + withStore' $ \db -> mergeContactRecords db userId to from toView $ CRContactsMerged to from saveConnInfo :: Connection -> ConnInfo -> m () @@ -1792,72 +1797,72 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo case chatMsgEvent of XInfo p -> do - ct <- withStore $ \st -> createDirectContact st userId activeConn p + ct <- withStore $ \db -> createDirectContact db userId activeConn p toView $ CRContactConnecting ct -- TODO show/log error, other events in SMP confirmation _ -> pure () xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> m () xGrpMemNew gInfo m memInfo@(MemberInfo memId _ _) = do - members <- withStore $ \st -> getGroupMembers st user gInfo + members <- withStore' $ \db -> getGroupMembers db user gInfo unless (sameMemberId memId $ membership gInfo) $ if isMember memId gInfo members then messageError "x.grp.mem.new error: member already exists" else do - newMember <- withStore $ \st -> createNewGroupMember st user gInfo memInfo GCPostMember GSMemAnnounced + newMember <- withStore $ \db -> createNewGroupMember db user gInfo memInfo GCPostMember GSMemAnnounced toView $ CRJoinedGroupMemberConnecting gInfo m newMember xGrpMemIntro :: Connection -> GroupInfo -> GroupMember -> MemberInfo -> m () xGrpMemIntro conn gInfo@GroupInfo {groupId} m memInfo@(MemberInfo memId _ _) = do case memberCategory m of GCHostMember -> do - members <- withStore $ \st -> getGroupMembers st user gInfo + members <- withStore' $ \db -> getGroupMembers db user gInfo if isMember memId gInfo members then messageWarning "x.grp.mem.intro ignored: member already exists" else do (groupConnId, groupConnReq) <- withAgent (`createConnection` SCMInvitation) (directConnId, directConnReq) <- withAgent (`createConnection` SCMInvitation) - newMember <- withStore $ \st -> createIntroReMember st user gInfo m memInfo groupConnId directConnId + newMember <- withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnId directConnId let msg = XGrpMemInv memId IntroInvitation {groupConnReq, directConnReq} void $ sendDirectMessage conn msg (GroupId groupId) - withStore $ \st -> updateGroupMemberStatus st userId newMember GSMemIntroInvited + withStore' $ \db -> updateGroupMemberStatus db userId newMember GSMemIntroInvited _ -> messageError "x.grp.mem.intro can be only sent by host member" xGrpMemInv :: GroupInfo -> GroupMember -> MemberId -> IntroInvitation -> m () xGrpMemInv gInfo m memId introInv = do case memberCategory m of GCInviteeMember -> do - members <- withStore $ \st -> getGroupMembers st user gInfo + members <- withStore' $ \db -> getGroupMembers db user gInfo case find (sameMemberId memId) members of Nothing -> messageError "x.grp.mem.inv error: referenced member does not exist" Just reMember -> do - GroupMemberIntro {introId} <- withStore $ \st -> saveIntroInvitation st reMember m introInv + GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv void $ sendXGrpMemInv gInfo reMember (XGrpMemFwd (memberInfo m) introInv) introId _ -> messageError "x.grp.mem.inv can be only sent by invitee member" xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m () xGrpMemFwd gInfo@GroupInfo {membership} m memInfo@(MemberInfo memId _ _) introInv@IntroInvitation {groupConnReq, directConnReq} = do - members <- withStore $ \st -> getGroupMembers st user gInfo + members <- withStore' $ \db -> getGroupMembers db user gInfo toMember <- case find (sameMemberId memId) members of -- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent -- the situation when member does not exist is an error -- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that. -- For now, this branch compensates for the lack of delayed message delivery. - Nothing -> withStore $ \st -> createNewGroupMember st user gInfo memInfo GCPostMember GSMemAnnounced + Nothing -> withStore $ \db -> createNewGroupMember db user gInfo memInfo GCPostMember GSMemAnnounced Just m' -> pure m' - withStore $ \st -> saveMemberInvitation st toMember introInv + withStore' $ \db -> saveMemberInvitation db toMember introInv let msg = XGrpMemInfo (memberId (membership :: GroupMember)) profile groupConnId <- withAgent $ \a -> joinConnection a groupConnReq $ directMessage msg directConnId <- withAgent $ \a -> joinConnection a directConnReq $ directMessage msg - withStore $ \st -> createIntroToMemberContact st userId m toMember groupConnId directConnId + withStore' $ \db -> createIntroToMemberContact db userId m toMember groupConnId directConnId xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> m () xGrpMemDel gInfo@GroupInfo {membership} m memId = do - members <- withStore $ \st -> getGroupMembers st user gInfo + members <- withStore' $ \db -> getGroupMembers db user gInfo if memberId (membership :: GroupMember) == memId then do mapM_ deleteMemberConnection members - withStore $ \st -> updateGroupMemberStatus st userId membership GSMemRemoved + withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved toView $ CRDeletedMemberUser gInfo m else case find (sameMemberId memId) members of Nothing -> messageError "x.grp.mem.del with unknown member ID" @@ -1867,7 +1872,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage then messageError "x.grp.mem.del with insufficient member permissions" else do deleteMemberConnection member - withStore $ \st -> updateGroupMemberStatus st userId member GSMemRemoved + withStore' $ \db -> updateGroupMemberStatus db userId member GSMemRemoved toView $ CRDeletedMember gInfo m member sameMemberId :: MemberId -> GroupMember -> Bool @@ -1876,15 +1881,15 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage xGrpLeave :: GroupInfo -> GroupMember -> m () xGrpLeave gInfo m = do deleteMemberConnection m - withStore $ \st -> updateGroupMemberStatus st userId m GSMemLeft + withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft toView $ CRLeftMember gInfo m xGrpDel :: GroupInfo -> GroupMember -> m () xGrpDel gInfo m@GroupMember {memberRole} = do when (memberRole /= GROwner) $ throwChatError CEGroupUserRole - ms <- withStore $ \st -> do - members <- getGroupMembers st user gInfo - updateGroupMemberStatus st userId (membership gInfo) GSMemGroupDeleted + ms <- withStore' $ \db -> do + members <- getGroupMembers db user gInfo + updateGroupMemberStatus db userId (membership gInfo) GSMemGroupDeleted pure members mapM_ deleteMemberConnection ms toView $ CRGroupDeleted gInfo m @@ -1895,13 +1900,13 @@ parseChatMessage = first (ChatError . CEInvalidChatMessage) . strDecode sendFileChunk :: ChatMonad m => User -> SndFileTransfer -> m () sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId acId} = unless (fileStatus == FSComplete || fileStatus == FSCancelled) $ - withStore (`createSndFileChunk` ft) >>= \case + withStore' (`createSndFileChunk` ft) >>= \case Just chunkNo -> sendFileChunkNo ft chunkNo Nothing -> do - ci <- withStore $ \st -> do - updateSndFileStatus st ft FSComplete - deleteSndFileChunks st ft - updateDirectCIFileStatus st user fileId CIFSSndComplete + ci <- withStore $ \db -> do + liftIO $ updateSndFileStatus db ft FSComplete + liftIO $ deleteSndFileChunks db ft + updateDirectCIFileStatus db user fileId CIFSSndComplete toView $ CRSndFileComplete ci ft closeFileHandle fileId sndFiles withAgent (`deleteConnection` acId) @@ -1910,7 +1915,7 @@ sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m () sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do chunkBytes <- readFileChunk ft chunkNo msgId <- withAgent $ \a -> sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunk {chunkNo, chunkBytes} - withStore $ \st -> updateSndFileChunkMsg st ft chunkNo msgId + withStore' $ \db -> updateSndFileChunkMsg db ft chunkNo msgId readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString readFileChunk SndFileTransfer {fileId, filePath, chunkSize} chunkNo = do @@ -1958,7 +1963,7 @@ appendFileChunk ft@RcvFileTransfer {fileId, fileStatus} chunkNo chunk = h <- getFileHandle fileId fsFilePath rcvFiles AppendMode E.try (liftIO $ B.hPut h chunk >> hFlush h) >>= \case Left (e :: E.SomeException) -> throwChatError . CEFileWrite fsFilePath $ show e - Right () -> withStore $ \st -> updatedRcvFileChunkStored st ft chunkNo + Right () -> withStore' $ \db -> updatedRcvFileChunkStored db ft chunkNo getFileHandle :: ChatMonad m => Int64 -> FilePath -> (ChatController -> TVar (Map Int64 Handle)) -> IOMode -> m Handle getFileHandle fileId filePath files ioMode = do @@ -1980,10 +1985,10 @@ isFileActive fileId files = do cancelRcvFileTransfer :: ChatMonad m => User -> RcvFileTransfer -> m () cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, fileStatus} = do closeFileHandle fileId rcvFiles - withStore $ \st -> do - updateFileCancelled st user fileId CIFSRcvCancelled - updateRcvFileStatus st ft FSCancelled - deleteRcvFileChunks st ft + withStore' $ \db -> do + updateFileCancelled db user fileId CIFSRcvCancelled + updateRcvFileStatus db ft FSCancelled + deleteRcvFileChunks db ft case fileStatus of RFSAccepted RcvFileInfo {agentConnId = AgentConnId acId} -> withAgent (`deleteConnection` acId) @@ -1993,15 +1998,15 @@ cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, fileStatus} = do cancelSndFile :: ChatMonad m => User -> FileTransferMeta -> [SndFileTransfer] -> m () cancelSndFile user FileTransferMeta {fileId} fts = do - withStore $ \st -> updateFileCancelled st user fileId CIFSSndCancelled + withStore' $ \db -> updateFileCancelled db user fileId CIFSSndCancelled forM_ fts $ \ft' -> cancelSndFileTransfer ft' cancelSndFileTransfer :: ChatMonad m => SndFileTransfer -> m () cancelSndFileTransfer ft@SndFileTransfer {agentConnId = AgentConnId acId, fileStatus} = unless (fileStatus == FSCancelled || fileStatus == FSComplete) $ do - withStore $ \st -> do - updateSndFileStatus st ft FSCancelled - deleteSndFileChunks st ft + withStore' $ \db -> do + updateSndFileStatus db ft FSCancelled + deleteSndFileChunks db ft withAgent $ \a -> do void (sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunkCancel) `catchError` \_ -> pure () deleteConnection a acId @@ -2019,8 +2024,8 @@ deleteMemberConnection :: ChatMonad m => GroupMember -> m () deleteMemberConnection m@GroupMember {activeConn} = do -- User {userId} <- asks currentUser withAgent (forM_ (memberConnId m) . deleteConnection) `catchError` const (pure ()) - -- withStore $ \st -> deleteGroupMemberConnection st userId m - forM_ activeConn $ \conn -> withStore $ \st -> updateConnectionStatus st conn ConnDeleted + -- withStore $ \db -> deleteGroupMemberConnection db userId m + forM_ activeConn $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted sendDirectContactMessage :: ChatMonad m => Contact -> ChatMsgEvent -> m SndMessage sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}} chatMsgEvent = do @@ -2037,7 +2042,7 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do createSndMessage :: ChatMonad m => ChatMsgEvent -> ConnOrGroupId -> m SndMessage createSndMessage chatMsgEvent connOrGroupId = do gVar <- asks idsDrg - withStore $ \st -> createNewSndMessage st gVar connOrGroupId $ \sharedMsgId -> + withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId -> let msgBody = strEncode ChatMessage {msgId = Just sharedMsgId, chatMsgEvent} in NewMessage {chatMsgEvent, msgBody} @@ -2049,7 +2054,7 @@ deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do let msgFlags = MsgFlags {notification = hasNotification cmEventTag} agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} - withStore $ \st -> createSndMsgDelivery st sndMsgDelivery msgId + withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId sendGroupMessage :: ChatMonad m => GroupInfo -> [GroupMember] -> ChatMsgEvent -> m SndMessage sendGroupMessage GroupInfo {groupId} members chatMsgEvent = @@ -2058,7 +2063,7 @@ sendGroupMessage GroupInfo {groupId} members chatMsgEvent = sendXGrpMemInv :: ChatMonad m => GroupInfo -> GroupMember -> ChatMsgEvent -> Int64 -> m SndMessage sendXGrpMemInv GroupInfo {groupId} reMember chatMsgEvent introId = sendGroupMessage' [reMember] chatMsgEvent groupId (Just introId) $ - withStore (\st -> updateIntroStatus st introId GMIntroInvForwarded) + withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded sendGroupMessage' :: ChatMonad m => [GroupMember] -> ChatMsgEvent -> Int64 -> Maybe Int64 -> m () -> m SndMessage sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do @@ -2066,25 +2071,25 @@ sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do -- TODO collect failed deliveries into a single error forM_ (filter memberCurrent members) $ \m@GroupMember {groupMemberId} -> case memberConn m of - Nothing -> withStore $ \st -> createPendingGroupMessage st groupMemberId msgId introId_ + Nothing -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ Just conn@Connection {connStatus} | connStatus == ConnSndReady || connStatus == ConnReady -> do let tag = toCMEventTag chatMsgEvent (deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ()) | connStatus == ConnDeleted -> pure () - | otherwise -> withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_) + | otherwise -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ pure msg sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m () sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do - pendingMessages <- withStore $ \st -> getPendingGroupMessages st groupMemberId + pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId -- TODO ensure order - pending messages interleave with user input messages forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} -> do deliverMessage conn cmEventTag msgBody msgId - withStore (\st -> deletePendingGroupMessage st groupMemberId msgId) + withStore' $ \db -> deletePendingGroupMessage db groupMemberId msgId when (cmEventTag == XGrpMemFwd_) $ case introId_ of Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName - Just introId -> withStore (\st -> updateIntroStatus st introId GMIntroInvForwarded) + Just introId -> withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> m RcvMessage saveRcvMSG Connection {connId} connOrGroupId agentMsgMeta msgBody = do @@ -2092,13 +2097,13 @@ saveRcvMSG Connection {connId} connOrGroupId agentMsgMeta msgBody = do let agentMsgId = fst $ recipient agentMsgMeta newMsg = NewMessage {chatMsgEvent, msgBody} rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} - withStore $ \st -> createNewMessageAndRcvMsgDelivery st connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery + withStore' $ \db -> createNewMessageAndRcvMsgDelivery db connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> m (ChatItem c 'MDSnd) saveSndChatItem user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem = do createdAt <- liftIO getCurrentTime - ciId <- withStore $ \st -> createNewSndChatItem st user cd msg content quotedItem createdAt - forM_ ciFile $ \CIFile {fileId} -> withStore $ \st -> updateFileTransferChatItemId st fileId ciId + ciId <- withStore' $ \db -> createNewSndChatItem db user cd msg content quotedItem createdAt + forM_ ciFile $ \CIFile {fileId} -> withStore' $ \db -> updateFileTransferChatItemId db fileId ciId liftIO $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) createdAt createdAt saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> MsgMeta -> CIContent 'MDRcv -> Maybe (CIFile 'MDRcv) -> m (ChatItem c 'MDRcv) @@ -2107,8 +2112,8 @@ saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} = saveRcvChatItem' user cd saveRcvChatItem' :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> MsgMeta -> CIContent 'MDRcv -> Maybe (CIFile 'MDRcv) -> m (ChatItem c 'MDRcv) saveRcvChatItem' user cd msg sharedMsgId_ MsgMeta {broker = (_, brokerTs)} content ciFile = do createdAt <- liftIO getCurrentTime - (ciId, quotedItem) <- withStore $ \st -> createNewRcvChatItem st user cd msg sharedMsgId_ content brokerTs createdAt - forM_ ciFile $ \CIFile {fileId} -> withStore $ \st -> updateFileTransferChatItemId st fileId ciId + (ciId, quotedItem) <- withStore' $ \db -> createNewRcvChatItem db user cd msg sharedMsgId_ content brokerTs createdAt + forM_ ciFile $ \CIFile {fileId} -> withStore' $ \db -> updateFileTransferChatItemId db fileId ciId liftIO $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ brokerTs createdAt mkChatItem :: MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> ChatItemTs -> UTCTime -> IO (ChatItem c d) @@ -2121,12 +2126,12 @@ mkChatItem cd ciId content file quotedItem sharedMsgId itemTs currentTs = do allowAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m () allowAgentConnection conn confId msg = do withAgent $ \a -> allowConnection a (aConnId conn) confId $ directMessage msg - withStore $ \st -> updateConnectionStatus st conn ConnAccepted + withStore' $ \db -> updateConnectionStatus db conn ConnAccepted getCreateActiveUser :: SQLiteStore -> IO User getCreateActiveUser st = do user <- - getUsers st >>= \case + withTransaction st getUsers >>= \case [] -> newUser users -> maybe (selectUser users) pure (find activeUser users) putStrLn $ "Current user: " <> userStr user @@ -2144,7 +2149,7 @@ getCreateActiveUser st = do loop = do displayName <- getContactName fullName <- T.pack <$> getWithPrompt "full name (optional)" - liftIO (runExceptT $ createUser st Profile {displayName, fullName, image = Nothing} True) >>= \case + withTransaction st (\db -> runExceptT $ createUser db Profile {displayName, fullName, image = Nothing} True) >>= \case Left SEDuplicateName -> do putStrLn "chosen display name is already used by another profile on this device, choose another one" loop @@ -2152,7 +2157,7 @@ getCreateActiveUser st = do Right user -> pure user selectUser :: [User] -> IO User selectUser [user] = do - liftIO $ setActiveUser st (userId user) + withTransaction st (`setActiveUser` userId user) pure user selectUser users = do putStrLn "Select user profile:" @@ -2167,7 +2172,7 @@ getCreateActiveUser st = do | n <= 0 || n > length users -> putStrLn "invalid user number" >> loop | otherwise -> do let user = users !! (n - 1) - liftIO $ setActiveUser st (userId user) + withTransaction st (`setActiveUser` userId user) pure user userStr :: User -> String userStr User {localDisplayName, profile = Profile {fullName}} = @@ -2214,16 +2219,20 @@ withAgent action = >>= runExceptT . action >>= liftEither . first ChatErrorAgent +withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a +withStore' action = withStore $ liftIO . action + withStore :: ChatMonad m => - (forall m'. (MonadUnliftIO m', MonadError StoreError m') => SQLiteStore -> m' a) -> + (DB.Connection -> ExceptT StoreError IO a) -> m a -withStore action = - asks chatStore - >>= runExceptT . action - -- use this line instead of above to log query errors - -- >>= (\st -> runExceptT $ action st `E.catch` \(e :: E.SomeException) -> liftIO (print e) >> E.throwIO e) - >>= liftEither . first ChatErrorStore +withStore action = do + st <- asks chatStore + liftEitherError ChatErrorStore $ + withTransaction st (runExceptT . action) `E.catch` handleInternal + where + handleInternal :: DB.SQLError -> IO (Either StoreError a) + handleInternal = pure . Left . SEInternalError . show chatCommandP :: Parser ChatCommand chatCommandP = diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index c8f05c4bec..4cd85330dd 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -85,7 +85,7 @@ defaultMobileConfig = type CJSONString = CString getActiveUser_ :: SQLiteStore -> IO (Maybe User) -getActiveUser_ st = find activeUser <$> getUsers st +getActiveUser_ st = find activeUser <$> withTransaction st getUsers chatInit :: String -> IO ChatController chatInit dbFilePrefix = do diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 14a4ab61b1..46da813a49 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -30,6 +30,7 @@ module Simplex.Chat.Store getConnReqContactXContactId, createDirectContact, getContactGroupNames, + deleteContactConnectionsAndFiles, deleteContact, getContactByName, getContact, @@ -160,6 +161,7 @@ module Simplex.Chat.Store overwriteSMPServers, getPendingContactConnection, deletePendingContactConnection, + withTransaction, ) where @@ -168,14 +170,13 @@ import Control.Concurrent.STM (stateTVar) import Control.Exception (Exception) import qualified Control.Exception as E import Control.Monad.Except -import Control.Monad.IO.Unlift import Crypto.Random (ChaChaDRG, randomBytesGenerate) import Data.Aeson (ToJSON) import qualified Data.Aeson as J import Data.Bifunctor (first) import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) -import Data.Either (isRight, rights) +import Data.Either (rights) import Data.Function (on) import Data.Functor (($>)) import Data.Int (Int64) @@ -207,13 +208,13 @@ import Simplex.Chat.Migrations.M20220514_profiles_user_id import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, withTransaction) +import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String (StrEncoding (strEncode)) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer) -import Simplex.Messaging.Util (eitherToMaybe, liftIOEither, (<$$>)) +import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM schemaMigrations :: [(String, Query)] @@ -243,8 +244,8 @@ createStore dbFilePath = createSQLiteStore dbFilePath migrations chatStoreFile :: FilePath -> FilePath chatStoreFile = (<> "_chat.db") -checkConstraint :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a) -checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err) +checkConstraint :: StoreError -> ExceptT StoreError IO a -> ExceptT StoreError IO a +checkConstraint err action = ExceptT $ runExceptT action `E.catch` (pure . Left . handleSQLError err) handleSQLError :: StoreError -> SQLError -> StoreError handleSQLError err e @@ -254,11 +255,9 @@ handleSQLError err e insertedRowId :: DB.Connection -> IO Int64 insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()" -type StoreMonad m = (MonadUnliftIO m, MonadError StoreError m) - -createUser :: StoreMonad m => SQLiteStore -> Profile -> Bool -> m User -createUser st Profile {displayName, fullName, image} activeUser = - liftIOEither . checkConstraint SEDuplicateName . withTransaction st $ \db -> do +createUser :: DB.Connection -> Profile -> Bool -> ExceptT StoreError IO User +createUser db Profile {displayName, fullName, image} activeUser = + checkConstraint SEDuplicateName . liftIO $ do currentTs <- getCurrentTime DB.execute db @@ -280,60 +279,56 @@ createUser st Profile {displayName, fullName, image} activeUser = (profileId, displayName, userId, True, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure . Right $ toUser (userId, contactId, activeUser, displayName, fullName, image) + pure $ toUser (userId, contactId, activeUser, displayName, fullName, image) -getUsers :: SQLiteStore -> IO [User] -getUsers st = - withTransaction st $ \db -> - map toUser - <$> DB.query_ - db - [sql| - SELECT u.user_id, u.contact_id, u.active_user, u.local_display_name, p.full_name, p.image - FROM users u - JOIN contacts c ON u.contact_id = c.contact_id - JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id - |] +getUsers :: DB.Connection -> IO [User] +getUsers db = + map toUser + <$> DB.query_ + db + [sql| + SELECT u.user_id, u.contact_id, u.active_user, u.local_display_name, p.full_name, p.image + FROM users u + JOIN contacts c ON u.contact_id = c.contact_id + JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id + |] toUser :: (UserId, Int64, Bool, ContactName, Text, Maybe ImageData) -> User toUser (userId, userContactId, activeUser, displayName, fullName, image) = let profile = Profile {displayName, fullName, image} in User {userId, userContactId, localDisplayName = displayName, profile, activeUser} -setActiveUser :: MonadUnliftIO m => SQLiteStore -> UserId -> m () -setActiveUser st userId = do - liftIO . withTransaction st $ \db -> do - DB.execute_ db "UPDATE users SET active_user = 0" - DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId) +setActiveUser :: DB.Connection -> UserId -> IO () +setActiveUser db userId = do + DB.execute_ db "UPDATE users SET active_user = 0" + DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId) -createConnReqConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> m PendingContactConnection -createConnReqConnection st userId acId cReqHash xContactId = do - liftIO . withTransaction st $ \db -> do - createdAt <- getCurrentTime - let pccConnStatus = ConnJoined - DB.execute - db - [sql| - INSERT INTO connections ( - user_id, agent_conn_id, conn_status, conn_type, - created_at, updated_at, via_contact_uri_hash, xcontact_id - ) VALUES (?,?,?,?,?,?,?,?) - |] - (userId, acId, pccConnStatus, ConnContact, createdAt, createdAt, cReqHash, xContactId) - pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, createdAt, updatedAt = createdAt} +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> IO PendingContactConnection +createConnReqConnection db userId acId cReqHash xContactId = do + createdAt <- getCurrentTime + let pccConnStatus = ConnJoined + DB.execute + db + [sql| + INSERT INTO connections ( + user_id, agent_conn_id, conn_status, conn_type, + created_at, updated_at, via_contact_uri_hash, xcontact_id + ) VALUES (?,?,?,?,?,?,?,?) + |] + (userId, acId, pccConnStatus, ConnContact, createdAt, createdAt, cReqHash, xContactId) + pccConnId <- insertedRowId db + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, createdAt, updatedAt = createdAt} -getConnReqContactXContactId :: MonadUnliftIO m => SQLiteStore -> UserId -> ConnReqUriHash -> m (Maybe Contact, Maybe XContactId) -getConnReqContactXContactId st userId cReqHash = do - liftIO . withTransaction st $ \db -> - getContact' db >>= \case - c@(Just _) -> pure (c, Nothing) - Nothing -> (Nothing,) <$> getXContactId db +getConnReqContactXContactId :: DB.Connection -> UserId -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) +getConnReqContactXContactId db userId cReqHash = do + getContact' >>= \case + c@(Just _) -> pure (c, Nothing) + Nothing -> (Nothing,) <$> getXContactId where - getContact' :: DB.Connection -> IO (Maybe Contact) - getContact' db = - fmap toContact . listToMaybe - <$> DB.query + getContact' :: IO (Maybe Contact) + getContact' = + maybeFirstRow toContact $ + DB.query db [sql| SELECT @@ -350,27 +345,26 @@ getConnReqContactXContactId st userId cReqHash = do LIMIT 1 |] (userId, cReqHash) - getXContactId :: DB.Connection -> IO (Maybe XContactId) - getXContactId db = - fmap fromOnly . listToMaybe - <$> DB.query + getXContactId :: IO (Maybe XContactId) + getXContactId = + maybeFirstRow fromOnly $ + DB.query db "SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1" (userId, cReqHash) -createDirectConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> ConnId -> ConnStatus -> m PendingContactConnection -createDirectConnection st userId acId pccConnStatus = - liftIO . withTransaction st $ \db -> do - createdAt <- getCurrentTime - DB.execute - db - [sql| - INSERT INTO connections - (user_id, agent_conn_id, conn_status, conn_type, created_at, updated_at) VALUES (?,?,?,?,?,?) - |] - (userId, acId, pccConnStatus, ConnContact, createdAt, createdAt) - pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, createdAt, updatedAt = createdAt} +createDirectConnection :: DB.Connection -> UserId -> ConnId -> ConnStatus -> IO PendingContactConnection +createDirectConnection db userId acId pccConnStatus = do + createdAt <- getCurrentTime + DB.execute + db + [sql| + INSERT INTO connections + (user_id, agent_conn_id, conn_status, conn_type, created_at, updated_at) VALUES (?,?,?,?,?,?) + |] + (userId, acId, pccConnStatus, ConnContact, createdAt, createdAt) + pccConnId <- insertedRowId db + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, createdAt, updatedAt = createdAt} createContactConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection createContactConnection_ db userId = createConnection_ db userId ConnContact Nothing @@ -393,16 +387,15 @@ createConnection_ db userId connType entityId acId viaContact connLevel currentT where ent ct = if connType == ct then entityId else Nothing -createDirectContact :: StoreMonad m => SQLiteStore -> UserId -> Connection -> Profile -> m Contact -createDirectContact st userId activeConn@Connection {connId} profile = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - createdAt <- liftIO getCurrentTime - (localDisplayName, contactId, _) <- ExceptT $ createContact_ db userId connId profile Nothing createdAt - pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, createdAt, updatedAt = createdAt} +createDirectContact :: DB.Connection -> UserId -> Connection -> Profile -> ExceptT StoreError IO Contact +createDirectContact db userId activeConn@Connection {connId} profile = do + createdAt <- liftIO getCurrentTime + (localDisplayName, contactId, _) <- createContact_ db userId connId profile Nothing createdAt + pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, createdAt, updatedAt = createdAt} -createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> UTCTime -> IO (Either StoreError (Text, Int64, Int64)) +createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, Int64, Int64) createContact_ db userId connId Profile {displayName, fullName, image} viaGroup currentTs = - withLocalDisplayName db userId displayName $ \ldn -> do + ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do DB.execute db "INSERT INTO contact_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" @@ -416,41 +409,40 @@ createContact_ db userId connId Profile {displayName, fullName, image} viaGroup DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId) pure (ldn, contactId, profileId) -getContactGroupNames :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> m [GroupName] -getContactGroupNames st userId Contact {contactId} = - liftIO . withTransaction st $ \db -> do - map fromOnly - <$> DB.query - db - [sql| - SELECT DISTINCT g.local_display_name - FROM groups g - JOIN group_members m ON m.group_id = g.group_id - WHERE g.user_id = ? AND m.contact_id = ? - |] - (userId, contactId) - -deleteContact :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> m () -deleteContact st userId Contact {contactId, localDisplayName} = do - liftIO . withTransaction st $ \db -> do - DB.execute +getContactGroupNames :: DB.Connection -> UserId -> Contact -> IO [GroupName] +getContactGroupNames db userId Contact {contactId} = + map fromOnly + <$> DB.query db [sql| - DELETE FROM connections WHERE connection_id IN ( - SELECT connection_id - FROM connections c - JOIN contacts ct ON ct.contact_id = c.contact_id - WHERE ct.user_id = ? AND ct.contact_id = ? - ) + SELECT DISTINCT g.local_display_name + FROM groups g + JOIN group_members m ON m.group_id = g.group_id + WHERE g.user_id = ? AND m.contact_id = ? |] (userId, contactId) - DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId) - -- in separate transaction to prevent crashes on android (race condition on integrity check?) - liftIO . withTransaction st $ \db -> do - DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) - deleteContactProfile_ db userId contactId - DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId) - DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + +deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () +deleteContactConnectionsAndFiles db userId Contact {contactId} = do + DB.execute + db + [sql| + DELETE FROM connections WHERE connection_id IN ( + SELECT connection_id + FROM connections c + JOIN contacts ct ON ct.contact_id = c.contact_id + WHERE ct.user_id = ? AND ct.contact_id = ? + ) + |] + (userId, contactId) + DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId) + +deleteContact :: DB.Connection -> UserId -> Contact -> IO () +deleteContact db userId Contact {contactId, localDisplayName} = do + DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) + deleteContactProfile_ db userId contactId + DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId) + DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) deleteContactProfile_ :: DB.Connection -> UserId -> ContactId -> IO () deleteContactProfile_ db userId contactId = @@ -466,13 +458,12 @@ deleteContactProfile_ db userId contactId = |] (userId, contactId) -updateUserProfile :: StoreMonad m => SQLiteStore -> User -> Profile -> m () -updateUserProfile st User {userId, userContactId, localDisplayName, profile = Profile {displayName}} p'@Profile {displayName = newName} +updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO () +updateUserProfile db User {userId, userContactId, localDisplayName, profile = Profile {displayName}} p'@Profile {displayName = newName} | displayName == newName = - liftIO . withTransaction st $ \db -> - updateContactProfile_ db userId userContactId p' + liftIO $ updateContactProfile_ db userId userContactId p' | otherwise = - liftIOEither . checkConstraint SEDuplicateName . withTransaction st $ \db -> do + checkConstraint SEDuplicateName . liftIO $ do currentTs <- getCurrentTime DB.execute db "UPDATE users SET local_display_name = ?, updated_at = ? WHERE user_id = ?" (newName, currentTs, userId) DB.execute @@ -481,20 +472,17 @@ updateUserProfile st User {userId, userContactId, localDisplayName, profile = Pr (newName, newName, userId, currentTs, currentTs) updateContactProfile_' db userId userContactId p' currentTs updateContact_ db userId userContactId localDisplayName newName currentTs - pure $ Right () -updateContactProfile :: StoreMonad m => SQLiteStore -> UserId -> Contact -> Profile -> m Contact -updateContactProfile st userId c@Contact {contactId, localDisplayName, profile = Profile {displayName}} p'@Profile {displayName = newName} +updateContactProfile :: DB.Connection -> UserId -> Contact -> Profile -> ExceptT StoreError IO Contact +updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = Profile {displayName}} p'@Profile {displayName = newName} | displayName == newName = - liftIO . withTransaction st $ \db -> - updateContactProfile_ db userId contactId p' $> (c :: Contact) {profile = p'} + liftIO $ updateContactProfile_ db userId contactId p' $> (c :: Contact) {profile = p'} | otherwise = - liftIOEither . withTransaction st $ \db -> - withLocalDisplayName db userId newName $ \ldn -> do - currentTs <- getCurrentTime - updateContactProfile_' db userId contactId p' currentTs - updateContact_ db userId contactId localDisplayName ldn currentTs - pure $ (c :: Contact) {localDisplayName = ldn, profile = p'} + ExceptT . withLocalDisplayName db userId newName $ \ldn -> do + currentTs <- getCurrentTime + updateContactProfile_' db userId contactId p' currentTs + updateContact_ db userId contactId localDisplayName ldn currentTs + pure $ (c :: Contact) {localDisplayName = ldn, profile = p'} updateContactProfile_ :: DB.Connection -> UserId -> Int64 -> Profile -> IO () updateContactProfile_ db userId contactId profile = do @@ -556,34 +544,33 @@ toContactOrError ((contactId, localDisplayName, viaGroup, displayName, fullName, -- TODO return the last connection that is ready, not any last connection -- requires updating connection status -getContactByName :: StoreMonad m => SQLiteStore -> UserId -> ContactName -> m Contact -getContactByName st userId localDisplayName = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - cId <- ExceptT $ getContactIdByName_ db userId localDisplayName - ExceptT $ getContact_ db userId cId +getContactByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Contact +getContactByName db userId localDisplayName = do + cId <- getContactIdByName db userId localDisplayName + getContact db userId cId -getUserContacts :: MonadUnliftIO m => SQLiteStore -> User -> m [Contact] -getUserContacts st User {userId} = - liftIO . withTransaction st $ \db -> do - contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ?" (Only userId) - rights <$> mapM (getContact_ db userId) contactIds +getUserContacts :: DB.Connection -> User -> IO [Contact] +getUserContacts db User {userId} = do + contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ?" (Only userId) + rights <$> mapM (runExceptT . getContact db userId) contactIds -createUserContactLink :: StoreMonad m => SQLiteStore -> UserId -> ConnId -> ConnReqContact -> m () -createUserContactLink st userId agentConnId cReq = - liftIOEither . checkConstraint SEDuplicateContactLink . withTransaction st $ \db -> do +createUserContactLink :: DB.Connection -> UserId -> ConnId -> ConnReqContact -> ExceptT StoreError IO () +createUserContactLink db userId agentConnId cReq = + checkConstraint SEDuplicateContactLink . liftIO $ do currentTs <- getCurrentTime DB.execute db "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" (userId, cReq, currentTs, currentTs) userContactLinkId <- insertedRowId db - Right () <$ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing 0 currentTs + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing 0 currentTs -getUserContactLinkConnections :: StoreMonad m => SQLiteStore -> UserId -> m [Connection] -getUserContactLinkConnections st userId = - liftIOEither . withTransaction st $ \db -> - connections - <$> DB.queryNamed +getUserContactLinkConnections :: DB.Connection -> UserId -> ExceptT StoreError IO [Connection] +getUserContactLinkConnections db userId = + connections =<< liftIO getConnections + where + getConnections = + DB.queryNamed db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, @@ -595,61 +582,54 @@ getUserContactLinkConnections st userId = AND uc.local_display_name = '' |] [":user_id" := userId] - where - connections [] = Left SEUserContactLinkNotFound - connections rows = Right $ map toConnection rows + connections [] = throwError SEUserContactLinkNotFound + connections rows = pure $ map toConnection rows -deleteUserContactLink :: MonadUnliftIO m => SQLiteStore -> UserId -> m () -deleteUserContactLink st userId = - liftIO . withTransaction st $ \db -> do - DB.execute - db - [sql| - DELETE FROM connections WHERE connection_id IN ( - SELECT connection_id - FROM connections c - JOIN user_contact_links uc USING (user_contact_link_id) - WHERE uc.user_id = ? AND uc.local_display_name = '' - ) - |] - (Only userId) - DB.executeNamed - db - [sql| - DELETE FROM display_names - WHERE user_id = :user_id - AND local_display_name in ( - SELECT cr.local_display_name - FROM contact_requests cr - JOIN user_contact_links uc USING (user_contact_link_id) - WHERE uc.user_id = :user_id - AND uc.local_display_name = '' - ) - |] - [":user_id" := userId] - DB.executeNamed - db - [sql| - DELETE FROM contact_profiles - WHERE contact_profile_id in ( - SELECT cr.contact_profile_id +deleteUserContactLink :: DB.Connection -> UserId -> IO () +deleteUserContactLink db userId = do + DB.execute + db + [sql| + DELETE FROM connections WHERE connection_id IN ( + SELECT connection_id + FROM connections c + JOIN user_contact_links uc USING (user_contact_link_id) + WHERE uc.user_id = ? AND uc.local_display_name = '' + ) + |] + (Only userId) + DB.executeNamed + db + [sql| + DELETE FROM display_names + WHERE user_id = :user_id + AND local_display_name in ( + SELECT cr.local_display_name FROM contact_requests cr JOIN user_contact_links uc USING (user_contact_link_id) WHERE uc.user_id = :user_id AND uc.local_display_name = '' ) - |] - [":user_id" := userId] - DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = ''" (Only userId) + |] + [":user_id" := userId] + DB.executeNamed + db + [sql| + DELETE FROM contact_profiles + WHERE contact_profile_id in ( + SELECT cr.contact_profile_id + FROM contact_requests cr + JOIN user_contact_links uc USING (user_contact_link_id) + WHERE uc.user_id = :user_id + AND uc.local_display_name = '' + ) + |] + [":user_id" := userId] + DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = ''" (Only userId) -getUserContactLink :: StoreMonad m => SQLiteStore -> UserId -> m (ConnReqContact, Bool) -getUserContactLink st userId = - liftIOEither . withTransaction st $ \db -> - getUserContactLink_ db userId - -getUserContactLink_ :: DB.Connection -> UserId -> IO (Either StoreError (ConnReqContact, Bool)) -getUserContactLink_ db userId = - firstRow id SEUserContactLinkNotFound $ +getUserContactLink :: DB.Connection -> UserId -> ExceptT StoreError IO (ConnReqContact, Bool) +getUserContactLink db userId = + ExceptT . firstRow id SEUserContactLinkNotFound $ DB.query db [sql| @@ -660,15 +640,14 @@ getUserContactLink_ db userId = |] (Only userId) -updateUserContactLinkAutoAccept :: StoreMonad m => SQLiteStore -> UserId -> Bool -> m (ConnReqContact, Bool) -updateUserContactLinkAutoAccept st userId autoAccept = do - liftIOEither . withTransaction st $ \db -> runExceptT $ do - (cReqUri, _) <- ExceptT $ getUserContactLink_ db userId - liftIO $ updateUserContactLinkAutoAccept_ db - pure (cReqUri, autoAccept) +updateUserContactLinkAutoAccept :: DB.Connection -> UserId -> Bool -> ExceptT StoreError IO (ConnReqContact, Bool) +updateUserContactLinkAutoAccept db userId autoAccept = do + (cReqUri, _) <- getUserContactLink db userId + liftIO updateUserContactLinkAutoAccept_ + pure (cReqUri, autoAccept) where - updateUserContactLinkAutoAccept_ :: DB.Connection -> IO () - updateUserContactLinkAutoAccept_ db = + updateUserContactLinkAutoAccept_ :: IO () + updateUserContactLinkAutoAccept_ = DB.execute db [sql| @@ -679,26 +658,21 @@ updateUserContactLinkAutoAccept st userId autoAccept = do |] (autoAccept, userId) -createOrUpdateContactRequest :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> m ContactOrRequest -createOrUpdateContactRequest st userId userContactLinkId invId profile xContactId_ = - liftIOEither . withTransaction st $ \db -> - createOrUpdateContactRequest_ db userId userContactLinkId invId profile xContactId_ - -createOrUpdateContactRequest_ :: DB.Connection -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> IO (Either StoreError ContactOrRequest) -createOrUpdateContactRequest_ db userId userContactLinkId invId Profile {displayName, fullName, image} xContactId_ = - maybeM getContact' xContactId_ >>= \case - Just contact -> pure . Right $ CORContact contact - Nothing -> CORRequest <$$> createOrUpdate_ +createOrUpdateContactRequest :: DB.Connection -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest +createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayName, fullName, image} xContactId_ = + liftIO (maybeM getContact' xContactId_) >>= \case + Just contact -> pure $ CORContact contact + Nothing -> CORRequest <$> createOrUpdate_ where maybeM = maybe (pure Nothing) - createOrUpdate_ :: IO (Either StoreError UserContactRequest) - createOrUpdate_ = runExceptT $ do + createOrUpdate_ :: ExceptT StoreError IO UserContactRequest + createOrUpdate_ = do cReqId <- ExceptT $ maybeM getContactRequest' xContactId_ >>= \case Nothing -> createContactRequest Just cr -> updateContactRequest cr $> Right (contactRequestId (cr :: UserContactRequest)) - ExceptT $ getContactRequest_ db userId cReqId + getContactRequest db userId cReqId createContactRequest :: IO (Either StoreError Int64) createContactRequest = do currentTs <- getCurrentTime @@ -721,8 +695,8 @@ createOrUpdateContactRequest_ db userId userContactLinkId invId Profile {display insertedRowId db getContact' :: XContactId -> IO (Maybe Contact) getContact' xContactId = - fmap toContact . listToMaybe - <$> DB.query + maybeFirstRow toContact $ + DB.query db [sql| SELECT @@ -741,8 +715,8 @@ createOrUpdateContactRequest_ db userId userContactLinkId invId Profile {display (userId, xContactId) getContactRequest' :: XContactId -> IO (Maybe UserContactRequest) getContactRequest' xContactId = - fmap toContactRequest . listToMaybe - <$> DB.query + maybeFirstRow toContactRequest $ + DB.query db [sql| SELECT @@ -784,14 +758,9 @@ createOrUpdateContactRequest_ db userId userContactLinkId invId Profile {display |] (displayName, fullName, image, currentTs, userId, cReqId) -getContactRequest :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m UserContactRequest -getContactRequest st userId contactRequestId = - liftIOEither . withTransaction st $ \db -> - getContactRequest_ db userId contactRequestId - -getContactRequest_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError UserContactRequest) -getContactRequest_ db userId contactRequestId = - firstRow toContactRequest (SEContactRequestNotFound contactRequestId) $ +getContactRequest :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO UserContactRequest +getContactRequest db userId contactRequestId = + ExceptT . firstRow toContactRequest (SEContactRequestNotFound contactRequestId) $ DB.query db [sql| @@ -813,121 +782,115 @@ toContactRequest (contactRequestId, localDisplayName, agentInvitationId, userCon let profile = Profile {displayName, fullName, image} in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt} -getContactRequestIdByName :: StoreMonad m => SQLiteStore -> UserId -> ContactName -> m Int64 -getContactRequestIdByName st userId cName = - liftIOEither . withTransaction st $ \db -> - firstRow fromOnly (SEContactRequestNotFoundByName cName) $ - DB.query db "SELECT contact_request_id FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, cName) +getContactRequestIdByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Int64 +getContactRequestIdByName db userId cName = + ExceptT . firstRow fromOnly (SEContactRequestNotFoundByName cName) $ + DB.query db "SELECT contact_request_id FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, cName) -deleteContactRequest :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> m () -deleteContactRequest st userId contactRequestId = - liftIO . withTransaction st $ \db -> do - DB.execute - db - [sql| - DELETE FROM contact_profiles - WHERE contact_profile_id in ( - SELECT contact_profile_id - FROM contact_requests - WHERE user_id = ? AND contact_request_id = ? - ) - |] - (userId, contactRequestId) - DB.execute - db - [sql| - DELETE FROM display_names - WHERE user_id = ? AND local_display_name = ( - SELECT local_display_name FROM contact_requests - WHERE user_id = ? AND contact_request_id = ? - ) - |] - (userId, userId, contactRequestId) - DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) +deleteContactRequest :: DB.Connection -> UserId -> Int64 -> IO () +deleteContactRequest db userId contactRequestId = do + DB.execute + db + [sql| + DELETE FROM contact_profiles + WHERE contact_profile_id in ( + SELECT contact_profile_id + FROM contact_requests + WHERE user_id = ? AND contact_request_id = ? + ) + |] + (userId, contactRequestId) + DB.execute + db + [sql| + DELETE FROM display_names + WHERE user_id = ? AND local_display_name = ( + SELECT local_display_name FROM contact_requests + WHERE user_id = ? AND contact_request_id = ? + ) + |] + (userId, userId, contactRequestId) + DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: MonadUnliftIO m => SQLiteStore -> UserId -> ConnId -> ContactName -> Int64 -> Profile -> Maybe XContactId -> m Contact -createAcceptedContact st userId agentConnId localDisplayName profileId profile xContactId = - liftIO . withTransaction st $ \db -> do - DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?)" - (userId, localDisplayName, profileId, currentTs, currentTs, xContactId) - contactId <- insertedRowId db - activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing 0 currentTs - pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, createdAt = currentTs, updatedAt = currentTs} +createAcceptedContact :: DB.Connection -> UserId -> ConnId -> ContactName -> Int64 -> Profile -> Maybe XContactId -> IO Contact +createAcceptedContact db userId agentConnId localDisplayName profileId profile xContactId = do + DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?)" + (userId, localDisplayName, profileId, currentTs, currentTs, xContactId) + contactId <- insertedRowId db + activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing 0 currentTs + pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, createdAt = currentTs, updatedAt = currentTs} -getLiveSndFileTransfers :: MonadUnliftIO m => SQLiteStore -> User -> m [SndFileTransfer] -getLiveSndFileTransfers st User {userId} = - liftIO . withTransaction st $ \db -> do - fileIds :: [Int64] <- - map fromOnly - <$> DB.query - db - [sql| - SELECT DISTINCT f.file_id - FROM files f - JOIN snd_files s - WHERE f.user_id = ? AND s.file_status IN (?, ?, ?) - |] - (userId, FSNew, FSAccepted, FSConnected) - concatMap (filter liveTransfer) . rights <$> mapM (getSndFileTransfers_ db userId) fileIds - where - liveTransfer :: SndFileTransfer -> Bool - liveTransfer SndFileTransfer {fileStatus} = fileStatus `elem` [FSNew, FSAccepted, FSConnected] - -getLiveRcvFileTransfers :: MonadUnliftIO m => SQLiteStore -> User -> m [RcvFileTransfer] -getLiveRcvFileTransfers st User {userId} = - liftIO . withTransaction st $ \db -> do - fileIds :: [Int64] <- - map fromOnly - <$> DB.query - db - [sql| - SELECT f.file_id - FROM files f - JOIN rcv_files r - WHERE f.user_id = ? AND r.file_status IN (?, ?) - |] - (userId, FSAccepted, FSConnected) - rights <$> mapM (getRcvFileTransfer_ db userId) fileIds - -getPendingSndChunks :: MonadUnliftIO m => SQLiteStore -> Int64 -> Int64 -> m [Integer] -getPendingSndChunks st fileId connId = - liftIO . withTransaction st $ \db -> +getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] +getLiveSndFileTransfers db User {userId} = do + fileIds :: [Int64] <- map fromOnly <$> DB.query db [sql| - SELECT chunk_number - FROM snd_file_chunks - WHERE file_id = ? AND connection_id = ? AND chunk_agent_msg_id IS NULL - ORDER BY chunk_number + SELECT DISTINCT f.file_id + FROM files f + JOIN snd_files s + WHERE f.user_id = ? AND s.file_status IN (?, ?, ?) |] - (fileId, connId) + (userId, FSNew, FSAccepted, FSConnected) + concatMap (filter liveTransfer) . rights <$> mapM (getSndFileTransfers_ db userId) fileIds + where + liveTransfer :: SndFileTransfer -> Bool + liveTransfer SndFileTransfer {fileStatus} = fileStatus `elem` [FSNew, FSAccepted, FSConnected] -getPendingConnections :: MonadUnliftIO m => SQLiteStore -> User -> m [Connection] -getPendingConnections st User {userId} = - liftIO . withTransaction st $ \db -> - map toConnection - <$> DB.queryNamed +getLiveRcvFileTransfers :: DB.Connection -> User -> IO [RcvFileTransfer] +getLiveRcvFileTransfers db user@User {userId} = do + fileIds :: [Int64] <- + map fromOnly + <$> DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_level, via_contact, - conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at - FROM connections - WHERE user_id = :user_id - AND conn_type = :conn_type - AND contact_id IS NULL + SELECT f.file_id + FROM files f + JOIN rcv_files r + WHERE f.user_id = ? AND r.file_status IN (?, ?) |] - [":user_id" := userId, ":conn_type" := ConnContact] + (userId, FSAccepted, FSConnected) + rights <$> mapM (runExceptT . getRcvFileTransfer db user) fileIds -getContactConnections :: StoreMonad m => SQLiteStore -> UserId -> Contact -> m [Connection] -getContactConnections st userId Contact {contactId} = - liftIOEither . withTransaction st $ \db -> - connections - <$> DB.query +getPendingSndChunks :: DB.Connection -> Int64 -> Int64 -> IO [Integer] +getPendingSndChunks db fileId connId = + map fromOnly + <$> DB.query + db + [sql| + SELECT chunk_number + FROM snd_file_chunks + WHERE file_id = ? AND connection_id = ? AND chunk_agent_msg_id IS NULL + ORDER BY chunk_number + |] + (fileId, connId) + +getPendingConnections :: DB.Connection -> User -> IO [Connection] +getPendingConnections db User {userId} = + map toConnection + <$> DB.queryNamed + db + [sql| + SELECT connection_id, agent_conn_id, conn_level, via_contact, + conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at + FROM connections + WHERE user_id = :user_id + AND conn_type = :conn_type + AND contact_id IS NULL + |] + [":user_id" := userId, ":conn_type" := ConnContact] + +getContactConnections :: DB.Connection -> UserId -> Contact -> ExceptT StoreError IO [Connection] +getContactConnections db userId Contact {contactId} = + connections =<< liftIO getConnections_ + where + getConnections_ = + DB.query db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, @@ -937,9 +900,8 @@ getContactConnections st userId Contact {contactId} = WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ? |] (userId, userId, contactId) - where - connections [] = Left $ SEContactNotFound contactId - connections rows = Right $ map toConnection rows + connections [] = throwError $ SEContactNotFound contactId + connections rows = pure $ map toConnection rows type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, UTCTime) @@ -962,171 +924,163 @@ toMaybeConnection (Just connId, Just agentConnId, Just connLevel, viaContact, Ju Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt) toMaybeConnection _ = Nothing -getMatchingContacts :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> m [Contact] -getMatchingContacts st userId Contact {contactId, profile = Profile {displayName, fullName, image}} = - liftIO . withTransaction st $ \db -> do - contactIds <- - map fromOnly - <$> DB.queryNamed - db - [sql| - SELECT ct.contact_id - FROM contacts ct - JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id - WHERE ct.user_id = :user_id AND ct.contact_id != :contact_id - AND p.display_name = :display_name AND p.full_name = :full_name - AND ((p.image IS NULL AND :image IS NULL) OR p.image = :image) - |] - [ ":user_id" := userId, - ":contact_id" := contactId, - ":display_name" := displayName, - ":full_name" := fullName, - ":image" := image - ] - rights <$> mapM (getContact_ db userId) contactIds - -createSentProbe :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> UserId -> Contact -> m (Probe, Int64) -createSentProbe st gVar userId _to@Contact {contactId} = - liftIOEither . withTransaction st $ \db -> - createWithRandomBytes 32 gVar $ \probe -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO sent_probes (contact_id, probe, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (contactId, probe, userId, currentTs, currentTs) - (Probe probe,) <$> insertedRowId db - -createSentProbeHash :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> Contact -> m () -createSentProbeHash st userId probeId _to@Contact {contactId} = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (probeId, contactId, userId, currentTs, currentTs) - -matchReceivedProbe :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> Probe -> m (Maybe Contact) -matchReceivedProbe st userId _from@Contact {contactId} (Probe probe) = - liftIO . withTransaction st $ \db -> do - let probeHash = C.sha256Hash probe - contactIds <- - map fromOnly - <$> DB.query - db - [sql| - SELECT c.contact_id - FROM contacts c - JOIN received_probes r ON r.contact_id = c.contact_id - WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL - |] - (userId, probeHash) - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO received_probes (contact_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (contactId, probe, probeHash, userId, currentTs, currentTs) - case contactIds of - [] -> pure Nothing - cId : _ -> eitherToMaybe <$> getContact_ db userId cId - -matchReceivedProbeHash :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> ProbeHash -> m (Maybe (Contact, Probe)) -matchReceivedProbeHash st userId _from@Contact {contactId} (ProbeHash probeHash) = - liftIO . withTransaction st $ \db -> do - namesAndProbes <- - DB.query +getMatchingContacts :: DB.Connection -> UserId -> Contact -> IO [Contact] +getMatchingContacts db userId Contact {contactId, profile = Profile {displayName, fullName, image}} = do + contactIds <- + map fromOnly + <$> DB.queryNamed db [sql| - SELECT c.contact_id, r.probe + SELECT ct.contact_id + FROM contacts ct + JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id + WHERE ct.user_id = :user_id AND ct.contact_id != :contact_id + AND p.display_name = :display_name AND p.full_name = :full_name + AND ((p.image IS NULL AND :image IS NULL) OR p.image = :image) + |] + [ ":user_id" := userId, + ":contact_id" := contactId, + ":display_name" := displayName, + ":full_name" := fullName, + ":image" := image + ] + rights <$> mapM (runExceptT . getContact db userId) contactIds + +createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> Contact -> ExceptT StoreError IO (Probe, Int64) +createSentProbe db gVar userId _to@Contact {contactId} = + createWithRandomBytes 32 gVar $ \probe -> do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO sent_probes (contact_id, probe, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" + (contactId, probe, userId, currentTs, currentTs) + (Probe probe,) <$> insertedRowId db + +createSentProbeHash :: DB.Connection -> UserId -> Int64 -> Contact -> IO () +createSentProbeHash db userId probeId _to@Contact {contactId} = do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" + (probeId, contactId, userId, currentTs, currentTs) + +matchReceivedProbe :: DB.Connection -> UserId -> Contact -> Probe -> IO (Maybe Contact) +matchReceivedProbe db userId _from@Contact {contactId} (Probe probe) = do + let probeHash = C.sha256Hash probe + contactIds <- + map fromOnly + <$> DB.query + db + [sql| + SELECT c.contact_id FROM contacts c JOIN received_probes r ON r.contact_id = c.contact_id - WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NOT NULL + WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL |] (userId, probeHash) - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO received_probes (contact_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (contactId, probeHash, userId, currentTs, currentTs) - case namesAndProbes of - [] -> pure Nothing - (cId, probe) : _ -> - either (const Nothing) (Just . (,Probe probe)) - <$> getContact_ db userId cId + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO received_probes (contact_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (contactId, probe, probeHash, userId, currentTs, currentTs) + case contactIds of + [] -> pure Nothing + cId : _ -> eitherToMaybe <$> runExceptT (getContact db userId cId) -matchSentProbe :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> Probe -> m (Maybe Contact) -matchSentProbe st userId _from@Contact {contactId} (Probe probe) = - liftIO . withTransaction st $ \db -> do - contactIds <- - map fromOnly - <$> DB.query - db - [sql| - SELECT c.contact_id - FROM contacts c - JOIN sent_probes s ON s.contact_id = c.contact_id - JOIN sent_probe_hashes h ON h.sent_probe_id = s.sent_probe_id - WHERE c.user_id = ? AND s.probe = ? AND h.contact_id = ? - |] - (userId, probe, contactId) - case contactIds of - [] -> pure Nothing - cId : _ -> eitherToMaybe <$> getContact_ db userId cId - -mergeContactRecords :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> Contact -> m () -mergeContactRecords st userId Contact {contactId = toContactId} Contact {contactId = fromContactId, localDisplayName} = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - "UPDATE connections SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?" - (toContactId, currentTs, fromContactId, userId) - DB.execute - db - "UPDATE connections SET via_contact = ?, updated_at = ? WHERE via_contact = ? AND user_id = ?" - (toContactId, currentTs, fromContactId, userId) - DB.execute - db - "UPDATE group_members SET invited_by = ?, updated_at = ? WHERE invited_by = ? AND user_id = ?" - (toContactId, currentTs, fromContactId, userId) - DB.executeNamed +matchReceivedProbeHash :: DB.Connection -> UserId -> Contact -> ProbeHash -> IO (Maybe (Contact, Probe)) +matchReceivedProbeHash db userId _from@Contact {contactId} (ProbeHash probeHash) = do + namesAndProbes <- + DB.query db [sql| - UPDATE group_members - SET contact_id = :to_contact_id, - local_display_name = (SELECT local_display_name FROM contacts WHERE contact_id = :to_contact_id), - contact_profile_id = (SELECT contact_profile_id FROM contacts WHERE contact_id = :to_contact_id), - updated_at = :updated_at - WHERE contact_id = :from_contact_id - AND user_id = :user_id + SELECT c.contact_id, r.probe + FROM contacts c + JOIN received_probes r ON r.contact_id = c.contact_id + WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NOT NULL |] - [ ":to_contact_id" := toContactId, - ":from_contact_id" := fromContactId, - ":user_id" := userId, - ":updated_at" := currentTs - ] - deleteContactProfile_ db userId fromContactId - DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId) - DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId) + (userId, probeHash) + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO received_probes (contact_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" + (contactId, probeHash, userId, currentTs, currentTs) + case namesAndProbes of + [] -> pure Nothing + (cId, probe) : _ -> + either (const Nothing) (Just . (,Probe probe)) + <$> runExceptT (getContact db userId cId) -getConnectionEntity :: StoreMonad m => SQLiteStore -> User -> ConnId -> m ConnectionEntity -getConnectionEntity st User {userId, userContactId} agentConnId = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - c@Connection {connType, entityId} <- getConnection_ db - case entityId of - Nothing -> - if connType == ConnContact - then pure $ RcvDirectMsgConnection c Nothing - else throwError $ SEInternalError $ "connection " <> show connType <> " without entity" - Just entId -> - case connType of - ConnMember -> uncurry (RcvGroupMsgConnection c) <$> getGroupAndMember_ db entId c - ConnContact -> RcvDirectMsgConnection c . Just <$> getContactRec_ db entId c - ConnSndFile -> SndFileConnection c <$> getConnSndFileTransfer_ db entId c - ConnRcvFile -> RcvFileConnection c <$> ExceptT (getRcvFileTransfer_ db userId entId) - ConnUserContact -> UserContactConnection c <$> getUserContact_ db entId +matchSentProbe :: DB.Connection -> UserId -> Contact -> Probe -> IO (Maybe Contact) +matchSentProbe db userId _from@Contact {contactId} (Probe probe) = do + contactIds <- + map fromOnly + <$> DB.query + db + [sql| + SELECT c.contact_id + FROM contacts c + JOIN sent_probes s ON s.contact_id = c.contact_id + JOIN sent_probe_hashes h ON h.sent_probe_id = s.sent_probe_id + WHERE c.user_id = ? AND s.probe = ? AND h.contact_id = ? + |] + (userId, probe, contactId) + case contactIds of + [] -> pure Nothing + cId : _ -> eitherToMaybe <$> runExceptT (getContact db userId cId) + +mergeContactRecords :: DB.Connection -> UserId -> Contact -> Contact -> IO () +mergeContactRecords db userId Contact {contactId = toContactId} Contact {contactId = fromContactId, localDisplayName} = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE connections SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?" + (toContactId, currentTs, fromContactId, userId) + DB.execute + db + "UPDATE connections SET via_contact = ?, updated_at = ? WHERE via_contact = ? AND user_id = ?" + (toContactId, currentTs, fromContactId, userId) + DB.execute + db + "UPDATE group_members SET invited_by = ?, updated_at = ? WHERE invited_by = ? AND user_id = ?" + (toContactId, currentTs, fromContactId, userId) + DB.executeNamed + db + [sql| + UPDATE group_members + SET contact_id = :to_contact_id, + local_display_name = (SELECT local_display_name FROM contacts WHERE contact_id = :to_contact_id), + contact_profile_id = (SELECT contact_profile_id FROM contacts WHERE contact_id = :to_contact_id), + updated_at = :updated_at + WHERE contact_id = :from_contact_id + AND user_id = :user_id + |] + [ ":to_contact_id" := toContactId, + ":from_contact_id" := fromContactId, + ":user_id" := userId, + ":updated_at" := currentTs + ] + deleteContactProfile_ db userId fromContactId + DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId) + DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId) + +getConnectionEntity :: DB.Connection -> User -> ConnId -> ExceptT StoreError IO ConnectionEntity +getConnectionEntity db user@User {userId, userContactId} agentConnId = do + c@Connection {connType, entityId} <- getConnection_ + case entityId of + Nothing -> + if connType == ConnContact + then pure $ RcvDirectMsgConnection c Nothing + else throwError $ SEInternalError $ "connection " <> show connType <> " without entity" + Just entId -> + case connType of + ConnMember -> uncurry (RcvGroupMsgConnection c) <$> getGroupAndMember_ entId c + ConnContact -> RcvDirectMsgConnection c . Just <$> getContactRec_ entId c + ConnSndFile -> SndFileConnection c <$> getConnSndFileTransfer_ entId c + ConnRcvFile -> RcvFileConnection c <$> getRcvFileTransfer db user entId + ConnUserContact -> UserContactConnection c <$> getUserContact_ entId where - getConnection_ :: DB.Connection -> ExceptT StoreError IO Connection - getConnection_ db = ExceptT $ do + getConnection_ :: ExceptT StoreError IO Connection + getConnection_ = ExceptT $ do connection <$> DB.query db @@ -1140,8 +1094,8 @@ getConnectionEntity st User {userId, userContactId} agentConnId = connection :: [ConnectionRow] -> Either StoreError Connection connection (connRow : _) = Right $ toConnection connRow connection _ = Left . SEConnectionNotFound $ AgentConnId agentConnId - getContactRec_ :: DB.Connection -> Int64 -> Connection -> ExceptT StoreError IO Contact - getContactRec_ db contactId c = ExceptT $ do + getContactRec_ :: Int64 -> Connection -> ExceptT StoreError IO Contact + getContactRec_ contactId c = ExceptT $ do toContact' contactId c <$> DB.query db @@ -1157,8 +1111,8 @@ getConnectionEntity st User {userId, userContactId} agentConnId = let profile = Profile {displayName, fullName, image} in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" - getGroupAndMember_ :: DB.Connection -> Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) - getGroupAndMember_ db groupMemberId c = ExceptT $ do + getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) + getGroupAndMember_ groupMemberId c = ExceptT $ do firstRow (toGroupAndMember c) (SEInternalError "referenced group member not found") $ DB.query db @@ -1188,8 +1142,8 @@ getConnectionEntity st User {userId, userContactId} agentConnId = let groupInfo = toGroupInfo userContactId groupInfoRow member = toGroupMember userContactId memberRow in (groupInfo, (member :: GroupMember) {activeConn = Just c}) - getConnSndFileTransfer_ :: DB.Connection -> Int64 -> Connection -> ExceptT StoreError IO SndFileTransfer - getConnSndFileTransfer_ db fileId Connection {connId} = + getConnSndFileTransfer_ :: Int64 -> Connection -> ExceptT StoreError IO SndFileTransfer + getConnSndFileTransfer_ fileId Connection {connId} = ExceptT $ sndFileTransfer_ fileId connId <$> DB.query @@ -1209,8 +1163,8 @@ getConnectionEntity st User {userId, userContactId} agentConnId = Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, recipientDisplayName, connId, agentConnId = AgentConnId agentConnId} Nothing -> Left $ SESndFileInvalid fileId sndFileTransfer_ fileId _ _ = Left $ SESndFileNotFound fileId - getUserContact_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UserContact - getUserContact_ db userContactLinkId = ExceptT $ do + getUserContact_ :: Int64 -> ExceptT StoreError IO UserContact + getUserContact_ userContactLinkId = ExceptT $ do userContact_ <$> DB.query db @@ -1225,62 +1179,60 @@ getConnectionEntity st User {userId, userContactId} agentConnId = userContact_ [Only cReq] = Right UserContact {userContactLinkId, connReqContact = cReq} userContact_ _ = Left SEUserContactLinkNotFound -getConnectionsContacts :: MonadUnliftIO m => SQLiteStore -> UserId -> [ConnId] -> m [ContactRef] -getConnectionsContacts st userId agentConnIds = - liftIO . withTransaction st $ \db -> do - DB.execute_ db "DROP TABLE IF EXISTS temp.conn_ids" - DB.execute_ db "CREATE TABLE temp.conn_ids (conn_id BLOB)" - DB.executeMany db "INSERT INTO temp.conn_ids (conn_id) VALUES (?)" $ map Only agentConnIds - conns <- - map (uncurry ContactRef) - <$> DB.query - db - [sql| - SELECT ct.contact_id, ct.local_display_name - FROM contacts ct - JOIN connections c ON c.contact_id = ct.contact_id - WHERE ct.user_id = ? - AND c.agent_conn_id IN (SELECT conn_id FROM temp.conn_ids) - AND c.conn_type = ? - |] - (userId, ConnContact) - DB.execute_ db "DROP TABLE temp.conn_ids" - pure conns - -getGroupAndMember :: StoreMonad m => SQLiteStore -> User -> Int64 -> m (GroupInfo, GroupMember) -getGroupAndMember st User {userId, userContactId} groupMemberId = - liftIOEither . withTransaction st $ \db -> - firstRow toGroupAndMember (SEInternalError "referenced group member not found") $ - DB.query +getConnectionsContacts :: DB.Connection -> UserId -> [ConnId] -> IO [ContactRef] +getConnectionsContacts db userId agentConnIds = do + DB.execute_ db "DROP TABLE IF EXISTS temp.conn_ids" + DB.execute_ db "CREATE TABLE temp.conn_ids (conn_id BLOB)" + DB.executeMany db "INSERT INTO temp.conn_ids (conn_id) VALUES (?)" $ map Only agentConnIds + conns <- + map (uncurry ContactRef) + <$> DB.query db [sql| - SELECT - -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, - -- GroupInfo {membership} - mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, - mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, - -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.image, - -- from GroupMember - m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at - FROM group_members m - JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id - JOIN groups g ON g.group_id = m.group_id - JOIN group_profiles gp USING (group_profile_id) - JOIN group_members mu ON g.group_id = mu.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id - LEFT JOIN connections c ON c.connection_id = ( - SELECT max(cc.connection_id) - FROM connections cc - where cc.group_member_id = m.group_member_id - ) - WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? + SELECT ct.contact_id, ct.local_display_name + FROM contacts ct + JOIN connections c ON c.contact_id = ct.contact_id + WHERE ct.user_id = ? + AND c.agent_conn_id IN (SELECT conn_id FROM temp.conn_ids) + AND c.conn_type = ? |] - (groupMemberId, userId, userContactId) + (userId, ConnContact) + DB.execute_ db "DROP TABLE temp.conn_ids" + pure conns + +getGroupAndMember :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO (GroupInfo, GroupMember) +getGroupAndMember db User {userId, userContactId} groupMemberId = + ExceptT . firstRow toGroupAndMember (SEInternalError "referenced group member not found") $ + DB.query + db + [sql| + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + -- GroupInfo {membership} + mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, + mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, + -- GroupInfo {membership = GroupMember {memberProfile}} + pu.display_name, pu.full_name, pu.image, + -- from GroupMember + m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, + m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + JOIN groups g ON g.group_id = m.group_id + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu ON g.group_id = mu.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + where cc.group_member_id = m.group_member_id + ) + WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? + |] + (groupMemberId, userId, userContactId) where toGroupAndMember :: (GroupInfoRow :. GroupMemberRow :. MaybeConnectionRow) -> (GroupInfo, GroupMember) toGroupAndMember (groupInfoRow :. memberRow :. connRow) = @@ -1288,16 +1240,15 @@ getGroupAndMember st User {userId, userContactId} groupMemberId = member = toGroupMember userContactId memberRow in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow}) -updateConnectionStatus :: MonadUnliftIO m => SQLiteStore -> Connection -> ConnStatus -> m () -updateConnectionStatus st Connection {connId} connStatus = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId) +updateConnectionStatus :: DB.Connection -> Connection -> ConnStatus -> IO () +updateConnectionStatus db Connection {connId} connStatus = do + currentTs <- getCurrentTime + DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId) -- | creates completely new group with a single member - the current user -createNewGroup :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> User -> GroupProfile -> m GroupInfo -createNewGroup st gVar user groupProfile = - liftIOEither . checkConstraint SEDuplicateName . withTransaction st $ \db -> do +createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo +createNewGroup db gVar user groupProfile = + checkConstraint SEDuplicateName . liftIO $ do let GroupProfile {displayName, fullName, image} = groupProfile uId = userId user currentTs <- getCurrentTime @@ -1317,24 +1268,22 @@ createNewGroup st gVar user groupProfile = groupId <- insertedRowId db memberId <- encodedRandomBytes gVar 12 membership <- createContactMember_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser currentTs - pure $ Right GroupInfo {groupId, localDisplayName = displayName, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} + pure GroupInfo {groupId, localDisplayName = displayName, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} -- | creates a new group record for the group the current user was invited to, or returns an existing one -createGroupInvitation :: - StoreMonad m => SQLiteStore -> User -> Contact -> GroupInvitation -> m GroupInfo -createGroupInvitation st user@User {userId} contact@Contact {contactId} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} = - liftIOEither . withTransaction st $ \db -> do - getInvitationGroupId_ db >>= \case - Nothing -> createGroupInvitation_ db - -- TODO treat the case that the invitation details could've changed - Just gId -> getGroupInfo_ db user gId +createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> ExceptT StoreError IO GroupInfo +createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} = + liftIO getInvitationGroupId_ >>= \case + Nothing -> ExceptT createGroupInvitation_ + -- TODO treat the case that the invitation details could've changed + Just gId -> getGroupInfo db user gId where - getInvitationGroupId_ :: DB.Connection -> IO (Maybe Int64) - getInvitationGroupId_ db = - listToMaybe . map fromOnly - <$> DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId) - createGroupInvitation_ :: DB.Connection -> IO (Either StoreError GroupInfo) - createGroupInvitation_ db = do + getInvitationGroupId_ :: IO (Maybe Int64) + getInvitationGroupId_ = + maybeFirstRow fromOnly $ + DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId) + createGroupInvitation_ :: IO (Either StoreError GroupInfo) + createGroupInvitation_ = do let GroupProfile {displayName, fullName, image} = groupProfile withLocalDisplayName db userId displayName $ \localDisplayName -> do currentTs <- getCurrentTime @@ -1354,70 +1303,61 @@ createGroupInvitation st user@User {userId} contact@Contact {contactId} GroupInv -- TODO return the last connection that is ready, not any last connection -- requires updating connection status -getGroupByName :: StoreMonad m => SQLiteStore -> User -> GroupName -> m Group -getGroupByName st user gName = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - groupId <- ExceptT $ getGroupIdByName_ db user gName - ExceptT $ getGroup_ db user groupId +getGroupByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO Group +getGroupByName db user gName = do + groupId <- getGroupIdByName db user gName + getGroup db user groupId -getGroup :: StoreMonad m => SQLiteStore -> User -> Int64 -> m Group -getGroup st user groupId = - liftIOEither . withTransaction st $ \db -> getGroup_ db user groupId - -getGroup_ :: DB.Connection -> User -> Int64 -> IO (Either StoreError Group) -getGroup_ db user groupId = runExceptT $ do - gInfo <- ExceptT $ getGroupInfo_ db user groupId - members <- liftIO $ getGroupMembers_ db user gInfo +getGroup :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Group +getGroup db user groupId = do + gInfo <- getGroupInfo db user groupId + members <- liftIO $ getGroupMembers db user gInfo pure $ Group gInfo members -deleteGroup :: MonadUnliftIO m => SQLiteStore -> User -> Group -> m () -deleteGroup st User {userId} (Group GroupInfo {groupId, localDisplayName} members) = - liftIO . withTransaction st $ \db -> do - forM_ members $ \m -> DB.execute db "DELETE FROM connections WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId m) - DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId) - DB.execute +deleteGroup :: DB.Connection -> User -> Group -> IO () +deleteGroup db User {userId} (Group GroupInfo {groupId, localDisplayName} members) = do + forM_ members $ \m -> DB.execute db "DELETE FROM connections WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId m) + DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId) + DB.execute + db + [sql| + DELETE FROM group_profiles + WHERE group_profile_id in ( + SELECT group_profile_id + FROM groups + WHERE user_id = ? AND group_id = ? + ) + |] + (userId, groupId) + DB.execute db "DELETE FROM groups WHERE user_id = ? AND group_id = ?" (userId, groupId) + DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + +getUserGroups :: DB.Connection -> User -> IO [Group] +getUserGroups db user@User {userId} = do + groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId) + rights <$> mapM (runExceptT . getGroup db user) groupIds + +getUserGroupDetails :: DB.Connection -> User -> IO [GroupInfo] +getUserGroupDetails db User {userId, userContactId} = + map (toGroupInfo userContactId) + <$> DB.query db [sql| - DELETE FROM group_profiles - WHERE group_profile_id in ( - SELECT group_profile_id - FROM groups - WHERE user_id = ? AND group_id = ? - ) + SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + m.group_member_id, g.group_id, m.member_id, m.member_role, m.member_category, m.member_status, + m.invited_by, m.local_display_name, m.contact_id, mp.display_name, mp.full_name, mp.image + FROM groups g + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members m USING (group_id) + JOIN contact_profiles mp USING (contact_profile_id) + WHERE g.user_id = ? AND m.contact_id = ? |] - (userId, groupId) - DB.execute db "DELETE FROM groups WHERE user_id = ? AND group_id = ?" (userId, groupId) - DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + (userId, userContactId) -getUserGroups :: MonadUnliftIO m => SQLiteStore -> User -> m [Group] -getUserGroups st user@User {userId} = - liftIO . withTransaction st $ \db -> do - groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId) - rights <$> mapM (getGroup_ db user) groupIds - -getUserGroupDetails :: MonadUnliftIO m => SQLiteStore -> User -> m [GroupInfo] -getUserGroupDetails st User {userId, userContactId} = - liftIO . withTransaction st $ \db -> - map (toGroupInfo userContactId) - <$> DB.query - db - [sql| - SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, - m.group_member_id, g.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, mp.display_name, mp.full_name, mp.image - FROM groups g - JOIN group_profiles gp USING (group_profile_id) - JOIN group_members m USING (group_id) - JOIN contact_profiles mp USING (contact_profile_id) - WHERE g.user_id = ? AND m.contact_id = ? - |] - (userId, userContactId) - -getGroupInfoByName :: StoreMonad m => SQLiteStore -> User -> GroupName -> m GroupInfo -getGroupInfoByName st user gName = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - gId <- ExceptT $ getGroupIdByName_ db user gName - ExceptT $ getGroupInfo_ db user gId +getGroupInfoByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupInfo +getGroupInfoByName db user gName = do + gId <- getGroupIdByName db user gName + getGroupInfo db user gId type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, UTCTime, UTCTime) :. GroupMemberRow @@ -1426,11 +1366,8 @@ toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, im let membership = toGroupMember userContactId userMemberRow in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, createdAt, updatedAt} -getGroupMembers :: MonadUnliftIO m => SQLiteStore -> User -> GroupInfo -> m [GroupMember] -getGroupMembers st user gInfo = liftIO . withTransaction st $ \db -> getGroupMembers_ db user gInfo - -getGroupMembers_ :: DB.Connection -> User -> GroupInfo -> IO [GroupMember] -getGroupMembers_ db User {userId, userContactId} GroupInfo {groupId} = do +getGroupMembers :: DB.Connection -> User -> GroupInfo -> IO [GroupMember] +getGroupMembers db User {userId, userContactId} GroupInfo {groupId} = do map toContactMember <$> DB.query db @@ -1457,20 +1394,19 @@ getGroupMembers_ db User {userId, userContactId} GroupInfo {groupId} = do -- TODO no need to load all members to find the member who invited the used, -- instead of findFromContact there could be a query -getGroupInvitation :: StoreMonad m => SQLiteStore -> User -> GroupName -> m ReceivedGroupInvitation -getGroupInvitation st user localDisplayName = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - cReq <- getConnRec_ db user - groupId <- ExceptT $ getGroupIdByName_ db user localDisplayName - Group groupInfo@GroupInfo {membership} members <- ExceptT $ getGroup_ db user groupId - when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined - case (cReq, findFromContact (invitedBy membership) members) of - (Just connRequest, Just fromMember) -> - pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo} - _ -> throwError SEGroupInvitationNotFound +getGroupInvitation :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO ReceivedGroupInvitation +getGroupInvitation db user localDisplayName = do + cReq <- getConnRec_ user + groupId <- getGroupIdByName db user localDisplayName + Group groupInfo@GroupInfo {membership} members <- getGroup db user groupId + when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined + case (cReq, findFromContact (invitedBy membership) members) of + (Just connRequest, Just fromMember) -> + pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo} + _ -> throwError SEGroupInvitationNotFound where - getConnRec_ :: DB.Connection -> User -> ExceptT StoreError IO (Maybe ConnReqInvitation) - getConnRec_ db User {userId} = ExceptT $ do + getConnRec_ :: User -> ExceptT StoreError IO (Maybe ConnReqInvitation) + getConnRec_ User {userId} = ExceptT $ do firstRow fromOnly (SEGroupNotFoundByName localDisplayName) $ DB.query db "SELECT g.inv_queue_info FROM groups g WHERE g.local_display_name = ? AND g.user_id = ?" (localDisplayName, userId) findFromContact :: InvitedBy -> [GroupMember] -> Maybe GroupMember @@ -1493,66 +1429,61 @@ toMaybeGroupMember userContactId (Just groupMemberId, Just groupId, Just memberI Just $ toGroupMember userContactId (groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, localDisplayName, memberContactId, displayName, fullName, image) toMaybeGroupMember _ _ = Nothing -createContactMember :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> User -> Int64 -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> m GroupMember -createContactMember st gVar user groupId contact memberRole agentConnId connRequest = - liftIOEither . withTransaction st $ \db -> - createWithRandomId gVar $ \memId -> do - currentTs <- getCurrentTime - member@GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId contact (MemberIdRole (MemberId memId) memberRole) GCInviteeMember GSMemInvited IBUser (Just connRequest) currentTs - void $ createMemberConnection_ db (userId user) groupMemberId agentConnId Nothing 0 currentTs - pure member - -getMemberInvitation :: StoreMonad m => SQLiteStore -> User -> Int64 -> m (Maybe ConnReqInvitation) -getMemberInvitation st User {userId} groupMemberId = - liftIO . withTransaction st $ \db -> - join . listToMaybe . map fromOnly - <$> DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId) - -createMemberConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> GroupMember -> ConnId -> m () -createMemberConnection st userId GroupMember {groupMemberId} agentConnId = - liftIO . withTransaction st $ \db -> do +createContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> Int64 -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember +createContactMember db gVar user groupId contact memberRole agentConnId connRequest = + createWithRandomId gVar $ \memId -> do currentTs <- getCurrentTime - void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs + member@GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId contact (MemberIdRole (MemberId memId) memberRole) GCInviteeMember GSMemInvited IBUser (Just connRequest) currentTs + void $ createMemberConnection_ db (userId user) groupMemberId agentConnId Nothing 0 currentTs + pure member -updateGroupMemberStatus :: MonadUnliftIO m => SQLiteStore -> UserId -> GroupMember -> GroupMemberStatus -> m () -updateGroupMemberStatus st userId GroupMember {groupMemberId} memStatus = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.executeNamed - db - [sql| - UPDATE group_members - SET member_status = :member_status, updated_at = :updated_at - WHERE user_id = :user_id AND group_member_id = :group_member_id - |] - [ ":user_id" := userId, - ":group_member_id" := groupMemberId, - ":member_status" := memStatus, - ":updated_at" := currentTs - ] +getMemberInvitation :: DB.Connection -> User -> Int64 -> IO (Maybe ConnReqInvitation) +getMemberInvitation db User {userId} groupMemberId = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId) + +createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> IO () +createMemberConnection db userId GroupMember {groupMemberId} agentConnId = do + currentTs <- getCurrentTime + void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs + +updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO () +updateGroupMemberStatus db userId GroupMember {groupMemberId} memStatus = do + currentTs <- getCurrentTime + DB.executeNamed + db + [sql| + UPDATE group_members + SET member_status = :member_status, updated_at = :updated_at + WHERE user_id = :user_id AND group_member_id = :group_member_id + |] + [ ":user_id" := userId, + ":group_member_id" := groupMemberId, + ":member_status" := memStatus, + ":updated_at" := currentTs + ] -- | add new member with profile -createNewGroupMember :: StoreMonad m => SQLiteStore -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> m GroupMember -createNewGroupMember st user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image}) memCategory memStatus = - liftIOEither . withTransaction st $ \db -> - withLocalDisplayName db userId displayName $ \localDisplayName -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO contact_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (displayName, fullName, image, userId, currentTs, currentTs) - memProfileId <- insertedRowId db - let newMember = - NewGroupMember - { memInfo, - memCategory, - memStatus, - memInvitedBy = IBUnknown, - localDisplayName, - memContactId = Nothing, - memProfileId - } - createNewMember_ db user gInfo newMember currentTs +createNewGroupMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember +createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image}) memCategory memStatus = + ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO contact_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (displayName, fullName, image, userId, currentTs, currentTs) + memProfileId <- insertedRowId db + let newMember = + NewGroupMember + { memInfo, + memCategory, + memStatus, + memInvitedBy = IBUnknown, + localDisplayName, + memContactId = Nothing, + memProfileId + } + createNewMember_ db user gInfo newMember currentTs createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> UTCTime -> IO GroupMember createNewMember_ @@ -1583,25 +1514,21 @@ createNewMember_ groupMemberId <- insertedRowId db pure GroupMember {..} -deleteGroupMemberConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> GroupMember -> m () -deleteGroupMemberConnection st userId m = - liftIO . withTransaction st $ \db -> deleteGroupMemberConnection_ db userId m - -deleteGroupMemberConnection_ :: DB.Connection -> UserId -> GroupMember -> IO () -deleteGroupMemberConnection_ db userId GroupMember {groupMemberId} = +deleteGroupMemberConnection :: DB.Connection -> UserId -> GroupMember -> IO () +deleteGroupMemberConnection db userId GroupMember {groupMemberId} = DB.execute db "DELETE FROM connections WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId) -createIntroductions :: MonadUnliftIO m => SQLiteStore -> [GroupMember] -> GroupMember -> m [GroupMemberIntro] -createIntroductions st members toMember = do +createIntroductions :: DB.Connection -> [GroupMember] -> GroupMember -> IO [GroupMemberIntro] +createIntroductions db members toMember = do let reMembers = filter (\m -> memberCurrent m && groupMemberId m /= groupMemberId toMember) members if null reMembers then pure [] - else liftIO . withTransaction st $ \db -> do + else do currentTs <- getCurrentTime - mapM (insertIntro_ db currentTs) reMembers + mapM (insertIntro_ currentTs) reMembers where - insertIntro_ :: DB.Connection -> UTCTime -> GroupMember -> IO GroupMemberIntro - insertIntro_ db ts reMember = do + insertIntro_ :: UTCTime -> GroupMember -> IO GroupMemberIntro + insertIntro_ ts reMember = do DB.execute db [sql| @@ -1613,63 +1540,60 @@ createIntroductions st members toMember = do introId <- insertedRowId db pure GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing} -updateIntroStatus :: MonadUnliftIO m => SQLiteStore -> Int64 -> GroupMemberIntroStatus -> m () -updateIntroStatus st introId introStatus = - liftIO . withTransaction st $ \db -> do +updateIntroStatus :: DB.Connection -> Int64 -> GroupMemberIntroStatus -> IO () +updateIntroStatus db introId introStatus = do + currentTs <- getCurrentTime + DB.executeNamed + db + [sql| + UPDATE group_member_intros + SET intro_status = :intro_status, updated_at = :updated_at + WHERE group_member_intro_id = :intro_id + |] + [":intro_status" := introStatus, ":updated_at" := currentTs, ":intro_id" := introId] + +saveIntroInvitation :: DB.Connection -> GroupMember -> GroupMember -> IntroInvitation -> ExceptT StoreError IO GroupMemberIntro +saveIntroInvitation db reMember toMember introInv = do + intro <- getIntroduction_ db reMember toMember + liftIO $ do currentTs <- getCurrentTime DB.executeNamed db [sql| UPDATE group_member_intros - SET intro_status = :intro_status, updated_at = :updated_at - WHERE group_member_intro_id = :intro_id - |] - [":intro_status" := introStatus, ":updated_at" := currentTs, ":intro_id" := introId] - -saveIntroInvitation :: StoreMonad m => SQLiteStore -> GroupMember -> GroupMember -> IntroInvitation -> m GroupMemberIntro -saveIntroInvitation st reMember toMember introInv = do - liftIOEither . withTransaction st $ \db -> runExceptT $ do - intro <- getIntroduction_ db reMember toMember - liftIO $ do - currentTs <- getCurrentTime - DB.executeNamed - db - [sql| - UPDATE group_member_intros - SET intro_status = :intro_status, - group_queue_info = :group_queue_info, - direct_queue_info = :direct_queue_info, - updated_at = :updated_at - WHERE group_member_intro_id = :intro_id - |] - [ ":intro_status" := GMIntroInvReceived, - ":group_queue_info" := groupConnReq introInv, - ":direct_queue_info" := directConnReq introInv, - ":updated_at" := currentTs, - ":intro_id" := introId intro - ] - pure intro {introInvitation = Just introInv, introStatus = GMIntroInvReceived} - -saveMemberInvitation :: StoreMonad m => SQLiteStore -> GroupMember -> IntroInvitation -> m () -saveMemberInvitation st GroupMember {groupMemberId} IntroInvitation {groupConnReq, directConnReq} = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.executeNamed - db - [sql| - UPDATE group_members - SET member_status = :member_status, + SET intro_status = :intro_status, group_queue_info = :group_queue_info, direct_queue_info = :direct_queue_info, updated_at = :updated_at - WHERE group_member_id = :group_member_id + WHERE group_member_intro_id = :intro_id |] - [ ":member_status" := GSMemIntroInvited, - ":group_queue_info" := groupConnReq, - ":direct_queue_info" := directConnReq, + [ ":intro_status" := GMIntroInvReceived, + ":group_queue_info" := groupConnReq introInv, + ":direct_queue_info" := directConnReq introInv, ":updated_at" := currentTs, - ":group_member_id" := groupMemberId + ":intro_id" := introId intro ] + pure intro {introInvitation = Just introInv, introStatus = GMIntroInvReceived} + +saveMemberInvitation :: DB.Connection -> GroupMember -> IntroInvitation -> IO () +saveMemberInvitation db GroupMember {groupMemberId} IntroInvitation {groupConnReq, directConnReq} = do + currentTs <- getCurrentTime + DB.executeNamed + db + [sql| + UPDATE group_members + SET member_status = :member_status, + group_queue_info = :group_queue_info, + direct_queue_info = :direct_queue_info, + updated_at = :updated_at + WHERE group_member_id = :group_member_id + |] + [ ":member_status" := GSMemIntroInvited, + ":group_queue_info" := groupConnReq, + ":direct_queue_info" := directConnReq, + ":updated_at" := currentTs, + ":group_member_id" := groupMemberId + ] getIntroduction_ :: DB.Connection -> GroupMember -> GroupMember -> ExceptT StoreError IO GroupMemberIntro getIntroduction_ db reMember toMember = ExceptT $ do @@ -1689,40 +1613,38 @@ getIntroduction_ db reMember toMember = ExceptT $ do in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} toIntro _ = Left SEIntroNotFound -createIntroReMember :: StoreMonad m => SQLiteStore -> User -> GroupInfo -> GroupMember -> MemberInfo -> ConnId -> ConnId -> m GroupMember -createIntroReMember st user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn - currentTs <- liftIO getCurrentTime - Connection {connId = directConnId} <- liftIO $ createContactConnection_ db userId directAgentConnId memberContactId cLevel currentTs - (localDisplayName, contactId, memProfileId) <- ExceptT $ createContact_ db userId directConnId memberProfile (Just groupId) currentTs - liftIO $ do - let newMember = - NewGroupMember - { memInfo, - memCategory = GCPreMember, - memStatus = GSMemIntroduced, - memInvitedBy = IBUnknown, - localDisplayName, - memContactId = Just contactId, - memProfileId - } - member <- createNewMember_ db user gInfo newMember currentTs - conn <- createMemberConnection_ db userId (groupMemberId member) groupAgentConnId memberContactId cLevel currentTs - pure (member :: GroupMember) {activeConn = Just conn} +createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> ConnId -> ConnId -> ExceptT StoreError IO GroupMember +createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId = do + let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn + currentTs <- liftIO getCurrentTime + Connection {connId = directConnId} <- liftIO $ createContactConnection_ db userId directAgentConnId memberContactId cLevel currentTs + (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile (Just groupId) currentTs + liftIO $ do + let newMember = + NewGroupMember + { memInfo, + memCategory = GCPreMember, + memStatus = GSMemIntroduced, + memInvitedBy = IBUnknown, + localDisplayName, + memContactId = Just contactId, + memProfileId + } + member <- createNewMember_ db user gInfo newMember currentTs + conn <- createMemberConnection_ db userId (groupMemberId member) groupAgentConnId memberContactId cLevel currentTs + pure (member :: GroupMember) {activeConn = Just conn} -createIntroToMemberContact :: StoreMonad m => SQLiteStore -> UserId -> GroupMember -> GroupMember -> ConnId -> ConnId -> m () -createIntroToMemberContact st userId GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} groupAgentConnId directAgentConnId = - liftIO . withTransaction st $ \db -> do - let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn - currentTs <- getCurrentTime - void $ createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs - Connection {connId = directConnId} <- createContactConnection_ db userId directAgentConnId viaContactId cLevel currentTs - contactId <- createMemberContact_ db directConnId currentTs - updateMember_ db contactId currentTs +createIntroToMemberContact :: DB.Connection -> UserId -> GroupMember -> GroupMember -> ConnId -> ConnId -> IO () +createIntroToMemberContact db userId GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} groupAgentConnId directAgentConnId = do + let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn + currentTs <- getCurrentTime + void $ createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs + Connection {connId = directConnId} <- createContactConnection_ db userId directAgentConnId viaContactId cLevel currentTs + contactId <- createMemberContact_ directConnId currentTs + updateMember_ contactId currentTs where - createMemberContact_ :: DB.Connection -> Int64 -> UTCTime -> IO Int64 - createMemberContact_ db connId ts = do + createMemberContact_ :: Int64 -> UTCTime -> IO Int64 + createMemberContact_ connId ts = do DB.execute db [sql| @@ -1735,8 +1657,8 @@ createIntroToMemberContact st userId GroupMember {memberContactId = viaContactId contactId <- insertedRowId db DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, ts, connId) pure contactId - updateMember_ :: DB.Connection -> Int64 -> UTCTime -> IO () - updateMember_ db contactId ts = + updateMember_ :: Int64 -> UTCTime -> IO () + updateMember_ contactId ts = DB.executeNamed db [sql| @@ -1790,215 +1712,199 @@ createContactMemberInv_ db User {userId, userContactId} groupId userOrContact Me ":updated_at" := createdAt ] -getViaGroupMember :: MonadUnliftIO m => SQLiteStore -> User -> Contact -> m (Maybe (GroupInfo, GroupMember)) -getViaGroupMember st User {userId, userContactId} Contact {contactId} = - liftIO . withTransaction st $ \db -> - toGroupAndMember - <$> DB.query - db - [sql| - SELECT - -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, - -- GroupInfo {membership} - mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, - mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, - -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.image, - -- via GroupMember - m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at - FROM group_members m - JOIN contacts ct ON ct.contact_id = m.contact_id - JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id - JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group - JOIN group_profiles gp USING (group_profile_id) - JOIN group_members mu ON g.group_id = mu.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id - LEFT JOIN connections c ON c.connection_id = ( - SELECT max(cc.connection_id) - FROM connections cc - where cc.group_member_id = m.group_member_id - ) - WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ? - |] - (userId, contactId, userContactId) - where - toGroupAndMember :: [GroupInfoRow :. GroupMemberRow :. MaybeConnectionRow] -> Maybe (GroupInfo, GroupMember) - toGroupAndMember [groupInfoRow :. memberRow :. connRow] = - let groupInfo = toGroupInfo userContactId groupInfoRow - member = toGroupMember userContactId memberRow - in Just (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow}) - toGroupAndMember _ = Nothing - -getViaGroupContact :: MonadUnliftIO m => SQLiteStore -> User -> GroupMember -> m (Maybe Contact) -getViaGroupContact st User {userId} GroupMember {groupMemberId} = - liftIO . withTransaction st $ \db -> - toContact' - <$> DB.query - db - [sql| - SELECT - ct.contact_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.created_at, ct.updated_at, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at - FROM contacts ct - JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id - JOIN connections c ON c.connection_id = ( - SELECT max(cc.connection_id) - FROM connections cc - where cc.contact_id = ct.contact_id - ) - JOIN groups g ON g.group_id = ct.via_group - JOIN group_members m ON m.group_id = g.group_id AND m.contact_id = ct.contact_id - WHERE ct.user_id = ? AND m.group_member_id = ? - |] - (userId, groupMemberId) - where - toContact' :: [(Int64, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime) :. ConnectionRow] -> Maybe Contact - toContact' [(contactId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt) :. connRow] = - let profile = Profile {displayName, fullName, image} - activeConn = toConnection connRow - in Just Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} - toContact' _ = Nothing - -createSndFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> FilePath -> FileInvitation -> ConnId -> Integer -> m Int64 -createSndFileTransfer st userId Contact {contactId} filePath FileInvitation {fileName, fileSize} acId chunkSize = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (userId, contactId, fileName, filePath, fileSize, chunkSize, CIFSSndStored, currentTs, currentTs) - fileId <- insertedRowId db - Connection {connId} <- createSndFileConnection_ db userId fileId acId - let fileStatus = FSNew - DB.execute - db - "INSERT INTO snd_files (file_id, file_status, connection_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (fileId, fileStatus, connId, currentTs, currentTs) - pure fileId - -createSndGroupFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> GroupInfo -> FilePath -> FileInvitation -> Integer -> m Int64 -createSndGroupFileTransfer st userId GroupInfo {groupId} filePath FileInvitation {fileName, fileSize} chunkSize = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO files (user_id, group_id, file_name, file_path, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (userId, groupId, fileName, filePath, fileSize, chunkSize, CIFSSndStored, currentTs, currentTs) - insertedRowId db - -createSndGroupFileTransferConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> ConnId -> GroupMember -> m () -createSndGroupFileTransferConnection st userId fileId acId GroupMember {groupMemberId} = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - Connection {connId} <- createSndFileConnection_ db userId fileId acId - DB.execute - db - "INSERT INTO snd_files (file_id, file_status, connection_id, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (fileId, FSAccepted, connId, groupMemberId, currentTs, currentTs) - -updateFileCancelled :: MsgDirectionI d => MonadUnliftIO m => SQLiteStore -> User -> Int64 -> CIFileStatus d -> m () -updateFileCancelled st User {userId} fileId ciFileStatus = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute db "UPDATE files SET cancelled = 1, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" (ciFileStatus, currentTs, userId, fileId) - -updateCIFileStatus :: (MsgDirectionI d, MonadUnliftIO m) => SQLiteStore -> User -> Int64 -> CIFileStatus d -> m () -updateCIFileStatus st user fileId ciFileStatus = - liftIO . withTransaction st $ \db -> updateCIFileStatus_ db user fileId ciFileStatus - -updateCIFileStatus_ :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO () -updateCIFileStatus_ db User {userId} fileId ciFileStatus = do - currentTs <- getCurrentTime - DB.execute db "UPDATE files SET ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" (ciFileStatus, currentTs, userId, fileId) - -getSharedMsgIdByFileId :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m SharedMsgId -getSharedMsgIdByFileId st userId fileId = - liftIOEither . withTransaction st $ \db -> - firstRow fromOnly (SESharedMsgIdNotFoundByFileId fileId) $ - DB.query - db - [sql| - SELECT i.shared_msg_id - FROM chat_items i - JOIN files f ON f.chat_item_id = i.chat_item_id - WHERE f.user_id = ? AND f.file_id = ? - |] - (userId, fileId) - -getFileIdBySharedMsgId :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> SharedMsgId -> m Int64 -getFileIdBySharedMsgId st userId contactId sharedMsgId = - liftIOEither . withTransaction st $ \db -> - firstRow fromOnly (SEFileIdNotFoundBySharedMsgId sharedMsgId) $ - DB.query - db - [sql| - SELECT f.file_id - FROM files f - JOIN chat_items i ON i.chat_item_id = f.chat_item_id - WHERE i.user_id = ? AND i.contact_id = ? AND i.shared_msg_id = ? - |] - (userId, contactId, sharedMsgId) - -getGroupFileIdBySharedMsgId :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> SharedMsgId -> m Int64 -getGroupFileIdBySharedMsgId st userId groupId sharedMsgId = - liftIOEither . withTransaction st $ \db -> - firstRow fromOnly (SEFileIdNotFoundBySharedMsgId sharedMsgId) $ - DB.query - db - [sql| - SELECT f.file_id - FROM files f - JOIN chat_items i ON i.chat_item_id = f.chat_item_id - WHERE i.user_id = ? AND i.group_id = ? AND i.shared_msg_id = ? - |] - (userId, groupId, sharedMsgId) - -getChatRefByFileId :: StoreMonad m => SQLiteStore -> User -> Int64 -> m ChatRef -getChatRefByFileId st User {userId} fileId = do - r <- liftIO . withTransaction st $ \db -> do +getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) +getViaGroupMember db User {userId, userContactId} Contact {contactId} = + maybeFirstRow toGroupAndMember $ DB.query db [sql| - SELECT contact_id, group_id - FROM files - WHERE user_id = ? AND file_id = ? - LIMIT 1 + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + -- GroupInfo {membership} + mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, + mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, + -- GroupInfo {membership = GroupMember {memberProfile}} + pu.display_name, pu.full_name, pu.image, + -- via GroupMember + m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, + m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + FROM group_members m + JOIN contacts ct ON ct.contact_id = m.contact_id + JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu ON g.group_id = mu.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + where cc.group_member_id = m.group_member_id + ) + WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ? + |] + (userId, contactId, userContactId) + where + toGroupAndMember :: (GroupInfoRow :. GroupMemberRow :. MaybeConnectionRow) -> (GroupInfo, GroupMember) + toGroupAndMember (groupInfoRow :. memberRow :. connRow) = + let groupInfo = toGroupInfo userContactId groupInfoRow + member = toGroupMember userContactId memberRow + in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow}) + +getViaGroupContact :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact) +getViaGroupContact db User {userId} GroupMember {groupMemberId} = + maybeFirstRow toContact' $ + DB.query + db + [sql| + SELECT + ct.contact_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.created_at, ct.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + FROM contacts ct + JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id + JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + where cc.contact_id = ct.contact_id + ) + JOIN groups g ON g.group_id = ct.via_group + JOIN group_members m ON m.group_id = g.group_id AND m.contact_id = ct.contact_id + WHERE ct.user_id = ? AND m.group_member_id = ? + |] + (userId, groupMemberId) + where + toContact' :: (Int64, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime) :. ConnectionRow -> Contact + toContact' ((contactId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt) :. connRow) = + let profile = Profile {displayName, fullName, image} + activeConn = toConnection connRow + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + +createSndFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> ConnId -> Integer -> IO Int64 +createSndFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize} acId chunkSize = do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (userId, contactId, fileName, filePath, fileSize, chunkSize, CIFSSndStored, currentTs, currentTs) + fileId <- insertedRowId db + Connection {connId} <- createSndFileConnection_ db userId fileId acId + let fileStatus = FSNew + DB.execute + db + "INSERT INTO snd_files (file_id, file_status, connection_id, created_at, updated_at) VALUES (?,?,?,?,?)" + (fileId, fileStatus, connId, currentTs, currentTs) + pure fileId + +createSndGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> FilePath -> FileInvitation -> Integer -> IO Int64 +createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation {fileName, fileSize} chunkSize = do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO files (user_id, group_id, file_name, file_path, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (userId, groupId, fileName, filePath, fileSize, chunkSize, CIFSSndStored, currentTs, currentTs) + insertedRowId db + +createSndGroupFileTransferConnection :: DB.Connection -> UserId -> Int64 -> ConnId -> GroupMember -> IO () +createSndGroupFileTransferConnection db userId fileId acId GroupMember {groupMemberId} = do + currentTs <- getCurrentTime + Connection {connId} <- createSndFileConnection_ db userId fileId acId + DB.execute + db + "INSERT INTO snd_files (file_id, file_status, connection_id, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (fileId, FSAccepted, connId, groupMemberId, currentTs, currentTs) + +updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO () +updateFileCancelled db User {userId} fileId ciFileStatus = do + currentTs <- getCurrentTime + DB.execute db "UPDATE files SET cancelled = 1, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" (ciFileStatus, currentTs, userId, fileId) + +updateCIFileStatus :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO () +updateCIFileStatus db User {userId} fileId ciFileStatus = do + currentTs <- getCurrentTime + DB.execute db "UPDATE files SET ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" (ciFileStatus, currentTs, userId, fileId) + +getSharedMsgIdByFileId :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO SharedMsgId +getSharedMsgIdByFileId db userId fileId = + ExceptT . firstRow fromOnly (SESharedMsgIdNotFoundByFileId fileId) $ + DB.query + db + [sql| + SELECT i.shared_msg_id + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE f.user_id = ? AND f.file_id = ? |] (userId, fileId) - case r of + +getFileIdBySharedMsgId :: DB.Connection -> UserId -> Int64 -> SharedMsgId -> ExceptT StoreError IO Int64 +getFileIdBySharedMsgId db userId contactId sharedMsgId = + ExceptT . firstRow fromOnly (SEFileIdNotFoundBySharedMsgId sharedMsgId) $ + DB.query + db + [sql| + SELECT f.file_id + FROM files f + JOIN chat_items i ON i.chat_item_id = f.chat_item_id + WHERE i.user_id = ? AND i.contact_id = ? AND i.shared_msg_id = ? + |] + (userId, contactId, sharedMsgId) + +getGroupFileIdBySharedMsgId :: DB.Connection -> UserId -> Int64 -> SharedMsgId -> ExceptT StoreError IO Int64 +getGroupFileIdBySharedMsgId db userId groupId sharedMsgId = + ExceptT . firstRow fromOnly (SEFileIdNotFoundBySharedMsgId sharedMsgId) $ + DB.query + db + [sql| + SELECT f.file_id + FROM files f + JOIN chat_items i ON i.chat_item_id = f.chat_item_id + WHERE i.user_id = ? AND i.group_id = ? AND i.shared_msg_id = ? + |] + (userId, groupId, sharedMsgId) + +getChatRefByFileId :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO ChatRef +getChatRefByFileId db User {userId} fileId = + liftIO getChatRef >>= \case [(Just contactId, Nothing)] -> pure $ ChatRef CTDirect contactId [(Nothing, Just groupId)] -> pure $ ChatRef CTGroup groupId _ -> throwError $ SEInternalError "could not retrieve chat ref by file id" + where + getChatRef = + DB.query + db + [sql| + SELECT contact_id, group_id + FROM files + WHERE user_id = ? AND file_id = ? + LIMIT 1 + |] + (userId, fileId) createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection createSndFileConnection_ db userId fileId agentConnId = do currentTs <- getCurrentTime createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing 0 currentTs -updateSndFileStatus :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> FileStatus -> m () -updateSndFileStatus st SndFileTransfer {fileId, connId} status = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute db "UPDATE snd_files SET file_status = ?, updated_at = ? WHERE file_id = ? AND connection_id = ?" (status, currentTs, fileId, connId) +updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () +updateSndFileStatus db SndFileTransfer {fileId, connId} status = do + currentTs <- getCurrentTime + DB.execute db "UPDATE snd_files SET file_status = ?, updated_at = ? WHERE file_id = ? AND connection_id = ?" (status, currentTs, fileId, connId) -createSndFileChunk :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> m (Maybe Integer) -createSndFileChunk st SndFileTransfer {fileId, connId, fileSize, chunkSize} = - liftIO . withTransaction st $ \db -> do - chunkNo <- getLastChunkNo db - insertChunk db chunkNo - pure chunkNo +createSndFileChunk :: DB.Connection -> SndFileTransfer -> IO (Maybe Integer) +createSndFileChunk db SndFileTransfer {fileId, connId, fileSize, chunkSize} = do + chunkNo <- getLastChunkNo + insertChunk chunkNo + pure chunkNo where - getLastChunkNo db = do + getLastChunkNo = do ns <- DB.query db "SELECT chunk_number FROM snd_file_chunks WHERE file_id = ? AND connection_id = ? AND chunk_sent = 1 ORDER BY chunk_number DESC LIMIT 1" (fileId, connId) pure $ case map fromOnly ns of [] -> Just 1 n : _ -> if n * chunkSize >= fileSize then Nothing else Just (n + 1) - insertChunk db = \case + insertChunk = \case Just chunkNo -> do currentTs <- getCurrentTime DB.execute @@ -2007,76 +1913,66 @@ createSndFileChunk st SndFileTransfer {fileId, connId, fileSize, chunkSize} = (fileId, connId, chunkNo, currentTs, currentTs) Nothing -> pure () -updateSndFileChunkMsg :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> Integer -> AgentMsgId -> m () -updateSndFileChunkMsg st SndFileTransfer {fileId, connId} chunkNo msgId = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - [sql| - UPDATE snd_file_chunks - SET chunk_agent_msg_id = ?, updated_at = ? - WHERE file_id = ? AND connection_id = ? AND chunk_number = ? - |] - (msgId, currentTs, fileId, connId, chunkNo) +updateSndFileChunkMsg :: DB.Connection -> SndFileTransfer -> Integer -> AgentMsgId -> IO () +updateSndFileChunkMsg db SndFileTransfer {fileId, connId} chunkNo msgId = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE snd_file_chunks + SET chunk_agent_msg_id = ?, updated_at = ? + WHERE file_id = ? AND connection_id = ? AND chunk_number = ? + |] + (msgId, currentTs, fileId, connId, chunkNo) -updateSndFileChunkSent :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> AgentMsgId -> m () -updateSndFileChunkSent st SndFileTransfer {fileId, connId} msgId = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - [sql| - UPDATE snd_file_chunks - SET chunk_sent = 1, updated_at = ? - WHERE file_id = ? AND connection_id = ? AND chunk_agent_msg_id = ? - |] - (currentTs, fileId, connId, msgId) +updateSndFileChunkSent :: DB.Connection -> SndFileTransfer -> AgentMsgId -> IO () +updateSndFileChunkSent db SndFileTransfer {fileId, connId} msgId = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE snd_file_chunks + SET chunk_sent = 1, updated_at = ? + WHERE file_id = ? AND connection_id = ? AND chunk_agent_msg_id = ? + |] + (currentTs, fileId, connId, msgId) -deleteSndFileChunks :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> m () -deleteSndFileChunks st SndFileTransfer {fileId, connId} = - liftIO . withTransaction st $ \db -> - DB.execute db "DELETE FROM snd_file_chunks WHERE file_id = ? AND connection_id = ?" (fileId, connId) +deleteSndFileChunks :: DB.Connection -> SndFileTransfer -> IO () +deleteSndFileChunks db SndFileTransfer {fileId, connId} = + DB.execute db "DELETE FROM snd_file_chunks WHERE file_id = ? AND connection_id = ?" (fileId, connId) -createRcvFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> FileInvitation -> Integer -> m RcvFileTransfer -createRcvFileTransfer st userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq} chunkSize = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (userId, contactId, fileName, fileSize, chunkSize, CIFSRcvInvitation, currentTs, currentTs) - fileId <- insertedRowId db - DB.execute - db - "INSERT INTO rcv_files (file_id, file_status, file_queue_info, created_at, updated_at) VALUES (?,?,?,?,?)" - (fileId, FSNew, fileConnReq, currentTs, currentTs) - pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing} +createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Integer -> IO RcvFileTransfer +createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq} chunkSize = do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (userId, contactId, fileName, fileSize, chunkSize, CIFSRcvInvitation, currentTs, currentTs) + fileId <- insertedRowId db + DB.execute + db + "INSERT INTO rcv_files (file_id, file_status, file_queue_info, created_at, updated_at) VALUES (?,?,?,?,?)" + (fileId, FSNew, fileConnReq, currentTs, currentTs) + pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing} -createRcvGroupFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> GroupMember -> FileInvitation -> Integer -> m RcvFileTransfer -createRcvGroupFileTransfer st userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq} chunkSize = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (userId, groupId, fileName, fileSize, chunkSize, CIFSRcvInvitation, currentTs, currentTs) - fileId <- insertedRowId db - DB.execute - db - "INSERT INTO rcv_files (file_id, file_status, file_queue_info, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (fileId, FSNew, fileConnReq, groupMemberId, currentTs, currentTs) - pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId} +createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Integer -> IO RcvFileTransfer +createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq} chunkSize = do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (userId, groupId, fileName, fileSize, chunkSize, CIFSRcvInvitation, currentTs, currentTs) + fileId <- insertedRowId db + DB.execute + db + "INSERT INTO rcv_files (file_id, file_status, file_queue_info, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (fileId, FSNew, fileConnReq, groupMemberId, currentTs, currentTs) + pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId} -getRcvFileTransfer :: StoreMonad m => SQLiteStore -> User -> Int64 -> m RcvFileTransfer -getRcvFileTransfer st User {userId} fileId = - liftIOEither . withTransaction st $ \db -> - getRcvFileTransfer_ db userId fileId - -getRcvFileTransfer_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError RcvFileTransfer) -getRcvFileTransfer_ db userId fileId = - rcvFileTransfer - <$> DB.query +getRcvFileTransfer :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO RcvFileTransfer +getRcvFileTransfer db User {userId} fileId = + ExceptT . firstRow' rcvFileTransfer (SERcvFileNotFound fileId) $ + DB.query db [sql| SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, @@ -2092,9 +1988,9 @@ getRcvFileTransfer_ db userId fileId = (userId, fileId) where rcvFileTransfer :: - [(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool, Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe Int64, Maybe AgentConnId)] -> + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool, Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe Int64, Maybe AgentConnId) -> Either StoreError RcvFileTransfer - rcvFileTransfer [(fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_, contactName_, memberName_, filePath_, connId_, agentConnId_)] = + rcvFileTransfer (fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_, contactName_, memberName_, filePath_, connId_, agentConnId_) = let fileInv = FileInvitation {fileName, fileSize, fileConnReq} fileInfo = (filePath_, connId_, agentConnId_) in case contactName_ <|> memberName_ of @@ -2114,45 +2010,41 @@ getRcvFileTransfer_ db userId fileId = (Just filePath, Just connId, Just agentConnId) -> Just RcvFileInfo {filePath, connId, agentConnId} _ -> Nothing cancelled = fromMaybe False cancelled_ - rcvFileTransfer _ = Left $ SERcvFileNotFound fileId -acceptRcvFileTransfer :: StoreMonad m => SQLiteStore -> User -> Int64 -> ConnId -> ConnStatus -> FilePath -> m AChatItem -acceptRcvFileTransfer st user@User {userId} fileId agentConnId connStatus filePath = - liftIOEither . withTransaction st $ \db -> do +acceptRcvFileTransfer :: DB.Connection -> User -> Int64 -> ConnId -> ConnStatus -> FilePath -> ExceptT StoreError IO AChatItem +acceptRcvFileTransfer db user@User {userId} fileId agentConnId connStatus filePath = ExceptT $ do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" + (filePath, CIFSRcvAccepted, currentTs, userId, fileId) + DB.execute + db + "UPDATE rcv_files SET file_status = ?, updated_at = ? WHERE file_id = ?" + (FSAccepted, currentTs, fileId) + DB.execute + db + "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (agentConnId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs) + runExceptT $ getChatItemByFileId db user fileId + +updateRcvFileStatus :: DB.Connection -> RcvFileTransfer -> FileStatus -> IO () +updateRcvFileStatus db RcvFileTransfer {fileId} status = do + currentTs <- getCurrentTime + DB.execute db "UPDATE rcv_files SET file_status = ?, updated_at = ? WHERE file_id = ?" (status, currentTs, fileId) + +createRcvFileChunk :: DB.Connection -> RcvFileTransfer -> Integer -> AgentMsgId -> IO RcvChunkStatus +createRcvFileChunk db RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileSize}, chunkSize} chunkNo msgId = do + status <- getLastChunkNo + unless (status == RcvChunkError) $ do currentTs <- getCurrentTime DB.execute db - "UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" - (filePath, CIFSRcvAccepted, currentTs, userId, fileId) - DB.execute - db - "UPDATE rcv_files SET file_status = ?, updated_at = ? WHERE file_id = ?" - (FSAccepted, currentTs, fileId) - DB.execute - db - "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (agentConnId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs) - getChatItemByFileId_ db user fileId - -updateRcvFileStatus :: MonadUnliftIO m => SQLiteStore -> RcvFileTransfer -> FileStatus -> m () -updateRcvFileStatus st RcvFileTransfer {fileId} status = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute db "UPDATE rcv_files SET file_status = ?, updated_at = ? WHERE file_id = ?" (status, currentTs, fileId) - -createRcvFileChunk :: MonadUnliftIO m => SQLiteStore -> RcvFileTransfer -> Integer -> AgentMsgId -> m RcvChunkStatus -createRcvFileChunk st RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileSize}, chunkSize} chunkNo msgId = - liftIO . withTransaction st $ \db -> do - status <- getLastChunkNo db - unless (status == RcvChunkError) $ do - currentTs <- getCurrentTime - DB.execute - db - "INSERT OR REPLACE INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (fileId, chunkNo, msgId, currentTs, currentTs) - pure status + "INSERT OR REPLACE INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id, created_at, updated_at) VALUES (?,?,?,?,?)" + (fileId, chunkNo, msgId, currentTs, currentTs) + pure status where - getLastChunkNo db = do + getLastChunkNo = do ns <- DB.query db "SELECT chunk_number FROM rcv_file_chunks WHERE file_id = ? ORDER BY chunk_number DESC LIMIT 1" (Only fileId) pure $ case map fromOnly ns of [] @@ -2173,71 +2065,60 @@ createRcvFileChunk st RcvFileTransfer {fileId, fileInvitation = FileInvitation { else RcvChunkOk | otherwise -> RcvChunkError -updatedRcvFileChunkStored :: MonadUnliftIO m => SQLiteStore -> RcvFileTransfer -> Integer -> m () -updatedRcvFileChunkStored st RcvFileTransfer {fileId} chunkNo = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - [sql| - UPDATE rcv_file_chunks - SET chunk_stored = 1, updated_at = ? - WHERE file_id = ? AND chunk_number = ? - |] - (currentTs, fileId, chunkNo) +updatedRcvFileChunkStored :: DB.Connection -> RcvFileTransfer -> Integer -> IO () +updatedRcvFileChunkStored db RcvFileTransfer {fileId} chunkNo = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE rcv_file_chunks + SET chunk_stored = 1, updated_at = ? + WHERE file_id = ? AND chunk_number = ? + |] + (currentTs, fileId, chunkNo) -deleteRcvFileChunks :: MonadUnliftIO m => SQLiteStore -> RcvFileTransfer -> m () -deleteRcvFileChunks st RcvFileTransfer {fileId} = - liftIO . withTransaction st $ \db -> - DB.execute db "DELETE FROM rcv_file_chunks WHERE file_id = ?" (Only fileId) +deleteRcvFileChunks :: DB.Connection -> RcvFileTransfer -> IO () +deleteRcvFileChunks db RcvFileTransfer {fileId} = + DB.execute db "DELETE FROM rcv_file_chunks WHERE file_id = ?" (Only fileId) -updateFileTransferChatItemId :: MonadUnliftIO m => SQLiteStore -> FileTransferId -> ChatItemId -> m () -updateFileTransferChatItemId st fileId ciId = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute db "UPDATE files SET chat_item_id = ?, updated_at = ? WHERE file_id = ?" (ciId, currentTs, fileId) +updateFileTransferChatItemId :: DB.Connection -> FileTransferId -> ChatItemId -> IO () +updateFileTransferChatItemId db fileId ciId = do + currentTs <- getCurrentTime + DB.execute db "UPDATE files SET chat_item_id = ?, updated_at = ? WHERE file_id = ?" (ciId, currentTs, fileId) -getFileTransfer :: StoreMonad m => SQLiteStore -> User -> Int64 -> m FileTransfer -getFileTransfer st User {userId} fileId = - liftIOEither . withTransaction st $ \db -> - getFileTransfer_ db userId fileId +getFileTransferProgress :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO (FileTransfer, [Integer]) +getFileTransferProgress db user fileId = do + ft <- getFileTransfer db user fileId + liftIO $ + (ft,) . map fromOnly <$> case ft of + FTSnd _ [] -> pure [Only 0] + FTSnd _ _ -> DB.query db "SELECT COUNT(*) FROM snd_file_chunks WHERE file_id = ? and chunk_sent = 1 GROUP BY connection_id" (Only fileId) + FTRcv _ -> DB.query db "SELECT COUNT(*) FROM rcv_file_chunks WHERE file_id = ? AND chunk_stored = 1" (Only fileId) -getFileTransferProgress :: StoreMonad m => SQLiteStore -> User -> Int64 -> m (FileTransfer, [Integer]) -getFileTransferProgress st User {userId} fileId = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - ft <- ExceptT $ getFileTransfer_ db userId fileId - liftIO $ - (ft,) . map fromOnly <$> case ft of - FTSnd _ [] -> pure [Only 0] - FTSnd _ _ -> DB.query db "SELECT COUNT(*) FROM snd_file_chunks WHERE file_id = ? and chunk_sent = 1 GROUP BY connection_id" (Only fileId) - FTRcv _ -> DB.query db "SELECT COUNT(*) FROM rcv_file_chunks WHERE file_id = ? AND chunk_stored = 1" (Only fileId) - -getFileTransfer_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError FileTransfer) -getFileTransfer_ db userId fileId = - fileTransfer - =<< DB.query - db - [sql| - SELECT s.file_id, r.file_id - FROM files f - LEFT JOIN snd_files s ON s.file_id = f.file_id - LEFT JOIN rcv_files r ON r.file_id = f.file_id - WHERE user_id = ? AND f.file_id = ? - |] - (userId, fileId) +getFileTransfer :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO FileTransfer +getFileTransfer db user@User {userId} fileId = + fileTransfer =<< liftIO getFileTransferRow where - fileTransfer :: [(Maybe Int64, Maybe Int64)] -> IO (Either StoreError FileTransfer) - fileTransfer [(Nothing, Just _)] = FTRcv <$$> getRcvFileTransfer_ db userId fileId - fileTransfer _ = runExceptT $ do - (ftm, fts) <- ExceptT $ getSndFileTransfer_ db userId fileId + fileTransfer :: [(Maybe Int64, Maybe Int64)] -> ExceptT StoreError IO FileTransfer + fileTransfer [(Nothing, Just _)] = FTRcv <$> getRcvFileTransfer db user fileId + fileTransfer _ = do + (ftm, fts) <- getSndFileTransfer db user fileId pure $ FTSnd {fileTransferMeta = ftm, sndFileTransfers = fts} + getFileTransferRow :: IO [(Maybe Int64, Maybe Int64)] + getFileTransferRow = + DB.query + db + [sql| + SELECT s.file_id, r.file_id + FROM files f + LEFT JOIN snd_files s ON s.file_id = f.file_id + LEFT JOIN rcv_files r ON r.file_id = f.file_id + WHERE user_id = ? AND f.file_id = ? + |] + (userId, fileId) -getSndFileTransfer :: StoreMonad m => SQLiteStore -> User -> Int64 -> m (FileTransferMeta, [SndFileTransfer]) -getSndFileTransfer st User {userId} fileId = - liftIOEither . withTransaction st $ \db -> getSndFileTransfer_ db userId fileId - -getSndFileTransfer_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError (FileTransferMeta, [SndFileTransfer])) -getSndFileTransfer_ db userId fileId = runExceptT $ do +getSndFileTransfer :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO (FileTransferMeta, [SndFileTransfer]) +getSndFileTransfer db User {userId} fileId = do fileTransferMeta <- ExceptT $ getFileTransferMeta_ db userId fileId sndFileTransfers <- ExceptT $ getSndFileTransfers_ db userId fileId pure (fileTransferMeta, sndFileTransfers) @@ -2283,37 +2164,35 @@ getFileTransferMeta_ db userId fileId = fileTransferMeta (fileName, fileSize, chunkSize, filePath, cancelled_) = FileTransferMeta {fileId, fileName, filePath, fileSize, chunkSize, cancelled = fromMaybe False cancelled_} -getContactFileInfo :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> m [CIFileInfo] -getContactFileInfo st userId Contact {contactId} = - liftIO . withTransaction st $ \db -> - map toFileInfo - <$> DB.query - db - [sql| +getContactFileInfo :: DB.Connection -> UserId -> Contact -> IO [CIFileInfo] +getContactFileInfo db userId Contact {contactId} = + map toFileInfo + <$> DB.query + db + [sql| SELECT f.file_id, f.ci_file_status, f.file_path FROM chat_items i JOIN files f ON f.chat_item_id = i.chat_item_id WHERE i.user_id = ? AND i.contact_id = ? |] - (userId, contactId) + (userId, contactId) toFileInfo :: (Int64, ACIFileStatus, Maybe FilePath) -> CIFileInfo toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, filePath} -getContactChatItemIdsAndFileInfo :: MonadUnliftIO m => SQLiteStore -> User -> ContactId -> m [(ChatItemId, UTCTime, Maybe CIFileInfo)] -getContactChatItemIdsAndFileInfo st User {userId} contactId = - liftIO . withTransaction st $ \db -> - map toItemIdAndFileInfo - <$> DB.query - db - [sql| - SELECT i.chat_item_id, i.item_ts, f.file_id, f.ci_file_status, f.file_path - FROM chat_items i - LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - WHERE i.user_id = ? AND i.contact_id = ? - ORDER BY i.item_ts ASC - |] - (userId, contactId) +getContactChatItemIdsAndFileInfo :: DB.Connection -> User -> ContactId -> IO [(ChatItemId, UTCTime, Maybe CIFileInfo)] +getContactChatItemIdsAndFileInfo db User {userId} contactId = + map toItemIdAndFileInfo + <$> DB.query + db + [sql| + SELECT i.chat_item_id, i.item_ts, f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.contact_id = ? + ORDER BY i.item_ts ASC + |] + (userId, contactId) toItemIdAndFileInfo :: (ChatItemId, UTCTime, Maybe Int64, Maybe ACIFileStatus, Maybe FilePath) -> (ChatItemId, UTCTime, Maybe CIFileInfo) toItemIdAndFileInfo (chatItemId, itemTs, fileId_, fileStatus_, filePath) = @@ -2321,28 +2200,26 @@ toItemIdAndFileInfo (chatItemId, itemTs, fileId_, fileStatus_, filePath) = (Just fileId, Just fileStatus) -> (chatItemId, itemTs, Just CIFileInfo {fileId, fileStatus, filePath}) _ -> (chatItemId, itemTs, Nothing) -updateContactTs :: MonadUnliftIO m => SQLiteStore -> User -> Contact -> UTCTime -> m () -updateContactTs st User {userId} Contact {contactId} updatedAt = - liftIO . withTransaction st $ \db -> - DB.execute - db - "UPDATE contacts SET updated_at = ? WHERE user_id = ? AND contact_id = ?" - (updatedAt, userId, contactId) +updateContactTs :: DB.Connection -> User -> Contact -> UTCTime -> IO () +updateContactTs db User {userId} Contact {contactId} updatedAt = + DB.execute + db + "UPDATE contacts SET updated_at = ? WHERE user_id = ? AND contact_id = ?" + (updatedAt, userId, contactId) -getGroupChatItemIdsAndFileInfo :: MonadUnliftIO m => SQLiteStore -> User -> Int64 -> m [(ChatItemId, UTCTime, Bool, Maybe CIFileInfo)] -getGroupChatItemIdsAndFileInfo st User {userId} groupId = - liftIO . withTransaction st $ \db -> - map toItemIdDeletedAndFileInfo - <$> DB.query - db - [sql| - SELECT i.chat_item_id, i.item_ts, i.item_deleted, f.file_id, f.ci_file_status, f.file_path - FROM chat_items i - LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - WHERE i.user_id = ? AND i.group_id = ? - ORDER BY i.item_ts ASC - |] - (userId, groupId) +getGroupChatItemIdsAndFileInfo :: DB.Connection -> User -> Int64 -> IO [(ChatItemId, UTCTime, Bool, Maybe CIFileInfo)] +getGroupChatItemIdsAndFileInfo db User {userId} groupId = + map toItemIdDeletedAndFileInfo + <$> DB.query + db + [sql| + SELECT i.chat_item_id, i.item_ts, i.item_deleted, f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.group_id = ? + ORDER BY i.item_ts ASC + |] + (userId, groupId) toItemIdDeletedAndFileInfo :: (ChatItemId, UTCTime, Bool, Maybe Int64, Maybe ACIFileStatus, Maybe FilePath) -> (ChatItemId, UTCTime, Bool, Maybe CIFileInfo) toItemIdDeletedAndFileInfo (chatItemId, itemTs, itemDeleted, fileId_, fileStatus_, filePath) = @@ -2350,79 +2227,73 @@ toItemIdDeletedAndFileInfo (chatItemId, itemTs, itemDeleted, fileId_, fileStatus (Just fileId, Just fileStatus) -> (chatItemId, itemTs, itemDeleted, Just CIFileInfo {fileId, fileStatus, filePath}) _ -> (chatItemId, itemTs, itemDeleted, Nothing) -updateGroupTs :: MonadUnliftIO m => SQLiteStore -> User -> GroupInfo -> UTCTime -> m () -updateGroupTs st User {userId} GroupInfo {groupId} updatedAt = - liftIO . withTransaction st $ \db -> +updateGroupTs :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO () +updateGroupTs db User {userId} GroupInfo {groupId} updatedAt = + DB.execute + db + "UPDATE groups SET updated_at = ? WHERE user_id = ? AND group_id = ?" + (updatedAt, userId, groupId) + +createNewSndMessage :: DB.Connection -> TVar ChaChaDRG -> ConnOrGroupId -> (SharedMsgId -> NewMessage) -> ExceptT StoreError IO SndMessage +createNewSndMessage db gVar connOrGroupId mkMessage = + createWithRandomId gVar $ \sharedMsgId -> do + let NewMessage {chatMsgEvent, msgBody} = mkMessage $ SharedMsgId sharedMsgId + createdAt <- getCurrentTime DB.execute db - "UPDATE groups SET updated_at = ? WHERE user_id = ? AND group_id = ?" - (updatedAt, userId, groupId) - -createNewSndMessage :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> ConnOrGroupId -> (SharedMsgId -> NewMessage) -> m SndMessage -createNewSndMessage st gVar connOrGroupId mkMessage = - liftIOEither . withTransaction st $ \db -> - createWithRandomId gVar $ \sharedMsgId -> do - let NewMessage {chatMsgEvent, msgBody} = mkMessage $ SharedMsgId sharedMsgId - createdAt <- getCurrentTime - DB.execute - db - [sql| - INSERT INTO messages ( - msg_sent, chat_msg_event, msg_body, connection_id, group_id, - shared_msg_id, shared_msg_id_user, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?) - |] - (MDSnd, toCMEventTag chatMsgEvent, msgBody, connId_, groupId_, sharedMsgId, Just True, createdAt, createdAt) - msgId <- insertedRowId db - pure SndMessage {msgId, sharedMsgId = SharedMsgId sharedMsgId, msgBody} + [sql| + INSERT INTO messages ( + msg_sent, chat_msg_event, msg_body, connection_id, group_id, + shared_msg_id, shared_msg_id_user, created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?) + |] + (MDSnd, toCMEventTag chatMsgEvent, msgBody, connId_, groupId_, sharedMsgId, Just True, createdAt, createdAt) + msgId <- insertedRowId db + pure SndMessage {msgId, sharedMsgId = SharedMsgId sharedMsgId, msgBody} where (connId_, groupId_) = case connOrGroupId of ConnectionId connId -> (Just connId, Nothing) GroupId groupId -> (Nothing, Just groupId) -createSndMsgDelivery :: MonadUnliftIO m => SQLiteStore -> SndMsgDelivery -> MessageId -> m () -createSndMsgDelivery st sndMsgDelivery messageId = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - msgDeliveryId <- createSndMsgDelivery_ db sndMsgDelivery messageId currentTs - createMsgDeliveryEvent_ db msgDeliveryId MDSSndAgent currentTs +createSndMsgDelivery :: DB.Connection -> SndMsgDelivery -> MessageId -> IO () +createSndMsgDelivery db sndMsgDelivery messageId = do + currentTs <- getCurrentTime + msgDeliveryId <- createSndMsgDelivery_ db sndMsgDelivery messageId currentTs + createMsgDeliveryEvent_ db msgDeliveryId MDSSndAgent currentTs -createNewMessageAndRcvMsgDelivery :: MonadUnliftIO m => SQLiteStore -> ConnOrGroupId -> NewMessage -> Maybe SharedMsgId -> RcvMsgDelivery -> m RcvMessage -createNewMessageAndRcvMsgDelivery st connOrGroupId NewMessage {chatMsgEvent, msgBody} sharedMsgId_ RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO messages (msg_sent, chat_msg_event, msg_body, created_at, updated_at, connection_id, group_id, shared_msg_id) VALUES (?,?,?,?,?,?,?,?)" - (MDRcv, toCMEventTag chatMsgEvent, msgBody, currentTs, currentTs, connId_, groupId_, sharedMsgId_) - msgId <- insertedRowId db - DB.execute - db - "INSERT INTO msg_deliveries (message_id, connection_id, agent_msg_id, agent_msg_meta, chat_ts, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, snd $ broker agentMsgMeta, currentTs, currentTs) - msgDeliveryId <- insertedRowId db - createMsgDeliveryEvent_ db msgDeliveryId MDSRcvAgent currentTs - pure RcvMessage {msgId, chatMsgEvent, sharedMsgId_, msgBody} +createNewMessageAndRcvMsgDelivery :: DB.Connection -> ConnOrGroupId -> NewMessage -> Maybe SharedMsgId -> RcvMsgDelivery -> IO RcvMessage +createNewMessageAndRcvMsgDelivery db connOrGroupId NewMessage {chatMsgEvent, msgBody} sharedMsgId_ RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} = do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO messages (msg_sent, chat_msg_event, msg_body, created_at, updated_at, connection_id, group_id, shared_msg_id) VALUES (?,?,?,?,?,?,?,?)" + (MDRcv, toCMEventTag chatMsgEvent, msgBody, currentTs, currentTs, connId_, groupId_, sharedMsgId_) + msgId <- insertedRowId db + DB.execute + db + "INSERT INTO msg_deliveries (message_id, connection_id, agent_msg_id, agent_msg_meta, chat_ts, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, snd $ broker agentMsgMeta, currentTs, currentTs) + msgDeliveryId <- insertedRowId db + createMsgDeliveryEvent_ db msgDeliveryId MDSRcvAgent currentTs + pure RcvMessage {msgId, chatMsgEvent, sharedMsgId_, msgBody} where (connId_, groupId_) = case connOrGroupId of ConnectionId connId' -> (Just connId', Nothing) GroupId groupId -> (Nothing, Just groupId) -createSndMsgDeliveryEvent :: StoreMonad m => SQLiteStore -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDSnd -> m () -createSndMsgDeliveryEvent st connId agentMsgId sndMsgDeliveryStatus = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - msgDeliveryId <- ExceptT $ getMsgDeliveryId_ db connId agentMsgId - liftIO $ do - currentTs <- getCurrentTime - createMsgDeliveryEvent_ db msgDeliveryId sndMsgDeliveryStatus currentTs +createSndMsgDeliveryEvent :: DB.Connection -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDSnd -> ExceptT StoreError IO () +createSndMsgDeliveryEvent db connId agentMsgId sndMsgDeliveryStatus = do + msgDeliveryId <- getMsgDeliveryId_ db connId agentMsgId + liftIO $ do + currentTs <- getCurrentTime + createMsgDeliveryEvent_ db msgDeliveryId sndMsgDeliveryStatus currentTs -createRcvMsgDeliveryEvent :: StoreMonad m => SQLiteStore -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDRcv -> m () -createRcvMsgDeliveryEvent st connId agentMsgId rcvMsgDeliveryStatus = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - msgDeliveryId <- ExceptT $ getMsgDeliveryId_ db connId agentMsgId - liftIO $ do - currentTs <- getCurrentTime - createMsgDeliveryEvent_ db msgDeliveryId rcvMsgDeliveryStatus currentTs +createRcvMsgDeliveryEvent :: DB.Connection -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDRcv -> ExceptT StoreError IO () +createRcvMsgDeliveryEvent db connId agentMsgId rcvMsgDeliveryStatus = do + msgDeliveryId <- getMsgDeliveryId_ db connId agentMsgId + liftIO $ do + currentTs <- getCurrentTime + createMsgDeliveryEvent_ db msgDeliveryId rcvMsgDeliveryStatus currentTs createSndMsgDelivery_ :: DB.Connection -> SndMsgDelivery -> MessageId -> UTCTime -> IO Int64 createSndMsgDelivery_ db SndMsgDelivery {connId, agentMsgId} messageId createdAt = do @@ -2447,9 +2318,9 @@ createMsgDeliveryEvent_ db msgDeliveryId msgDeliveryStatus createdAt = do |] (msgDeliveryId, msgDeliveryStatus, createdAt, createdAt) -getMsgDeliveryId_ :: DB.Connection -> Int64 -> AgentMsgId -> IO (Either StoreError Int64) +getMsgDeliveryId_ :: DB.Connection -> Int64 -> AgentMsgId -> ExceptT StoreError IO Int64 getMsgDeliveryId_ db connId agentMsgId = - firstRow fromOnly (SENoMsgDelivery connId agentMsgId) $ + ExceptT . firstRow fromOnly (SENoMsgDelivery connId agentMsgId) $ DB.query db [sql| @@ -2460,47 +2331,43 @@ getMsgDeliveryId_ db connId agentMsgId = |] (connId, agentMsgId) -createPendingGroupMessage :: MonadUnliftIO m => SQLiteStore -> Int64 -> MessageId -> Maybe Int64 -> m () -createPendingGroupMessage st groupMemberId messageId introId_ = - liftIO . withTransaction st $ \db -> do - currentTs <- getCurrentTime - DB.execute +createPendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> Maybe Int64 -> IO () +createPendingGroupMessage db groupMemberId messageId introId_ = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + INSERT INTO pending_group_messages + (group_member_id, message_id, group_member_intro_id, created_at, updated_at) VALUES (?,?,?,?,?) + |] + (groupMemberId, messageId, introId_, currentTs, currentTs) + +getPendingGroupMessages :: DB.Connection -> Int64 -> IO [PendingGroupMessage] +getPendingGroupMessages db groupMemberId = + map pendingGroupMessage + <$> DB.query db [sql| - INSERT INTO pending_group_messages - (group_member_id, message_id, group_member_intro_id, created_at, updated_at) VALUES (?,?,?,?,?) + SELECT pgm.message_id, m.chat_msg_event, m.msg_body, pgm.group_member_intro_id + FROM pending_group_messages pgm + JOIN messages m USING (message_id) + WHERE pgm.group_member_id = ? + ORDER BY pgm.message_id ASC |] - (groupMemberId, messageId, introId_, currentTs, currentTs) - -getPendingGroupMessages :: MonadUnliftIO m => SQLiteStore -> Int64 -> m [PendingGroupMessage] -getPendingGroupMessages st groupMemberId = - liftIO . withTransaction st $ \db -> - map pendingGroupMessage - <$> DB.query - db - [sql| - SELECT pgm.message_id, m.chat_msg_event, m.msg_body, pgm.group_member_intro_id - FROM pending_group_messages pgm - JOIN messages m USING (message_id) - WHERE pgm.group_member_id = ? - ORDER BY pgm.message_id ASC - |] - (Only groupMemberId) + (Only groupMemberId) where pendingGroupMessage (msgId, cmEventTag, msgBody, introId_) = PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} -deletePendingGroupMessage :: MonadUnliftIO m => SQLiteStore -> Int64 -> MessageId -> m () -deletePendingGroupMessage st groupMemberId messageId = - liftIO . withTransaction st $ \db -> - DB.execute db "DELETE FROM pending_group_messages WHERE group_member_id = ? AND message_id = ?" (groupMemberId, messageId) +deletePendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO () +deletePendingGroupMessage db groupMemberId messageId = + DB.execute db "DELETE FROM pending_group_messages WHERE group_member_id = ? AND message_id = ?" (groupMemberId, messageId) type NewQuoteRow = (Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool, Maybe MemberId) -createNewSndChatItem :: MonadUnliftIO m => SQLiteStore -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> UTCTime -> m ChatItemId -createNewSndChatItem st user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem createdAt = - liftIO . withTransaction st $ \db -> - createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow createdAt createdAt +createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> UTCTime -> IO ChatItemId +createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem createdAt = + createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow createdAt createdAt where createdByMsgId = if msgId == 0 then Nothing else Just msgId quoteRow :: NewQuoteRow @@ -2514,12 +2381,11 @@ createNewSndChatItem st user chatDirection SndMessage {msgId, sharedMsgId} ciCon CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId) CIQGroupRcv Nothing -> (Just False, Nothing) -createNewRcvChatItem :: MonadUnliftIO m => SQLiteStore -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> UTCTime -> UTCTime -> m (ChatItemId, Maybe (CIQuote c)) -createNewRcvChatItem st user chatDirection RcvMessage {msgId, chatMsgEvent} sharedMsgId_ ciContent itemTs createdAt = - liftIO . withTransaction st $ \db -> do - ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow itemTs createdAt - quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg - pure (ciId, quotedItem) +createNewRcvChatItem :: DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c)) +createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent} sharedMsgId_ ciContent itemTs createdAt = do + ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow itemTs createdAt + quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg + pure (ciId, quotedItem) where quotedMsg = cmToQuotedMsg chatMsgEvent quoteRow :: NewQuoteRow @@ -2531,10 +2397,9 @@ createNewRcvChatItem st user chatDirection RcvMessage {msgId, chatMsgEvent} shar CDGroupRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ -> (Just $ Just userMemberId == memberId, memberId) -createNewChatItemNoMsg :: forall c d m. (MsgDirectionI d, MonadUnliftIO m) => SQLiteStore -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> m ChatItemId -createNewChatItemNoMsg st user chatDirection ciContent itemTs createdAt = - liftIO . withTransaction st $ \db -> - createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow itemTs createdAt +createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> IO ChatItemId +createNewChatItemNoMsg db user chatDirection ciContent itemTs createdAt = + createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow itemTs createdAt where quoteRow :: NewQuoteRow quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) @@ -2588,8 +2453,8 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe ciQuote itemId dir = CIQuote dir itemId msgId sentAt content . parseMaybeMarkdownList $ msgContentText content getDirectChatItemQuote_ :: Int64 -> Bool -> IO (CIQuote 'CTDirect) getDirectChatItemQuote_ contactId userSent = do - ciQuoteDirect . listToMaybe . map fromOnly - <$> DB.query + fmap ciQuoteDirect . maybeFirstRow fromOnly $ + DB.query db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND shared_msg_id = ? AND item_sent = ?" (userId, contactId, msgId, userSent) @@ -2598,15 +2463,15 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe ciQuoteDirect = (`ciQuote` if userSent then CIQDirectSnd else CIQDirectRcv) getUserGroupChatItemId_ :: Int64 -> IO (Maybe ChatItemId) getUserGroupChatItemId_ groupId = - listToMaybe . map fromOnly - <$> DB.query + maybeFirstRow fromOnly $ + DB.query db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? AND item_sent = ? AND group_member_id IS NULL" (userId, groupId, msgId, MDSnd) getGroupChatItemId_ :: Int64 -> MemberId -> IO (Maybe ChatItemId) getGroupChatItemId_ groupId mId = - listToMaybe . map fromOnly - <$> DB.query + maybeFirstRow fromOnly $ + DB.query db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? AND item_sent = ? AND group_member_id = ?" (userId, groupId, msgId, MDRcv, mId) @@ -2635,14 +2500,13 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow -getChatPreviews :: MonadUnliftIO m => SQLiteStore -> User -> Bool -> m [AChat] -getChatPreviews st user withPCC = - liftIO . withTransaction st $ \db -> do - directChats <- getDirectChatPreviews_ db user - groupChats <- getGroupChatPreviews_ db user - cReqChats <- getContactRequestChatPreviews_ db user - connChats <- getContactConnectionChatPreviews_ db user withPCC - pure $ sortOn (Down . ts) (directChats <> groupChats <> cReqChats <> connChats) +getChatPreviews :: DB.Connection -> User -> Bool -> IO [AChat] +getChatPreviews db user withPCC = do + directChats <- getDirectChatPreviews_ db user + groupChats <- getGroupChatPreviews_ db user + cReqChats <- getContactRequestChatPreviews_ db user + connChats <- getContactConnectionChatPreviews_ db user withPCC + pure $ sortOn (Down . ts) (directChats <> groupChats <> cReqChats <> connChats) where ts :: AChat -> UTCTime ts (AChat _ Chat {chatInfo, chatItems = ci : _}) = max (chatItemTs ci) (chatInfoUpdatedAt chatInfo) @@ -2820,55 +2684,52 @@ getContactConnectionChatPreviews_ db User {userId} _ = stats = ChatStats {unreadCount = 0, minUnreadItemId = 0} in AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats -getPendingContactConnection :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m PendingContactConnection -getPendingContactConnection st userId connId = - liftIOEither . withTransaction st $ \db -> do - firstRow toPendingContactConnection (SEPendingConnectionNotFound connId) $ - DB.query - db - [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, created_at, updated_at - FROM connections - WHERE user_id = ? - AND connection_id = ? - AND conn_type = ? - AND contact_id IS NULL - AND conn_level = 0 - AND via_contact IS NULL - |] - (userId, connId, ConnContact) - -deletePendingContactConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> m () -deletePendingContactConnection st userId connId = - liftIO . withTransaction st $ \db -> - DB.execute +getPendingContactConnection :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection +getPendingContactConnection db userId connId = do + ExceptT . firstRow toPendingContactConnection (SEPendingConnectionNotFound connId) $ + DB.query db [sql| - DELETE FROM connections - WHERE user_id = ? - AND connection_id = ? - AND conn_type = ? - AND contact_id IS NULL - AND conn_level = 0 - AND via_contact IS NULL + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, created_at, updated_at + FROM connections + WHERE user_id = ? + AND connection_id = ? + AND conn_type = ? + AND contact_id IS NULL + AND conn_level = 0 + AND via_contact IS NULL |] (userId, connId, ConnContact) +deletePendingContactConnection :: DB.Connection -> UserId -> Int64 -> IO () +deletePendingContactConnection db userId connId = + DB.execute + db + [sql| + DELETE FROM connections + WHERE user_id = ? + AND connection_id = ? + AND conn_type = ? + AND contact_id IS NULL + AND conn_level = 0 + AND via_contact IS NULL + |] + (userId, connId, ConnContact) + toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, UTCTime, UTCTime) -> PendingContactConnection toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, createdAt, updatedAt) = PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, createdAt, updatedAt} -getDirectChat :: StoreMonad m => SQLiteStore -> User -> Int64 -> ChatPagination -> m (Chat 'CTDirect) -getDirectChat st user contactId pagination = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - case pagination of - CPLast count -> getDirectChatLast_ db user contactId count - CPAfter afterId count -> getDirectChatAfter_ db user contactId afterId count - CPBefore beforeId count -> getDirectChatBefore_ db user contactId beforeId count +getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChat db user contactId pagination = do + case pagination of + CPLast count -> getDirectChatLast_ db user contactId count + CPAfter afterId count -> getDirectChatAfter_ db user contactId afterId count + CPBefore beforeId count -> getDirectChatBefore_ db user contactId beforeId count getDirectChatLast_ :: DB.Connection -> User -> Int64 -> Int -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChatLast_ db User {userId} contactId count = do - contact <- ExceptT $ getContact_ db userId contactId + contact <- getContact db userId contactId stats <- liftIO $ getDirectChatStats_ db userId contactId chatItems <- ExceptT getDirectChatItemsLast_ pure $ Chat (DirectChat contact) (reverse chatItems) stats @@ -2899,7 +2760,7 @@ getDirectChatLast_ db User {userId} contactId count = do getDirectChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChatAfter_ db User {userId} contactId afterChatItemId count = do - contact <- ExceptT $ getContact_ db userId contactId + contact <- getContact db userId contactId stats <- liftIO $ getDirectChatStats_ db userId contactId chatItems <- ExceptT getDirectChatItemsAfter_ pure $ Chat (DirectChat contact) chatItems stats @@ -2930,7 +2791,7 @@ getDirectChatAfter_ db User {userId} contactId afterChatItemId count = do getDirectChatBefore_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChatBefore_ db User {userId} contactId beforeChatItemId count = do - contact <- ExceptT $ getContact_ db userId contactId + contact <- getContact db userId contactId stats <- liftIO $ getDirectChatStats_ db userId contactId chatItems <- ExceptT getDirectChatItemsBefore_ pure $ Chat (DirectChat contact) (reverse chatItems) stats @@ -2976,64 +2837,51 @@ getDirectChatStats_ db userId contactId = toChatStats' [statsRow] = toChatStats statsRow toChatStats' _ = ChatStats {unreadCount = 0, minUnreadItemId = 0} -getContactIdByName :: StoreMonad m => SQLiteStore -> UserId -> ContactName -> m Int64 -getContactIdByName st userId cName = - liftIOEither . withTransaction st $ \db -> getContactIdByName_ db userId cName - -getContactIdByName_ :: DB.Connection -> UserId -> ContactName -> IO (Either StoreError Int64) -getContactIdByName_ db userId cName = - firstRow fromOnly (SEContactNotFoundByName cName) $ +getContactIdByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Int64 +getContactIdByName db userId cName = + ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $ DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ?" (userId, cName) -getContact :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m Contact -getContact st userId contactId = - liftIOEither . withTransaction st $ \db -> getContact_ db userId contactId +getContact :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO Contact +getContact db userId contactId = + ExceptT . fmap join . firstRow toContactOrError (SEContactNotFound contactId) $ + DB.query + db + [sql| + SELECT + -- Contact + ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + LEFT JOIN connections c ON c.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.contact_id = ? + AND c.connection_id = ( + SELECT cc_connection_id FROM ( + SELECT + cc.connection_id AS cc_connection_id, + (CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord + FROM connections cc + WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id + ORDER BY cc_conn_status_ord DESC, cc_connection_id DESC + LIMIT 1 + ) + ) + |] + (userId, contactId, ConnReady, ConnSndReady) -getContact_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError Contact) -getContact_ db userId contactId = - join - <$> firstRow - toContactOrError - (SEContactNotFound contactId) - ( DB.query - db - [sql| - SELECT - -- Contact - ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, - -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at - FROM contacts ct - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - LEFT JOIN connections c ON c.contact_id = ct.contact_id - WHERE ct.user_id = ? AND ct.contact_id = ? - AND c.connection_id = ( - SELECT cc_connection_id FROM ( - SELECT - cc.connection_id AS cc_connection_id, - (CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord - FROM connections cc - WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id - ORDER BY cc_conn_status_ord DESC, cc_connection_id DESC - LIMIT 1 - ) - ) - |] - (userId, contactId, ConnReady, ConnSndReady) - ) - -getGroupChat :: StoreMonad m => SQLiteStore -> User -> Int64 -> ChatPagination -> m (Chat 'CTGroup) -getGroupChat st user groupId pagination = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - case pagination of - CPLast count -> getGroupChatLast_ db user groupId count - CPAfter afterId count -> getGroupChatAfter_ db user groupId afterId count - CPBefore beforeId count -> getGroupChatBefore_ db user groupId beforeId count +getGroupChat :: DB.Connection -> User -> Int64 -> ChatPagination -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChat db user groupId pagination = do + case pagination of + CPLast count -> getGroupChatLast_ db user groupId count + CPAfter afterId count -> getGroupChatAfter_ db user groupId afterId count + CPBefore beforeId count -> getGroupChatBefore_ db user groupId beforeId count getGroupChatLast_ :: DB.Connection -> User -> Int64 -> Int -> ExceptT StoreError IO (Chat 'CTGroup) getGroupChatLast_ db user@User {userId, userContactId} groupId count = do - groupInfo <- ExceptT $ getGroupInfo_ db user groupId + groupInfo <- getGroupInfo db user groupId stats <- liftIO $ getGroupChatStats_ db userId groupId chatItems <- ExceptT getGroupChatItemsLast_ pure $ Chat (GroupChat groupInfo) (reverse chatItems) stats @@ -3076,7 +2924,7 @@ getGroupChatLast_ db user@User {userId, userContactId} groupId count = do getGroupChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTGroup) getGroupChatAfter_ db user@User {userId, userContactId} groupId afterChatItemId count = do - groupInfo <- ExceptT $ getGroupInfo_ db user groupId + groupInfo <- getGroupInfo db user groupId stats <- liftIO $ getGroupChatStats_ db userId groupId chatItems <- ExceptT getGroupChatItemsAfter_ pure $ Chat (GroupChat groupInfo) chatItems stats @@ -3119,7 +2967,7 @@ getGroupChatAfter_ db user@User {userId, userContactId} groupId afterChatItemId getGroupChatBefore_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTGroup) getGroupChatBefore_ db user@User {userId, userContactId} groupId beforeChatItemId count = do - groupInfo <- ExceptT $ getGroupInfo_ db user groupId + groupInfo <- getGroupInfo db user groupId stats <- liftIO $ getGroupChatStats_ db userId groupId chatItems <- ExceptT getGroupChatItemsBefore_ pure $ Chat (GroupChat groupInfo) (reverse chatItems) stats @@ -3177,14 +3025,9 @@ getGroupChatStats_ db userId groupId = toChatStats' [statsRow] = toChatStats statsRow toChatStats' _ = ChatStats {unreadCount = 0, minUnreadItemId = 0} -getGroupInfo :: StoreMonad m => SQLiteStore -> User -> Int64 -> m GroupInfo -getGroupInfo st user groupId = - liftIOEither . withTransaction st $ \db -> - getGroupInfo_ db user groupId - -getGroupInfo_ :: DB.Connection -> User -> Int64 -> IO (Either StoreError GroupInfo) -getGroupInfo_ db User {userId, userContactId} groupId = - firstRow (toGroupInfo userContactId) (SEGroupNotFound groupId) $ +getGroupInfo :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO GroupInfo +getGroupInfo db User {userId, userContactId} groupId = + ExceptT . firstRow (toGroupInfo userContactId) (SEGroupNotFound groupId) $ DB.query db [sql| @@ -3203,13 +3046,12 @@ getGroupInfo_ db User {userId, userContactId} groupId = |] (groupId, userId, userContactId) -getAllChatItems :: StoreMonad m => SQLiteStore -> User -> ChatPagination -> m [AChatItem] -getAllChatItems st user pagination = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - case pagination of - CPLast count -> getAllChatItemsLast_ db user count - CPAfter _afterId _count -> throwError $ SEInternalError "not implemented" - CPBefore _beforeId _count -> throwError $ SEInternalError "not implemented" +getAllChatItems :: DB.Connection -> User -> ChatPagination -> ExceptT StoreError IO [AChatItem] +getAllChatItems db user pagination = do + case pagination of + CPLast count -> getAllChatItemsLast_ db user count + CPAfter _afterId _count -> throwError $ SEInternalError "not implemented" + CPBefore _beforeId _count -> throwError $ SEInternalError "not implemented" getAllChatItemsLast_ :: DB.Connection -> User -> Int -> ExceptT StoreError IO [AChatItem] getAllChatItemsLast_ db user@User {userId} count = do @@ -3228,23 +3070,15 @@ getAllChatItemsLast_ db user@User {userId} count = do (userId, count) mapM (uncurry $ getAChatItem_ db user) itemRefs -getGroupIdByName :: StoreMonad m => SQLiteStore -> User -> GroupName -> m Int64 -getGroupIdByName st user gName = - liftIOEither . withTransaction st $ \db -> getGroupIdByName_ db user gName - -getGroupIdByName_ :: DB.Connection -> User -> GroupName -> IO (Either StoreError Int64) -getGroupIdByName_ db User {userId} gName = - firstRow fromOnly (SEGroupNotFoundByName gName) $ +getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO Int64 +getGroupIdByName db User {userId} gName = + ExceptT . firstRow fromOnly (SEGroupNotFoundByName gName) $ DB.query db "SELECT group_id FROM groups WHERE user_id = ? AND local_display_name = ?" (userId, gName) -getChatItemIdByAgentMsgId :: StoreMonad m => SQLiteStore -> Int64 -> AgentMsgId -> m (Maybe ChatItemId) -getChatItemIdByAgentMsgId st connId msgId = - liftIO . withTransaction st $ \db -> getChatItemIdByAgentMsgId_ db connId msgId - -getChatItemIdByAgentMsgId_ :: DB.Connection -> Int64 -> AgentMsgId -> IO (Maybe ChatItemId) -getChatItemIdByAgentMsgId_ db connId msgId = - join . listToMaybe . map fromOnly - <$> DB.query +getChatItemIdByAgentMsgId :: DB.Connection -> Int64 -> AgentMsgId -> IO (Maybe ChatItemId) +getChatItemIdByAgentMsgId db connId msgId = + fmap join . maybeFirstRow fromOnly $ + DB.query db [sql| SELECT chat_item_id @@ -3258,28 +3092,26 @@ getChatItemIdByAgentMsgId_ db connId msgId = |] (connId, msgId) -updateDirectChatItemStatus :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> UserId -> Int64 -> ChatItemId -> CIStatus d -> m (ChatItem 'CTDirect d) -updateDirectChatItemStatus st userId contactId itemId itemStatus = do - liftIOEither . withTransaction st $ \db -> runExceptT $ do - ci <- ExceptT $ (correctDir =<<) <$> getDirectChatItem_ db userId contactId itemId - currentTs <- liftIO getCurrentTime - liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId) - pure ci {meta = (meta ci) {itemStatus}} +updateDirectChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> UserId -> Int64 -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItemStatus db userId contactId itemId itemStatus = do + ci <- liftEither . correctDir =<< getDirectChatItem db userId contactId itemId + currentTs <- liftIO getCurrentTime + liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId) + pure ci {meta = (meta ci) {itemStatus}} where correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateDirectChatItem :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> UserId -> Int64 -> ChatItemId -> CIContent d -> Maybe MessageId -> m (ChatItem 'CTDirect d) -updateDirectChatItem st userId contactId itemId newContent msgId_ = - liftIOEither . withTransaction st $ \db -> do - currentTs <- liftIO getCurrentTime - ci <- updateDirectChatItem_ db userId contactId itemId newContent currentTs - when (isRight ci) . forM_ msgId_ $ \msgId -> liftIO $ insertChatItemMessage_ db itemId msgId currentTs - pure ci +updateDirectChatItem :: forall d. MsgDirectionI d => DB.Connection -> UserId -> Int64 -> ChatItemId -> CIContent d -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItem db userId contactId itemId newContent msgId_ = do + currentTs <- liftIO getCurrentTime + ci <- updateDirectChatItem_ db userId contactId itemId newContent currentTs + forM_ msgId_ $ \msgId -> liftIO $ insertChatItemMessage_ db itemId msgId currentTs + pure ci -updateDirectChatItem_ :: forall d. (MsgDirectionI d) => DB.Connection -> UserId -> Int64 -> ChatItemId -> CIContent d -> UTCTime -> IO (Either StoreError (ChatItem 'CTDirect d)) -updateDirectChatItem_ db userId contactId itemId newContent currentTs = runExceptT $ do - ci <- ExceptT $ (correctDir =<<) <$> getDirectChatItem_ db userId contactId itemId +updateDirectChatItem_ :: forall d. (MsgDirectionI d) => DB.Connection -> UserId -> Int64 -> ChatItemId -> CIContent d -> UTCTime -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItem_ db userId contactId itemId newContent currentTs = do + ci <- liftEither . correctDir =<< getDirectChatItem db userId contactId itemId let newText = ciContentToText newContent liftIO $ do DB.execute @@ -3295,15 +3127,14 @@ updateDirectChatItem_ db userId contactId itemId newContent currentTs = runExcep correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -deleteDirectChatItemLocal :: StoreMonad m => SQLiteStore -> UserId -> Contact -> ChatItemId -> CIDeleteMode -> m AChatItem -deleteDirectChatItemLocal st userId ct itemId mode = - liftIOEither . withTransaction st $ \db -> do - deleteChatItemMessages_ db itemId - deleteDirectChatItem_ db userId ct itemId mode +deleteDirectChatItemLocal :: DB.Connection -> UserId -> Contact -> ChatItemId -> CIDeleteMode -> ExceptT StoreError IO AChatItem +deleteDirectChatItemLocal db userId ct itemId mode = do + liftIO $ deleteChatItemMessages_ db itemId + deleteDirectChatItem_ db userId ct itemId mode -deleteDirectChatItem_ :: DB.Connection -> UserId -> Contact -> ChatItemId -> CIDeleteMode -> IO (Either StoreError AChatItem) -deleteDirectChatItem_ db userId ct@Contact {contactId} itemId mode = runExceptT $ do - (CChatItem msgDir ci) <- ExceptT $ getDirectChatItem_ db userId contactId itemId +deleteDirectChatItem_ :: DB.Connection -> UserId -> Contact -> ChatItemId -> CIDeleteMode -> ExceptT StoreError IO AChatItem +deleteDirectChatItem_ db userId ct@Contact {contactId} itemId mode = do + (CChatItem msgDir ci) <- getDirectChatItem db userId contactId itemId let toContent = msgDirToDeletedContent_ msgDir mode liftIO $ do DB.execute @@ -3346,16 +3177,15 @@ setChatItemMessagesDeleted_ db itemId = where xMsgDeletedBody = strEncode ChatMessage {msgId = Nothing, chatMsgEvent = XMsgDeleted} -deleteDirectChatItemRcvBroadcast :: StoreMonad m => SQLiteStore -> UserId -> Contact -> ChatItemId -> MessageId -> m AChatItem -deleteDirectChatItemRcvBroadcast st userId ct itemId msgId = - liftIOEither . withTransaction st $ \db -> do - currentTs <- liftIO getCurrentTime - insertChatItemMessage_ db itemId msgId currentTs - updateDirectChatItemRcvDeleted_ db userId ct itemId currentTs +deleteDirectChatItemRcvBroadcast :: DB.Connection -> UserId -> Contact -> ChatItemId -> MessageId -> ExceptT StoreError IO AChatItem +deleteDirectChatItemRcvBroadcast db userId ct itemId msgId = do + currentTs <- liftIO getCurrentTime + liftIO $ insertChatItemMessage_ db itemId msgId currentTs + updateDirectChatItemRcvDeleted_ db userId ct itemId currentTs -updateDirectChatItemRcvDeleted_ :: DB.Connection -> UserId -> Contact -> ChatItemId -> UTCTime -> IO (Either StoreError AChatItem) -updateDirectChatItemRcvDeleted_ db userId ct@Contact {contactId} itemId currentTs = runExceptT $ do - (CChatItem msgDir ci) <- ExceptT $ getDirectChatItem_ db userId contactId itemId +updateDirectChatItemRcvDeleted_ :: DB.Connection -> UserId -> Contact -> ChatItemId -> UTCTime -> ExceptT StoreError IO AChatItem +updateDirectChatItemRcvDeleted_ db userId ct@Contact {contactId} itemId currentTs = do + (CChatItem msgDir ci) <- getDirectChatItem db userId contactId itemId let toContent = msgDirToDeletedContent_ msgDir CIDMBroadcast toText = ciDeleteModeToText CIDMBroadcast liftIO $ do @@ -3380,25 +3210,19 @@ deleteQuote_ db itemId = |] (Only itemId) -getDirectChatItem :: StoreMonad m => SQLiteStore -> UserId -> ContactId -> ChatItemId -> m (CChatItem 'CTDirect) -getDirectChatItem st userId contactId itemId = - liftIOEither . withTransaction st $ \db -> getDirectChatItem_ db userId contactId itemId +getDirectChatItemBySharedMsgId :: DB.Connection -> UserId -> ContactId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTDirect) +getDirectChatItemBySharedMsgId db userId contactId sharedMsgId = do + itemId <- getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId + getDirectChatItem db userId contactId itemId -getDirectChatItemBySharedMsgId :: StoreMonad m => SQLiteStore -> UserId -> ContactId -> SharedMsgId -> m (CChatItem 'CTDirect) -getDirectChatItemBySharedMsgId st userId contactId sharedMsgId = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - itemId <- ExceptT $ getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId - liftIOEither $ getDirectChatItem_ db userId contactId itemId +getDirectChatItemByAgentMsgId :: DB.Connection -> UserId -> ContactId -> Int64 -> AgentMsgId -> IO (Maybe (CChatItem 'CTDirect)) +getDirectChatItemByAgentMsgId db userId contactId connId msgId = do + itemId_ <- getChatItemIdByAgentMsgId db connId msgId + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getDirectChatItem db userId contactId) itemId_ -getDirectChatItemByAgentMsgId :: MonadUnliftIO m => SQLiteStore -> UserId -> ContactId -> Int64 -> AgentMsgId -> m (Maybe (CChatItem 'CTDirect)) -getDirectChatItemByAgentMsgId st userId contactId connId msgId = - liftIO . withTransaction st $ \db -> do - itemId_ <- getChatItemIdByAgentMsgId_ db connId msgId - maybe (pure Nothing) (fmap eitherToMaybe . getDirectChatItem_ db userId contactId) itemId_ - -getDirectChatItemIdBySharedMsgId_ :: DB.Connection -> UserId -> Int64 -> SharedMsgId -> IO (Either StoreError Int64) +getDirectChatItemIdBySharedMsgId_ :: DB.Connection -> UserId -> Int64 -> SharedMsgId -> ExceptT StoreError IO Int64 getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId = - firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $ + ExceptT . firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $ DB.query db [sql| @@ -3410,8 +3234,8 @@ getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId = |] (userId, contactId, sharedMsgId) -getDirectChatItem_ :: DB.Connection -> UserId -> Int64 -> ChatItemId -> IO (Either StoreError (CChatItem 'CTDirect)) -getDirectChatItem_ db userId contactId itemId = do +getDirectChatItem :: DB.Connection -> UserId -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTDirect) +getDirectChatItem db userId contactId itemId = ExceptT $ do tz <- getCurrentTimeZone currentTs <- getCurrentTime join <$> firstRow (toDirectChatItem tz currentTs) (SEChatItemNotFound itemId) getItem @@ -3434,28 +3258,23 @@ getDirectChatItem_ db userId contactId itemId = do |] (userId, contactId, itemId) -getDirectChatItemIdByText :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> SMsgDirection d -> Text -> m ChatItemId -getDirectChatItemIdByText st userId contactId msgDir quotedMsg = - liftIOEither . withTransaction st $ \db -> - firstRow fromOnly SEQuotedChatItemNotFound $ - DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND item_sent = ? AND item_text like ? - ORDER BY chat_item_id DESC - LIMIT 1 - |] - (userId, contactId, msgDir, quotedMsg <> "%") +getDirectChatItemIdByText :: DB.Connection -> UserId -> Int64 -> SMsgDirection d -> Text -> ExceptT StoreError IO ChatItemId +getDirectChatItemIdByText db userId contactId msgDir quotedMsg = + ExceptT . firstRow fromOnly SEQuotedChatItemNotFound $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_sent = ? AND item_text like ? + ORDER BY chat_item_id DESC + LIMIT 1 + |] + (userId, contactId, msgDir, quotedMsg <> "%") -updateGroupChatItem :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> User -> Int64 -> ChatItemId -> CIContent d -> MessageId -> m (ChatItem 'CTGroup d) -updateGroupChatItem st user groupId itemId newContent msgId = - liftIOEither . withTransaction st $ \db -> updateGroupChatItem_ db user groupId itemId newContent msgId - -updateGroupChatItem_ :: forall d. (MsgDirectionI d) => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> MessageId -> IO (Either StoreError (ChatItem 'CTGroup d)) -updateGroupChatItem_ db user@User {userId} groupId itemId newContent msgId = runExceptT $ do - ci <- ExceptT $ (correctDir =<<) <$> getGroupChatItem_ db user groupId itemId +updateGroupChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> MessageId -> ExceptT StoreError IO (ChatItem 'CTGroup d) +updateGroupChatItem db user@User {userId} groupId itemId newContent msgId = do + ci <- liftEither . correctDir =<< getGroupChatItem db user groupId itemId currentTs <- liftIO getCurrentTime let newText = ciContentToText newContent liftIO $ do @@ -3473,36 +3292,34 @@ updateGroupChatItem_ db user@User {userId} groupId itemId newContent msgId = run correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -deleteGroupChatItemInternal :: StoreMonad m => SQLiteStore -> User -> GroupInfo -> ChatItemId -> m AChatItem -deleteGroupChatItemInternal st user gInfo itemId = - liftIOEither . withTransaction st $ \db -> do - currentTs <- liftIO getCurrentTime - ci <- deleteGroupChatItem_ db user gInfo itemId CIDMInternal True currentTs - setChatItemMessagesDeleted_ db itemId - DB.execute db "DELETE FROM files WHERE chat_item_id = ?" (Only itemId) - pure ci +deleteGroupChatItemInternal :: DB.Connection -> User -> GroupInfo -> ChatItemId -> ExceptT StoreError IO AChatItem +deleteGroupChatItemInternal db user gInfo itemId = do + currentTs <- liftIO getCurrentTime + ci <- deleteGroupChatItem_ db user gInfo itemId CIDMInternal True currentTs + liftIO $ setChatItemMessagesDeleted_ db itemId + liftIO $ DB.execute db "DELETE FROM files WHERE chat_item_id = ?" (Only itemId) + pure ci -deleteGroupChatItemRcvBroadcast :: StoreMonad m => SQLiteStore -> User -> GroupInfo -> ChatItemId -> MessageId -> m AChatItem -deleteGroupChatItemRcvBroadcast st user gInfo itemId msgId = - liftIOEither . withTransaction st $ \db -> deleteGroupChatItemBroadcast_ db user gInfo itemId False msgId +deleteGroupChatItemRcvBroadcast :: DB.Connection -> User -> GroupInfo -> ChatItemId -> MessageId -> ExceptT StoreError IO AChatItem +deleteGroupChatItemRcvBroadcast db user gInfo itemId msgId = + deleteGroupChatItemBroadcast_ db user gInfo itemId False msgId -deleteGroupChatItemSndBroadcast :: StoreMonad m => SQLiteStore -> User -> GroupInfo -> ChatItemId -> MessageId -> m AChatItem -deleteGroupChatItemSndBroadcast st user gInfo itemId msgId = - liftIOEither . withTransaction st $ \db -> do - ci <- deleteGroupChatItemBroadcast_ db user gInfo itemId True msgId - setChatItemMessagesDeleted_ db itemId - DB.execute db "DELETE FROM files WHERE chat_item_id = ?" (Only itemId) - pure ci +deleteGroupChatItemSndBroadcast :: DB.Connection -> User -> GroupInfo -> ChatItemId -> MessageId -> ExceptT StoreError IO AChatItem +deleteGroupChatItemSndBroadcast db user gInfo itemId msgId = do + ci <- deleteGroupChatItemBroadcast_ db user gInfo itemId True msgId + liftIO $ setChatItemMessagesDeleted_ db itemId + liftIO $ DB.execute db "DELETE FROM files WHERE chat_item_id = ?" (Only itemId) + pure ci -deleteGroupChatItemBroadcast_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Bool -> MessageId -> IO (Either StoreError AChatItem) +deleteGroupChatItemBroadcast_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Bool -> MessageId -> ExceptT StoreError IO AChatItem deleteGroupChatItemBroadcast_ db user gInfo itemId itemDeleted msgId = do currentTs <- liftIO getCurrentTime - insertChatItemMessage_ db itemId msgId currentTs + liftIO $ insertChatItemMessage_ db itemId msgId currentTs deleteGroupChatItem_ db user gInfo itemId CIDMBroadcast itemDeleted currentTs -deleteGroupChatItem_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> CIDeleteMode -> Bool -> UTCTime -> IO (Either StoreError AChatItem) -deleteGroupChatItem_ db user@User {userId} gInfo@GroupInfo {groupId} itemId mode itemDeleted currentTs = runExceptT $ do - (CChatItem msgDir ci) <- ExceptT $ getGroupChatItem_ db user groupId itemId +deleteGroupChatItem_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> CIDeleteMode -> Bool -> UTCTime -> ExceptT StoreError IO AChatItem +deleteGroupChatItem_ db user@User {userId} gInfo@GroupInfo {groupId} itemId mode itemDeleted currentTs = do + (CChatItem msgDir ci) <- getGroupChatItem db user groupId itemId let toContent = msgDirToDeletedContent_ msgDir mode liftIO $ do DB.execute @@ -3518,34 +3335,26 @@ deleteGroupChatItem_ db user@User {userId} gInfo@GroupInfo {groupId} itemId mode where toText = ciDeleteModeToText mode -getGroupChatItem :: StoreMonad m => SQLiteStore -> User -> Int64 -> ChatItemId -> m (CChatItem 'CTGroup) -getGroupChatItem st user groupId itemId = - liftIOEither . withTransaction st $ \db -> getGroupChatItem_ db user groupId itemId +getGroupChatItemBySharedMsgId :: DB.Connection -> User -> Int64 -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) +getGroupChatItemBySharedMsgId db user@User {userId} groupId sharedMsgId = do + itemId <- + ExceptT . firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? + ORDER BY chat_item_id DESC + LIMIT 1 + |] + (userId, groupId, sharedMsgId) + getGroupChatItem db user groupId itemId -getGroupChatItemBySharedMsgId :: StoreMonad m => SQLiteStore -> User -> Int64 -> SharedMsgId -> m (CChatItem 'CTGroup) -getGroupChatItemBySharedMsgId st user groupId sharedMsgId = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - itemId <- ExceptT $ getGroupChatItemIdBySharedMsgId_ db user groupId sharedMsgId - liftIOEither $ getGroupChatItem_ db user groupId itemId - -getGroupChatItemIdBySharedMsgId_ :: DB.Connection -> User -> Int64 -> SharedMsgId -> IO (Either StoreError Int64) -getGroupChatItemIdBySharedMsgId_ db User {userId} groupId sharedMsgId = - firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $ - DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? - ORDER BY chat_item_id DESC - LIMIT 1 - |] - (userId, groupId, sharedMsgId) - -getGroupChatItem_ :: DB.Connection -> User -> Int64 -> ChatItemId -> IO (Either StoreError (CChatItem 'CTGroup)) -getGroupChatItem_ db User {userId, userContactId} groupId itemId = do +getGroupChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup) +getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do tz <- getCurrentTimeZone - currentTs <- liftIO getCurrentTime + currentTs <- getCurrentTime join <$> firstRow (toGroupChatItem tz currentTs userContactId) (SEChatItemNotFound itemId) getItem where getItem = @@ -3578,16 +3387,15 @@ getGroupChatItem_ db User {userId, userContactId} groupId itemId = do |] (userId, groupId, itemId) -getGroupChatItemIdByText :: StoreMonad m => SQLiteStore -> User -> Int64 -> Maybe ContactName -> Text -> m ChatItemId -getGroupChatItemIdByText st User {userId, localDisplayName = userName} groupId contactName_ quotedMsg = - liftIOEither . withTransaction st $ \db -> - firstRow fromOnly SEQuotedChatItemNotFound $ case contactName_ of - Nothing -> anyMemberChatItem_ db - Just cName - | userName == cName -> userChatItem_ db - | otherwise -> memberChatItem_ db cName +getGroupChatItemIdByText :: DB.Connection -> User -> Int64 -> Maybe ContactName -> Text -> ExceptT StoreError IO ChatItemId +getGroupChatItemIdByText db User {userId, localDisplayName = userName} groupId contactName_ quotedMsg = + ExceptT . firstRow fromOnly SEQuotedChatItemNotFound $ case contactName_ of + Nothing -> anyMemberChatItem_ + Just cName + | userName == cName -> userChatItem_ + | otherwise -> memberChatItem_ cName where - anyMemberChatItem_ db = + anyMemberChatItem_ = DB.query db [sql| @@ -3598,7 +3406,7 @@ getGroupChatItemIdByText st User {userId, localDisplayName = userName} groupId c LIMIT 1 |] (userId, groupId, quotedMsg <> "%") - userChatItem_ db = + userChatItem_ = DB.query db [sql| @@ -3609,7 +3417,7 @@ getGroupChatItemIdByText st User {userId, localDisplayName = userName} groupId c LIMIT 1 |] (userId, groupId, quotedMsg <> "%") - memberChatItem_ db cName = + memberChatItem_ cName = DB.query db [sql| @@ -3623,51 +3431,42 @@ getGroupChatItemIdByText st User {userId, localDisplayName = userName} groupId c |] (userId, groupId, cName, quotedMsg <> "%") -getChatItemByFileId :: StoreMonad m => SQLiteStore -> User -> Int64 -> m AChatItem -getChatItemByFileId st user fileId = - liftIOEither . withTransaction st $ \db -> - getChatItemByFileId_ db user fileId - -getChatItemByFileId_ :: DB.Connection -> User -> Int64 -> IO (Either StoreError AChatItem) -getChatItemByFileId_ db user@User {userId} fileId = runExceptT $ do - (itemId, chatRef) <- ExceptT $ getChatItemIdByFileId_ db userId fileId +getChatItemByFileId :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO AChatItem +getChatItemByFileId db user@User {userId} fileId = do + (itemId, chatRef) <- + ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByFileId fileId) $ + DB.query + db + [sql| + SELECT i.chat_item_id, i.contact_id, i.group_id + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE f.user_id = ? AND f.file_id = ? + LIMIT 1 + |] + (userId, fileId) getAChatItem_ db user itemId chatRef getAChatItem_ :: DB.Connection -> User -> ChatItemId -> ChatRef -> ExceptT StoreError IO AChatItem getAChatItem_ db user@User {userId} itemId = \case ChatRef CTDirect contactId -> do - ct <- ExceptT $ getContact_ db userId contactId - (CChatItem msgDir ci) <- ExceptT $ getDirectChatItem_ db userId contactId itemId + ct <- getContact db userId contactId + (CChatItem msgDir ci) <- getDirectChatItem db userId contactId itemId pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci ChatRef CTGroup groupId -> do - gInfo <- ExceptT $ getGroupInfo_ db user groupId - (CChatItem msgDir ci) <- ExceptT $ getGroupChatItem_ db user groupId itemId + gInfo <- getGroupInfo db user groupId + (CChatItem msgDir ci) <- getGroupChatItem db user groupId itemId pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) ci _ -> throwError $ SEChatItemNotFound itemId -getChatItemIdByFileId_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError (ChatItemId, ChatRef)) -getChatItemIdByFileId_ db userId fileId = - firstRow' toChatItemRef (SEChatItemNotFoundByFileId fileId) $ - DB.query - db - [sql| - SELECT i.chat_item_id, i.contact_id, i.group_id - FROM chat_items i - JOIN files f ON f.chat_item_id = i.chat_item_id - WHERE f.user_id = ? AND f.file_id = ? - LIMIT 1 - |] - (userId, fileId) - -updateDirectCIFileStatus :: forall d m. (MsgDirectionI d, StoreMonad m) => SQLiteStore -> User -> Int64 -> CIFileStatus d -> m AChatItem -updateDirectCIFileStatus st user fileId fileStatus = - liftIOEither . withTransaction st $ \db -> runExceptT $ do - aci@(AChatItem cType d cInfo ci) <- ExceptT $ getChatItemByFileId_ db user fileId - case (cType, testEquality d $ msgDirection @d) of - (SCTDirect, Just Refl) -> do - liftIO $ updateCIFileStatus_ db user fileId fileStatus - pure $ AChatItem SCTDirect d cInfo $ updateFileStatus ci fileStatus - _ -> pure aci +updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem +updateDirectCIFileStatus db user fileId fileStatus = do + aci@(AChatItem cType d cInfo ci) <- getChatItemByFileId db user fileId + case (cType, testEquality d $ msgDirection @d) of + (SCTDirect, Just Refl) -> do + liftIO $ updateCIFileStatus db user fileId fileStatus + pure $ AChatItem SCTDirect d cInfo $ updateFileStatus ci fileStatus + _ -> pure aci toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64) -> Either StoreError (ChatItemId, ChatRef) toChatItemRef = \case @@ -3675,49 +3474,47 @@ toChatItemRef = \case (itemId, Nothing, Just groupId) -> Right (itemId, ChatRef CTGroup groupId) (itemId, _, _) -> Left $ SEBadChatItem itemId -updateDirectChatItemsRead :: (StoreMonad m) => SQLiteStore -> Int64 -> Maybe (ChatItemId, ChatItemId) -> m () -updateDirectChatItemsRead st contactId itemsRange_ = do - currentTs <- liftIO getCurrentTime - liftIO . withTransaction st $ \db -> - case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE contact_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? - |] - (CISRcvRead, currentTs, contactId, fromItemId, toItemId, CISRcvNew) - _ -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE contact_id = ? AND item_status = ? - |] - (CISRcvRead, currentTs, contactId, CISRcvNew) +updateDirectChatItemsRead :: DB.Connection -> Int64 -> Maybe (ChatItemId, ChatItemId) -> IO () +updateDirectChatItemsRead db contactId itemsRange_ = do + currentTs <- getCurrentTime + case itemsRange_ of + Just (fromItemId, toItemId) -> + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE contact_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? + |] + (CISRcvRead, currentTs, contactId, fromItemId, toItemId, CISRcvNew) + _ -> + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE contact_id = ? AND item_status = ? + |] + (CISRcvRead, currentTs, contactId, CISRcvNew) -updateGroupChatItemsRead :: (StoreMonad m) => SQLiteStore -> Int64 -> Maybe (ChatItemId, ChatItemId) -> m () -updateGroupChatItemsRead st groupId itemsRange_ = do - currentTs <- liftIO getCurrentTime - liftIO . withTransaction st $ \db -> - case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE group_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? - |] - (CISRcvRead, currentTs, groupId, fromItemId, toItemId, CISRcvNew) - _ -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE group_id = ? AND item_status = ? - |] - (CISRcvRead, currentTs, groupId, CISRcvNew) +updateGroupChatItemsRead :: DB.Connection -> Int64 -> Maybe (ChatItemId, ChatItemId) -> IO () +updateGroupChatItemsRead db groupId itemsRange_ = do + currentTs <- getCurrentTime + case itemsRange_ of + Just (fromItemId, toItemId) -> + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE group_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? + |] + (CISRcvRead, currentTs, groupId, fromItemId, toItemId, CISRcvNew) + _ -> + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE group_id = ? AND item_status = ? + |] + (CISRcvRead, currentTs, groupId, CISRcvNew) type ChatStatsRow = (Int, ChatItemId) @@ -3815,25 +3612,24 @@ toGroupChatItemList tz currentTs userContactId (((Just itemId, Just itemTs, Just either (const []) (: []) $ toGroupChatItem tz currentTs userContactId (((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt, updatedAt) :. fileRow) :. memberRow_ :. quoteRow :. quotedMemberRow_) toGroupChatItemList _ _ _ _ = [] -getSMPServers :: MonadUnliftIO m => SQLiteStore -> User -> m [SMPServer] -getSMPServers st User {userId} = - liftIO . withTransaction st $ \db -> - map toSmpServer - <$> DB.query - db - [sql| - SELECT host, port, key_hash - FROM smp_servers - WHERE user_id = ?; - |] - (Only userId) +getSMPServers :: DB.Connection -> User -> IO [SMPServer] +getSMPServers db User {userId} = + map toSmpServer + <$> DB.query + db + [sql| + SELECT host, port, key_hash + FROM smp_servers + WHERE user_id = ?; + |] + (Only userId) where toSmpServer :: (String, String, C.KeyHash) -> SMPServer toSmpServer (host, port, keyHash) = SMPServer host port keyHash -overwriteSMPServers :: StoreMonad m => SQLiteStore -> User -> [SMPServer] -> m () -overwriteSMPServers st User {userId} smpServers = do - liftIOEither . checkConstraint SEUniqueID . withTransaction st $ \db -> do +overwriteSMPServers :: DB.Connection -> User -> [SMPServer] -> ExceptT StoreError IO () +overwriteSMPServers db User {userId} smpServers = + checkConstraint SEUniqueID . ExceptT $ do currentTs <- getCurrentTime DB.execute db "DELETE FROM smp_servers WHERE user_id = ?" (Only userId) forM_ smpServers $ \ProtocolServer {host, port, keyHash} -> @@ -3885,21 +3681,21 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate |] (ldn, displayName, ldnSuffix, userId, ts, ts) -createWithRandomId :: forall a. TVar ChaChaDRG -> (ByteString -> IO a) -> IO (Either StoreError a) +createWithRandomId :: forall a. TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a createWithRandomId = createWithRandomBytes 12 -createWithRandomBytes :: forall a. Int -> TVar ChaChaDRG -> (ByteString -> IO a) -> IO (Either StoreError a) +createWithRandomBytes :: forall a. Int -> TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a createWithRandomBytes size gVar create = tryCreate 3 where - tryCreate :: Int -> IO (Either StoreError a) - tryCreate 0 = pure $ Left SEUniqueID + tryCreate :: Int -> ExceptT StoreError IO a + tryCreate 0 = throwError SEUniqueID tryCreate n = do - id' <- encodedRandomBytes gVar size - E.try (create id') >>= \case - Right x -> pure $ Right x + id' <- liftIO $ encodedRandomBytes gVar size + liftIO (E.try $ create id') >>= \case + Right x -> pure x Left e | DB.sqlError e == DB.ErrorConstraint -> tryCreate (n - 1) - | otherwise -> pure . Left . SEInternalError $ show e + | otherwise -> throwError . SEInternalError $ show e encodedRandomBytes :: TVar ChaChaDRG -> Int -> IO ByteString encodedRandomBytes gVar = fmap B64.encode . randomBytes gVar @@ -3907,13 +3703,6 @@ encodedRandomBytes gVar = fmap B64.encode . randomBytes gVar randomBytes :: TVar ChaChaDRG -> Int -> IO ByteString randomBytes gVar = atomically . stateTVar gVar . randomBytesGenerate -listToEither :: e -> [a] -> Either e a -listToEither _ (x : _) = Right x -listToEither e _ = Left e - -firstRow' :: (a -> Either e b) -> e -> IO [a] -> IO (Either e b) -firstRow' f e a = (f <=< listToEither e) <$> a - -- These error type constructors must be added to mobile apps data StoreError = SEDuplicateName diff --git a/stack.yaml b/stack.yaml index fec37b2e53..3f3f2e42b4 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: b58523d4a29235c29c56a461edb686ed93bc1e89 + commit: d1db7d6f79b527a689af1807e7d91a92076d2d8c # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index fc98aff18a..ad284c4b95 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -100,14 +100,14 @@ createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC createTestChat cfg opts dbPrefix profile = do let dbFilePrefix = testDBPrefix <> dbPrefix st <- createStore (dbFilePrefix <> "_chat.db") False - Right user <- runExceptT $ createUser st profile True + Right user <- withTransaction st $ \db -> runExceptT $ createUser db profile True startTestChat_ st cfg opts dbFilePrefix user startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC startTestChat cfg opts dbPrefix = do let dbFilePrefix = testDBPrefix <> dbPrefix st <- createStore (dbFilePrefix <> "_chat.db") False - Just user <- find activeUser <$> getUsers st + Just user <- find activeUser <$> withTransaction st getUsers startTestChat_ st cfg opts dbFilePrefix user startTestChat_ :: SQLiteStore -> ChatConfig -> ChatOpts -> FilePath -> User -> IO TestCC diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 8573d00496..ee6eae98c3 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -83,7 +83,7 @@ testChatApi :: IO () testChatApi = withTmpFiles $ do let f = chatStoreFile $ testDBPrefix <> "1" st <- createStore f True - Right _ <- runExceptT $ createUser st aliceProfile True + Right _ <- withTransaction st $ \db -> runExceptT $ createUser db aliceProfile True cc <- chatInit $ testDBPrefix <> "1" chatSendCmd cc "/u" `shouldReturn` activeUser chatSendCmd cc "/u alice Alice" `shouldReturn` activeUserExists From 60e9ed9476f9888f2535c078cc9fafbadaae6866 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 19 Jun 2022 14:44:13 +0100 Subject: [PATCH 16/89] core: api to get notification messages and set app phase (#741) * core: api to get notification messages and set app phase * update simplexmq * update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 10 ++++++++++ src/Simplex/Chat/Controller.hs | 11 ++++++++++- src/Simplex/Chat/Protocol.hs | 8 ++++++-- src/Simplex/Chat/Types.hs | 6 +++++- src/Simplex/Chat/View.hs | 2 ++ stack.yaml | 2 +- 8 files changed, 36 insertions(+), 7 deletions(-) diff --git a/cabal.project b/cabal.project index c13077743a..d50291f9bb 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: d1db7d6f79b527a689af1807e7d91a92076d2d8c + tag: 0ab44b18362114752e529405da33b85f091495ec source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 5388d80e6a..89725a1c15 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."d1db7d6f79b527a689af1807e7d91a92076d2d8c" = "1zlp925qq8akv8v846g03czn0yb293vdsgd7vs6g0axs4yx6c9ap"; + "https://github.com/simplex-chat/simplexmq.git"."0ab44b18362114752e529405da33b85f091495ec" = "0pskp5ni8c0w71ck60laq7n2vf3sivaf6lvjbcv7rpkhibm6svqm"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 4a6213f904..0aac23fecc 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -39,6 +39,7 @@ import Data.Maybe (fromMaybe, isJust, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds) +import Data.Time.Clock.System (SystemTime, systemToUTCTime) import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime) import Data.Word (Word32) import qualified Database.SQLite.Simple as DB @@ -204,6 +205,7 @@ processChatCommand = \case APIStopChat -> do ask >>= stopChatController pure CRChatStopped + APISetAppPhase phase -> withAgent (`setAgentPhase` phase) $> CRCmdOk ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk SetFilesFolder filesFolder' -> withUser $ \_ -> do createDirectoryIfMissing True filesFolder' @@ -542,6 +544,12 @@ processChatCommand = \case APIVerifyToken token code nonce -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token code nonce) $> CRCmdOk APIIntervalNofication token interval -> withUser $ \_ -> withAgent (\a -> enableNtfCron a token interval) $> CRCmdOk APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) $> CRCmdOk + APIGetNtfMessage nonce encNtfInfo -> withUser $ \user -> do + (NotificationInfo {ntfConnId, ntfMsgMeta}, msgs) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo + let ntfMessages = map (\SMP.SMPMsgMeta {msgTs, msgFlags} -> NtfMsgInfo {msgTs = systemToUTCTime msgTs, msgFlags}) msgs + msgTs' = systemToUTCTime . (SMP.msgTs :: SMP.NMsgMeta -> SystemTime) <$> ntfMsgMeta + connEntity <- withStore (\db -> Just <$> getConnectionEntity db user ntfConnId) `catchError` \_ -> pure Nothing + pure CRNtfMessages {connEntity, msgTs = msgTs', ntfMessages} GetUserSMPServers -> CRUserSMPServers <$> withUser (\user -> withStore' (`getSMPServers` user)) SetUserSMPServers smpServers -> withUser $ \user -> withChatLock $ do withStore $ \db -> overwriteSMPServers db user smpServers @@ -2240,6 +2248,7 @@ chatCommandP = <|> ("/user" <|> "/u") $> ShowActiveUser <|> "/_start" $> StartChat <|> "/_stop" $> APIStopChat + <|> "/_app phase " *> (APISetAppPhase <$> strP) <|> "/_resubscribe all" $> ResubscribeAllConnections <|> "/_files_folder " *> (SetFilesFolder <$> filePath) <|> "/_db export " *> (APIExportArchive <$> jsonP) @@ -2270,6 +2279,7 @@ chatCommandP = <|> "/_ntf verify " *> (APIVerifyToken <$> tokenP <* A.space <*> strP <* A.space <*> strP) <|> "/_ntf interval " *> (APIIntervalNofication <$> tokenP <* A.space <*> A.decimal) <|> "/_ntf delete " *> (APIDeleteToken <$> tokenP) + <|> "/_ntf message " *> (APIGetNtfMessage <$> strP <* A.space <*> strP) <|> "/smp_servers default" $> SetUserSMPServers [] <|> "/smp_servers " *> (SetUserSMPServers <$> smpServersP) <|> "/smp_servers" $> GetUserSMPServers diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index dd58d5e71a..a24274688c 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -40,7 +40,7 @@ import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON) -import Simplex.Messaging.Protocol (CorrId) +import Simplex.Messaging.Protocol (CorrId, MsgFlags) import Simplex.Messaging.TMap (TMap) import System.IO (Handle) import UnliftIO.STM @@ -101,6 +101,7 @@ data ChatCommand | CreateActiveUser Profile | StartChat | APIStopChat + | APISetAppPhase AgentPhase | ResubscribeAllConnections | SetFilesFolder FilePath | APIExportArchive ArchiveConfig @@ -131,6 +132,7 @@ data ChatCommand | APIVerifyToken DeviceToken ByteString C.CbNonce | APIIntervalNofication DeviceToken Word16 | APIDeleteToken DeviceToken + | APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString} | GetUserSMPServers | SetUserSMPServers [SMPServer] | ChatHelp HelpSection @@ -183,6 +185,7 @@ data ChatResponse | CRChatStarted | CRChatRunning | CRChatStopped + | CRAppPhase {appPhase :: AgentPhase} | CRApiChats {chats :: [AChat]} | CRApiChat {chat :: AChat} | CRLastMessages {chatItems :: [AChatItem]} @@ -273,6 +276,7 @@ data ChatResponse | CRUserContactLinkSubscribed | CRUserContactLinkSubError {chatError :: ChatError} | CRNtfTokenStatus {status :: NtfTknStatus} + | CRNtfMessages {connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]} | CRNewContactConnection {connection :: PendingContactConnection} | CRContactConnectionDeleted {connection :: PendingContactConnection} | CRMessageError {severity :: Text, errorMessage :: Text} @@ -323,6 +327,11 @@ data ComposedMessage = ComposedMessage } deriving (Show, Generic, FromJSON) +data NtfMsgInfo = NtfMsgInfo {msgTs :: UTCTime, msgFlags :: MsgFlags} + deriving (Show, Generic) + +instance ToJSON NtfMsgInfo where toEncoding = J.genericToEncoding J.defaultOptions + data ChatError = ChatError {errorType :: ChatErrorType} | ChatErrorAgent {agentError :: AgentErrorType} diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 9418d26223..df6dc640ba 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -33,7 +33,7 @@ import Simplex.Chat.Call import Simplex.Chat.Types import Simplex.Chat.Util (safeDecodeUtf8) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (fromTextField_) +import Simplex.Messaging.Parsers (fromTextField_, sumTypeJSON) import Simplex.Messaging.Util (eitherToMaybe, (<$?>)) data ConnectionEntity @@ -42,7 +42,11 @@ data ConnectionEntity | SndFileConnection {entityConnection :: Connection, sndFileTransfer :: SndFileTransfer} | RcvFileConnection {entityConnection :: Connection, rcvFileTransfer :: RcvFileTransfer} | UserContactConnection {entityConnection :: Connection, userContact :: UserContact} - deriving (Eq, Show) + deriving (Eq, Show, Generic) + +instance ToJSON ConnectionEntity where + toJSON = J.genericToJSON $ sumTypeJSON id + toEncoding = J.genericToEncoding $ sumTypeJSON id updateEntityConnStatus :: ConnectionEntity -> ConnStatus -> ConnectionEntity updateEntityConnStatus connEntity connStatus = case connEntity of diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index d123a90355..e3c679372b 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -99,7 +99,11 @@ data UserContact = UserContact { userContactLinkId :: Int64, connReqContact :: ConnReqContact } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + +instance ToJSON UserContact where + toJSON = J.genericToJSON J.defaultOptions + toEncoding = J.genericToEncoding J.defaultOptions data UserContactRequest = UserContactRequest { contactRequestId :: Int64, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 6ac5b0faa0..090fe4fcff 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -55,6 +55,7 @@ responseToView testView = \case CRChatStarted -> ["chat started"] CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] + CRAppPhase _ -> [] CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] @@ -161,6 +162,7 @@ responseToView testView = \case CRNewContactConnection _ -> [] CRContactConnectionDeleted PendingContactConnection {pccConnId} -> ["connection :" <> sShow pccConnId <> " deleted"] CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)] + CRNtfMessages {} -> [] CRMessageError prefix err -> [plain prefix <> ": " <> plain err] CRChatError e -> viewChatError e where diff --git a/stack.yaml b/stack.yaml index 3f3f2e42b4..4bdf273bae 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: d1db7d6f79b527a689af1807e7d91a92076d2d8c + commit: 0ab44b18362114752e529405da33b85f091495ec # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 65369aa47b094a5981ec83e4da31760fdb36287e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 19 Jun 2022 19:22:54 +0100 Subject: [PATCH 17/89] update simplexmq --- .gitignore | 1 + cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- tests/ChatClient.hs | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index e645225e93..42477be207 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ stack.yaml.lock # Temporary test files tests/tmp +logs/ diff --git a/cabal.project b/cabal.project index d07117a669..e02582e5ed 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 964daf5442e1069634762450bc28cfd69a2968a1 + tag: 31c78e516dbc4fa2ca7cb61dd1838a3ed2028daa source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 984e8afc7a..7f6f6969af 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."964daf5442e1069634762450bc28cfd69a2968a1" = "1vsbiawqlvi6v48ws2rmg5cmp5qphnry3ymg6458p2w8wdm2gsng"; + "https://github.com/simplex-chat/simplexmq.git"."31c78e516dbc4fa2ca7cb61dd1838a3ed2028daa" = "19fyq6570x4jmqyznnz4qx3nq6sfcpvil0m9iz9q1mi50nn6c6aj"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 5b6b00b395..9fa25b112f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 964daf5442e1069634762450bc28cfd69a2968a1 + commit: 31c78e516dbc4fa2ca7cb61dd1838a3ed2028daa # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 2aba3feb20..3ece951de9 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -209,6 +209,7 @@ serverCfg = queueIdBytes = 12, msgIdBytes = 6, storeLogFile = Nothing, + storeMsgsFile = Nothing, allowNewQueues = True, messageExpiration = Just defaultMessageExpiration, inactiveClientExpiration = Just defaultInactiveClientExpiration, From 84323994587c16295d0cc82a17cce906be21a8b2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 19 Jun 2022 19:26:47 +0100 Subject: [PATCH 18/89] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index d50291f9bb..5d0b6887d6 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 0ab44b18362114752e529405da33b85f091495ec + tag: 7fa4862a8358cd27d85178add5f9017c66d4de95 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 89725a1c15..8f488aafb0 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."0ab44b18362114752e529405da33b85f091495ec" = "0pskp5ni8c0w71ck60laq7n2vf3sivaf6lvjbcv7rpkhibm6svqm"; + "https://github.com/simplex-chat/simplexmq.git"."7fa4862a8358cd27d85178add5f9017c66d4de95" = "1qr1wa4x73f8nvi0hmyym3k929078rxkxzpyybjr28amxzz2clkh"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 4bdf273bae..956ba979f6 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 0ab44b18362114752e529405da33b85f091495ec + commit: 7fa4862a8358cd27d85178add5f9017c66d4de95 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 2e34ae3b1c535fe1db2157499c217c9a92a1f6b8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 19 Jun 2022 19:44:57 +0100 Subject: [PATCH 19/89] update tls --- stack.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack.yaml b/stack.yaml index 9fa25b112f..ac764b57ee 100644 --- a/stack.yaml +++ b/stack.yaml @@ -38,7 +38,7 @@ extra-deps: - cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881 - network-3.1.2.7@sha256:e3d78b13db9512aeb106e44a334ab42b7aa48d26c097299084084cb8be5c5568,4888 - simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079 - - tls-1.5.7@sha256:1cc30253a9696b65a9cafc0317fbf09f7dcea15e3a145ed6c9c0e28c632fa23a,6991 + - tls-1.6.0@sha256:7ae39373fd2de27fb80e90f76d22aeeb9a074a0ddd120cbd02c9c52f516a9e55,6987 # below hackage dependencies are to update Aeson to 2.0.3 - OneTuple-0.3.1@sha256:a848c096c9d29e82ffdd30a9998aa2931cbccb3a1bc137539d80f6174d31603e,2262 - attoparsec-0.14.4@sha256:79584bdada8b730cb5138fca8c35c76fbef75fc1d1e01e6b1d815a5ee9843191,5810 From 291096d87fe28c5e0ce43baafc71df4a732f411c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 19 Jun 2022 19:49:39 +0100 Subject: [PATCH 20/89] ios: receive message in NSE (#742) --- apps/ios/Shared/Model/SimpleXAPI.swift | 24 ++- apps/ios/Shared/SimpleXApp.swift | 28 +-- .../ios/SimpleX NSE/NotificationService.swift | 27 ++- apps/ios/SimpleXChat/APITypes.swift | 24 +++ apps/ios/SimpleXChat/AppGroup.swift | 184 ++++-------------- apps/ios/SimpleXChat/ChatTypes.swift | 19 ++ src/Simplex/Chat.hs | 1 + src/Simplex/Chat/Protocol.hs | 6 +- 8 files changed, 132 insertions(+), 181 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e29a92a46d..7fca87c52a 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -47,7 +47,7 @@ enum TerminalItem: Identifiable { } } -private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) { +func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) { var id: UIBackgroundTaskIdentifier! var running = true let endTask = { @@ -143,6 +143,20 @@ func apiStartChat() throws -> Bool { } } +func apiStopChat() throws { + let r = chatSendCmdSync(.apiStopChat) + switch r { + case .chatStopped: return + default: throw r + } +} + +func apiSetAppPhase(appPhase: AgentPhase) { + let r = chatSendCmdSync(.apiSetAppPhase(appPhase: appPhase)) + if case .cmdOk = r { return } + logger.error("apiSetAppPhase error: \(String(describing: r))") +} + func apiSetFilesFolder(filesFolder: String) throws { let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder)) if case .cmdOk = r { return } @@ -512,7 +526,7 @@ class ChatReceiver { func receiveMsgLoop() async { let msg = await chatRecvMsg() self._lastMsgTime = .now - processReceivedMsg(msg) + await processReceivedMsg(msg) if self.receiveMessages { do { try await Task.sleep(nanoseconds: 7_500_000) } catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") } @@ -528,9 +542,9 @@ class ChatReceiver { } } -func processReceivedMsg(_ res: ChatResponse) { +func processReceivedMsg(_ res: ChatResponse) async { let m = ChatModel.shared - DispatchQueue.main.async { + await MainActor.run { m.terminalItems.append(.resp(.now, res)) logger.debug("processReceivedMsg: \(res.responseType)") switch res { @@ -682,6 +696,8 @@ func processReceivedMsg(_ res: ChatResponse) { m.callCommand = .end // CallController.shared.reportCallRemoteEnded(call: call) } + case let .appPhase(appPhase): + setAppState(AppState(appPhase: appPhase)) default: logger.debug("unsupported event: \(res.responseType)") } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 8ce48b9da9..4c46ecde5c 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -11,13 +11,6 @@ import SimpleXChat let logger = Logger() -let machMessenger = MachMessenger(APP_MACH_PORT, callback: receivedNSEMachMessage) - -func receivedNSEMachMessage(msgId: Int32, msg: String) -> String? { - logger.debug("MachMessenger: receivedNSEMachMessage \"\(msg)\" from NSE, replying") - return "reply from App to: \(msg)" -} - @main struct SimpleXApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @@ -34,7 +27,6 @@ struct SimpleXApp: App { UserDefaults.standard.register(defaults: appDefaults) BGManager.shared.register() NtfManager.shared.registerCategories() - machMessenger.start() } var body: some Scene { @@ -50,20 +42,18 @@ struct SimpleXApp: App { } .onChange(of: scenePhase) { phase in logger.debug("scenePhase \(String(describing: scenePhase))") - let res = machMessenger.sendMessageWithReply(NSE_MACH_PORT, msg: "App scenePhase changed to \(String(describing: scenePhase))") - logger.debug("MachMessenger \(String(describing: res), privacy: .public)") - setAppState(phase) switch (phase) { case .background: + pauseApp() BGManager.shared.schedule() if userAuthorized == true { enteredBackground = ProcessInfo.processInfo.systemUptime } doAuthenticate = false - machMessenger.stop() case .active: + setAppState(.active) + apiSetAppPhase(appPhase: .active) doAuthenticate = authenticationExpired() - machMessenger.start() default: break } @@ -71,6 +61,18 @@ struct SimpleXApp: App { } } + private func pauseApp() { + setAppState(.pausing) + apiSetAppPhase(appPhase: .paused) + let endTask = beginBGTask { + if getAppState() != .active { + setAppState(.suspending) + apiSetAppPhase(appPhase: .suspended) + } + } + DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask) + } + private func authenticationExpired() -> Bool { if let enteredBackground = enteredBackground { return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30 diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 41668e9543..7cac78b46f 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -12,29 +12,28 @@ import SimpleXChat let logger = Logger() -let machMessenger = MachMessenger(NSE_MACH_PORT, callback: receivedAppMachMessage) - class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("NotificationService.didReceive") - machMessenger.start() - let res = machMessenger.sendMessageWithReply(APP_MACH_PORT, msg: "starting NSE didReceive") - logger.debug("MachMessenger \(String(describing: res), privacy: .public)") - if getAppState() != .background { + let appState = getAppState() + if appState.running { contentHandler(request.content) - machMessenger.stop() return } logger.debug("NotificationService: app is in the background") self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - if let _ = startChat() { + let userInfo = request.content.userInfo + if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], + let nonce = ntfData["nonce"] as? String, + let encNtfInfo = ntfData["message"] as? String, + let _ = startChat() { + apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) let content = receiveMessages() contentHandler (content) - machMessenger.stop() return } @@ -44,7 +43,6 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } - machMessenger.stop() } override func serviceExtensionTimeWillExpire() { @@ -145,3 +143,12 @@ func apiSetFilesFolder(filesFolder: String) throws { throw r } +func apiGetNtfMessage(nonce: String, encNtfInfo: String) { + let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) + if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r { + print(connEntity) + print(msgTs) + print(ntfMessages) + return + } +} diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index d45ceef19e..4424cf2c73 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -15,6 +15,8 @@ public enum ChatCommand { case showActiveUser case createActiveUser(profile: Profile) case startChat + case apiStopChat + case apiSetAppPhase(appPhase: AgentPhase) case setFilesFolder(filesFolder: String) case apiGetChats case apiGetChat(type: ChatType, id: Int64) @@ -25,6 +27,7 @@ public enum ChatCommand { case apiVerifyToken(token: String, code: String, nonce: String) case apiIntervalNofication(token: String, interval: Int) case apiDeleteToken(token: String) + case apiGetNtfMessage(nonce: String, encNtfInfo: String) case getUserSMPServers case setUserSMPServers(smpServers: [String]) case addContact @@ -56,6 +59,8 @@ public enum ChatCommand { case .showActiveUser: return "/u" case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)" case .startChat: return "/_start" + case .apiStopChat: return "/_stop" + case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase)" case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" case .apiGetChats: return "/_get chats pcc=on" case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100" @@ -68,6 +73,7 @@ public enum ChatCommand { case let .apiVerifyToken(token, code, nonce): return "/_ntf verify apns \(token) \(code) \(nonce)" case let .apiIntervalNofication(token, interval): return "/_ntf interval apns \(token) \(interval)" case let .apiDeleteToken(token): return "/_ntf delete apns \(token)" + case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" case .getUserSMPServers: return "/smp_servers" case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case .addContact: return "/connect" @@ -101,6 +107,8 @@ public enum ChatCommand { case .showActiveUser: return "showActiveUser" case .createActiveUser: return "createActiveUser" case .startChat: return "startChat" + case .apiStopChat: return "apiStopChat" + case .apiSetAppPhase: return "apiSetAppPhase" case .setFilesFolder: return "setFilesFolder" case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" @@ -111,6 +119,7 @@ public enum ChatCommand { case .apiVerifyToken: return "apiVerifyToken" case .apiIntervalNofication: return "apiIntervalNofication" case .apiDeleteToken: return "apiDeleteToken" + case .apiGetNtfMessage: return "apiGetNtfMessage" case .getUserSMPServers: return "getUserSMPServers" case .setUserSMPServers: return "setUserSMPServers" case .addContact: return "addContact" @@ -156,6 +165,8 @@ public enum ChatResponse: Decodable, Error { case activeUser(user: User) case chatStarted case chatRunning + case chatStopped + case appPhase(appPhase: AgentPhase) case apiChats(chats: [ChatData]) case apiChat(chat: ChatData) case userSMPServers(smpServers: [String]) @@ -205,6 +216,7 @@ public enum ChatResponse: Decodable, Error { case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo) case callEnded(contact: Contact) case ntfTokenStatus(status: NtfTknStatus) + case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) case newContactConnection(connection: PendingContactConnection) case contactConnectionDeleted(connection: PendingContactConnection) case cmdOk @@ -218,6 +230,8 @@ public enum ChatResponse: Decodable, Error { case .activeUser: return "activeUser" case .chatStarted: return "chatStarted" case .chatRunning: return "chatRunning" + case .chatStopped: return "chatStopped" + case .appPhase: return "appPhase" case .apiChats: return "apiChats" case .apiChat: return "apiChat" case .userSMPServers: return "userSMPServers" @@ -265,6 +279,7 @@ public enum ChatResponse: Decodable, Error { case .callExtraInfo: return "callExtraInfo" case .callEnded: return "callEnded" case .ntfTokenStatus: return "ntfTokenStatus" + case .ntfMessages: return "ntfMessages" case .newContactConnection: return "newContactConnection" case .contactConnectionDeleted: return "contactConnectionDeleted" case .cmdOk: return "cmdOk" @@ -281,6 +296,8 @@ public enum ChatResponse: Decodable, Error { case let .activeUser(user): return String(describing: user) case .chatStarted: return noDetails case .chatRunning: return noDetails + case .chatStopped: return noDetails + case let .appPhase(appPhase): return appPhase.rawValue case let .apiChats(chats): return String(describing: chats) case let .apiChat(chat): return String(describing: chat) case let .userSMPServers(smpServers): return String(describing: smpServers) @@ -328,6 +345,7 @@ public enum ChatResponse: Decodable, Error { case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))" case let .callEnded(contact): return "contact: \(contact.id)" case let .ntfTokenStatus(status): return String(describing: status) + case let .ntfMessages(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))" case let .newContactConnection(connection): return String(describing: connection) case let .contactConnectionDeleted(connection): return String(describing: connection) case .cmdOk: return noDetails @@ -346,6 +364,12 @@ struct ComposedMessage: Encodable { var msgContent: MsgContent } +public enum AgentPhase: String, Codable { + case active = "ACTIVE" + case paused = "PAUSED" + case suspended = "SUSPENDED" +} + public func decodeJSON(_ json: String) -> T? { if let data = json.data(using: .utf8) { return try? jsonDecoder.decode(T.self, from: data) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 92573e0354..abb996ecab 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -9,169 +9,51 @@ import Foundation import SwiftUI -let GROUP_DEFAULT_APP_IN_BACKGROUND = "appInBackground" +let GROUP_DEFAULT_APP_STATE = "appState" let APP_GROUP_NAME = "group.chat.simplex.app" -public let NSE_MACH_PORT = "\(APP_GROUP_NAME).nse" as CFString - -public let APP_MACH_PORT = "\(APP_GROUP_NAME).app" as CFString - func getGroupDefaults() -> UserDefaults? { UserDefaults(suiteName: APP_GROUP_NAME) } -public func setAppState(_ phase: ScenePhase) { +public enum AppState: String { + case active + case pausing + case paused + case suspending + case suspended + + public init(appPhase: AgentPhase) { + switch appPhase { + case .active: self = .active + case .paused: self = .paused + case .suspended: self = .suspended + } + } + + public var running: Bool { + switch self { + case .paused: return false + case .suspending: return false + case .suspended: return false + default: return true + } + } +} + +public func setAppState(_ state: AppState) { if let defaults = getGroupDefaults() { - defaults.set(phase == .background, forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) + defaults.set(state.rawValue, forKey: GROUP_DEFAULT_APP_STATE) defaults.synchronize() } } -public func getAppState() -> ScenePhase { - if let defaults = getGroupDefaults() { - if defaults.bool(forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) { - return .background - } +public func getAppState() -> AppState { + if let defaults = getGroupDefaults(), + let rawValue = defaults.string(forKey: GROUP_DEFAULT_APP_STATE), + let state = AppState(rawValue: rawValue) { + return state } return .active } - -let MACH_SEND_TIMEOUT: CFTimeInterval = 1.0 -let MACH_REPLY_TIMEOUT: CFTimeInterval = 1.0 - -public class MachMessenger { - public init(_ localPortName: CFString, callback: @escaping Callback) { - self.localPortName = localPortName - self.callback = callback - self.localPort = nil - } - - var localPortName: CFString - var callback: Callback - var localPort: CFMessagePort? - - public enum SendError: Error { - case sndTimeout - case rcvTimeout - case portInvalid - case sendError(Int32) - case msgError - } - - public typealias Callback = (_ msgId: Int32, _ msg: String) -> String? - - class CallbackInfo { - internal init(callback: @escaping MachMessenger.Callback) { - self.callback = callback - } - - var callback: Callback - } - - public static func sendError(_ code: Int32) -> SendError? { - switch code { - case kCFMessagePortSuccess: return nil - case kCFMessagePortSendTimeout: return .sndTimeout - case kCFMessagePortReceiveTimeout: return .rcvTimeout - case kCFMessagePortIsInvalid: return .portInvalid - case kCFMessagePortBecameInvalidError: return .portInvalid - default: return .sendError(code) - } - } - - public func start() { - logger.debug("MachMessenger.start") - localPort = createLocalPort(localPortName, callback: callback) - } - - public func stop() { - if let port = localPort { - logger.debug("MachMessenger.stop") - CFMessagePortInvalidate(port) - localPort = nil - } - } - - public func sendMessage(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> SendError? { - logger.debug("MachMessenger.sendMessage") - if let port = createRemotePort(remotePortName) { - logger.debug("MachMessenger.sendMessage: sending...") - return sendMessage(port, msgId: msgId, msg: msg) - } else { - logger.debug("MachMessenger.sendMessage: no remote port") - return .portInvalid - } - } - - public func sendMessageWithReply(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> Result { - logger.debug("MachMessenger.sendMessageWithReply") - if let port = createRemotePort(remotePortName) { - logger.debug("MachMessenger.sendMessageWithReply: sending...") - return sendMessageWithReply(port, msgId: msgId, msg: msg) - } else { - logger.debug("MachMessenger.sendMessageWithReply: no remote port") - return .failure(.portInvalid) - } - } - - private func createLocalPort(_ portName: CFString, callback: @escaping Callback) -> CFMessagePort? { - logger.debug("MachMessenger.createLocalPort") - if let port = localPort { return port } - logger.debug("MachMessenger.createLocalPort: creating...") - var context = CFMessagePortContext() - context.version = 0 - context.info = Unmanaged.passRetained(CallbackInfo(callback: callback)).toOpaque() - let callout: CFMessagePortCallBack = { port, msgId, msgData, info in - logger.debug("MachMessenger CFMessagePortCallBack called") - if let data = msgData, - let msg = String(data: data as Data, encoding: .utf8), - let info = info, - let resp = Unmanaged.fromOpaque(info).takeUnretainedValue().callback(msgId, msg), - let respData = resp.data(using: .utf8) { - return Unmanaged.passRetained(respData as CFData) - } - return nil - } - return withUnsafeMutablePointer(to: &context) { cxt in - let port = CFMessagePortCreateLocal(kCFAllocatorDefault, portName, callout, cxt, nil) - CFMessagePortSetDispatchQueue(port, DispatchQueue.main); - localPort = port - logger.debug("MachMessenger.createLocalPort created: \(portName)") - return port - } - } - - private func createRemotePort(_ portName: CFString) -> CFMessagePort? { - CFMessagePortCreateRemote(kCFAllocatorDefault, portName) - } - - private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> SendError? { - if let data = msg.data(using: .utf8) { - logger.debug("MachMessenger sendMessage") - let msgData = data as CFData - let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, 0, nil, nil) - logger.debug("MachMessenger sendMessage \(code)") - return MachMessenger.sendError(code) - } - return .msgError - } - - private func sendMessageWithReply(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> Result { - if let data = msg.data(using: .utf8) { - let msgData = data as CFData - var respData: Unmanaged? = nil - let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, MACH_REPLY_TIMEOUT, CFRunLoopMode.defaultMode.rawValue, &respData) - if let err = MachMessenger.sendError(code) { - return .failure(err) - } else if let data = respData?.takeUnretainedValue(), - let resp = String(data: data as Data, encoding: .utf8) { - return .success(resp) - } else { - return .success(nil) - } - - } - return .failure(.msgError) - } -} diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index fd631a0cc4..1b97a14a0c 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -264,6 +264,9 @@ public struct Connection: Decodable { ) } +public struct UserContact: Decodable { +} + public struct UserContactRequest: Decodable, NamedChat { var contactRequestId: Int64 var localDisplayName: ContactName @@ -437,6 +440,18 @@ public struct MemberSubError: Decodable { var memberError: ChatError } +public enum ConnectionEntity: Decodable { + case rcvDirectMsgConnection(entityConnection: Connection, contact: Contact?) + case rcvGroupMsgConnection(entityConnection: Connection, groupInfo: GroupInfo, groupMember: GroupMember) + case sndFileConnection(entityConnection: Connection, sndFileTransfer: SndFileTransfer) + case rcvFileConnection(entityConnection: Connection, rcvFileTransfer: RcvFileTransfer) + case userContactConnection(entityConnection: Connection, userContact: UserContact) +} + +public struct NtfMsgInfo: Decodable { + +} + public struct AChatItem: Decodable { public var chatInfo: ChatInfo public var chatItem: ChatItem @@ -901,6 +916,10 @@ public struct SndFileTransfer: Decodable { } +public struct RcvFileTransfer: Decodable { + +} + public struct FileTransferMeta: Decodable { } diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 0aac23fecc..3da2d37979 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1096,6 +1096,7 @@ processAgentMessage Nothing _ _ = throwChatError CENoActiveUser processAgentMessage (Just User {userId}) "" agentMessage = case agentMessage of DOWN srv conns -> serverEvent srv conns CRContactsDisconnected "disconnected" UP srv conns -> serverEvent srv conns CRContactsSubscribed "connected" + PHASE phase -> toView $ CRAppPhase phase _ -> pure () where serverEvent srv@SMP.ProtocolServer {host, port} conns event str = do diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index df6dc640ba..43194387a5 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -33,7 +33,7 @@ import Simplex.Chat.Call import Simplex.Chat.Types import Simplex.Chat.Util (safeDecodeUtf8) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (fromTextField_, sumTypeJSON) +import Simplex.Messaging.Parsers (fromTextField_, fstToLower, sumTypeJSON) import Simplex.Messaging.Util (eitherToMaybe, (<$?>)) data ConnectionEntity @@ -45,8 +45,8 @@ data ConnectionEntity deriving (Eq, Show, Generic) instance ToJSON ConnectionEntity where - toJSON = J.genericToJSON $ sumTypeJSON id - toEncoding = J.genericToEncoding $ sumTypeJSON id + toJSON = J.genericToJSON $ sumTypeJSON fstToLower + toEncoding = J.genericToEncoding $ sumTypeJSON fstToLower updateEntityConnStatus :: ConnectionEntity -> ConnStatus -> ConnectionEntity updateEntityConnStatus connEntity connStatus = case connEntity of From 4b263510eef4e4d609b1a9550c71a259ea27e522 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 19 Jun 2022 21:48:33 +0100 Subject: [PATCH 21/89] update flake.nix --- flake.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flake.nix b/flake.nix index 6e13c24a64..5001484ab8 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,11 @@ inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; #angerman/nixpkgs/patch-1"; # based on 21.11, still need this, until everything is merged into 21.11. inputs.haskellNix.url = "github:input-output-hk/haskell.nix?ref=angerman/try-no-libcharset"; inputs.haskellNix.inputs.nixpkgs.follows = "nixpkgs"; + inputs.hackage = { + url = "github:input-output-hk/hackage.nix"; + flake = false; + }; + inputs.haskellNix.inputs.hackage.follows = "hackage"; inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, haskellNix, nixpkgs, flake-utils }: let systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; in From 7df8c23c81758efb1c67cc24691bb99d6b1ee44f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 20 Jun 2022 12:34:41 +0100 Subject: [PATCH 22/89] nix: update nix for new tls version (#744) --- flake.lock | 11 +++++++---- flake.nix | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index d2deeaa040..7c1fd19982 100644 --- a/flake.lock +++ b/flake.lock @@ -133,11 +133,11 @@ "hackage": { "flake": false, "locked": { - "lastModified": 1647047557, - "narHash": "sha256-6A7jjz77f53GkvFxqVmeuqqXyDWsU24rUtFtOg68CAg=", + "lastModified": 1655687627, + "narHash": "sha256-E1uyic0vWHC6gtz2FYtnFE3JWhvmmBj1TU93qoJSxdg=", "owner": "input-output-hk", "repo": "hackage.nix", - "rev": "fc07d4d4f2597334caa96f455cec190bdcc931f4", + "rev": "6634069475ef594a8a2be6c33301ba7e59149980", "type": "github" }, "original": { @@ -155,7 +155,9 @@ "cardano-shell": "cardano-shell", "flake-utils": "flake-utils_2", "ghc-8.6.5-iohk": "ghc-8.6.5-iohk", - "hackage": "hackage", + "hackage": [ + "hackage" + ], "hpc-coveralls": "hpc-coveralls", "nix-tools": "nix-tools", "nixpkgs": [ @@ -315,6 +317,7 @@ "root": { "inputs": { "flake-utils": "flake-utils", + "hackage": "hackage", "haskellNix": "haskellNix", "nixpkgs": "nixpkgs" } diff --git a/flake.nix b/flake.nix index 5001484ab8..2474dcbf78 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ }; inputs.haskellNix.inputs.hackage.follows = "hackage"; inputs.flake-utils.url = "github:numtide/flake-utils"; - outputs = { self, haskellNix, nixpkgs, flake-utils }: + outputs = { self, haskellNix, nixpkgs, flake-utils, ... }: let systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; in flake-utils.lib.eachSystem systems (system: let pkgs = haskellNix.legacyPackages.${system}; in From 7723e4ca7ad8d84aa65d2bf681a48c340474fd7c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 21 Jun 2022 11:25:12 +0100 Subject: [PATCH 23/89] core: allow starting chat without making SMP subscriptions (to use GET in NSE) (#745) --- src/Simplex/Chat.hs | 23 ++++++++++++----------- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Core.hs | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 3da2d37979..a0d39cc587 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -147,17 +147,17 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de pure ss {smp = fromMaybe defaultSMPServers $ nonEmpty userSmpServers} _ -> pure ss -runChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m () -runChatController = race_ notificationSubscriber . agentSubscriber +runChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m () +runChatController user subConns = race_ notificationSubscriber $ agentSubscriber user subConns -startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m (Async ()) -startChatController user = do +startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m (Async ()) +startChatController user subConns = do asks smpAgent >>= resumeAgentClient s <- asks agentAsync readTVarIO s >>= maybe (start s) pure where start s = do - a <- async $ runChatController user + a <- async $ runChatController user subConns atomically . writeTVar s $ Just a pure a @@ -194,14 +194,14 @@ processChatCommand = \case user <- withStore $ \db -> createUser db p True atomically . writeTVar u $ Just user pure $ CRActiveUser user - StartChat -> withUser' $ \user -> + StartChat subConns -> withUser' $ \user -> asks agentAsync >>= readTVarIO >>= \case Just _ -> pure CRChatRunning _ -> ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) - (startChatController user $> CRChatStarted) + (startChatController user subConns $> CRChatStarted) APIStopChat -> do ask >>= stopChatController pure CRChatStopped @@ -1000,11 +1000,11 @@ acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationI connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profile withStore' $ \db -> createAcceptedContact db userId connId cName profileId p xContactId -agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m () -agentSubscriber user = do +agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m () +agentSubscriber user subConns = do q <- asks $ subQ . smpAgent l <- asks chatLock - subscribeUserConnections subscribeConnection user + when subConns $ subscribeUserConnections subscribeConnection user forever $ do (_, connId, msg) <- atomically $ readTBQueue q u <- readTVarIO =<< asks currentUser @@ -2247,7 +2247,8 @@ chatCommandP :: Parser ChatCommand chatCommandP = ("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile) <|> ("/user" <|> "/u") $> ShowActiveUser - <|> "/_start" $> StartChat + <|> "/_start subscribe=" *> (StartChat <$> ("on" $> True <|> "off" $> False)) + <|> "/_start" $> StartChat True <|> "/_stop" $> APIStopChat <|> "/_app phase " *> (APISetAppPhase <$> strP) <|> "/_resubscribe all" $> ResubscribeAllConnections diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index a24274688c..6e9976ef5f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -99,7 +99,7 @@ instance ToJSON HelpSection where data ChatCommand = ShowActiveUser | CreateActiveUser Profile - | StartChat + | StartChat {subscribeConnections :: Bool} | APIStopChat | APISetAppPhase AgentPhase | ResubscribeAllConnections diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 07bcff63e5..b58f4857de 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -33,7 +33,7 @@ runSimplexChat ChatOpts {maintenance} u cc chat | maintenance = wait =<< async (chat u cc) | otherwise = do a1 <- async $ chat u cc - a2 <- runReaderT (startChatController u) cc + a2 <- runReaderT (startChatController u True) cc waitEither_ a1 a2 sendChatCmd :: ChatController -> String -> IO ChatResponse From 29eafa9a7450b83c572320780dd1e7bc4acdc01b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 21 Jun 2022 11:38:36 +0100 Subject: [PATCH 24/89] update ntf server --- apps/ios/Shared/AppDelegate.swift | 1 + apps/ios/SimpleX NSE/NotificationService.swift | 1 + src/Simplex/Chat.hs | 2 +- src/Simplex/Chat/Terminal.hs | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index ee981ffbba..7b0a2bea8d 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -42,6 +42,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { logger.debug("AppDelegate: didReceiveRemoteNotification") + print("*** userInfo", userInfo) if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], UserDefaults.standard.bool(forKey: "useNotifications") { if let verification = ntfData["verification"] as? String, diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 7cac78b46f..91313e288f 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -18,6 +18,7 @@ class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("NotificationService.didReceive") + print("*** userInfo", request.content.userInfo) let appState = getAppState() if appState.running { contentHandler(request.content) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index a0d39cc587..a69be007a6 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -104,7 +104,7 @@ _defaultSMPServers = ] _defaultNtfServers :: [NtfServer] -_defaultNtfServers = ["smp://ZH1Dkt2_EQRbxUUyjLlcUjg1KAhBrqfvE0xfn7Ki0Zg=@ntf1.simplex.im"] +_defaultNtfServers = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@139.162.221.251"] maxImageSize :: Integer maxImageSize = 236700 diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index c1bfcc8bbd..c6730a1595 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -28,7 +28,7 @@ terminalChatConfig = "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im" ], - ntf = ["smp://ZH1Dkt2_EQRbxUUyjLlcUjg1KAhBrqfvE0xfn7Ki0Zg=@ntf1.simplex.im"] + ntf = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@139.162.221.251"] } } From 976b1c919fad34795c3303f997c4536d848227c1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 21 Jun 2022 19:27:30 +0100 Subject: [PATCH 25/89] revert tls to 1.5.7 and nix config changes (#746) * Revert "nix: update nix for new tls version (#744)" This reverts commit 7df8c23c81758efb1c67cc24691bb99d6b1ee44f. * Revert "update flake.nix" This reverts commit 4b263510eef4e4d609b1a9550c71a259ea27e522. * Revert "update tls" This reverts commit 2e34ae3b1c535fe1db2157499c217c9a92a1f6b8. * update simplexmq --- cabal.project | 2 +- flake.lock | 11 ++++------- flake.nix | 7 +------ scripts/nix/sha256map.nix | 2 +- stack.yaml | 4 ++-- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/cabal.project b/cabal.project index e02582e5ed..d72a77b02c 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 31c78e516dbc4fa2ca7cb61dd1838a3ed2028daa + tag: 5b61c6bccdce8563bca2041484054a28707d8e50 source-repository-package type: git diff --git a/flake.lock b/flake.lock index 7c1fd19982..d2deeaa040 100644 --- a/flake.lock +++ b/flake.lock @@ -133,11 +133,11 @@ "hackage": { "flake": false, "locked": { - "lastModified": 1655687627, - "narHash": "sha256-E1uyic0vWHC6gtz2FYtnFE3JWhvmmBj1TU93qoJSxdg=", + "lastModified": 1647047557, + "narHash": "sha256-6A7jjz77f53GkvFxqVmeuqqXyDWsU24rUtFtOg68CAg=", "owner": "input-output-hk", "repo": "hackage.nix", - "rev": "6634069475ef594a8a2be6c33301ba7e59149980", + "rev": "fc07d4d4f2597334caa96f455cec190bdcc931f4", "type": "github" }, "original": { @@ -155,9 +155,7 @@ "cardano-shell": "cardano-shell", "flake-utils": "flake-utils_2", "ghc-8.6.5-iohk": "ghc-8.6.5-iohk", - "hackage": [ - "hackage" - ], + "hackage": "hackage", "hpc-coveralls": "hpc-coveralls", "nix-tools": "nix-tools", "nixpkgs": [ @@ -317,7 +315,6 @@ "root": { "inputs": { "flake-utils": "flake-utils", - "hackage": "hackage", "haskellNix": "haskellNix", "nixpkgs": "nixpkgs" } diff --git a/flake.nix b/flake.nix index 2474dcbf78..6e13c24a64 100644 --- a/flake.nix +++ b/flake.nix @@ -3,13 +3,8 @@ inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; #angerman/nixpkgs/patch-1"; # based on 21.11, still need this, until everything is merged into 21.11. inputs.haskellNix.url = "github:input-output-hk/haskell.nix?ref=angerman/try-no-libcharset"; inputs.haskellNix.inputs.nixpkgs.follows = "nixpkgs"; - inputs.hackage = { - url = "github:input-output-hk/hackage.nix"; - flake = false; - }; - inputs.haskellNix.inputs.hackage.follows = "hackage"; inputs.flake-utils.url = "github:numtide/flake-utils"; - outputs = { self, haskellNix, nixpkgs, flake-utils, ... }: + outputs = { self, haskellNix, nixpkgs, flake-utils }: let systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; in flake-utils.lib.eachSystem systems (system: let pkgs = haskellNix.legacyPackages.${system}; in diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 7f6f6969af..4dd6c80f63 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."31c78e516dbc4fa2ca7cb61dd1838a3ed2028daa" = "19fyq6570x4jmqyznnz4qx3nq6sfcpvil0m9iz9q1mi50nn6c6aj"; + "https://github.com/simplex-chat/simplexmq.git"."5b61c6bccdce8563bca2041484054a28707d8e50" = "11bsndjrj0s2vwgz2lg6plj1c1hjg2wqrgwqfsy1w0hx19mv4c0m"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index ac764b57ee..8ecd1c1f20 100644 --- a/stack.yaml +++ b/stack.yaml @@ -38,7 +38,7 @@ extra-deps: - cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881 - network-3.1.2.7@sha256:e3d78b13db9512aeb106e44a334ab42b7aa48d26c097299084084cb8be5c5568,4888 - simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079 - - tls-1.6.0@sha256:7ae39373fd2de27fb80e90f76d22aeeb9a074a0ddd120cbd02c9c52f516a9e55,6987 + - tls-1.5.7@sha256:1cc30253a9696b65a9cafc0317fbf09f7dcea15e3a145ed6c9c0e28c632fa23a,6991 # below hackage dependencies are to update Aeson to 2.0.3 - OneTuple-0.3.1@sha256:a848c096c9d29e82ffdd30a9998aa2931cbccb3a1bc137539d80f6174d31603e,2262 - attoparsec-0.14.4@sha256:79584bdada8b730cb5138fca8c35c76fbef75fc1d1e01e6b1d815a5ee9843191,5810 @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 31c78e516dbc4fa2ca7cb61dd1838a3ed2028daa + commit: 5b61c6bccdce8563bca2041484054a28707d8e50 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 8d93f228b39f0fa547525742624e5be42766d388 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 21 Jun 2022 19:31:20 +0100 Subject: [PATCH 26/89] update simplexmq --- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8f488aafb0..47be0d5fa5 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."7fa4862a8358cd27d85178add5f9017c66d4de95" = "1qr1wa4x73f8nvi0hmyym3k929078rxkxzpyybjr28amxzz2clkh"; + "https://github.com/simplex-chat/simplexmq.git"."a7c3133c35fdf23023a6f1c6c0fc9b951f4387d6" = "1ka7nclmasrn3nps8jpxvipjrgizg88jifvyrx30kfv1cqmqk17k"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index b5df7a2e8b..011b42e675 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 7fa4862a8358cd27d85178add5f9017c66d4de95 + commit: a7c3133c35fdf23023a6f1c6c0fc9b951f4387d6 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 4d9e446489707e7b20aaaecf77a32ccb22695f55 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 23 Jun 2022 21:20:56 +0100 Subject: [PATCH 27/89] core: set files folder without user (to allow archive import) (#748) --- src/Simplex/Chat.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index a69be007a6..67876a5268 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -207,7 +207,7 @@ processChatCommand = \case pure CRChatStopped APISetAppPhase phase -> withAgent (`setAgentPhase` phase) $> CRCmdOk ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk - SetFilesFolder filesFolder' -> withUser $ \_ -> do + SetFilesFolder filesFolder' -> do createDirectoryIfMissing True filesFolder' ff <- asks filesFolder atomically . writeTVar ff $ Just filesFolder' From 6a2f2a512fbdc58808fb5ef7d9b64aef28adb2d8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 24 Jun 2022 13:52:20 +0100 Subject: [PATCH 28/89] ios: UI to export/import/delete chat database (#743) * ios: UI to export/import/delete chat database * move files * ui for database migration * migration screen layout * ios: export archive and delete chat database * import archive * refactor, update texts * database migration (almost works) * fix missing import * delete legacy database * update migration errors --- apps/ios/Shared/ContentView.swift | 47 ++- apps/ios/Shared/Model/BGManager.swift | 7 +- apps/ios/Shared/Model/ChatModel.swift | 2 + apps/ios/Shared/Model/SimpleXAPI.swift | 90 ++--- apps/ios/Shared/SimpleXApp.swift | 37 +- .../Chat/ComposeMessage/ComposeView.swift | 20 +- .../Shared/Views/ChatList/ChatListView.swift | 18 +- .../Views/Database/ChatArchiveView.swift | 65 ++++ .../Shared/Views/Database/DatabaseView.swift | 331 ++++++++++++++++++ .../Database/MigrateToAppGroupView.swift | 248 +++++++++++++ .../Views/Onboarding/CreateProfile.swift | 4 +- .../Views/Onboarding/MakeConnection.swift | 10 +- .../Views/UserSettings/SettingsView.swift | 40 ++- .../ios/SimpleX NSE/NotificationService.swift | 85 ++--- apps/ios/SimpleX.xcodeproj/project.pbxproj | 76 ++-- .../xcschemes/SimpleX (iOS).xcscheme | 2 +- apps/ios/SimpleXChat/API.swift | 57 ++- apps/ios/SimpleXChat/APITypes.swift | 25 +- apps/ios/SimpleXChat/AppGroup.swift | 72 +++- apps/ios/SimpleXChat/FileUtils.swift | 65 +++- apps/ios/SimpleXChat/SimpleX.h | 2 + 21 files changed, 1097 insertions(+), 206 deletions(-) create mode 100644 apps/ios/Shared/Views/Database/ChatArchiveView.swift create mode 100644 apps/ios/Shared/Views/Database/DatabaseView.swift create mode 100644 apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index e1e3b808b7..356292334b 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -14,6 +14,7 @@ struct ContentView: View { @Binding var doAuthenticate: Bool @Binding var userAuthorized: Bool? @State private var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication + @State private var v3DBMigration = v3DBMigrationDefault.get() @AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @@ -26,35 +27,43 @@ struct ContentView: View { if let step = chatModel.onboardingStage { if case .onboardingComplete = step, chatModel.currentUser != nil { - ZStack(alignment: .top) { - ChatListView(showChatInfo: $showChatInfo) - .onAppear { - NtfManager.shared.requestAuthorization(onDeny: { - alertManager.showAlert(notificationAlert()) - }) - // Local Authentication notice is to be shown on next start after onboarding is complete - if (!prefLANoticeShown && prefShowLANotice) { - prefLANoticeShown = true - alertManager.showAlert(laNoticeAlert()) - } - prefShowLANotice = true - } - if chatModel.showCallView, let call = chatModel.activeCall { - ActiveCallView(call: call) - } - IncomingCallView() - } + mainView() } else { OnboardingView(onboarding: step) } + } else if !v3DBMigrationDefault.get().startChat { + MigrateToAppGroupView() } } } - .onAppear { if doAuthenticate { runAuthenticate() } } + .onAppear { + if doAuthenticate { runAuthenticate() } + } .onChange(of: doAuthenticate) { _ in if doAuthenticate { runAuthenticate() } } .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } } + private func mainView() -> some View { + ZStack(alignment: .top) { + ChatListView(showChatInfo: $showChatInfo) + .onAppear { + NtfManager.shared.requestAuthorization(onDeny: { + alertManager.showAlert(notificationAlert()) + }) + // Local Authentication notice is to be shown on next start after onboarding is complete + if (!prefLANoticeShown && prefShowLANotice) { + prefLANoticeShown = true + alertManager.showAlert(laNoticeAlert()) + } + prefShowLANotice = true + } + if chatModel.showCallView, let call = chatModel.activeCall { + ActiveCallView(call: call) + } + IncomingCallView() + } + } + private func runAuthenticate() { if !prefPerformLA { userAuthorized = true diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index b76097bc79..e1d9e819b1 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -8,6 +8,7 @@ import Foundation import BackgroundTasks +import SimpleXChat private let receiveTaskId = "chat.simplex.app.receive" @@ -71,7 +72,11 @@ class BGManager { } self.completed = false DispatchQueue.main.async { - initializeChat() + do { + try initializeChat(start: true) + } catch let error { + fatalError("Failed to start or load chats: \(responseError(error))") + } if ChatModel.shared.currentUser == nil { completeReceiving("no current user") return diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 0696b0b6e9..e60f51d534 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -15,6 +15,8 @@ import SimpleXChat final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var currentUser: User? + @Published var chatRunning: Bool? + @Published var chatDbChanged = false // list of chat "previews" @Published var chats: [Chat] = [] // current chat diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 7fca87c52a..ed40b1eb26 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -72,7 +72,7 @@ func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) { let msgDelay: Double = 7.5 let maxTaskDuration: Double = 15 -private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse) -> ChatResponse { +private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> T) -> T { let endTask = beginBGTask() DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask) let r = f() @@ -93,11 +93,9 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = if case let .response(_, json) = resp { logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") } - if case .apiParseMarkdown = cmd {} else { - DispatchQueue.main.async { - ChatModel.shared.terminalItems.append(.cmd(.now, cmd)) - ChatModel.shared.terminalItems.append(.resp(.now, resp)) - } + DispatchQueue.main.async { + ChatModel.shared.terminalItems.append(.cmd(.now, cmd)) + ChatModel.shared.terminalItems.append(.resp(.now, resp)) } return resp } @@ -108,10 +106,10 @@ func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil } } -func chatRecvMsg() async -> ChatResponse { +func chatRecvMsg() async -> ChatResponse? { await withCheckedContinuation { cont in - _ = withBGTask(bgDelay: msgDelay) { - let resp = chatResponse(chat_recv_msg(getChatCtrl())!) + _ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in + let resp = recvSimpleXMsg() cont.resume(returning: resp) return resp } @@ -143,8 +141,8 @@ func apiStartChat() throws -> Bool { } } -func apiStopChat() throws { - let r = chatSendCmdSync(.apiStopChat) +func apiStopChat() async throws { + let r = await chatSendCmd(.apiStopChat) switch r { case .chatStopped: return default: throw r @@ -163,6 +161,18 @@ func apiSetFilesFolder(filesFolder: String) throws { throw r } +func apiExportArchive(config: ArchiveConfig) async throws { + try await sendCommandOkResp(.apiExportArchive(config: config)) +} + +func apiImportArchive(config: ArchiveConfig) async throws { + try await sendCommandOkResp(.apiImportArchive(config: config)) +} + +func apiDeleteStorage() async throws { + try await sendCommandOkResp(.apiDeleteStorage) +} + func apiGetChats() throws -> [Chat] { let r = chatSendCmdSync(.apiGetChats) if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } } @@ -326,12 +336,6 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? { } } -func apiParseMarkdown(text: String) throws -> [FormattedText]? { - let r = chatSendCmdSync(.apiParseMarkdown(text: text)) - if case let .apiParsedMarkdown(formattedText) = r { return formattedText } - throw r -} - func apiCreateUserAddress() async throws -> String { let r = await chatSendCmd(.createMyAddress) if case let .userContactLinkCreated(connReq) = r { return connReq } @@ -468,42 +472,42 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws { throw r } -func initializeChat() { +func initializeChat(start: Bool) throws { logger.debug("initializeChat") do { let m = ChatModel.shared + try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) m.currentUser = try apiGetActiveUser() if m.currentUser == nil { m.onboardingStage = .step1_SimpleXInfo + } else if start { + try startChat() } else { - startChat() + m.chatRunning = false } } catch { - fatalError("Failed to initialize chat controller or database: \(error)") + fatalError("Failed to initialize chat controller or database: \(responseError(error))") } } -func startChat() { +func startChat() throws { logger.debug("startChat") - do { - let m = ChatModel.shared - // TODO set file folder once, before chat is started - let justStarted = try apiStartChat() - if justStarted { - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) - m.userAddress = try apiGetUserAddress() - m.userSMPServers = try getUserSMPServers() - m.chats = try apiGetChats() - withAnimation { - m.onboardingStage = m.chats.isEmpty - ? .step3_MakeConnection - : .onboardingComplete - } + let m = ChatModel.shared + // TODO set file folder once, before chat is started + let justStarted = try apiStartChat() + if justStarted { + m.userAddress = try apiGetUserAddress() + m.userSMPServers = try getUserSMPServers() + m.chats = try apiGetChats() + withAnimation { + m.onboardingStage = m.chats.isEmpty + ? .step3_MakeConnection + : .onboardingComplete } - ChatReceiver.shared.start() - } catch { - fatalError("Failed to start or load chats: \(error)") } + ChatReceiver.shared.start() + m.chatRunning = true + chatLastStartGroupDefault.set(Date.now) } class ChatReceiver { @@ -524,9 +528,11 @@ class ChatReceiver { } func receiveMsgLoop() async { - let msg = await chatRecvMsg() - self._lastMsgTime = .now - await processReceivedMsg(msg) + // TODO use function that has timeout + if let msg = await chatRecvMsg() { + self._lastMsgTime = .now + await processReceivedMsg(msg) + } if self.receiveMessages { do { try await Task.sleep(nanoseconds: 7_500_000) } catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") } @@ -697,7 +703,7 @@ func processReceivedMsg(_ res: ChatResponse) async { // CallController.shared.reportCallRemoteEnded(call: call) } case let .appPhase(appPhase): - setAppState(AppState(appPhase: appPhase)) + appStateGroupDefault.set(AppState(appPhase: appPhase)) default: logger.debug("unsupported event: \(res.responseType)") } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 4c46ecde5c..557ef1f532 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -25,6 +25,7 @@ struct SimpleXApp: App { init() { hs_init(0, nil) UserDefaults.standard.register(defaults: appDefaults) + setDbContainer() BGManager.shared.register() NtfManager.shared.registerCategories() } @@ -38,7 +39,11 @@ struct SimpleXApp: App { chatModel.appOpenUrl = url } .onAppear() { - initializeChat() + do { + try initializeChat(start: v3DBMigrationDefault.get().startChat) + } catch let error { + fatalError("Failed to start or load chats: \(responseError(error))") + } } .onChange(of: scenePhase) { phase in logger.debug("scenePhase \(String(describing: scenePhase))") @@ -51,7 +56,7 @@ struct SimpleXApp: App { } doAuthenticate = false case .active: - setAppState(.active) + appStateGroupDefault.set(.active) apiSetAppPhase(appPhase: .active) doAuthenticate = authenticationExpired() default: @@ -61,12 +66,34 @@ struct SimpleXApp: App { } } + private func setDbContainer() { +// Uncomment and run once to open DB in app documents folder: + // dbContainerGroupDefault.set(.documents) + // v3DBMigrationDefault.set(.offer) +// to create database in app documents folder also uncomment: + // let legacyDatabase = true + let legacyDatabase = hasLegacyDatabase() + if legacyDatabase, case .documents = dbContainerGroupDefault.get() { + dbContainerGroupDefault.set(.documents) + switch v3DBMigrationDefault.get() { + case .migrated: () + default: v3DBMigrationDefault.set(.offer) + } + logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db") + } else { + dbContainerGroupDefault.set(.group) + v3DBMigrationDefault.set(.ready) + logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db") + logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present") + } + } + private func pauseApp() { - setAppState(.pausing) + appStateGroupDefault.set(.pausing) apiSetAppPhase(appPhase: .paused) let endTask = beginBGTask { - if getAppState() != .active { - setAppState(.suspending) + if appStateGroupDefault.get() != .active { + appStateGroupDefault.set(.suspending) apiSetAppPhase(appPhase: .suspended) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 87f6c44e39..c97719dc1c 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -210,9 +210,8 @@ struct ComposeView: View { allowedContentTypes: [.data], allowsMultipleSelection: false ) { result in - if case .success = result { + if case let .success(files) = result, let fileURL = files.first { do { - let fileURL: URL = try result.get().first! var fileSize: Int? = nil if fileURL.startAccessingSecurityScopedResource() { let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) @@ -392,17 +391,12 @@ struct ComposeView: View { } private func parseMessage(_ msg: String) -> URL? { - do { - let parsedMsg = try apiParseMarkdown(text: msg) - let uri = parsedMsg?.first(where: { ft in - ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) - }) - if let uri = uri { return URL(string: uri.text) } - else { return nil } - } catch { - logger.error("apiParseMarkdown error: \(error.localizedDescription)") - return nil - } + let parsedMsg = parseSimpleXMarkdown(msg) + let uri = parsedMsg?.first(where: { ft in + ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) + }) + if let uri = uri { return URL(string: uri.text) } + else { return nil } } private func isSimplexLink(_ link: String) -> Bool { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 721365c2f1..e4b3d87b18 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -23,6 +23,7 @@ struct ChatListView: View { ForEach(filteredChats()) { chat in ChatListNavLink(chat: chat, showChatInfo: $showChatInfo) .padding(.trailing, -16) + .disabled(chatModel.chatRunning != true) } } .onChange(of: chatModel.chatId) { _ in @@ -46,7 +47,11 @@ struct ChatListView: View { SettingsButton() } ToolbarItem(placement: .navigationBarTrailing) { - NewChatButton() + switch chatModel.chatRunning { + case .some(true): NewChatButton() + case .some(false): chatStoppedIcon() + case .none: EmptyView() + } } } } @@ -74,6 +79,17 @@ struct ChatListView: View { } } +func chatStoppedIcon() -> some View { + Button { + AlertManager.shared.showAlertMsg( + title: "Chat is stopped", + message: "You can start chat via app Settings / Database or by restarting the app" + ) + } label: { + Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red) + } +} + struct ChatListView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() diff --git a/apps/ios/Shared/Views/Database/ChatArchiveView.swift b/apps/ios/Shared/Views/Database/ChatArchiveView.swift new file mode 100644 index 0000000000..65913343d5 --- /dev/null +++ b/apps/ios/Shared/Views/Database/ChatArchiveView.swift @@ -0,0 +1,65 @@ +// +// ChatArchiveView.swift +// SimpleXChat +// +// Created by Evgeny on 23/06/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ChatArchiveView: View { + var archiveName: String + @AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String? + @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 + @State private var showDeleteAlert = false + + var body: some View { + let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName) + let fileTs = chatArchiveTimeDefault.get() + List { + Section { + settingsRow("square.and.arrow.up") { + Button { + showShareSheet(items: [fileUrl]) + } label: { + Text("Save archive") + } + } + settingsRow("trash") { + Button { + showDeleteAlert = true + } label: { + Text("Delete archive").foregroundColor(.red) + } + } + } header: { + Text("Chat archive") + } footer: { + Text("Created on \(fileTs)") + } + } + .alert(isPresented: $showDeleteAlert) { + Alert( + title: Text("Delete chat archive?"), + primaryButton: .destructive(Text("Delete")) { + do { + try FileManager.default.removeItem(atPath: fileUrl.path) + chatArchiveName = nil + chatArchiveTime = 0 + } catch let error { + logger.error("removeItem error \(String(describing: error))") + } + }, + secondaryButton: .cancel() + ) + } + } +} + +struct ChatArchiveView_Previews: PreviewProvider { + static var previews: some View { + ChatArchiveView(archiveName: "") + } +} diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift new file mode 100644 index 0000000000..6ff5a10348 --- /dev/null +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -0,0 +1,331 @@ +// +// DatabaseView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 19/06/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +enum DatabaseAlert: Identifiable { + case stopChat + case importArchive + case archiveImported + case deleteChat + case chatDeleted + case deleteLegacyDatabase + case error(title: LocalizedStringKey, error: String = "") + + var id: String { + switch self { + case .stopChat: return "stopChat" + case .importArchive: return "importArchive" + case .archiveImported: return "archiveImported" + case .deleteChat: return "deleteChat" + case .chatDeleted: return "chatDeleted" + case .deleteLegacyDatabase: return "deleteLegacyDatabase" + case let .error(title, _): return "error \(title)" + } + } +} + +struct DatabaseView: View { + @EnvironmentObject var m: ChatModel + @Binding var showSettings: Bool + @State private var runChat = false + @State private var alert: DatabaseAlert? = nil + @State private var showFileImporter = false + @State private var importedArchivePath: URL? + @State private var progressIndicator = false + @AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String? + @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 + @State private var dbContainer = dbContainerGroupDefault.get() + @State private var legacyDatabase = hasLegacyDatabase() + + var body: some View { + ZStack { + chatDatabaseView() + if progressIndicator { + ProgressView().scaleEffect(2) + } + } + } + + private func chatDatabaseView() -> some View { + List { + let stopped = m.chatRunning == false + Section { + settingsRow( + stopped ? "exclamationmark.octagon.fill" : "play.fill", + color: stopped ? .red : .green + ) { + Toggle( + stopped ? "Chat is stopped" : "Chat is running", + isOn: $runChat + ) + .onChange(of: runChat) { _ in + if (runChat) { + startChat() + } else { + alert = .stopChat + } + } + } + } header: { + Text("Run chat") + } footer: { + if case .documents = dbContainer { + Text("Database will be migrated when the app restarts") + } + } + + Section { + settingsRow("square.and.arrow.up") { + Button { + exportArchive() + } label: { + Text("Export database") + } + } + settingsRow("square.and.arrow.down") { + Button(role: .destructive) { + showFileImporter = true + } label: { + Text("Import database") + } + } + if let archiveName = chatArchiveName { + let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get() + ? "Old database archive" + : "New database archive" + settingsRow("archivebox") { + NavigationLink { + ChatArchiveView(archiveName: archiveName) + .navigationTitle(title) + } label: { + Text(title) + } + } + } + settingsRow("trash.slash") { + Button(role: .destructive) { + alert = .deleteChat + } label: { + Text("Delete database") + } + } + } header: { + Text("Chat database") + } footer: { + Text( + stopped + ? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." + : "Stop chat to enable database actions" + ) + } + .disabled(!stopped) + + if case .group = dbContainer, legacyDatabase { + Section("Old database") { + settingsRow("trash") { + Button { + alert = .deleteLegacyDatabase + } label: { + Text("Delete old database") + } + } + } + } + } + .onAppear { runChat = m.chatRunning ?? true } + .alert(item: $alert) { item in databaseAlert(item) } + .fileImporter( + isPresented: $showFileImporter, + allowedContentTypes: [.zip], + allowsMultipleSelection: false + ) { result in + if case let .success(files) = result, let fileURL = files.first { + importedArchivePath = fileURL + alert = .importArchive + } + } + } + + private func databaseAlert(_ alertItem: DatabaseAlert) -> Alert { + switch alertItem { + case .stopChat: + return Alert( + title: Text("Stop chat?"), + message: Text("Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped."), + primaryButton: .destructive(Text("Stop")) { + stopChat() + }, + secondaryButton: .cancel { + withAnimation { runChat = true } + } + ) + case .importArchive: + if let fileURL = importedArchivePath { + return Alert( + title: Text("Import chat database?"), + message: Text("Your current chat database will be DELETED and REPLACED with the imported one.\n") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."), + primaryButton: .destructive(Text("Import")) { + importArchive(fileURL) + }, + secondaryButton: .cancel() + ) + } else { + return Alert(title: Text("Error: no database file")) + } + case .archiveImported: + return Alert( + title: Text("Chat database imported"), + message: Text("Restart the app to use imported chat database"), + primaryButton: .default(Text("Ok")), + secondaryButton: .cancel() + ) + + case .deleteChat: + return Alert( + title: Text("Delete chat profile?"), + message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."), + primaryButton: .destructive(Text("Delete")) { + deleteChat() + }, + secondaryButton: .cancel() + ) + case .chatDeleted: + return Alert( + title: Text("Chat database deleted"), + message: Text("Restart the app to create a new chat profile"), + primaryButton: .default(Text("Ok")), + secondaryButton: .cancel() + ) + case .deleteLegacyDatabase: + return Alert( + title: Text("Delete old database?"), + message: Text("The old database was not removed during the migration, it can be deleted."), + primaryButton: .destructive(Text("Delete")) { + deleteLegacyDatabase() + }, + secondaryButton: .cancel() + ) + case let .error(title, error): + return Alert(title: Text(title), message: Text("\(error)")) + } + } + + private func stopChat() { + Task { + do { + try await apiStopChat() + await MainActor.run { m.chatRunning = false } + } catch let error { + await MainActor.run { + runChat = true + alert = .error(title: "Error stopping chat", error: responseError(error)) + } + } + } + } + + private func exportArchive() { + progressIndicator = true + Task { + do { + let archivePath = try await exportChatArchive() + showShareSheet(items: [archivePath]) + await MainActor.run { progressIndicator = false } + } catch let error { + await MainActor.run { + alert = .error(title: "Error exporting chat database", error: responseError(error)) + progressIndicator = false + } + } + } + } + + private func importArchive(_ archivePath: URL) { + if archivePath.startAccessingSecurityScopedResource() { + progressIndicator = true + Task { + do { + try await apiDeleteStorage() + do { + let config = ArchiveConfig(archivePath: archivePath.path) + try await apiImportArchive(config: config) + await operationEnded(.archiveImported) + } catch let error { + await operationEnded(.error(title: "Error importing chat database", error: responseError(error))) + } + } catch let error { + await operationEnded(.error(title: "Error deleting chat database", error: responseError(error))) + } + archivePath.stopAccessingSecurityScopedResource() + } + } else { + alert = .error(title: "Error accessing database file") + } + } + + private func deleteChat() { + progressIndicator = true + Task { + do { + try await apiDeleteStorage() + await operationEnded(.chatDeleted) + } catch let error { + await operationEnded(.error(title: "Error deleting database", error: responseError(error))) + } + } + } + + private func deleteLegacyDatabase() { + if removeLegacyDatabaseAndFiles() { + legacyDatabase = false + } else { + alert = .error(title: "Error deleting old database") + } + } + + private func operationEnded(_ dbAlert: DatabaseAlert) async { + await MainActor.run { + m.chatDbChanged = true + progressIndicator = false + alert = dbAlert + } + } + + private func startChat() { + if m.chatDbChanged { + showSettings = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + resetChatCtrl() + do { + try initializeChat(start: true) + m.chatDbChanged = false + } catch let error { + fatalError("Error starting chat \(responseError(error))") + } + } + } else { + do { + _ = try apiStartChat() + runChat = true + m.chatRunning = true + chatLastStartGroupDefault.set(Date.now) + } catch let error { + runChat = false + alert = .error(title: "Error starting chat", error: responseError(error)) + } + } + } +} + +struct DatabaseView_Previews: PreviewProvider { + static var previews: some View { + DatabaseView(showSettings: Binding.constant(false)) + } +} diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift new file mode 100644 index 0000000000..3749047b3f --- /dev/null +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -0,0 +1,248 @@ +// +// MigrateToGroupView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 20/06/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +enum V3DBMigrationState: String { + case offer + case postponed + case exporting + case export_error + case exported + case migrating + case migration_error + case migrated + case ready + + var startChat: Bool { + switch self { + case .postponed: return true + case .ready: return true + default: return false + } + } +} + +let v3DBMigrationDefault = EnumDefault( + defaults: UserDefaults.standard, + forKey: DEFAULT_CHAT_V3_DB_MIGRATION, + withDefault: .offer +) + +struct MigrateToAppGroupView: View { + @EnvironmentObject var chatModel: ChatModel + @State private var v3DBMigration = v3DBMigrationDefault.get() + @State private var migrationError = "" + @AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String? + @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 + + var body: some View { + ZStack(alignment: .topLeading) { + Text("Database migration").font(.largeTitle) + + switch v3DBMigration { + case .offer: + VStack(alignment: .leading, spacing: 16) { + Text("To support instant push notifications the chat database has to be migrated.") + Text("If you need to use the chat now tap **Skip** below (you will be offered to migrate the database when you restart the app).") + } + .padding(.top, 56) + center { + Button { + migrateDatabaseToV3() + } label: { + Text("Start migration") + .font(.title) + .frame(maxWidth: .infinity) + } + } + skipMigration() + case .exporting: + center { + ProgressView(value: 0.33) + Text("Exporting database archive...") + } + migrationProgress() + case .export_error: + migrationFailed().padding(.top, 56) + center { + Text("Export error:").font(.headline) + Text(migrationError) + } + skipMigration() + case .exported: + center { + Text("Exported database archive.") + } + case .migrating: + center { + ProgressView(value: 0.67) + Text("Migrating database archive...") + } + migrationProgress() + case .migration_error: + VStack(alignment: .leading, spacing: 16) { + migrationFailed() + Text("The created archive is available via app Settings / Database / Old database archive.") + } + .padding(.top, 56) + center { + Text("Migration error:").font(.headline) + Text(migrationError) + } + skipMigration() + case .migrated: + center { + ProgressView(value: 1.0) + Text("Migration is completed") + } + VStack { + Spacer() + Spacer() + Spacer() + Button { + do { + resetChatCtrl() + try initializeChat(start: true) + setV3DBMigration(.ready) + } catch let error { + dbContainerGroupDefault.set(.documents) + setV3DBMigration(.migration_error) + migrationError = "Error starting chat: \(responseError(error))" + } + deleteOldArchive() + } label: { + Text("Start using chat") + .font(.title) + .frame(maxWidth: .infinity) + } + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + default: + Spacer() + Text("Unexpected migration state") + Text("\(v3DBMigration.rawValue)") + Spacer() + skipMigration() + } + } + .padding() + } + + private func center(@ViewBuilder c: @escaping () -> Content) -> some View where Content: View { + VStack(alignment: .leading, spacing: 8) { c() } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + } + + private func migrationProgress() -> some View { + VStack { + Spacer() + ProgressView().scaleEffect(2) + Spacer() + Spacer() + Spacer() + } + .frame(maxWidth: .infinity) + } + + private func migrationFailed() -> some View { + Text("Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).") + } + + private func skipMigration() -> some View { + ZStack { + Button { + setV3DBMigration(.postponed) + do { + try startChat() + } catch let error { + fatalError("Failed to start or load chats: \(responseError(error))") + } + } label: { + Text("Skip and start using chat") + .frame(maxWidth: .infinity, alignment: .trailing) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) + } + + private func setV3DBMigration(_ value: V3DBMigrationState) { + v3DBMigration = value + v3DBMigrationDefault.set(value) + } + + func migrateDatabaseToV3() { + setV3DBMigration(.exporting) + let archiveTime = Date.now + let archiveName = "simplex-chat.\(archiveTime.ISO8601Format()).zip" + chatArchiveTime = archiveTime.timeIntervalSince1970 + chatArchiveName = archiveName + let config = ArchiveConfig(archivePath: getDocumentsDirectory().appendingPathComponent(archiveName).path) + Task { + do { + try await apiExportArchive(config: config) + await MainActor.run { setV3DBMigration(.exported) } + } catch let error { + await MainActor.run { + setV3DBMigration(.export_error) + migrationError = responseError(error) + } + return + } + + do { + await MainActor.run { setV3DBMigration(.migrating) } + dbContainerGroupDefault.set(.group) + resetChatCtrl() + try initializeChat(start: false) + try await apiImportArchive(config: config) + await MainActor.run { setV3DBMigration(.migrated) } + } catch let error { + dbContainerGroupDefault.set(.documents) + await MainActor.run { + setV3DBMigration(.migration_error) + migrationError = responseError(error) + } + } + } + } +} + +func exportChatArchive() async throws -> URL { + let archiveTime = Date.now + let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted)) + let archiveName = "simplex-chat.\(ts).zip" + let archivePath = getDocumentsDirectory().appendingPathComponent(archiveName) + let config = ArchiveConfig(archivePath: archivePath.path) + try await apiExportArchive(config: config) + deleteOldArchive() + UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME) + chatArchiveTimeDefault.set(archiveTime) + return archivePath +} + +func deleteOldArchive() { + let d = UserDefaults.standard + if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) { + do { + try FileManager.default.removeItem(atPath: getDocumentsDirectory().appendingPathComponent(archiveName).path) + d.set(nil, forKey: DEFAULT_CHAT_ARCHIVE_NAME) + d.set(0, forKey: DEFAULT_CHAT_ARCHIVE_TIME) + } catch let error { + logger.error("removeItem error \(String(describing: error))") + } + } +} + +struct MigrateToGroupView_Previews: PreviewProvider { + static var previews: some View { + MigrateToAppGroupView() + } +} diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 578cd85d64..511ff82b36 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -95,11 +95,11 @@ struct CreateProfile: View { ) do { m.currentUser = try apiCreateActiveUser(profile) - startChat() + try startChat() withAnimation { m.onboardingStage = .step3_MakeConnection } } catch { - fatalError("Failed to create user: \(error)") + fatalError("Failed to create user or start chat: \(responseError(error))") } } diff --git a/apps/ios/Shared/Views/Onboarding/MakeConnection.swift b/apps/ios/Shared/Views/Onboarding/MakeConnection.swift index 88676535b8..92ae376b9b 100644 --- a/apps/ios/Shared/Views/Onboarding/MakeConnection.swift +++ b/apps/ios/Shared/Views/Onboarding/MakeConnection.swift @@ -16,7 +16,14 @@ struct MakeConnection: View { var body: some View { VStack(alignment: .leading) { - SettingsButton().padding(.bottom, 1) + HStack { + SettingsButton() + if m.chatRunning == false { + Spacer() + chatStoppedIcon() + } + } + .padding(.bottom, 1) if let user = m.currentUser { Text("Welcome \(user.displayName)!") @@ -66,6 +73,7 @@ struct MakeConnection: View { } } } + .disabled(m.chatRunning != true) } Spacer() diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 017360e30c..5226e11b1e 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -24,6 +24,9 @@ let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay" let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls" +let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName" +let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime" +let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration" let appDefaults: [String: Any] = [ DEFAULT_SHOW_LA_NOTICE: false, @@ -34,11 +37,14 @@ let appDefaults: [String: Any] = [ DEFAULT_WEBRTC_POLICY_RELAY: true, DEFAULT_PRIVACY_ACCEPT_IMAGES: true, DEFAULT_PRIVACY_LINK_PREVIEWS: true, - DEFAULT_EXPERIMENTAL_CALLS: false + DEFAULT_EXPERIMENTAL_CALLS: false, + DEFAULT_CHAT_V3_DB_MIGRATION: "offer" ] private var indent: CGFloat = 36 +let chatArchiveTimeDefault = DateDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CHAT_ARCHIVE_TIME) + struct SettingsView: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var chatModel: ChatModel @@ -52,7 +58,7 @@ struct SettingsView: View { var body: some View { let user: User = chatModel.currentUser! - return NavigationView { + NavigationView { List { Section("You") { NavigationLink { @@ -62,12 +68,30 @@ struct SettingsView: View { ProfilePreview(profileOf: user) .padding(.leading, -8) } + .disabled(chatModel.chatRunning != true) + NavigationLink { UserAddress() .navigationTitle("Your chat address") } label: { settingsRow("qrcode") { Text("Your SimpleX contact address") } } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + DatabaseView(showSettings: $showSettings) + .navigationTitle("Your chat database") + } label: { + settingsRow("internaldrive") { + HStack { + Text("Database export & import") + Spacer() + if chatModel.chatRunning == false { + Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red) + } + } + } + } } Section("Settings") { @@ -76,7 +100,9 @@ struct SettingsView: View { CallSettings() .navigationTitle("Your calls") } label: { - settingsRow("video") { Text("Audio & video calls") } + settingsRow("video") { + Text("Audio & video calls") + } } } NavigationLink { @@ -95,6 +121,7 @@ struct SettingsView: View { settingsRow("server.rack") { Text("SMP servers") } } } + .disabled(chatModel.chatRunning != true) Section("Help") { NavigationLink { @@ -128,6 +155,7 @@ struct SettingsView: View { Text("Chat with the developers") } } + .disabled(chatModel.chatRunning != true) settingsRow("envelope") { Text("[Send us email](mailto:chat@simplex.chat)") } } @@ -137,6 +165,7 @@ struct SettingsView: View { } label: { settingsRow("terminal") { Text("Chat console") } } + .disabled(chatModel.chatRunning != true) ZStack(alignment: .leading) { Image(colorScheme == .dark ? "github_light" : "github") .resizable() @@ -156,6 +185,7 @@ struct SettingsView: View { notificationsIcon() notificationsToggle(token) } + .disabled(chatModel.chatRunning != true) } Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") } @@ -261,9 +291,9 @@ struct SettingsView: View { } } -func settingsRow(_ icon: String, content: @escaping () -> Content) -> some View { +func settingsRow(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View { ZStack(alignment: .leading) { - Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.secondary) + Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(color) content().padding(.leading, indent) } } diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 91313e288f..6c1d1a6c71 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -18,9 +18,9 @@ class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("NotificationService.didReceive") - print("*** userInfo", request.content.userInfo) - let appState = getAppState() + let appState = appStateGroupDefault.get() if appState.running { + print("userInfo", request.content.userInfo) contentHandler(request.content) return } @@ -33,9 +33,10 @@ class NotificationService: UNNotificationServiceExtension { let encNtfInfo = ntfData["message"] as? String, let _ = startChat() { apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) - let content = receiveMessages() - contentHandler (content) - return + if let content = receiveMessages() { + contentHandler(content) + return + } } if let bestAttemptContent = bestAttemptContent { @@ -68,6 +69,7 @@ func startChat() -> User? { do { try apiStartChat() try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + chatLastStartGroupDefault.set(Date.now) return user } catch { logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)") @@ -78,40 +80,43 @@ func startChat() -> User? { return nil } -func receiveMessages() -> UNNotificationContent { +func receiveMessages() -> UNNotificationContent? { logger.debug("NotificationService receiveMessages started") while true { - let res = chatResponse(chat_recv_msg(getChatCtrl())!) - logger.debug("NotificationService receiveMessages: \(res.responseType)") - switch res { -// case let .newContactConnection(connection): -// case let .contactConnectionDeleted(connection): - case let .contactConnected(contact): - return createContactConnectedNtf(contact) -// case let .contactConnecting(contact): -// TODO profile update - case let .receivedContactRequest(contactRequest): - return createContactRequestNtf(contactRequest) -// case let .contactUpdated(toContact): -// TODO profile updated - case let .newChatItem(aChatItem): - let cInfo = aChatItem.chatInfo - let cItem = aChatItem.chatItem - return createMessageReceivedNtf(cInfo, cItem) -// case let .chatItemUpdated(aChatItem): -// TODO message updated -// let cInfo = aChatItem.chatInfo -// let cItem = aChatItem.chatItem -// NtfManager.shared.notifyMessageReceived(cInfo, cItem) -// case let .chatItemDeleted(_, toChatItem): -// TODO message updated -// case let .rcvFileComplete(aChatItem): -// TODO file received? -// let cInfo = aChatItem.chatInfo -// let cItem = aChatItem.chatItem -// NtfManager.shared.notifyMessageReceived(cInfo, cItem) - default: - logger.debug("NotificationService ignored event: \(res.responseType)") + if let res = recvSimpleXMsg() { + logger.debug("NotificationService receiveMessages: \(res.responseType)") + switch res { + // case let .newContactConnection(connection): + // case let .contactConnectionDeleted(connection): + case let .contactConnected(contact): + return createContactConnectedNtf(contact) + // case let .contactConnecting(contact): + // TODO profile update + case let .receivedContactRequest(contactRequest): + return createContactRequestNtf(contactRequest) + // case let .contactUpdated(toContact): + // TODO profile updated + case let .newChatItem(aChatItem): + let cInfo = aChatItem.chatInfo + let cItem = aChatItem.chatItem + return createMessageReceivedNtf(cInfo, cItem) + // case let .chatItemUpdated(aChatItem): + // TODO message updated + // let cInfo = aChatItem.chatInfo + // let cItem = aChatItem.chatItem + // NtfManager.shared.notifyMessageReceived(cInfo, cItem) + // case let .chatItemDeleted(_, toChatItem): + // TODO message updated + // case let .rcvFileComplete(aChatItem): + // TODO file received? + // let cInfo = aChatItem.chatInfo + // let cItem = aChatItem.chatItem + // NtfManager.shared.notifyMessageReceived(cInfo, cItem) + default: + logger.debug("NotificationService ignored event: \(res.responseType)") + } + } else { + return nil } } } @@ -147,9 +152,9 @@ func apiSetFilesFolder(filesFolder: String) throws { func apiGetNtfMessage(nonce: String, encNtfInfo: String) { let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r { - print(connEntity) - print(msgTs) - print(ntfMessages) + if let connEntity = connEntity { print("connEntity", connEntity) } + if let msgTs = msgTs { print("msgTs", msgTs) } + print("ntfMessages", ntfMessages) return } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index cea0f83938..ed5e204143 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; }; 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; }; 5C3F1D5A2844B4DE00EC8A82 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */; }; + 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; }; 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; @@ -40,11 +41,6 @@ 5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; }; 5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; }; 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; }; - 5C69D5B22852379F009B27A4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3728522C9A00103588 /* libgmp.a */; }; - 5C69D5B32852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */; }; - 5C69D5B42852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */; }; - 5C69D5B52852379F009B27A4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3828522C9A00103588 /* libffi.a */; }; - 5C69D5B62852379F009B27A4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3928522C9A00103588 /* libgmpxx.a */; }; 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; @@ -102,6 +98,13 @@ 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; + 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; + 5CFA59CA2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */; }; + 5CFA59CB2864464A00863A68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C62864464A00863A68 /* libffi.a */; }; + 5CFA59CC2864464A00863A68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C72864464A00863A68 /* libgmpxx.a */; }; + 5CFA59CD2864464A00863A68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C82864464A00863A68 /* libgmp.a */; }; + 5CFA59CE2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */; }; + 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; }; 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; }; @@ -198,6 +201,7 @@ 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = ""; }; 5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesView.swift; sourceTree = ""; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; + 5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = ""; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = ""; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = ""; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; }; @@ -209,11 +213,6 @@ 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = ""; }; 5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = ""; }; - 5C6F2A3728522C9A00103588 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C6F2A3828522C9A00103588 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C6F2A3928522C9A00103588 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a"; sourceTree = ""; }; - 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a"; sourceTree = ""; }; 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = ""; }; 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = ""; }; 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = ""; }; @@ -269,6 +268,13 @@ 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; + 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; + 5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a"; sourceTree = ""; }; + 5CFA59C62864464A00863A68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5CFA59C72864464A00863A68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CFA59C82864464A00863A68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a"; sourceTree = ""; }; + 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = ""; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = ""; }; 6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; }; @@ -313,13 +319,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C69D5B62852379F009B27A4 /* libgmpxx.a in Frameworks */, + 5CFA59CE2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a in Frameworks */, + 5CFA59CA2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a in Frameworks */, + 5CFA59CC2864464A00863A68 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C69D5B32852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */, - 5C69D5B22852379F009B27A4 /* libgmp.a in Frameworks */, - 5C69D5B42852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C69D5B52852379F009B27A4 /* libffi.a in Frameworks */, + 5CFA59CD2864464A00863A68 /* libgmp.a in Frameworks */, + 5CFA59CB2864464A00863A68 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -349,6 +355,7 @@ 5C5F4AC227A5E9AF00B51EF1 /* Chat */, 5CB9250B27A942F300ACCCDD /* ChatList */, 5CB924DD27A8622200ACCCDD /* NewChat */, + 5CFA59C22860B04D00863A68 /* Database */, 5CB924DF27A8678B00ACCCDD /* UserSettings */, 5C2E261127A30FEA00F70299 /* TerminalView.swift */, ); @@ -372,11 +379,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C6F2A3828522C9A00103588 /* libffi.a */, - 5C6F2A3728522C9A00103588 /* libgmp.a */, - 5C6F2A3928522C9A00103588 /* libgmpxx.a */, - 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */, - 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */, + 5CFA59C62864464A00863A68 /* libffi.a */, + 5CFA59C82864464A00863A68 /* libgmp.a */, + 5CFA59C72864464A00863A68 /* libgmpxx.a */, + 5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */, + 5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */, ); path = Libraries; sourceTree = ""; @@ -584,6 +591,16 @@ path = ComposeMessage; sourceTree = ""; }; + 5CFA59C22860B04D00863A68 /* Database */ = { + isa = PBXGroup; + children = ( + 5C4B3B09285FB130003915F2 /* DatabaseView.swift */, + 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */, + 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */, + ); + path = Database; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -685,7 +702,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1330; - LastUpgradeCheck = 1330; + LastUpgradeCheck = 1340; ORGANIZATIONNAME = "SimpleX Chat"; TargetAttributes = { 5CA059C9279559F40002BEB4 = { @@ -713,6 +730,7 @@ knownRegions = ( en, ru, + Base, ); mainGroup = 5CA059BD279559F40002BEB4; packageReferences = ( @@ -786,7 +804,9 @@ 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, 5C029EAA283942EA004A9677 /* CallController.swift in Sources */, 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */, + 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */, 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */, + 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */, 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, @@ -825,6 +845,7 @@ 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */, 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */, + 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */, 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */, 5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */, 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */, @@ -948,6 +969,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -1008,6 +1030,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -1094,7 +1117,6 @@ PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -1217,7 +1239,6 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -1284,10 +1305,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Libraries", - ); "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( "$(inherited)", "$(PROJECT_DIR)/Libraries/ios", @@ -1305,7 +1322,6 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = ""; SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -1335,10 +1351,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Libraries", - ); "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( "$(inherited)", "$(PROJECT_DIR)/Libraries/ios", diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme index ecdec033c9..6a1d4192e6 100644 --- a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme @@ -1,6 +1,6 @@ chat_ctrl { if let controller = chatController { return controller } - let dataDir = getDocumentsDirectory().path + "/mobile_v1" - logger.debug("documents directory \(dataDir)") - var cstr = dataDir.cString(using: .utf8)! + let dbPath = getAppDatabasePath().path + logger.debug("getChatCtrl DB path: \(dbPath)") + var cstr = dbPath.cString(using: .utf8)! chatController = chat_init(&cstr) logger.debug("getChatCtrl: chat_init") return chatController! } -public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse { - var c = cmd.cmdString.cString(using: .utf8)! - return chatResponse(chat_send_cmd(getChatCtrl(), &c)) +public func resetChatCtrl() { + chatController = nil } -public func chatResponse(_ cjson: UnsafeMutablePointer) -> ChatResponse { - let s = String.init(cString: cjson) +public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse { + var c = cmd.cmdString.cString(using: .utf8)! + let cjson = chat_send_cmd(getChatCtrl(), &c)! + return chatResponse(fromCString(cjson)) +} + +// in microseconds +let MESSAGE_TIMEOUT: Int32 = 15_000_000 + +public func recvSimpleXMsg() -> ChatResponse? { + if let cjson = chat_recv_msg_wait(getChatCtrl(), MESSAGE_TIMEOUT) { + let s = fromCString(cjson) + return s == "" ? nil : chatResponse(s) + } + return nil +} + +public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? { + var c = s.cString(using: .utf8)! + if let cjson = chat_parse_markdown(&c) { + if let d = fromCString(cjson).data(using: .utf8) { + do { + let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d) + return r.formattedText + } catch { + logger.error("parseSimpleXMarkdown jsonDecoder.decode error: \(error.localizedDescription)") + } + } + } + return nil +} + +struct ParsedMarkdown: Decodable { + var formattedText: [FormattedText]? +} + +private func fromCString(_ c: UnsafeMutablePointer) -> String { + let s = String.init(cString: c) + free(c) + return s +} + +public func chatResponse(_ s: String) -> ChatResponse { let d = s.data(using: .utf8)! // TODO is there a way to do it without copying the data? e.g: // let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) @@ -46,7 +86,6 @@ public func chatResponse(_ cjson: UnsafeMutablePointer) -> ChatResponse { } json = prettyJSON(j) } - free(cjson) return ChatResponse.response(type: type ?? "invalid", json: json ?? s) } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 4424cf2c73..999f0531f4 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -18,6 +18,9 @@ public enum ChatCommand { case apiStopChat case apiSetAppPhase(appPhase: AgentPhase) case setFilesFolder(filesFolder: String) + case apiExportArchive(config: ArchiveConfig) + case apiImportArchive(config: ArchiveConfig) + case apiDeleteStorage case apiGetChats case apiGetChat(type: ChatType, id: Int64) case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) @@ -35,7 +38,6 @@ public enum ChatCommand { case apiDeleteChat(type: ChatType, id: Int64) case apiClearChat(type: ChatType, id: Int64) case apiUpdateProfile(profile: Profile) - case apiParseMarkdown(text: String) case createMyAddress case deleteMyAddress case showMyAddress @@ -62,6 +64,9 @@ public enum ChatCommand { case .apiStopChat: return "/_stop" case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase)" case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" + case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" + case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" + case .apiDeleteStorage: return "/_db delete" case .apiGetChats: return "/_get chats pcc=on" case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100" case let .apiSendMessage(type, id, file, quotedItemId, mc): @@ -81,7 +86,6 @@ public enum ChatCommand { case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))" case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))" - case let .apiParseMarkdown(text): return "/_parse \(text)" case .createMyAddress: return "/address" case .deleteMyAddress: return "/delete_address" case .showMyAddress: return "/show_address" @@ -110,6 +114,9 @@ public enum ChatCommand { case .apiStopChat: return "apiStopChat" case .apiSetAppPhase: return "apiSetAppPhase" case .setFilesFolder: return "setFilesFolder" + case .apiExportArchive: return "apiExportArchive" + case .apiImportArchive: return "apiImportArchive" + case .apiDeleteStorage: return "apiDeleteStorage" case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" case .apiSendMessage: return "apiSendMessage" @@ -127,7 +134,6 @@ public enum ChatCommand { case .apiDeleteChat: return "apiDeleteChat" case .apiClearChat: return "apiClearChat" case .apiUpdateProfile: return "apiUpdateProfile" - case .apiParseMarkdown: return "apiParseMarkdown" case .createMyAddress: return "createMyAddress" case .deleteMyAddress: return "deleteMyAddress" case .showMyAddress: return "showMyAddress" @@ -178,7 +184,6 @@ public enum ChatResponse: Decodable, Error { case chatCleared(chatInfo: ChatInfo) case userProfileNoChange case userProfileUpdated(fromProfile: Profile, toProfile: Profile) - case apiParsedMarkdown(formattedText: [FormattedText]?) case userContactLink(connReqContact: String) case userContactLinkCreated(connReqContact: String) case userContactLinkDeleted @@ -243,7 +248,6 @@ public enum ChatResponse: Decodable, Error { case .chatCleared: return "chatCleared" case .userProfileNoChange: return "userProfileNoChange" case .userProfileUpdated: return "userProfileUpdated" - case .apiParsedMarkdown: return "apiParsedMarkdown" case .userContactLink: return "userContactLink" case .userContactLinkCreated: return "userContactLinkCreated" case .userContactLinkDeleted: return "userContactLinkDeleted" @@ -309,7 +313,6 @@ public enum ChatResponse: Decodable, Error { case let .chatCleared(chatInfo): return String(describing: chatInfo) case .userProfileNoChange: return noDetails case let .userProfileUpdated(_, toProfile): return String(describing: toProfile) - case let .apiParsedMarkdown(formattedText): return String(describing: formattedText) case let .userContactLink(connReq): return connReq case let .userContactLinkCreated(connReq): return connReq case .userContactLinkDeleted: return noDetails @@ -370,6 +373,16 @@ public enum AgentPhase: String, Codable { case suspended = "SUSPENDED" } +public struct ArchiveConfig: Encodable { + var archivePath: String + var disableCompression: Bool? + + public init(archivePath: String, disableCompression: Bool? = nil) { + self.archivePath = archivePath + self.disableCompression = disableCompression + } +} + public func decodeJSON(_ json: String) -> T? { if let data = json.data(using: .utf8) { return try? jsonDecoder.decode(T.self, from: data) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index abb996ecab..15c26d896f 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -10,12 +10,12 @@ import Foundation import SwiftUI let GROUP_DEFAULT_APP_STATE = "appState" +let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" +public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" let APP_GROUP_NAME = "group.chat.simplex.app" -func getGroupDefaults() -> UserDefaults? { - UserDefaults(suiteName: APP_GROUP_NAME) -} +public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)! public enum AppState: String { case active @@ -42,18 +42,66 @@ public enum AppState: String { } } -public func setAppState(_ state: AppState) { - if let defaults = getGroupDefaults() { - defaults.set(state.rawValue, forKey: GROUP_DEFAULT_APP_STATE) +public enum DBContainer: String { + case documents + case group +} + +public let appStateGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_APP_STATE, + withDefault: .active +) + +public let dbContainerGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_DB_CONTAINER, + withDefault: .documents +) + +public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START) + +public class DateDefault { + var defaults: UserDefaults + var key: String + + public init(defaults: UserDefaults = UserDefaults.standard, forKey: String) { + self.defaults = defaults + self.key = forKey + } + + public func get() -> Date { + let ts = defaults.double(forKey: key) + return Date(timeIntervalSince1970: ts) + } + + public func set(_ ts: Date) { + defaults.set(ts.timeIntervalSince1970, forKey: key) defaults.synchronize() } } -public func getAppState() -> AppState { - if let defaults = getGroupDefaults(), - let rawValue = defaults.string(forKey: GROUP_DEFAULT_APP_STATE), - let state = AppState(rawValue: rawValue) { - return state +public class EnumDefault where T.RawValue == String { + var defaults: UserDefaults + var key: String + var defaultValue: T + + public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) { + self.defaults = defaults + self.key = forKey + self.defaultValue = withDefault + } + + public func get() -> T { + if let rawValue = defaults.string(forKey: key), + let value = T(rawValue: rawValue) { + return value + } + return defaultValue + } + + public func set(_ value: T) { + defaults.set(value.rawValue, forKey: key) + defaults.synchronize() } - return .active } diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 0393039316..b236712910 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -17,13 +17,55 @@ public let maxImageSize: Int64 = 236700 public let maxFileSize: Int64 = 8000000 -func getDocumentsDirectory() -> URL { -// FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! +public func getDocumentsDirectory() -> URL { + FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! +} + +func getGroupContainerDirectory() -> URL { FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)! } +func getAppDirectory() -> URL { + dbContainerGroupDefault.get() == .group + ? getGroupContainerDirectory() + : getDocumentsDirectory() +// getDocumentsDirectory() +} + +let DB_FILE_PREFIX = "simplex_v1" + +func getLegacyDatabasePath() -> URL { + getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false) +} + +public func getAppDatabasePath() -> URL { + dbContainerGroupDefault.get() == .group + ? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false) + : getLegacyDatabasePath() +// getLegacyDatabasePath() +} + +public func hasLegacyDatabase() -> Bool { + let dbPath = getLegacyDatabasePath() + let fm = FileManager.default + return fm.isReadableFile(atPath: dbPath.path + "_agent.db") && + fm.isReadableFile(atPath: dbPath.path + "_chat.db") +} + +public func removeLegacyDatabaseAndFiles() -> Bool { + let dbPath = getLegacyDatabasePath() + let appFiles = getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true) + let fm = FileManager.default + let r1 = nil != (try? fm.removeItem(atPath: dbPath.path + "_agent.db")) + let r2 = nil != (try? fm.removeItem(atPath: dbPath.path + "_chat.db")) + try? fm.removeItem(atPath: dbPath.path + "_agent.db.bak") + try? fm.removeItem(atPath: dbPath.path + "_chat.db.bak") + try? fm.removeItem(at: appFiles) + return r1 && r2 +} + public func getAppFilesDirectory() -> URL { - getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true) + getAppDirectory().appendingPathComponent("app_files", isDirectory: true) } func getAppFilePath(_ fileName: String) -> URL { @@ -96,8 +138,9 @@ private func saveFile(_ data: Data, _ fileName: String) -> String? { private func uniqueCombine(_ fileName: String) -> String { func tryCombine(_ fileName: String, _ n: Int) -> String { - let name = fileName.deletingPathExtension - let ext = fileName.pathExtension + let ns = fileName as NSString + let name = ns.deletingPathExtension + let ext = ns.pathExtension let suffix = (n == 0) ? "" : "_\(n)" let f = "\(name)\(suffix).\(ext)" return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f @@ -105,18 +148,6 @@ private func uniqueCombine(_ fileName: String) -> String { return tryCombine(fileName, 0) } -private extension String { - var ns: NSString { - return self as NSString - } - var pathExtension: String { - return ns.pathExtension - } - var deletingPathExtension: String { - return ns.deletingPathExtension - } -} - public func removeFile(_ fileName: String) { do { try FileManager.default.removeItem(atPath: getAppFilePath(fileName).path) diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index dab7fa3eb6..e848ad5cdd 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -18,3 +18,5 @@ typedef void* chat_ctrl; extern chat_ctrl chat_init(char *path); extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); extern char *chat_recv_msg(chat_ctrl ctl); +extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); +extern char *chat_parse_markdown(char *str); From 4e979aee7e8e8a769c189271ad32632ba42236f6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 24 Jun 2022 15:28:38 +0100 Subject: [PATCH 29/89] core: update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 05554113c0..30df5f67b1 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a7c3133c35fdf23023a6f1c6c0fc9b951f4387d6 + tag: 70e7c71762abf7fea364d587da55ae3b7bac9b32 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 47be0d5fa5..4a23c6801f 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a7c3133c35fdf23023a6f1c6c0fc9b951f4387d6" = "1ka7nclmasrn3nps8jpxvipjrgizg88jifvyrx30kfv1cqmqk17k"; + "https://github.com/simplex-chat/simplexmq.git"."70e7c71762abf7fea364d587da55ae3b7bac9b32" = "1mkzjhhkks5qfvf0rfni6ankg11b02y529ac387d53phv4qb82ck"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 011b42e675..f9b23f972f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: a7c3133c35fdf23023a6f1c6c0fc9b951f4387d6 + commit: 70e7c71762abf7fea364d587da55ae3b7bac9b32 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 313bc654575e4334cbbc87a92d208aac0854985a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 25 Jun 2022 11:49:46 +0100 Subject: [PATCH 30/89] core: start NSE without subscriptions, update simplexmq (fix agent phase) (#750) --- apps/ios/Shared/Model/SimpleXAPI.swift | 2 +- apps/ios/SimpleX NSE/NotificationService.swift | 2 +- apps/ios/SimpleXChat/APITypes.swift | 8 ++++---- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/View.hs | 2 +- stack.yaml | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ed40b1eb26..bcab112b64 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -133,7 +133,7 @@ func apiCreateActiveUser(_ p: Profile) throws -> User { } func apiStartChat() throws -> Bool { - let r = chatSendCmdSync(.startChat) + let r = chatSendCmdSync(.startChat(subscribe: true)) switch r { case .chatStarted: return true case .chatRunning: return false diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 6c1d1a6c71..769c0754b3 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -135,7 +135,7 @@ func apiGetActiveUser() -> User? { } func apiStartChat() throws { - let r = sendSimpleXCmd(.startChat) + let r = sendSimpleXCmd(.startChat(subscribe: false)) switch r { case .chatStarted: return case .chatRunning: return diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 999f0531f4..42a012b4bd 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -14,7 +14,7 @@ let jsonEncoder = getJSONEncoder() public enum ChatCommand { case showActiveUser case createActiveUser(profile: Profile) - case startChat + case startChat(subscribe: Bool) case apiStopChat case apiSetAppPhase(appPhase: AgentPhase) case setFilesFolder(filesFolder: String) @@ -60,9 +60,9 @@ public enum ChatCommand { switch self { case .showActiveUser: return "/u" case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)" - case .startChat: return "/_start" + case let .startChat(subscribe): return "/_start subscribe=\(subscribe ? "on" : "off")" case .apiStopChat: return "/_stop" - case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase)" + case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase.rawValue)" case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" @@ -367,7 +367,7 @@ struct ComposedMessage: Encodable { var msgContent: MsgContent } -public enum AgentPhase: String, Codable { +public enum AgentPhase: String, Decodable { case active = "ACTIVE" case paused = "PAUSED" case suspended = "SUSPENDED" diff --git a/cabal.project b/cabal.project index 30df5f67b1..96e57fc744 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 70e7c71762abf7fea364d587da55ae3b7bac9b32 + tag: c2f1dabcd0a2b8f7959b6946623c9620e036a103 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 4a23c6801f..b1470a1b92 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."70e7c71762abf7fea364d587da55ae3b7bac9b32" = "1mkzjhhkks5qfvf0rfni6ankg11b02y529ac387d53phv4qb82ck"; + "https://github.com/simplex-chat/simplexmq.git"."c2f1dabcd0a2b8f7959b6946623c9620e036a103" = "1sa0b1hl8vcr43w2zqiab2lnssdbl0jpj5gd9n5mh59b5ynvlmdr"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 090fe4fcff..a0ed279e82 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -55,7 +55,7 @@ responseToView testView = \case CRChatStarted -> ["chat started"] CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] - CRAppPhase _ -> [] + CRAppPhase p -> ["app phase " <> sShow p] CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] diff --git a/stack.yaml b/stack.yaml index f9b23f972f..7fdb5f799a 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 70e7c71762abf7fea364d587da55ae3b7bac9b32 + commit: c2f1dabcd0a2b8f7959b6946623c9620e036a103 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 5367ffe418f6e2549b13afa03e68ef1f081c4366 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 25 Jun 2022 17:02:16 +0100 Subject: [PATCH 31/89] core: update api to get/register tokens (#751) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 16 ++++++---------- src/Simplex/Chat/Controller.hs | 10 +++++++--- src/Simplex/Chat/View.hs | 1 + stack.yaml | 2 +- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/cabal.project b/cabal.project index 96e57fc744..d9d9a1d26b 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: c2f1dabcd0a2b8f7959b6946623c9620e036a103 + tag: a8260290e7896a1e75ae595c255171175237cb06 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b1470a1b92..61e6a6aaa8 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."c2f1dabcd0a2b8f7959b6946623c9620e036a103" = "1sa0b1hl8vcr43w2zqiab2lnssdbl0jpj5gd9n5mh59b5ynvlmdr"; + "https://github.com/simplex-chat/simplexmq.git"."a8260290e7896a1e75ae595c255171175237cb06" = "0nmndbxzskyg7cmc7m8nviwalrq8144bzz6y7ywpaai36fdjmj5l"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 67876a5268..5ff8aeaaeb 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -540,9 +540,9 @@ processChatCommand = \case updateCallItemStatus userId ct call receivedStatus Nothing $> Just call APIUpdateProfile profile -> withUser (`updateProfile` profile) APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text - APIRegisterToken token -> CRNtfTokenStatus <$> withUser (\_ -> withAgent (`registerNtfToken` token)) + APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken + APIRegisterToken token mode -> CRNtfTokenStatus <$> withUser (\_ -> withAgent $ \a -> registerNtfToken a token mode) APIVerifyToken token code nonce -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token code nonce) $> CRCmdOk - APIIntervalNofication token interval -> withUser $ \_ -> withAgent (\a -> enableNtfCron a token interval) $> CRCmdOk APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) $> CRCmdOk APIGetNtfMessage nonce encNtfInfo -> withUser $ \user -> do (NotificationInfo {ntfConnId, ntfMsgMeta}, msgs) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo @@ -2277,10 +2277,10 @@ chatCommandP = <|> "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP) <|> "/_profile " *> (APIUpdateProfile <$> jsonP) <|> "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString) - <|> "/_ntf register " *> (APIRegisterToken <$> tokenP) - <|> "/_ntf verify " *> (APIVerifyToken <$> tokenP <* A.space <*> strP <* A.space <*> strP) - <|> "/_ntf interval " *> (APIIntervalNofication <$> tokenP <* A.space <*> A.decimal) - <|> "/_ntf delete " *> (APIDeleteToken <$> tokenP) + <|> "/_ntf get " $> APIGetNtfToken + <|> "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP) + <|> "/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP) + <|> "/_ntf delete " *> (APIDeleteToken <$> strP) <|> "/_ntf message " *> (APIGetNtfMessage <$> strP <* A.space <*> strP) <|> "/smp_servers default" $> SetUserSMPServers [] <|> "/smp_servers " *> (SetUserSMPServers <$> smpServersP) @@ -2346,10 +2346,6 @@ chatCommandP = mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString msgContentP = "text " *> mcTextP <|> "json " *> jsonP ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal - tokenP = "apns " *> (DeviceToken PPApns <$> hexStringP) - hexStringP = - A.takeWhile (\c -> (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) >>= \s -> - if even (B.length s) then pure s else fail "odd number of hex characters" displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' ')) sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> A.takeByteString quotedMsg = A.char '(' *> A.takeTill (== ')') <* A.char ')' <* optional A.space diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 6e9976ef5f..c895fd7cc9 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -4,6 +4,7 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module Simplex.Chat.Controller where @@ -23,7 +24,6 @@ import Data.Text (Text) import Data.Time (ZonedTime) import Data.Time.Clock (UTCTime) import Data.Version (showVersion) -import Data.Word (Word16) import GHC.Generics (Generic) import Numeric.Natural import qualified Paths_simplex_chat as SC @@ -128,9 +128,9 @@ data ChatCommand | APICallStatus ContactId WebRTCCallStatus | APIUpdateProfile Profile | APIParseMarkdown Text - | APIRegisterToken DeviceToken + | APIGetNtfToken + | APIRegisterToken DeviceToken NotificationsMode | APIVerifyToken DeviceToken ByteString C.CbNonce - | APIIntervalNofication DeviceToken Word16 | APIDeleteToken DeviceToken | APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString} | GetUserSMPServers @@ -276,6 +276,7 @@ data ChatResponse | CRUserContactLinkSubscribed | CRUserContactLinkSubError {chatError :: ChatError} | CRNtfTokenStatus {status :: NtfTknStatus} + | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode} | CRNtfMessages {connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]} | CRNewContactConnection {connection :: PendingContactConnection} | CRContactConnectionDeleted {connection :: PendingContactConnection} @@ -332,6 +333,9 @@ data NtfMsgInfo = NtfMsgInfo {msgTs :: UTCTime, msgFlags :: MsgFlags} instance ToJSON NtfMsgInfo where toEncoding = J.genericToEncoding J.defaultOptions +crNtfToken :: (DeviceToken, NtfTknStatus, NotificationsMode) -> ChatResponse +crNtfToken (token, status, ntfMode) = CRNtfToken {token, status, ntfMode} + data ChatError = ChatError {errorType :: ChatErrorType} | ChatErrorAgent {agentError :: AgentErrorType} diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index a0ed279e82..7808f1ffec 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -162,6 +162,7 @@ responseToView testView = \case CRNewContactConnection _ -> [] CRContactConnectionDeleted PendingContactConnection {pccConnId} -> ["connection :" <> sShow pccConnId <> " deleted"] CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)] + CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)] CRNtfMessages {} -> [] CRMessageError prefix err -> [plain prefix <> ": " <> plain err] CRChatError e -> viewChatError e diff --git a/stack.yaml b/stack.yaml index 7fdb5f799a..d991665da1 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: c2f1dabcd0a2b8f7959b6946623c9620e036a103 + commit: a8260290e7896a1e75ae595c255171175237cb06 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 463f644bcef58c872310c7e2a88897432259ac2f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 26 Jun 2022 15:04:44 +0100 Subject: [PATCH 32/89] core: change API to suspend agent (#752) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 9 +++++---- src/Simplex/Chat/Controller.hs | 5 +++-- src/Simplex/Chat/View.hs | 2 +- stack.yaml | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cabal.project b/cabal.project index d9d9a1d26b..3b8c5538a1 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a8260290e7896a1e75ae595c255171175237cb06 + tag: 51d0b48ce15433d8473770522b1eb814688d2aea source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 61e6a6aaa8..fcc28b64f2 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a8260290e7896a1e75ae595c255171175237cb06" = "0nmndbxzskyg7cmc7m8nviwalrq8144bzz6y7ywpaai36fdjmj5l"; + "https://github.com/simplex-chat/simplexmq.git"."51d0b48ce15433d8473770522b1eb814688d2aea" = "1v1n96lq9k6cvayqhlc7ywr1zwj2qnnsirv4dilqwl507gb6gqyj"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 5ff8aeaaeb..6db48345cd 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -60,7 +60,6 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Client (NtfServer) -import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), PushProvider (..)) import Simplex.Messaging.Parsers (base64P, parseAll) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..)) import qualified Simplex.Messaging.Protocol as SMP @@ -205,7 +204,8 @@ processChatCommand = \case APIStopChat -> do ask >>= stopChatController pure CRChatStopped - APISetAppPhase phase -> withAgent (`setAgentPhase` phase) $> CRCmdOk + APIActivateChat -> withAgent activateAgent $> CRCmdOk + APISuspendChat t -> withAgent (`suspendAgent` t) $> CRCmdOk ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk SetFilesFolder filesFolder' -> do createDirectoryIfMissing True filesFolder' @@ -1096,7 +1096,7 @@ processAgentMessage Nothing _ _ = throwChatError CENoActiveUser processAgentMessage (Just User {userId}) "" agentMessage = case agentMessage of DOWN srv conns -> serverEvent srv conns CRContactsDisconnected "disconnected" UP srv conns -> serverEvent srv conns CRContactsSubscribed "connected" - PHASE phase -> toView $ CRAppPhase phase + SUSPENDED -> toView CRChatSuspended _ -> pure () where serverEvent srv@SMP.ProtocolServer {host, port} conns event str = do @@ -2250,7 +2250,8 @@ chatCommandP = <|> "/_start subscribe=" *> (StartChat <$> ("on" $> True <|> "off" $> False)) <|> "/_start" $> StartChat True <|> "/_stop" $> APIStopChat - <|> "/_app phase " *> (APISetAppPhase <$> strP) + <|> "/_app activate" $> APIActivateChat + <|> "/_app suspend " *> (APISuspendChat <$> A.decimal) <|> "/_resubscribe all" $> ResubscribeAllConnections <|> "/_files_folder " *> (SetFilesFolder <$> filePath) <|> "/_db export " *> (APIExportArchive <$> jsonP) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index c895fd7cc9..67ab50029d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -101,7 +101,8 @@ data ChatCommand | CreateActiveUser Profile | StartChat {subscribeConnections :: Bool} | APIStopChat - | APISetAppPhase AgentPhase + | APIActivateChat + | APISuspendChat {suspendTimeout :: Int} | ResubscribeAllConnections | SetFilesFolder FilePath | APIExportArchive ArchiveConfig @@ -185,7 +186,7 @@ data ChatResponse | CRChatStarted | CRChatRunning | CRChatStopped - | CRAppPhase {appPhase :: AgentPhase} + | CRChatSuspended | CRApiChats {chats :: [AChat]} | CRApiChat {chat :: AChat} | CRLastMessages {chatItems :: [AChatItem]} diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 7808f1ffec..448e9c6582 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -55,7 +55,7 @@ responseToView testView = \case CRChatStarted -> ["chat started"] CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] - CRAppPhase p -> ["app phase " <> sShow p] + CRChatSuspended -> ["chat suspended"] CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] diff --git a/stack.yaml b/stack.yaml index d991665da1..63b756b335 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: a8260290e7896a1e75ae595c255171175237cb06 + commit: 51d0b48ce15433d8473770522b1eb814688d2aea # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 6d259914179ea29e1d0e35dd537aad426be04c67 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 27 Jun 2022 10:28:30 +0100 Subject: [PATCH 33/89] ios: process notifications, suspend app, notifications settings UI (#754) --- apps/ios/Shared/AppDelegate.swift | 2 +- apps/ios/Shared/Model/BGManager.swift | 9 + apps/ios/Shared/Model/ChatModel.swift | 2 + apps/ios/Shared/Model/SimpleXAPI.swift | 20 ++- apps/ios/Shared/Model/SuspendChat.swift | 52 ++++++ apps/ios/Shared/SimpleXApp.swift | 23 +-- .../Shared/Views/Database/DatabaseView.swift | 2 + .../UserSettings/NotificationsView.swift | 163 ++++++++++++++++++ .../Views/UserSettings/SettingsView.swift | 15 +- .../ios/SimpleX NSE/NotificationService.swift | 72 ++++---- apps/ios/SimpleX.xcodeproj/project.pbxproj | 8 + apps/ios/SimpleXChat/APITypes.swift | 67 +++++-- apps/ios/SimpleXChat/AppGroup.swift | 20 +-- 13 files changed, 366 insertions(+), 89 deletions(-) create mode 100644 apps/ios/Shared/Model/SuspendChat.swift create mode 100644 apps/ios/Shared/Views/UserSettings/NotificationsView.swift diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 7b0a2bea8d..ec8a19cde6 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -26,7 +26,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { if useNotifications { Task { do { - m.tokenStatus = try await apiRegisterToken(token: token) + m.tokenStatus = try await apiRegisterToken(token: token, notificationMode: .instant) } catch { logger.error("apiRegisterToken error: \(responseError(error))") } diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index e1d9e819b1..6f35732d15 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -17,11 +17,14 @@ private let waitForMessages: TimeInterval = 6 private let bgRefreshInterval: TimeInterval = 450 +private let maxTimerCount = 9 + class BGManager { static let shared = BGManager() var chatReceiver: ChatReceiver? var bgTimer: Timer? var completed = true + var timerCount = 0 func register() { logger.debug("BGManager.register") @@ -60,6 +63,8 @@ class BGManager { self.chatReceiver = nil self.bgTimer?.invalidate() self.bgTimer = nil + self.timerCount = 0 + suspendBgRefresh() complete() } } @@ -82,14 +87,18 @@ class BGManager { return } logger.debug("BGManager.receiveMessages: starting chat") + activateChat(appState: .bgRefresh) let cr = ChatReceiver() self.chatReceiver = cr cr.start() RunLoop.current.add(Timer(timeInterval: 2, repeats: true) { timer in logger.debug("BGManager.receiveMessages: timer") self.bgTimer = timer + self.timerCount += 1 if cr.lastMsgTime.distance(to: Date.now) >= waitForMessages { completeReceiving("timer (no messages after \(waitForMessages) seconds)") + } else if self.timerCount >= maxTimerCount { + completeReceiving("timer (called \(maxTimerCount) times") } }, forMode: .default) } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index e60f51d534..78fc66bf1c 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -30,6 +30,8 @@ final class ChatModel: ObservableObject { @Published var appOpenUrl: URL? @Published var deviceToken: String? @Published var tokenStatus = NtfTknStatus.new + @Published var notificationMode = NotificationMode.off + @Published var notificationPreview: NotificationPreviewMode? = .message // current WebRTC call @Published var callInvitations: Dictionary = [:] @Published var activeCall: Call? diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index bcab112b64..e353ebf1ed 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -149,10 +149,16 @@ func apiStopChat() async throws { } } -func apiSetAppPhase(appPhase: AgentPhase) { - let r = chatSendCmdSync(.apiSetAppPhase(appPhase: appPhase)) +func apiActivateChat() { + let r = chatSendCmdSync(.apiActivateChat) if case .cmdOk = r { return } - logger.error("apiSetAppPhase error: \(String(describing: r))") + logger.error("apiActivateChat error: \(String(describing: r))") +} + +func apiSuspendChat(timeoutMicroseconds: Int) { + let r = chatSendCmdSync(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds)) + if case .cmdOk = r { return } + logger.error("apiSuspendChat error: \(String(describing: r))") } func apiSetFilesFolder(filesFolder: String) throws { @@ -220,8 +226,8 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM throw r } -func apiRegisterToken(token: String) async throws -> NtfTknStatus { - let r = await chatSendCmd(.apiRegisterToken(token: token)) +func apiRegisterToken(token: String, notificationMode: NotificationMode) async throws -> NtfTknStatus { + let r = await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode)) if case let .ntfTokenStatus(status) = r { return status } throw r } @@ -702,8 +708,8 @@ func processReceivedMsg(_ res: ChatResponse) async { m.callCommand = .end // CallController.shared.reportCallRemoteEnded(call: call) } - case let .appPhase(appPhase): - appStateGroupDefault.set(AppState(appPhase: appPhase)) + case .chatSuspended: + chatSuspended() default: logger.debug("unsupported event: \(res.responseType)") } diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift new file mode 100644 index 0000000000..ab552ad9f7 --- /dev/null +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -0,0 +1,52 @@ +// +// SuspendChat.swift +// SimpleX (iOS) +// +// Created by Evgeny on 26/06/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import SimpleXChat + +private let suspendLockQueue = DispatchQueue(label: "chat.simplex.app.suspend.lock") + +let appSuspendTimeout: Int = 15 // seconds + +let bgSuspendTimeout: Int = 5 // seconds + +private func _suspendChat(timeout: Int) { + appStateGroupDefault.set(.suspending) + apiSuspendChat(timeoutMicroseconds: timeout * 1000000) + let endTask = beginBGTask(chatSuspended) + DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask) +} + +func suspendChat() { + suspendLockQueue.sync { + _suspendChat(timeout: appSuspendTimeout) + } +} + +func suspendBgRefresh() { + suspendLockQueue.sync { + if case .bgRefresh = appStateGroupDefault.get() { + _suspendChat(timeout: bgSuspendTimeout) + } + } +} + +func chatSuspended() { + suspendLockQueue.sync { + if case .suspending = appStateGroupDefault.get() { + appStateGroupDefault.set(.suspended) + } + } +} + +func activateChat(appState: AppState = .active) { + suspendLockQueue.sync { + appStateGroupDefault.set(appState) + apiActivateChat() + } +} diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 557ef1f532..5d983832a7 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -49,15 +49,20 @@ struct SimpleXApp: App { logger.debug("scenePhase \(String(describing: scenePhase))") switch (phase) { case .background: - pauseApp() + suspendChat() + if chatModel.chatRunning == true { + ChatReceiver.shared.stop() + } BGManager.shared.schedule() if userAuthorized == true { enteredBackground = ProcessInfo.processInfo.systemUptime } doAuthenticate = false case .active: - appStateGroupDefault.set(.active) - apiSetAppPhase(appPhase: .active) + activateChat() + if chatModel.chatRunning == true { + ChatReceiver.shared.start() + } doAuthenticate = authenticationExpired() default: break @@ -88,18 +93,6 @@ struct SimpleXApp: App { } } - private func pauseApp() { - appStateGroupDefault.set(.pausing) - apiSetAppPhase(appPhase: .paused) - let endTask = beginBGTask { - if appStateGroupDefault.get() != .active { - appStateGroupDefault.set(.suspending) - apiSetAppPhase(appPhase: .suspended) - } - } - DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask) - } - private func authenticationExpired() -> Bool { if let enteredBackground = enteredBackground { return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30 diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 6ff5a10348..9611089914 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -221,6 +221,7 @@ struct DatabaseView: View { Task { do { try await apiStopChat() + ChatReceiver.shared.stop() await MainActor.run { m.chatRunning = false } } catch let error { await MainActor.run { @@ -315,6 +316,7 @@ struct DatabaseView: View { _ = try apiStartChat() runChat = true m.chatRunning = true + ChatReceiver.shared.start() chatLastStartGroupDefault.set(Date.now) } catch let error { runChat = false diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift new file mode 100644 index 0000000000..ce1eefcbfe --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -0,0 +1,163 @@ +// +// NotificationsView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 26/06/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct NotificationsView: View { + @EnvironmentObject var m: ChatModel + @State private var notificationMode: NotificationMode? + @State private var alert: NotificationMode? + + var body: some View { + List { + Section { + NavigationLink { + List { + Section { + SelectionListView(list: NotificationMode.values, selection: $notificationMode) { mode in + alert = mode + } + } footer: { + VStack(alignment: .leading) { + if let mode = notificationMode { + Text(ntfModeDescription(mode)) + } + } + .font(.callout) + .padding(.top, 1) + } + } + .navigationTitle("Send notifications") + .navigationBarTitleDisplayMode(.inline) + .alert(item: $alert) { notificationAlert($0) } + .onAppear { notificationMode = m.notificationMode } + } label: { + HStack { + Text("Send notifications") + Spacer() + Text(m.notificationMode.label) + } + } + + NavigationLink { + List { + Section { + SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) + } footer: { + + } + } + .navigationTitle("Show preview") + .navigationBarTitleDisplayMode(.inline) + } label: { + HStack { + Text("Show preview") + Spacer() + Text(m.notificationPreview?.label ?? "") + } + } + } header: { + Text("Message notifications") + } + } + } + + private func notificationAlert(_ mode: NotificationMode) -> Alert { + switch mode { + case .off: + return Alert( + title: Text("Turn off notifications?"), + message: Text(ntfModeDescription(mode)), + primaryButton: .default(Text("Turn off")) { + notificationMode = mode + m.notificationMode = mode + }, + secondaryButton: .cancel() { + notificationMode = m.notificationMode + } + ) + case .periodic: + return Alert( + title: Text("Enable periodic notifcations?"), + message: Text(ntfModeDescription(mode)), + primaryButton: .default(Text("Enable")) { + notificationMode = mode + m.notificationMode = mode + }, + secondaryButton: .cancel() { + notificationMode = m.notificationMode + } + ) + case .instant: + return Alert( + title: Text("Enable instant notifications?"), + message: Text(ntfModeDescription(mode)), + primaryButton: .default(Text("Enable")) { + notificationMode = mode + m.notificationMode = mode + }, + secondaryButton: .cancel() { + notificationMode = m.notificationMode + } + ) + } + } +} + +func ntfModeDescription(_ mode: NotificationMode) -> LocalizedStringKey { + switch mode { + case .off: return "**Maximum privacy**: push notifications are off.\nNo meta-data is shared with SimpleX Chat notification server." + case .periodic: return "**High privacy**: new messages are checked every 20 minutes.\nYour device token is shared with SimpleX Chat notification server, but it cannot see how many connections you have or how many messages you receive." + case .instant: return "**Medium privacy** (recommended): notifications are sent instantly.\nYour device token and notifications are sent to SimpleX Chat notification server, but it cannot access the message content, size or who is it from." + } +} + +struct SelectionListView: View { + var list: [Item] + @Binding var selection: Item? + var onSelection: ((Item) -> Void)? + @State private var tapped: Item? = nil + + var body: some View { + ForEach(list) { item in + HStack { + Text(item.label) + Spacer() + if selection == item { + Image(systemName: "checkmark") + .resizable().scaledToFit().frame(width: 16) + .foregroundColor(.accentColor) + } + } + .contentShape(Rectangle()) + .listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground)) + .onTapGesture { + if let f = onSelection { + f(item) + } else { + selection = item + } + } + ._onButtonGesture { down in + if down { + tapped = item + } else { + tapped = nil + } + } perform: {} + } + .environment(\.editMode, .constant(.active)) + } +} + +struct NotificationsView_Previews: PreviewProvider { + static var previews: some View { + NotificationsView() + } +} diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 5226e11b1e..47dff51fe1 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -95,14 +95,21 @@ struct SettingsView: View { } Section("Settings") { + NavigationLink { + NotificationsView() + .navigationTitle("Notifications") + } label: { + HStack { + notificationsIcon() + Text("Notifications") + } + } if enableCalls { NavigationLink { CallSettings() .navigationTitle("Your calls") } label: { - settingsRow("video") { - Text("Audio & video calls") - } + settingsRow("video") { Text("Audio & video calls") } } } NavigationLink { @@ -269,7 +276,7 @@ struct SettingsView: View { primaryButton: .destructive(Text("Confirm")) { Task { do { - chatModel.tokenStatus = try await apiRegisterToken(token: token) + chatModel.tokenStatus = try await apiRegisterToken(token: token, notificationMode: .instant) } catch { DispatchQueue.main.async { useNotifications = false diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 769c0754b3..56419b281a 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -18,50 +18,60 @@ class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("NotificationService.didReceive") + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) let appState = appStateGroupDefault.get() - if appState.running { + switch appState { + case .suspended: + logger.debug("NotificationService: app is suspended") + self.contentHandler = contentHandler + receiveNtfMessages(request, contentHandler) + case .suspending: + self.contentHandler = contentHandler + receiveNtfMessages(request, contentHandler) + default: print("userInfo", request.content.userInfo) contentHandler(request.content) - return } - logger.debug("NotificationService: app is in the background") - self.contentHandler = contentHandler - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + } + + func receiveNtfMessages(_ request: UNNotificationRequest, _ contentHandler: @escaping (UNNotificationContent) -> Void) { let userInfo = request.content.userInfo if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], let nonce = ntfData["nonce"] as? String, let encNtfInfo = ntfData["message"] as? String, let _ = startChat() { - apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) - if let content = receiveMessages() { - contentHandler(content) - return + if let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { + if let content = receiveMessageForNotification() { + contentHandler(content) + } else if let connEntity = ntfMsgInfo.connEntity { + switch connEntity { + case let .rcvDirectMsgConnection(_, contact): + () + case let .rcvGroupMsgConnection(_, groupInfo, groupMember): + () + case let .sndFileConnection(_, sndFileTransfer): + () + case let .rcvFileConnection(_, rcvFileTransfer): + () + case let .userContactConnection(_, userContact): + () + } + contentHandler(request.content) + } } } - - if let bestAttemptContent = bestAttemptContent { - // Modify the notification content here... - bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" - - contentHandler(bestAttemptContent) - } } - + override func serviceExtensionTimeWillExpire() { logger.debug("NotificationService.serviceExtensionTimeWillExpire") // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + if let contentHandler = self.contentHandler, let bestAttemptContent = self.bestAttemptContent { contentHandler(bestAttemptContent) } } } -func receivedAppMachMessage(msgId: Int32, msg: String) -> String? { - logger.debug("MachMessenger: receivedAppMachMessage \"\(msg)\" from App, replying") - return "reply from NSE to: \(msg)" -} - func startChat() -> User? { hs_init(0, nil) if let user = apiGetActiveUser() { @@ -80,7 +90,7 @@ func startChat() -> User? { return nil } -func receiveMessages() -> UNNotificationContent? { +func receiveMessageForNotification() -> UNNotificationContent? { logger.debug("NotificationService receiveMessages started") while true { if let res = recvSimpleXMsg() { @@ -149,12 +159,16 @@ func apiSetFilesFolder(filesFolder: String) throws { throw r } -func apiGetNtfMessage(nonce: String, encNtfInfo: String) { +func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r { - if let connEntity = connEntity { print("connEntity", connEntity) } - if let msgTs = msgTs { print("msgTs", msgTs) } - print("ntfMessages", ntfMessages) - return + return NtfMessages(connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages) } + return nil +} + +struct NtfMessages { + var connEntity: ConnectionEntity? + var msgTs: Date? + var ntfMessages: [NtfMsgInfo] } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index ed5e204143..c408623643 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -63,6 +63,8 @@ 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; }; 5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA952827143500B3292C /* MakeConnection.swift */; }; 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; }; + 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; }; + 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; }; 5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; }; 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; @@ -236,6 +238,8 @@ 5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = ""; }; 5CB0BA952827143500B3292C /* MakeConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeConnection.swift; sourceTree = ""; }; 5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = ""; }; + 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = ""; }; + 5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = ""; }; 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; @@ -405,6 +409,7 @@ 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */, + 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */, ); path = Model; sourceTree = ""; @@ -506,6 +511,7 @@ children = ( 5CB924D327A853F100ACCCDD /* SettingsButton.swift */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */, + 5CB346E62868D76D001FD2EF /* NotificationsView.swift */, 5C05DF522840AA1D00C683F9 /* CallSettings.swift */, 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, 5CB924E327A8683A00ACCCDD /* UserAddress.swift */, @@ -832,6 +838,7 @@ 5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */, 5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */, 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */, + 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, @@ -847,6 +854,7 @@ 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */, 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */, 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */, + 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */, 5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */, 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */, 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 42a012b4bd..f5474623f3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -7,6 +7,7 @@ // import Foundation +import SwiftUI let jsonDecoder = getJSONDecoder() let jsonEncoder = getJSONEncoder() @@ -16,7 +17,8 @@ public enum ChatCommand { case createActiveUser(profile: Profile) case startChat(subscribe: Bool) case apiStopChat - case apiSetAppPhase(appPhase: AgentPhase) + case apiActivateChat + case apiSuspendChat(timeoutMicroseconds: Int) case setFilesFolder(filesFolder: String) case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) @@ -26,7 +28,7 @@ public enum ChatCommand { case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) - case apiRegisterToken(token: String) + case apiRegisterToken(token: String, notificationMode: NotificationMode) case apiVerifyToken(token: String, code: String, nonce: String) case apiIntervalNofication(token: String, interval: Int) case apiDeleteToken(token: String) @@ -62,7 +64,8 @@ public enum ChatCommand { case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)" case let .startChat(subscribe): return "/_start subscribe=\(subscribe ? "on" : "off")" case .apiStopChat: return "/_stop" - case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase.rawValue)" + case .apiActivateChat: return "/_app activate" + case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" @@ -74,7 +77,7 @@ public enum ChatCommand { return "/_send \(ref(type, id)) json \(msg)" case let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)" case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" - case let .apiRegisterToken(token): return "/_ntf register apns \(token)" + case let .apiRegisterToken(token, notificationMode): return "/_ntf register apns \(token) \(notificationMode.rawValue)" case let .apiVerifyToken(token, code, nonce): return "/_ntf verify apns \(token) \(code) \(nonce)" case let .apiIntervalNofication(token, interval): return "/_ntf interval apns \(token) \(interval)" case let .apiDeleteToken(token): return "/_ntf delete apns \(token)" @@ -112,7 +115,8 @@ public enum ChatCommand { case .createActiveUser: return "createActiveUser" case .startChat: return "startChat" case .apiStopChat: return "apiStopChat" - case .apiSetAppPhase: return "apiSetAppPhase" + case .apiActivateChat: return "apiActivateChat" + case .apiSuspendChat: return "apiSuspendChat" case .setFilesFolder: return "setFilesFolder" case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" @@ -172,7 +176,7 @@ public enum ChatResponse: Decodable, Error { case chatStarted case chatRunning case chatStopped - case appPhase(appPhase: AgentPhase) + case chatSuspended case apiChats(chats: [ChatData]) case apiChat(chat: ChatData) case userSMPServers(smpServers: [String]) @@ -236,7 +240,7 @@ public enum ChatResponse: Decodable, Error { case .chatStarted: return "chatStarted" case .chatRunning: return "chatRunning" case .chatStopped: return "chatStopped" - case .appPhase: return "appPhase" + case .chatSuspended: return "chatSuspended" case .apiChats: return "apiChats" case .apiChat: return "apiChat" case .userSMPServers: return "userSMPServers" @@ -301,7 +305,7 @@ public enum ChatResponse: Decodable, Error { case .chatStarted: return noDetails case .chatRunning: return noDetails case .chatStopped: return noDetails - case let .appPhase(appPhase): return appPhase.rawValue + case .chatSuspended: return noDetails case let .apiChats(chats): return String(describing: chats) case let .apiChat(chat): return String(describing: chat) case let .userSMPServers(smpServers): return String(describing: smpServers) @@ -367,12 +371,6 @@ struct ComposedMessage: Encodable { var msgContent: MsgContent } -public enum AgentPhase: String, Decodable { - case active = "ACTIVE" - case paused = "PAUSED" - case suspended = "SUSPENDED" -} - public struct ArchiveConfig: Encodable { var archivePath: String var disableCompression: Bool? @@ -383,6 +381,47 @@ public struct ArchiveConfig: Encodable { } } +public protocol SelectableItem: Hashable, Identifiable { + var label: LocalizedStringKey { get } + static var values: [Self] { get } +} + +public enum NotificationMode: String, SelectableItem { + case off = "OFF" + case periodic = "PERIODIC" + case instant = "INSTANT" + + public var label: LocalizedStringKey { + switch self { + case .off: return "Off" + case .periodic: return "Periodically" + case .instant: return "Instantly" + } + } + + public var id: String { self.rawValue } + + public static var values: [NotificationMode] = [.instant, .periodic, .off] +} + +public enum NotificationPreviewMode: String, SelectableItem { + case hidden + case contact + case message + + public var label: LocalizedStringKey { + switch self { + case .hidden: return "Hidden" + case .contact: return "Contact" + case .message: return "Message" + } + } + + public var id: String { self.rawValue } + + public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden] +} + public func decodeJSON(_ json: String) -> T? { if let data = json.data(using: .utf8) { return try? jsonDecoder.decode(T.self, from: data) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 15c26d896f..b55c559bcd 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -19,27 +19,9 @@ public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)! public enum AppState: String { case active - case pausing - case paused + case bgRefresh case suspending case suspended - - public init(appPhase: AgentPhase) { - switch appPhase { - case .active: self = .active - case .paused: self = .paused - case .suspended: self = .suspended - } - } - - public var running: Bool { - switch self { - case .paused: return false - case .suspending: return false - case .suspended: return false - default: return true - } - } } public enum DBContainer: String { From 41c9742b0d2087d73b5e6b05cbe85f03935a1337 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 27 Jun 2022 19:41:25 +0100 Subject: [PATCH 34/89] core: auto-reply message for user contact addresses (#755) * core: auto-reply message for user contact addresses * terminal: show auto accept status and message * test --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 25 +- src/Simplex/Chat/Bot.hs | 2 +- src/Simplex/Chat/Controller.hs | 6 +- .../Chat/Migrations/M20220626_auto_reply.hs | 15 + src/Simplex/Chat/Migrations/chat_schema.sql | 376 +++++++++++------- src/Simplex/Chat/Store.hs | 104 +++-- src/Simplex/Chat/Types.hs | 3 +- src/Simplex/Chat/Util.hs | 3 + src/Simplex/Chat/View.hs | 9 +- tests/ChatTests.hs | 24 ++ tests/SchemaDump.hs | 2 +- 12 files changed, 366 insertions(+), 204 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20220626_auto_reply.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 61b0e1f5ae..e2eedcecf9 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -39,6 +39,7 @@ library Simplex.Chat.Migrations.M20220321_chat_item_edited Simplex.Chat.Migrations.M20220404_files_status_fields Simplex.Chat.Migrations.M20220514_profiles_user_id + Simplex.Chat.Migrations.M20220626_auto_reply Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.Protocol diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 6db48345cd..8a99e2deb5 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -52,7 +52,7 @@ import Simplex.Chat.Options (ChatOpts (..), smpServersP) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Types -import Simplex.Chat.Util (safeDecodeUtf8) +import Simplex.Chat.Util (safeDecodeUtf8, uncurry3) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), defaultAgentConfig) import Simplex.Messaging.Agent.Protocol @@ -592,9 +592,9 @@ processChatCommand = \case withStore' (`deleteUserContactLink` userId) pure CRUserContactLinkDeleted ShowMyAddress -> withUser $ \User {userId} -> - uncurry CRUserContactLink <$> withStore (`getUserContactLink` userId) - AddressAutoAccept onOff -> withUser $ \User {userId} -> do - uncurry CRUserContactLinkUpdated <$> withStore (\db -> updateUserContactLinkAutoAccept db userId onOff) + uncurry3 CRUserContactLink <$> withStore (`getUserContactLink` userId) + AddressAutoAccept onOff msgContent -> withUser $ \User {userId} -> do + uncurry3 CRUserContactLinkUpdated <$> withStore (\db -> updateUserContactLinkAutoAccept db userId onOff msgContent) AcceptContact cName -> withUser $ \User {userId} -> do connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIAcceptContact connReqId @@ -996,9 +996,9 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f) acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> m Contact -acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, xContactId} = do +acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} = do connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profile - withStore' $ \db -> createAcceptedContact db userId connId cName profileId p xContactId + withStore' $ \db -> createAcceptedContact db userId connId cName profileId p userContactLinkId xContactId agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m () agentSubscriber user subConns = do @@ -1136,7 +1136,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> Nothing processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m () - processDirectMessage agentMsg conn@Connection {connId} = \case + processDirectMessage agentMsg conn@Connection {connId, viaUserContactLink} = \case Nothing -> case agentMsg of CONF confId connInfo -> do saveConnInfo conn connInfo @@ -1205,6 +1205,13 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage toView $ CRContactConnected ct setActive $ ActiveC c showToast (c <> "> ") "connected" + forM_ viaUserContactLink $ \userContactLinkId -> do + withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case + Just (_, True, Just mc) -> do + msg <- sendDirectContactMessage ct (XMsgNew $ MCSimple (ExtMsgContent mc Nothing)) + ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) Nothing Nothing + toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci + _ -> pure () Just (gInfo, m@GroupMember {activeConn}) -> do when (maybe False ((== ConnReady) . connStatus) activeConn) $ do notifyMemberConnected gInfo m @@ -1438,7 +1445,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage withStore (\db -> createOrUpdateContactRequest db userId userContactLinkId invId p xContactId_) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted contact CORRequest cReq@UserContactRequest {localDisplayName} -> do - (_, autoAccept) <- withStore $ \db -> getUserContactLink db userId + (_, autoAccept, _) <- withStore $ \db -> getUserContactLink db userId if autoAccept then acceptContactRequest user cReq >>= toView . CRAcceptingContactRequest else do @@ -2325,7 +2332,7 @@ chatCommandP = <|> ("/address" <|> "/ad") $> CreateMyAddress <|> ("/delete_address" <|> "/da") $> DeleteMyAddress <|> ("/show_address" <|> "/sa") $> ShowMyAddress - <|> "/auto_accept " *> (AddressAutoAccept <$> onOffP) + <|> "/auto_accept " *> (AddressAutoAccept <$> onOffP <*> optional (A.space *> msgContentP)) <|> ("/accept @" <|> "/accept " <|> "/ac @" <|> "/ac ") *> (AcceptContact <$> displayName) <|> ("/reject @" <|> "/reject " <|> "/rc @" <|> "/rc ") *> (RejectContact <$> displayName) <|> ("/markdown" <|> "/m") $> ChatHelp HSMarkdown diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 83a8be168e..ef055143a6 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -38,7 +38,7 @@ chatBotRepl welcome answer _user cc = do initializeBotAddress :: ChatController -> IO () initializeBotAddress cc = do sendChatCmd cc "/show_address" >>= \case - CRUserContactLink uri _ -> showBotAddress uri + CRUserContactLink uri _ _ -> showBotAddress uri CRChatCmdError (ChatErrorStore SEUserContactLinkNotFound) -> do putStrLn $ "No bot address, creating..." sendChatCmd cc "/address" >>= \case diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 67ab50029d..0b12fb7bae 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -147,7 +147,7 @@ data ChatCommand | CreateMyAddress | DeleteMyAddress | ShowMyAddress - | AddressAutoAccept Bool + | AddressAutoAccept Bool (Maybe MsgContent) | AcceptContact ContactName | RejectContact ContactName | SendMessage ChatName ByteString @@ -206,8 +206,8 @@ data ChatResponse | CRGroupCreated {groupInfo :: GroupInfo} | CRGroupMembers {group :: Group} | CRContactsList {contacts :: [Contact]} - | CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool} - | CRUserContactLinkUpdated {connReqContact :: ConnReqContact, autoAccept :: Bool} + | CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent} + | CRUserContactLinkUpdated {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent} | CRContactRequestRejected {contactRequest :: UserContactRequest} | CRUserAcceptedGroupSent {groupInfo :: GroupInfo} | CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember} diff --git a/src/Simplex/Chat/Migrations/M20220626_auto_reply.hs b/src/Simplex/Chat/Migrations/M20220626_auto_reply.hs new file mode 100644 index 0000000000..6ac72ac804 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220626_auto_reply.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220626_auto_reply where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220626_auto_reply :: Query +m20220626_auto_reply = + [sql| +ALTER TABLE user_contact_links ADD COLUMN auto_reply_msg_content TEXT DEFAULT NULL; + +ALTER TABLE connections ADD COLUMN via_user_contact_link INTEGER DEFAULT NULL + REFERENCES user_contact_links (user_contact_link_id) ON DELETE SET NULL; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index f3fa1f3b48..55429b04a5 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -1,130 +1,167 @@ -CREATE TABLE migrations ( - name TEXT NOT NULL, - ts TEXT NOT NULL, - PRIMARY KEY (name) - ); -CREATE TABLE contact_profiles ( -- remote user profile +CREATE TABLE migrations( + name TEXT NOT NULL, + ts TEXT NOT NULL, + PRIMARY KEY(name) +); +CREATE TABLE contact_profiles( + -- remote user profile contact_profile_id INTEGER PRIMARY KEY, - display_name TEXT NOT NULL, -- contact name set by remote user (not unique), this name must not contain spaces + display_name TEXT NOT NULL, -- contact name set by remote user(not unique), this name must not contain spaces full_name TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}' -- JSON with contact profile properties -, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE); -CREATE INDEX contact_profiles_index ON contact_profiles (display_name, full_name); -CREATE TABLE users ( + , + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + image TEXT, + user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE +); +CREATE INDEX contact_profiles_index ON contact_profiles( + display_name, + full_name +); +CREATE TABLE users( user_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE - DEFERRABLE INITIALLY DEFERRED, + DEFERRABLE INITIALLY DEFERRED, local_display_name TEXT NOT NULL UNIQUE, - active_user INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- 1 for active user - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON DELETE CASCADE - ON UPDATE CASCADE - DEFERRABLE INITIALLY DEFERRED + active_user INTEGER NOT NULL DEFAULT 0, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- 1 for active user + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE + DEFERRABLE INITIALLY DEFERRED ); -CREATE TABLE display_names ( +CREATE TABLE display_names( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, ldn_base TEXT NOT NULL, - ldn_suffix INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), - PRIMARY KEY (user_id, local_display_name) ON CONFLICT FAIL, - UNIQUE (user_id, ldn_base, ldn_suffix) ON CONFLICT FAIL + ldn_suffix INTEGER NOT NULL DEFAULT 0, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + PRIMARY KEY(user_id, local_display_name) ON CONFLICT FAIL, + UNIQUE(user_id, ldn_base, ldn_suffix) ON CONFLICT FAIL ) WITHOUT ROWID; -CREATE TABLE contacts ( +CREATE TABLE contacts( contact_id INTEGER PRIMARY KEY, contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- NULL if it's an incognito profile - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, - local_display_name TEXT NOT NULL, - is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user - via_group INTEGER REFERENCES groups (group_id) ON DELETE SET NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT CHECK (updated_at NOT NULL), xcontact_id BLOB, - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON DELETE CASCADE - ON UPDATE CASCADE, - UNIQUE (user_id, local_display_name), - UNIQUE (user_id, contact_profile_id) +user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, +local_display_name TEXT NOT NULL, +is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user + via_group INTEGER REFERENCES groups(group_id) ON DELETE SET NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT CHECK(updated_at NOT NULL), + xcontact_id BLOB, + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(user_id, local_display_name), + UNIQUE(user_id, contact_profile_id) ); -CREATE TABLE sent_probes ( +CREATE TABLE sent_probes( sent_probe_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE, probe BLOB NOT NULL, - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), - UNIQUE (user_id, probe) + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + UNIQUE(user_id, probe) ); -CREATE TABLE sent_probe_hashes ( +CREATE TABLE sent_probe_hashes( sent_probe_hash_id INTEGER PRIMARY KEY, sent_probe_id INTEGER NOT NULL REFERENCES sent_probes ON DELETE CASCADE, contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), - UNIQUE (sent_probe_id, contact_id) + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + UNIQUE(sent_probe_id, contact_id) ); -CREATE TABLE received_probes ( +CREATE TABLE received_probes( received_probe_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, probe BLOB, probe_hash BLOB NOT NULL, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE -, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL)); + , + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); CREATE TABLE known_servers( server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, port TEXT NOT NULL, key_hash BLOB, - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), - UNIQUE (user_id, host, port) + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + UNIQUE(user_id, host, port) ) WITHOUT ROWID; -CREATE TABLE group_profiles ( -- shared group profiles +CREATE TABLE group_profiles( + -- shared group profiles group_profile_id INTEGER PRIMARY KEY, display_name TEXT NOT NULL, -- this name must not contain spaces full_name TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}' -- JSON with user or contact profile -, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE); -CREATE TABLE groups ( + , + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + image TEXT, + user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE +); +CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, -- local group name without spaces group_profile_id INTEGER REFERENCES group_profiles ON DELETE SET NULL, -- shared group profile - inv_queue_info BLOB, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- received - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON DELETE CASCADE - ON UPDATE CASCADE, - UNIQUE (user_id, local_display_name), - UNIQUE (user_id, group_profile_id) + inv_queue_info BLOB, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- received + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(user_id, local_display_name), + UNIQUE(user_id, group_profile_id) ); -CREATE INDEX idx_groups_inv_queue_info ON groups (inv_queue_info); -CREATE TABLE group_members ( -- group members, excluding the local user +CREATE INDEX idx_groups_inv_queue_info ON groups(inv_queue_info); +CREATE TABLE group_members( + -- group members, excluding the local user group_member_id INTEGER PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, member_id BLOB NOT NULL, -- shared member ID, unique per group member_role TEXT NOT NULL, -- owner, admin, member member_category TEXT NOT NULL, -- see GroupMemberCategory member_status TEXT NOT NULL, -- see GroupMemberStatus - invited_by INTEGER REFERENCES contacts (contact_id) ON DELETE SET NULL, -- NULL for the members who joined before the current user and for the group creator + invited_by INTEGER REFERENCES contacts(contact_id) ON DELETE SET NULL, -- NULL for the members who joined before the current user and for the group creator sent_inv_queue_info BLOB, -- sent group_queue_info BLOB, -- received direct_queue_info BLOB, -- received user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, -- should be the same as contact contact_profile_id INTEGER NOT NULL REFERENCES contact_profiles ON DELETE CASCADE, - contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON DELETE CASCADE - ON UPDATE CASCADE, - UNIQUE (group_id, member_id) + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(group_id, member_id) ); -CREATE TABLE group_member_intros ( +CREATE TABLE group_member_intros( group_member_intro_id INTEGER PRIMARY KEY, - re_group_member_id INTEGER NOT NULL REFERENCES group_members (group_member_id) ON DELETE CASCADE, - to_group_member_id INTEGER NOT NULL REFERENCES group_members (group_member_id) ON DELETE CASCADE, + re_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, + to_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, group_queue_info BLOB, direct_queue_info BLOB, - intro_status TEXT NOT NULL, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- see GroupMemberIntroStatus - UNIQUE (re_group_member_id, to_group_member_id) + intro_status TEXT NOT NULL, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- see GroupMemberIntroStatus + UNIQUE(re_group_member_id, to_group_member_id) ); -CREATE TABLE files ( +CREATE TABLE files( file_id INTEGER PRIMARY KEY, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_id INTEGER REFERENCES groups ON DELETE CASCADE, @@ -132,152 +169,203 @@ CREATE TABLE files ( file_path TEXT, file_size INTEGER NOT NULL, chunk_size INTEGER NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT(datetime('now')), user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE -, chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), cancelled INTEGER, ci_file_status TEXT); -CREATE TABLE snd_files ( + , + chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE CASCADE, + updated_at TEXT CHECK(updated_at NOT NULL), + cancelled INTEGER, + ci_file_status TEXT +); +CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE, file_status TEXT NOT NULL, -- new, accepted, connected, completed - group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), - PRIMARY KEY (file_id, connection_id) + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + PRIMARY KEY(file_id, connection_id) ) WITHOUT ROWID; -CREATE TABLE rcv_files ( +CREATE TABLE rcv_files( file_id INTEGER PRIMARY KEY REFERENCES files ON DELETE CASCADE, file_status TEXT NOT NULL, -- new, accepted, connected, completed group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, file_queue_info BLOB -, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL)); -CREATE TABLE snd_file_chunks ( + , + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); +CREATE TABLE snd_file_chunks( file_id INTEGER NOT NULL, connection_id INTEGER NOT NULL, chunk_number INTEGER NOT NULL, chunk_agent_msg_id INTEGER, - chunk_sent INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- 0 (sent to agent), 1 (sent to server) - FOREIGN KEY (file_id, connection_id) REFERENCES snd_files ON DELETE CASCADE, - PRIMARY KEY (file_id, connection_id, chunk_number) + chunk_sent INTEGER NOT NULL DEFAULT 0, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- 0(sent to agent), 1(sent to server) + FOREIGN KEY(file_id, connection_id) REFERENCES snd_files ON DELETE CASCADE, + PRIMARY KEY(file_id, connection_id, chunk_number) ) WITHOUT ROWID; -CREATE TABLE rcv_file_chunks ( +CREATE TABLE rcv_file_chunks( file_id INTEGER NOT NULL REFERENCES rcv_files ON DELETE CASCADE, chunk_number INTEGER NOT NULL, chunk_agent_msg_id INTEGER NOT NULL, - chunk_stored INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- 0 (received), 1 (appended to file) - PRIMARY KEY (file_id, chunk_number) + chunk_stored INTEGER NOT NULL DEFAULT 0, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- 0(received), 1(appended to file) + PRIMARY KEY(file_id, chunk_number) ) WITHOUT ROWID; -CREATE TABLE connections ( -- all SMP agent connections +CREATE TABLE connections( + -- all SMP agent connections connection_id INTEGER PRIMARY KEY, agent_conn_id BLOB NOT NULL UNIQUE, conn_level INTEGER NOT NULL DEFAULT 0, - via_contact INTEGER REFERENCES contacts (contact_id) ON DELETE SET NULL, + via_contact INTEGER REFERENCES contacts(contact_id) ON DELETE SET NULL, conn_status TEXT NOT NULL, conn_type TEXT NOT NULL, -- contact, member, rcv_file, snd_file user_contact_link_id INTEGER REFERENCES user_contact_links ON DELETE CASCADE, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, snd_file_id INTEGER, - rcv_file_id INTEGER REFERENCES rcv_files (file_id) ON DELETE CASCADE, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), via_contact_uri_hash BLOB, xcontact_id BLOB, - FOREIGN KEY (snd_file_id, connection_id) - REFERENCES snd_files (file_id, connection_id) - ON DELETE CASCADE - DEFERRABLE INITIALLY DEFERRED + rcv_file_id INTEGER REFERENCES rcv_files(file_id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + updated_at TEXT CHECK(updated_at NOT NULL), + via_contact_uri_hash BLOB, + xcontact_id BLOB, + via_user_contact_link INTEGER DEFAULT NULL + REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL, + FOREIGN KEY(snd_file_id, connection_id) + REFERENCES snd_files(file_id, connection_id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED ); -CREATE TABLE user_contact_links ( +CREATE TABLE user_contact_links( user_contact_link_id INTEGER PRIMARY KEY, conn_req_contact BLOB NOT NULL, local_display_name TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL DEFAULT (datetime('now')), - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), auto_accept INTEGER DEFAULT 0, - UNIQUE (user_id, local_display_name) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + updated_at TEXT CHECK(updated_at NOT NULL), + auto_accept INTEGER DEFAULT 0, + auto_reply_msg_content TEXT DEFAULT NULL, + UNIQUE(user_id, local_display_name) ); -CREATE TABLE contact_requests ( +CREATE TABLE contact_requests( contact_request_id INTEGER PRIMARY KEY, user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links - ON UPDATE CASCADE ON DELETE CASCADE, - agent_invitation_id BLOB NOT NULL, + ON UPDATE CASCADE ON DELETE CASCADE, + agent_invitation_id BLOB NOT NULL, contact_profile_id INTEGER REFERENCES contact_profiles - ON DELETE SET NULL -- NULL if it's an incognito profile - DEFERRABLE INITIALLY DEFERRED, - local_display_name TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK (updated_at NOT NULL), xcontact_id BLOB, - FOREIGN KEY (user_id, local_display_name) - REFERENCES display_names (user_id, local_display_name) - ON UPDATE CASCADE - ON DELETE CASCADE - DEFERRABLE INITIALLY DEFERRED, - UNIQUE (user_id, local_display_name), - UNIQUE (user_id, contact_profile_id) + ON DELETE SET NULL -- NULL if it's an incognito profile +DEFERRABLE INITIALLY DEFERRED, +local_display_name TEXT NOT NULL, +created_at TEXT NOT NULL DEFAULT(datetime('now')), +user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK(updated_at NOT NULL), xcontact_id BLOB, +FOREIGN KEY(user_id, local_display_name) +REFERENCES display_names(user_id, local_display_name) +ON UPDATE CASCADE +ON DELETE CASCADE +DEFERRABLE INITIALLY DEFERRED, +UNIQUE(user_id, local_display_name), +UNIQUE(user_id, contact_profile_id) ); -CREATE TABLE messages ( +CREATE TABLE messages( message_id INTEGER PRIMARY KEY, msg_sent INTEGER NOT NULL, -- 0 for received, 1 for sent - chat_msg_event TEXT NOT NULL, -- message event tag (the constructor of CMEventTag) + chat_msg_event TEXT NOT NULL, -- message event tag(the constructor of CMEventTag) msg_body BLOB, -- agent message body as received or sent - created_at TEXT NOT NULL DEFAULT (datetime('now')) -, updated_at TEXT CHECK (updated_at NOT NULL), connection_id INTEGER DEFAULT NULL REFERENCES connections ON DELETE CASCADE, group_id INTEGER DEFAULT NULL REFERENCES groups ON DELETE CASCADE, shared_msg_id BLOB, shared_msg_id_user INTEGER); -CREATE TABLE msg_deliveries ( + created_at TEXT NOT NULL DEFAULT(datetime('now')) + , + updated_at TEXT CHECK(updated_at NOT NULL), + connection_id INTEGER DEFAULT NULL REFERENCES connections ON DELETE CASCADE, + group_id INTEGER DEFAULT NULL REFERENCES groups ON DELETE CASCADE, + shared_msg_id BLOB, + shared_msg_id_user INTEGER +); +CREATE TABLE msg_deliveries( msg_delivery_id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, -- non UNIQUE for group messages connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE, - agent_msg_id INTEGER, -- internal agent message ID (NULL while pending) + agent_msg_id INTEGER, -- internal agent message ID(NULL while pending) agent_msg_meta TEXT, -- JSON with timestamps etc. sent in MSG, NULL for sent - chat_ts TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT CHECK (created_at NOT NULL), updated_at TEXT CHECK (updated_at NOT NULL), -- broker_ts for received, created_at for sent - UNIQUE (connection_id, agent_msg_id) + chat_ts TEXT NOT NULL DEFAULT(datetime('now')), + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), -- broker_ts for received, created_at for sent + UNIQUE(connection_id, agent_msg_id) ); -CREATE TABLE msg_delivery_events ( +CREATE TABLE msg_delivery_events( msg_delivery_event_id INTEGER PRIMARY KEY, msg_delivery_id INTEGER NOT NULL REFERENCES msg_deliveries ON DELETE CASCADE, -- non UNIQUE for multiple events per msg delivery delivery_status TEXT NOT NULL, -- see MsgDeliveryStatus for allowed values - created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT CHECK (updated_at NOT NULL), - UNIQUE (msg_delivery_id, delivery_status) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT CHECK(updated_at NOT NULL), + UNIQUE(msg_delivery_id, delivery_status) ); -CREATE TABLE pending_group_messages ( +CREATE TABLE pending_group_messages( pending_group_message_id INTEGER PRIMARY KEY, group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, group_member_intro_id INTEGER REFERENCES group_member_intros ON DELETE CASCADE, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); -CREATE TABLE chat_items ( +CREATE TABLE chat_items( chat_item_id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_id INTEGER REFERENCES groups ON DELETE CASCADE, group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, -- NULL for sent even if group_id is not chat_msg_id INTEGER, -- sent as part of the message that created the item - created_by_msg_id INTEGER UNIQUE REFERENCES messages (message_id) ON DELETE SET NULL, + created_by_msg_id INTEGER UNIQUE REFERENCES messages(message_id) ON DELETE SET NULL, item_sent INTEGER NOT NULL, -- 0 for received, 1 for sent item_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent item_deleted INTEGER NOT NULL DEFAULT 0, -- 1 for deleted, item_content TEXT NOT NULL, -- JSON item_text TEXT NOT NULL, -- textual representation - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) -, item_status TEXT CHECK (item_status NOT NULL), shared_msg_id BLOB, quoted_shared_msg_id BLOB, quoted_sent_at TEXT, quoted_content TEXT, quoted_sent INTEGER, quoted_member_id BLOB, item_edited INTEGER); -CREATE TABLE chat_item_messages ( + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + item_status TEXT CHECK(item_status NOT NULL), + shared_msg_id BLOB, + quoted_shared_msg_id BLOB, + quoted_sent_at TEXT, + quoted_content TEXT, + quoted_sent INTEGER, + quoted_member_id BLOB, + item_edited INTEGER +); +CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (chat_item_id, message_id) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')), + UNIQUE(chat_item_id, message_id) ); -CREATE INDEX idx_connections_via_contact_uri_hash ON connections (via_contact_uri_hash); -CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests (xcontact_id); -CREATE INDEX idx_contacts_xcontact_id ON contacts (xcontact_id); -CREATE TABLE smp_servers ( +CREATE INDEX idx_connections_via_contact_uri_hash ON connections( + via_contact_uri_hash +); +CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests(xcontact_id); +CREATE INDEX idx_contacts_xcontact_id ON contacts(xcontact_id); +CREATE TABLE smp_servers( smp_server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, port TEXT NOT NULL, key_hash BLOB NOT NULL, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (host, port) + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')), + UNIQUE(host, port) ); -CREATE INDEX idx_messages_shared_msg_id ON messages (shared_msg_id); -CREATE UNIQUE INDEX idx_messages_direct_shared_msg_id ON messages (connection_id, shared_msg_id_user, shared_msg_id); -CREATE UNIQUE INDEX idx_messages_group_shared_msg_id ON messages (group_id, shared_msg_id_user, shared_msg_id); -CREATE INDEX idx_chat_items_shared_msg_id ON chat_items (shared_msg_id); +CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id); +CREATE UNIQUE INDEX idx_messages_direct_shared_msg_id ON messages( + connection_id, + shared_msg_id_user, + shared_msg_id +); +CREATE UNIQUE INDEX idx_messages_group_shared_msg_id ON messages( + group_id, + shared_msg_id_user, + shared_msg_id +); +CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id); diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 46da813a49..3c994c47de 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -42,6 +42,7 @@ module Simplex.Chat.Store getUserContactLinkConnections, deleteUserContactLink, getUserContactLink, + getUserContactLinkById, updateUserContactLinkAutoAccept, createOrUpdateContactRequest, getContactRequest, @@ -205,6 +206,7 @@ import Simplex.Chat.Migrations.M20220304_msg_quotes import Simplex.Chat.Migrations.M20220321_chat_item_edited import Simplex.Chat.Migrations.M20220404_files_status_fields import Simplex.Chat.Migrations.M20220514_profiles_user_id +import Simplex.Chat.Migrations.M20220626_auto_reply import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -229,7 +231,8 @@ schemaMigrations = ("20220304_msg_quotes", m20220304_msg_quotes), ("20220321_chat_item_edited", m20220321_chat_item_edited), ("20220404_files_status_fields", m20220404_files_status_fields), - ("20220514_profiles_user_id", m20220514_profiles_user_id) + ("20220514_profiles_user_id", m20220514_profiles_user_id), + ("20220626_auto_reply", m20220626_auto_reply) ] -- | The list of migrations in ascending order by date @@ -335,7 +338,7 @@ getConnReqContactXContactId db userId cReqHash = do -- Contact ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -366,24 +369,24 @@ createDirectConnection db userId acId pccConnStatus = do pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, createdAt, updatedAt = createdAt} -createContactConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createContactConnection_ db userId = createConnection_ db userId ConnContact Nothing +createMemberContactConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection +createMemberContactConnection_ db userId agentConnId viaContact = createConnection_ db userId ConnContact Nothing agentConnId viaContact Nothing -createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createConnection_ db userId connType entityId acId viaContact connLevel currentTs = do +createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe Int64 -> Maybe Int64 -> Int -> UTCTime -> IO Connection +createConnection_ db userId connType entityId acId viaContact viaUserContactLink connLevel currentTs = do DB.execute db [sql| INSERT INTO connections ( - user_id, agent_conn_id, conn_level, via_contact, conn_status, conn_type, + user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, connLevel, viaContact, ConnNew, connType) + ( (userId, acId, connLevel, viaContact, viaUserContactLink, ConnNew, connType) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) ) connId <- insertedRowId db - pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, connLevel, connStatus = ConnNew, createdAt = currentTs} + pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, connLevel, connStatus = ConnNew, createdAt = currentTs} where ent ct = if connType == ct then entityId else Nothing @@ -563,7 +566,7 @@ createUserContactLink db userId agentConnId cReq = "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" (userId, cReq, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing 0 currentTs + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing 0 currentTs getUserContactLinkConnections :: DB.Connection -> UserId -> ExceptT StoreError IO [Connection] getUserContactLinkConnections db userId = @@ -573,7 +576,7 @@ getUserContactLinkConnections db userId = DB.queryNamed db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -627,24 +630,37 @@ deleteUserContactLink db userId = do [":user_id" := userId] DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = ''" (Only userId) -getUserContactLink :: DB.Connection -> UserId -> ExceptT StoreError IO (ConnReqContact, Bool) +getUserContactLink :: DB.Connection -> UserId -> ExceptT StoreError IO (ConnReqContact, Bool, Maybe MsgContent) getUserContactLink db userId = ExceptT . firstRow id SEUserContactLinkNotFound $ DB.query db [sql| - SELECT conn_req_contact, auto_accept + SELECT conn_req_contact, auto_accept, auto_reply_msg_content FROM user_contact_links WHERE user_id = ? AND local_display_name = '' |] (Only userId) -updateUserContactLinkAutoAccept :: DB.Connection -> UserId -> Bool -> ExceptT StoreError IO (ConnReqContact, Bool) -updateUserContactLinkAutoAccept db userId autoAccept = do - (cReqUri, _) <- getUserContactLink db userId +getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> IO (Maybe (ConnReqContact, Bool, Maybe MsgContent)) +getUserContactLinkById db userId userContactLinkId = + maybeFirstRow id $ + DB.query + db + [sql| + SELECT conn_req_contact, auto_accept, auto_reply_msg_content + FROM user_contact_links + WHERE user_id = ? + AND user_contact_link_id = ? + |] + (userId, userContactLinkId) + +updateUserContactLinkAutoAccept :: DB.Connection -> UserId -> Bool -> Maybe MsgContent -> ExceptT StoreError IO (ConnReqContact, Bool, Maybe MsgContent) +updateUserContactLinkAutoAccept db userId autoAccept msgContent = do + (cReqUri, _, _) <- getUserContactLink db userId liftIO updateUserContactLinkAutoAccept_ - pure (cReqUri, autoAccept) + pure (cReqUri, autoAccept, msgContent) where updateUserContactLinkAutoAccept_ :: IO () updateUserContactLinkAutoAccept_ = @@ -652,11 +668,11 @@ updateUserContactLinkAutoAccept db userId autoAccept = do db [sql| UPDATE user_contact_links - SET auto_accept = ? + SET auto_accept = ?, auto_reply_msg_content = ? WHERE user_id = ? AND local_display_name = '' |] - (autoAccept, userId) + (autoAccept, msgContent, userId) createOrUpdateContactRequest :: DB.Connection -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayName, fullName, image} xContactId_ = @@ -703,7 +719,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN -- Contact ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -812,8 +828,8 @@ deleteContactRequest db userId contactRequestId = do (userId, userId, contactRequestId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: DB.Connection -> UserId -> ConnId -> ContactName -> Int64 -> Profile -> Maybe XContactId -> IO Contact -createAcceptedContact db userId agentConnId localDisplayName profileId profile xContactId = do +createAcceptedContact :: DB.Connection -> UserId -> ConnId -> ContactName -> Int64 -> Profile -> Int64 -> Maybe XContactId -> IO Contact +createAcceptedContact db userId agentConnId localDisplayName profileId profile userContactLinkId xContactId = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) currentTs <- getCurrentTime DB.execute @@ -821,7 +837,7 @@ createAcceptedContact db userId agentConnId localDisplayName profileId profile x "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?)" (userId, localDisplayName, profileId, currentTs, currentTs, xContactId) contactId <- insertedRowId db - activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing 0 currentTs + activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) 0 currentTs pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, createdAt = currentTs, updatedAt = currentTs} getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] @@ -876,7 +892,7 @@ getPendingConnections db User {userId} = <$> DB.queryNamed db [sql| - SELECT connection_id, agent_conn_id, conn_level, via_contact, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at FROM connections WHERE user_id = :user_id @@ -893,7 +909,7 @@ getContactConnections db userId Contact {contactId} = DB.query db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id @@ -903,14 +919,16 @@ getContactConnections db userId Contact {contactId} = connections [] = throwError $ SEContactNotFound contactId connections rows = pure $ map toConnection rows -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, UTCTime) +type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, ConnStatus, ConnType) :. EntityIdsRow :. Only UTCTime + +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe ConnStatus, Maybe ConnType) :. EntityIdsRow :. Only (Maybe UTCTime) toConnection :: ConnectionRow -> Connection -toConnection (connId, acId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt) = +toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, connStatus, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) = let entityId = entityId_ connType - in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, connStatus, connType, entityId, createdAt} + in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, connStatus, connType, entityId, createdAt} where entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId @@ -920,8 +938,8 @@ toConnection (connId, acId, connLevel, viaContact, connStatus, connType, contact entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: MaybeConnectionRow -> Maybe Connection -toMaybeConnection (Just connId, Just agentConnId, Just connLevel, viaContact, Just connStatus, Just connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, Just createdAt) = - Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt) +toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just connStatus, Just connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only (Just createdAt)) = + Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, connStatus, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) toMaybeConnection _ = Nothing getMatchingContacts :: DB.Connection -> UserId -> Contact -> IO [Contact] @@ -1085,7 +1103,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do <$> DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_level, via_contact, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at FROM connections WHERE user_id = ? AND agent_conn_id = ? @@ -1217,7 +1235,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id @@ -1375,7 +1393,7 @@ getGroupMembers db User {userId, userContactId} GroupInfo {groupId} = do SELECT m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id @@ -1617,7 +1635,7 @@ createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> Memb createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId = do let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- liftIO getCurrentTime - Connection {connId = directConnId} <- liftIO $ createContactConnection_ db userId directAgentConnId memberContactId cLevel currentTs + Connection {connId = directConnId} <- liftIO $ createMemberContactConnection_ db userId directAgentConnId memberContactId cLevel currentTs (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile (Just groupId) currentTs liftIO $ do let newMember = @@ -1639,7 +1657,7 @@ createIntroToMemberContact db userId GroupMember {memberContactId = viaContactId let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- getCurrentTime void $ createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs - Connection {connId = directConnId} <- createContactConnection_ db userId directAgentConnId viaContactId cLevel currentTs + Connection {connId = directConnId} <- createMemberContactConnection_ db userId directAgentConnId viaContactId cLevel currentTs contactId <- createMemberContact_ directConnId currentTs updateMember_ contactId currentTs where @@ -1669,7 +1687,7 @@ createIntroToMemberContact db userId GroupMember {memberContactId = viaContactId [":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId] createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createMemberConnection_ db userId groupMemberId = createConnection_ db userId ConnMember (Just groupMemberId) +createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId viaContact Nothing createContactMember_ :: IsContact a => DB.Connection -> User -> Int64 -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> UTCTime -> IO GroupMember createContactMember_ db user groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy = @@ -1729,7 +1747,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = -- via GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id @@ -1761,7 +1779,7 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = [sql| SELECT ct.contact_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.created_at, ct.updated_at, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id @@ -1886,7 +1904,7 @@ getChatRefByFileId db User {userId} fileId = createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection createSndFileConnection_ db userId fileId agentConnId = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing 0 currentTs + createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing Nothing 0 currentTs updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do @@ -2524,7 +2542,7 @@ getDirectChatPreviews_ db User {userId} = do -- Contact ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), @@ -2852,7 +2870,7 @@ getContact db userId contactId = -- Contact ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index e3c679372b..f980bb1450 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -669,7 +669,8 @@ data Connection = Connection { connId :: Int64, agentConnId :: AgentConnId, connLevel :: Int, - viaContact :: Maybe Int64, + viaContact :: Maybe Int64, -- group member contact ID, if not direct connection + viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address" connType :: ConnType, connStatus :: ConnStatus, entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID diff --git a/src/Simplex/Chat/Util.hs b/src/Simplex/Chat/Util.hs index d2fe0c3d40..835dedce55 100644 --- a/src/Simplex/Chat/Util.hs +++ b/src/Simplex/Chat/Util.hs @@ -8,3 +8,6 @@ safeDecodeUtf8 :: ByteString -> Text safeDecodeUtf8 = decodeUtf8With onError where onError _ _ = Just '?' + +uncurry3 :: (a -> b -> c -> d) -> ((a, b, c) -> d) +uncurry3 f ~(a, b, c) = f a b c diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 448e9c6582..670a1cdbd2 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -79,8 +79,8 @@ responseToView testView = \case HSMarkdown -> markdownInfo CRWelcome user -> chatWelcome user CRContactsList cs -> viewContactsList cs - CRUserContactLink cReqUri _ -> connReqContact_ "Your chat address:" cReqUri - CRUserContactLinkUpdated _ autoAccept -> ["auto_accept " <> if autoAccept then "on" else "off"] + CRUserContactLink cReqUri autoAccept autoReply -> connReqContact_ "Your chat address:" cReqUri <> autoAcceptStatus_ autoAccept autoReply + CRUserContactLinkUpdated _ autoAccept autoReply -> autoAcceptStatus_ autoAccept autoReply CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"] CRGroupCreated g -> viewGroupCreated g CRGroupMembers g -> viewGroupMembers g @@ -361,6 +361,11 @@ connReqContact_ intro cReq = "to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)" ] +autoAcceptStatus_ :: Bool -> Maybe MsgContent -> [StyledString] +autoAcceptStatus_ autoAccept autoReply = + ("auto_accept " <> if autoAccept then "on" else "off") : + maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply + viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest c Profile {fullName} = [ ttyFullName c fullName <> " wants to connect to you!", diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 6a01223d12..43e3b1fbfe 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -82,6 +82,7 @@ chatTests = do it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange it "reject contact and delete contact link" testRejectContactAndDeleteUserContact it "delete connection requests when contact link deleted" testDeleteConnectionRequests + it "auto-reply message" testAutoReplyMessage describe "SMP servers" $ it "get and set SMP servers" testGetSetSMPServers describe "async connection handshake" $ do @@ -1772,6 +1773,7 @@ testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathPr alice ##> "/sa" cLink' <- getContactLink alice False + alice <## "auto_accept off" cLink' `shouldBe` cLink alice ##> "/da" @@ -1803,6 +1805,28 @@ testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ cath ##> ("/c " <> cLink') alice <#? cath +testAutoReplyMessage :: IO () +testAutoReplyMessage = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/ad" + cLink <- getContactLink alice True + alice ##> "/auto_accept on text hello!" + alice <## "auto_accept on" + alice <## "auto reply:" + alice <## "hello!" + + bob ##> ("/c " <> cLink) + bob <## "connection request sent!" + alice <## "bob (Bob): accepting contact request..." + concurrentlyN_ + [ do + bob <## "alice (Alice): contact is connected" + bob <# "alice> hello!", + do + alice <## "bob (Bob): contact is connected" + alice <# "@bob hello!" + ] + testGetSetSMPServers :: IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index a876b10afc..bbf9e15588 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -25,6 +25,6 @@ testVerifySchemaDump = void $ readCreateProcess (shell $ "touch " <> schema) "" savedSchema <- readFile schema savedSchema `seq` pure () - void $ readCreateProcess (shell $ "sqlite3 " <> testDB <> " .schema > " <> schema) "" + void $ readCreateProcess (shell $ "sqlite3 " <> testDB <> " '.schema --indent' > " <> schema) "" currentSchema <- readFile schema savedSchema `shouldBe` currentSchema From 54126eba6ba6ae5cf6739497df42890a62d03940 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:03:27 +0100 Subject: [PATCH 35/89] APNS push environments (#756) --- apps/ios/Shared/AppDelegate.swift | 7 ++-- apps/ios/Shared/Model/ChatModel.swift | 2 +- apps/ios/Shared/Model/PushEnvironment.swift | 41 ++++++++++++++++++ apps/ios/Shared/Model/SimpleXAPI.swift | 10 ++--- .../Views/UserSettings/SettingsView.swift | 4 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 ++ apps/ios/SimpleXChat/APITypes.swift | 42 +++++++++++++++---- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 2 +- src/Simplex/Chat/Controller.hs | 2 +- stack.yaml | 2 +- 12 files changed, 96 insertions(+), 24 deletions(-) create mode 100644 apps/ios/Shared/Model/PushEnvironment.swift diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index ec8a19cde6..c58912dd71 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -21,12 +21,13 @@ class AppDelegate: NSObject, UIApplicationDelegate { let token = deviceToken.map { String(format: "%02hhx", $0) }.joined() logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)") let m = ChatModel.shared - m.deviceToken = token + let deviceToken = DeviceToken(env: pushEnvironment, token: token) + m.deviceToken = deviceToken let useNotifications = UserDefaults.standard.bool(forKey: "useNotifications") if useNotifications { Task { do { - m.tokenStatus = try await apiRegisterToken(token: token, notificationMode: .instant) + m.tokenStatus = try await apiRegisterToken(token: deviceToken, notificationMode: .instant) } catch { logger.error("apiRegisterToken error: \(responseError(error))") } @@ -53,7 +54,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { let m = ChatModel.shared do { if case .active = m.tokenStatus {} else { m.tokenStatus = .confirmed } - try await apiVerifyToken(token: token, code: verification, nonce: nonce) + try await apiVerifyToken(token: token, nonce: nonce, code: verification) m.tokenStatus = .active try await apiIntervalNofication(token: token, interval: 20) } catch { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 78fc66bf1c..be3e1fe446 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -28,7 +28,7 @@ final class ChatModel: ObservableObject { @Published var userAddress: String? @Published var userSMPServers: [String]? @Published var appOpenUrl: URL? - @Published var deviceToken: String? + @Published var deviceToken: DeviceToken? @Published var tokenStatus = NtfTknStatus.new @Published var notificationMode = NotificationMode.off @Published var notificationPreview: NotificationPreviewMode? = .message diff --git a/apps/ios/Shared/Model/PushEnvironment.swift b/apps/ios/Shared/Model/PushEnvironment.swift new file mode 100644 index 0000000000..7fa7ef0b99 --- /dev/null +++ b/apps/ios/Shared/Model/PushEnvironment.swift @@ -0,0 +1,41 @@ +// +// PushEnvironment.swift +// SimpleX (iOS) +// +// Created by Evgeny on 27/06/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import SimpleXChat + +let pushEnvironment: PushEnvironment = { + guard let provisioningProfile = try? provisioningProfile(), + let entitlements = provisioningProfile["Entitlements"] as? [String: Any], + let environment = entitlements["aps-environment"] as? String, + let env = PushEnvironment(rawValue: environment) + else { + logger.warning("pushEnvironment: unknown, assuming production") + return .production + } + logger.debug("pushEnvironment: \(env.rawValue)") + return env +}() + +private func provisioningProfile() throws -> [String: Any]? { + guard let url = Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") else { + return nil + } + + let binaryString = try String(contentsOf: url, encoding: .isoLatin1) + + let scanner = Scanner(string: binaryString) + guard scanner.scanUpToString(""), + let data = (plistString + "").data(using: .isoLatin1) + else { + return nil + } + + return try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] +} diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e353ebf1ed..a40e5c0e48 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -226,21 +226,21 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM throw r } -func apiRegisterToken(token: String, notificationMode: NotificationMode) async throws -> NtfTknStatus { +func apiRegisterToken(token: DeviceToken, notificationMode: NotificationMode) async throws -> NtfTknStatus { let r = await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode)) if case let .ntfTokenStatus(status) = r { return status } throw r } -func apiVerifyToken(token: String, code: String, nonce: String) async throws { - try await sendCommandOkResp(.apiVerifyToken(token: token, code: code, nonce: nonce)) +func apiVerifyToken(token: DeviceToken, nonce: String, code: String) async throws { + try await sendCommandOkResp(.apiVerifyToken(token: token, nonce: nonce, code: code)) } -func apiIntervalNofication(token: String, interval: Int) async throws { +func apiIntervalNofication(token: DeviceToken, interval: Int) async throws { try await sendCommandOkResp(.apiIntervalNofication(token: token, interval: interval)) } -func apiDeleteToken(token: String) async throws { +func apiDeleteToken(token: DeviceToken) async throws { try await sendCommandOkResp(.apiDeleteToken(token: token)) } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 47dff51fe1..cc48759c4e 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -234,7 +234,7 @@ struct SettingsView: View { .foregroundColor(color) } - private func notificationsToggle(_ token: String) -> some View { + private func notificationsToggle(_ token: DeviceToken) -> some View { Toggle("Check messages", isOn: $useNotifications) .onChange(of: useNotifications) { enable in if enable { @@ -269,7 +269,7 @@ struct SettingsView: View { } } - private func enableNotificationsAlert(_ token: String) -> Alert { + private func enableNotificationsAlert(_ token: DeviceToken) -> Alert { Alert( title: Text("Enable notifications? (BETA)"), message: Text("The app can receive background notifications every 20 minutes to check the new messages.\n*Please note*: if you confirm, your device token will be sent to SimpleX Chat notifications server."), diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index c408623643..4edc81a91b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; }; 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; }; 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; }; + 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; }; 5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; }; 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; @@ -240,6 +241,7 @@ 5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = ""; }; 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = ""; }; 5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; + 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = ""; }; 5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = ""; }; 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; @@ -410,6 +412,7 @@ 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */, 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */, + 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */, ); path = Model; sourceTree = ""; @@ -850,6 +853,7 @@ 5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */, 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */, 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, + 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */, 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */, 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */, 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index f5474623f3..7b07940416 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -28,10 +28,10 @@ public enum ChatCommand { case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) - case apiRegisterToken(token: String, notificationMode: NotificationMode) - case apiVerifyToken(token: String, code: String, nonce: String) - case apiIntervalNofication(token: String, interval: Int) - case apiDeleteToken(token: String) + case apiRegisterToken(token: DeviceToken, notificationMode: NotificationMode) + case apiVerifyToken(token: DeviceToken, nonce: String, code: String) + case apiIntervalNofication(token: DeviceToken, interval: Int) + case apiDeleteToken(token: DeviceToken) case apiGetNtfMessage(nonce: String, encNtfInfo: String) case getUserSMPServers case setUserSMPServers(smpServers: [String]) @@ -77,10 +77,10 @@ public enum ChatCommand { return "/_send \(ref(type, id)) json \(msg)" case let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)" case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" - case let .apiRegisterToken(token, notificationMode): return "/_ntf register apns \(token) \(notificationMode.rawValue)" - case let .apiVerifyToken(token, code, nonce): return "/_ntf verify apns \(token) \(code) \(nonce)" - case let .apiIntervalNofication(token, interval): return "/_ntf interval apns \(token) \(interval)" - case let .apiDeleteToken(token): return "/_ntf delete apns \(token)" + case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" + case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" + case let .apiIntervalNofication(token, interval): return "/_ntf interval \(token.cmdString) \(interval)" + case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" case .getUserSMPServers: return "/smp_servers" case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" @@ -386,6 +386,32 @@ public protocol SelectableItem: Hashable, Identifiable { static var values: [Self] { get } } +public struct DeviceToken { + var env: PushEnvironment + var token: String + + public init(env: PushEnvironment, token: String) { + self.env = env + self.token = token + } + + public var cmdString: String { + "\(env.pushProvider) \(token)" + } +} + +public enum PushEnvironment: String { + case development + case production + + public var pushProvider: String { + switch self { + case .development: return "apns_dev" + case .production: return "apns_prod" + } + } +} + public enum NotificationMode: String, SelectableItem { case off = "OFF" case periodic = "PERIODIC" diff --git a/cabal.project b/cabal.project index 3b8c5538a1..7af4430249 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 51d0b48ce15433d8473770522b1eb814688d2aea + tag: ba40d75886c14d70fb6af61c22f917069d7b478f source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index fcc28b64f2..bec8b11529 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."51d0b48ce15433d8473770522b1eb814688d2aea" = "1v1n96lq9k6cvayqhlc7ywr1zwj2qnnsirv4dilqwl507gb6gqyj"; + "https://github.com/simplex-chat/simplexmq.git"."ba40d75886c14d70fb6af61c22f917069d7b478f" = "1cashma9g0lw1bvaw4fcvfzzjqbpwjxhz42fvl83pi43j8i2plpn"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 8a99e2deb5..b1d6ff4fd5 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -542,7 +542,7 @@ processChatCommand = \case APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken APIRegisterToken token mode -> CRNtfTokenStatus <$> withUser (\_ -> withAgent $ \a -> registerNtfToken a token mode) - APIVerifyToken token code nonce -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token code nonce) $> CRCmdOk + APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) $> CRCmdOk APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) $> CRCmdOk APIGetNtfMessage nonce encNtfInfo -> withUser $ \user -> do (NotificationInfo {ntfConnId, ntfMsgMeta}, msgs) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0b12fb7bae..1a3980cc93 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -131,7 +131,7 @@ data ChatCommand | APIParseMarkdown Text | APIGetNtfToken | APIRegisterToken DeviceToken NotificationsMode - | APIVerifyToken DeviceToken ByteString C.CbNonce + | APIVerifyToken DeviceToken C.CbNonce ByteString | APIDeleteToken DeviceToken | APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString} | GetUserSMPServers diff --git a/stack.yaml b/stack.yaml index 63b756b335..89564af995 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 51d0b48ce15433d8473770522b1eb814688d2aea + commit: ba40d75886c14d70fb6af61c22f917069d7b478f # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From e8c9f6d5ab1150b5361d5168e15f34466be6128e Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 28 Jun 2022 16:50:40 +0400 Subject: [PATCH 36/89] core: use domain name in ntf server address (#757) --- src/Simplex/Chat.hs | 2 +- src/Simplex/Chat/Terminal.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index b1d6ff4fd5..27eb69dff6 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -103,7 +103,7 @@ _defaultSMPServers = ] _defaultNtfServers :: [NtfServer] -_defaultNtfServers = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@139.162.221.251"] +_defaultNtfServers = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"] maxImageSize :: Integer maxImageSize = 236700 diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index c6730a1595..27014b091e 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -28,7 +28,7 @@ terminalChatConfig = "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im" ], - ntf = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@139.162.221.251"] + ntf = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"] } } From 7226e5d37af0280a12ce83c8b6344ea00994cb73 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 28 Jun 2022 19:03:39 +0100 Subject: [PATCH 37/89] ios: notifications UI (#758) * ios: notifications UI * Apply suggestions from code review Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- .../UserSettings/NotificationsView.swift | 116 ++++++++++++------ .../Views/UserSettings/SettingsView.swift | 73 ----------- 2 files changed, 79 insertions(+), 110 deletions(-) diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index ce1eefcbfe..3cf7d39198 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -12,7 +12,8 @@ import SimpleXChat struct NotificationsView: View { @EnvironmentObject var m: ChatModel @State private var notificationMode: NotificationMode? - @State private var alert: NotificationMode? + @State private var showAlert: NotificationAlert? + @AppStorage(DEFAULT_USE_NOTIFICATIONS) private var useNotifications = false var body: some View { List { @@ -21,7 +22,7 @@ struct NotificationsView: View { List { Section { SelectionListView(list: NotificationMode.values, selection: $notificationMode) { mode in - alert = mode + showAlert = .setMode(mode: mode) } } footer: { VStack(alignment: .leading) { @@ -35,7 +36,13 @@ struct NotificationsView: View { } .navigationTitle("Send notifications") .navigationBarTitleDisplayMode(.inline) - .alert(item: $alert) { notificationAlert($0) } + .alert(item: $showAlert) { alert in + if let token = m.deviceToken { + return notificationAlert(alert, token) + } else { + return Alert(title: Text("No device token!")) + } + } .onAppear { notificationMode = m.notificationMode } } label: { HStack { @@ -68,44 +75,67 @@ struct NotificationsView: View { } } - private func notificationAlert(_ mode: NotificationMode) -> Alert { + private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert { + switch alert { + case let .setMode(mode): + return Alert( + title: Text(ntfModeAlertTitle(mode)), + message: Text(ntfModeDescription(mode)), + primaryButton: .default(Text(mode == .off ? "Turn off" : "Enable")) { + setNotificationsMode(mode, token) + }, + secondaryButton: .cancel() { + notificationMode = m.notificationMode + } + ) + case let .error(title, error): + return Alert(title: Text(title), message: Text(error)) + } + } + + private func ntfModeAlertTitle(_ mode: NotificationMode) -> LocalizedStringKey { switch mode { - case .off: - return Alert( - title: Text("Turn off notifications?"), - message: Text(ntfModeDescription(mode)), - primaryButton: .default(Text("Turn off")) { - notificationMode = mode - m.notificationMode = mode - }, - secondaryButton: .cancel() { - notificationMode = m.notificationMode + case .off: return "Turn off notifications?" + case .periodic: return "Enable periodic notifications?" + case .instant: return "Enable instant notifications?" + } + } + + private func setNotificationsMode(_ mode: NotificationMode, _ token: DeviceToken) { + Task { + switch mode { + case .off: + do { + try await apiDeleteToken(token: token) + useNotifications = false + m.tokenStatus = .new + notificationMode = .off + m.notificationMode = .off } - ) - case .periodic: - return Alert( - title: Text("Enable periodic notifcations?"), - message: Text(ntfModeDescription(mode)), - primaryButton: .default(Text("Enable")) { - notificationMode = mode - m.notificationMode = mode - }, - secondaryButton: .cancel() { - notificationMode = m.notificationMode + catch let error { + DispatchQueue.main.async { + let err = responseError(error) + logger.error("apiDeleteToken error: \(err)") + showAlert = .error(title: "Error deleting token", error: err) + } } - ) - case .instant: - return Alert( - title: Text("Enable instant notifications?"), - message: Text(ntfModeDescription(mode)), - primaryButton: .default(Text("Enable")) { - notificationMode = mode - m.notificationMode = mode - }, - secondaryButton: .cancel() { - notificationMode = m.notificationMode + default: + do { + do { + m.tokenStatus = try await apiRegisterToken(token: token, notificationMode: mode) + useNotifications = true + notificationMode = mode + m.notificationMode = mode + } catch let error { + DispatchQueue.main.async { + useNotifications = notificationMode != .off + let err = responseError(error) + logger.error("apiRegisterToken error: \(err)") + showAlert = .error(title: "Error enabling notifications", error: err) + } + } } - ) + } } } } @@ -156,6 +186,18 @@ struct SelectionListView: View { } } +enum NotificationAlert: Identifiable { + case setMode(mode: NotificationMode) + case error(title: LocalizedStringKey, error: String) + + var id: String { + switch self { + case let .setMode(mode): return "enable \(mode.rawValue)" + case let .error(title, error): return "error \(title): \(error)" + } + } +} + struct NotificationsView_Previews: PreviewProvider { static var previews: some View { NotificationsView() diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index cc48759c4e..97e0ba5c0f 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -49,11 +49,8 @@ struct SettingsView: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var chatModel: ChatModel @Binding var showSettings: Bool - @AppStorage(DEFAULT_USE_NOTIFICATIONS) private var useNotifications = false @AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true @AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false - @State var showNotificationsAlert: Bool = false - @State var whichNotificationsAlert = NotificationAlert.enable var body: some View { let user: User = chatModel.currentUser! @@ -187,13 +184,6 @@ struct SettingsView: View { } label: { settingsRow("gauge") { Text("Experimental features") } } - if let token = chatModel.deviceToken { - HStack { - notificationsIcon() - notificationsToggle(token) - } - .disabled(chatModel.chatRunning != true) - } Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") } } @@ -233,69 +223,6 @@ struct SettingsView: View { .padding(.trailing, 9) .foregroundColor(color) } - - private func notificationsToggle(_ token: DeviceToken) -> some View { - Toggle("Check messages", isOn: $useNotifications) - .onChange(of: useNotifications) { enable in - if enable { - showNotificationsAlert = true - whichNotificationsAlert = .enable - } else { - Task { - do { - try await apiDeleteToken(token: token) - chatModel.tokenStatus = .new - } - catch { - DispatchQueue.main.async { - if let cr = error as? ChatResponse { - let err = String(describing: cr) - logger.error("apiDeleteToken error: \(err)") - showNotificationsAlert = true - whichNotificationsAlert = .error("Error deleting token", err) - } else { - logger.error("apiDeleteToken unknown error: \(error.localizedDescription)") - } - } - } - } - } - } - .alert(isPresented: $showNotificationsAlert) { - switch (whichNotificationsAlert) { - case .enable: return enableNotificationsAlert(token) - case let .error(title, err): return Alert(title: Text(title), message: Text(err)) - } - } - } - - private func enableNotificationsAlert(_ token: DeviceToken) -> Alert { - Alert( - title: Text("Enable notifications? (BETA)"), - message: Text("The app can receive background notifications every 20 minutes to check the new messages.\n*Please note*: if you confirm, your device token will be sent to SimpleX Chat notifications server."), - primaryButton: .destructive(Text("Confirm")) { - Task { - do { - chatModel.tokenStatus = try await apiRegisterToken(token: token, notificationMode: .instant) - } catch { - DispatchQueue.main.async { - useNotifications = false - if let cr = error as? ChatResponse { - let err = String(describing: cr) - logger.error("apiRegisterToken error: \(err)") - showNotificationsAlert = true - whichNotificationsAlert = .error("Error registering token", err) - } else { - logger.error("apiRegisterToken unknown error: \(error.localizedDescription)") - } - } - } - } - }, secondaryButton: .cancel() { - withAnimation() { useNotifications = false } - } - ) - } } func settingsRow(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View { From 785fab16532b8640387c5d5dccbdc46cba159d15 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 28 Jun 2022 19:39:00 +0100 Subject: [PATCH 38/89] ios: remove interval notifications method (#759) --- apps/ios/Shared/AppDelegate.swift | 1 - apps/ios/Shared/Model/SimpleXAPI.swift | 4 ---- apps/ios/SimpleXChat/APITypes.swift | 3 --- 3 files changed, 8 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index c58912dd71..66184591c8 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -56,7 +56,6 @@ class AppDelegate: NSObject, UIApplicationDelegate { if case .active = m.tokenStatus {} else { m.tokenStatus = .confirmed } try await apiVerifyToken(token: token, nonce: nonce, code: verification) m.tokenStatus = .active - try await apiIntervalNofication(token: token, interval: 20) } catch { if let cr = error as? ChatResponse, case .chatCmdError(.errorAgent(.NTF(.AUTH))) = cr { m.tokenStatus = .expired diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a40e5c0e48..a9795453eb 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -236,10 +236,6 @@ func apiVerifyToken(token: DeviceToken, nonce: String, code: String) async throw try await sendCommandOkResp(.apiVerifyToken(token: token, nonce: nonce, code: code)) } -func apiIntervalNofication(token: DeviceToken, interval: Int) async throws { - try await sendCommandOkResp(.apiIntervalNofication(token: token, interval: interval)) -} - func apiDeleteToken(token: DeviceToken) async throws { try await sendCommandOkResp(.apiDeleteToken(token: token)) } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 7b07940416..8d9b06447a 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -30,7 +30,6 @@ public enum ChatCommand { case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) case apiRegisterToken(token: DeviceToken, notificationMode: NotificationMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) - case apiIntervalNofication(token: DeviceToken, interval: Int) case apiDeleteToken(token: DeviceToken) case apiGetNtfMessage(nonce: String, encNtfInfo: String) case getUserSMPServers @@ -79,7 +78,6 @@ public enum ChatCommand { case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" - case let .apiIntervalNofication(token, interval): return "/_ntf interval \(token.cmdString) \(interval)" case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" case .getUserSMPServers: return "/smp_servers" @@ -128,7 +126,6 @@ public enum ChatCommand { case .apiDeleteChatItem: return "apiDeleteChatItem" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" - case .apiIntervalNofication: return "apiIntervalNofication" case .apiDeleteToken: return "apiDeleteToken" case .apiGetNtfMessage: return "apiGetNtfMessage" case .getUserSMPServers: return "getUserSMPServers" From 043005d186c215b0233d5eacce1fa3c270134cdc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:14:39 +0100 Subject: [PATCH 39/89] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 7af4430249..ca2964c973 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: ba40d75886c14d70fb6af61c22f917069d7b478f + tag: 09b51dc44ffa2a55e441d5664fab0777f8dbbfb9 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index bec8b11529..4581f966d0 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ba40d75886c14d70fb6af61c22f917069d7b478f" = "1cashma9g0lw1bvaw4fcvfzzjqbpwjxhz42fvl83pi43j8i2plpn"; + "https://github.com/simplex-chat/simplexmq.git"."09b51dc44ffa2a55e441d5664fab0777f8dbbfb9" = "19y3d5iwc6hwphiiyxdlxkvi651cmfnabfss694samjgr55pfxq3"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 89564af995..69276e6729 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: ba40d75886c14d70fb6af61c22f917069d7b478f + commit: 09b51dc44ffa2a55e441d5664fab0777f8dbbfb9 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 13603f009b05d684620451f7af28866647ebf1fb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 29 Jun 2022 09:04:53 +0100 Subject: [PATCH 40/89] update simplexmq (fixes v2 connecting to v1 contact link) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- tests/ChatClient.hs | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cabal.project b/cabal.project index ca2964c973..b7e99b9f03 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 09b51dc44ffa2a55e441d5664fab0777f8dbbfb9 + tag: c9501a23a0626bf441756c4342acf08a5900aad8 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 4581f966d0..a7b36b931d 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."09b51dc44ffa2a55e441d5664fab0777f8dbbfb9" = "19y3d5iwc6hwphiiyxdlxkvi651cmfnabfss694samjgr55pfxq3"; + "https://github.com/simplex-chat/simplexmq.git"."c9501a23a0626bf441756c4342acf08a5900aad8" = "0rra32hd86yawr6k8vqv86jq97lffyyaqxr2n8gq34vz7c8kmg4d"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 69276e6729..1f3a2217d7 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 09b51dc44ffa2a55e441d5664fab0777f8dbbfb9 + commit: c9501a23a0626bf441756c4342acf08a5900aad8 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index ad284c4b95..202191d8a7 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -27,6 +27,7 @@ import Simplex.Chat.Terminal.Output (newChatTerminal) import Simplex.Chat.Types (Profile, User (..)) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval +import Simplex.Messaging.Client (ProtocolClientConfig (..)) import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM import Simplex.Messaging.Transport @@ -90,7 +91,8 @@ testAgentCfgV1 :: AgentConfig testAgentCfgV1 = testAgentCfg { smpAgentVersion = 1, - smpAgentVRange = mkVersionRange 1 1 + smpAgentVRange = mkVersionRange 1 1, + smpCfg = (smpCfg testAgentCfg) {smpServerVRange = mkVersionRange 1 1} } testCfgV1 :: ChatConfig From c8a85f651dfd1f3048d5b3db074d52ee37e1b1e5 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 29 Jun 2022 17:23:42 +0400 Subject: [PATCH 41/89] update simplexmq (v3.0.0-beta.0) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index b7e99b9f03..acf6a5435c 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: c9501a23a0626bf441756c4342acf08a5900aad8 + tag: 0b25b57e3c561f12914ccff13169388397b815c3 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index a7b36b931d..90a5a0fc4b 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."c9501a23a0626bf441756c4342acf08a5900aad8" = "0rra32hd86yawr6k8vqv86jq97lffyyaqxr2n8gq34vz7c8kmg4d"; + "https://github.com/simplex-chat/simplexmq.git"."0b25b57e3c561f12914ccff13169388397b815c3" = "1d65dwrfha1lbnkyvjkn3s9f6bhbmz3cdzxkvwq830mcqnr8jb1b"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 1f3a2217d7..6223dcdc32 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: c9501a23a0626bf441756c4342acf08a5900aad8 + commit: 0b25b57e3c561f12914ccff13169388397b815c3 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 904945a67df4b25391c93227e7170e7a6f3bad85 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 29 Jun 2022 17:35:59 +0400 Subject: [PATCH 42/89] fix simplexmq range --- package.yaml | 2 +- simplex-chat.cabal | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.yaml b/package.yaml index b4a4d3bb83..afac726977 100644 --- a/package.yaml +++ b/package.yaml @@ -31,7 +31,7 @@ dependencies: - optparse-applicative >= 0.15 && < 0.17 - process == 1.6.* - simple-logger == 0.1.* - - simplexmq >= 1.1 && < 3.0 + - simplexmq >= 3.0 - sqlite-simple == 0.4.* - stm == 2.5.* - terminal == 0.2.* diff --git a/simplex-chat.cabal b/simplex-chat.cabal index e2eedcecf9..0b3fe1c97e 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -77,7 +77,7 @@ library , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , simple-logger ==0.1.* - , simplexmq >=1.1 && <3.0 + , simplexmq >=3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* @@ -116,7 +116,7 @@ executable simplex-bot , process ==1.6.* , simple-logger ==0.1.* , simplex-chat - , simplexmq >=1.1 && <3.0 + , simplexmq >=3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* @@ -155,7 +155,7 @@ executable simplex-bot-advanced , process ==1.6.* , simple-logger ==0.1.* , simplex-chat - , simplexmq >=1.1 && <3.0 + , simplexmq >=3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* @@ -196,7 +196,7 @@ executable simplex-chat , process ==1.6.* , simple-logger ==0.1.* , simplex-chat - , simplexmq >=1.1 && <3.0 + , simplexmq >=3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* @@ -245,7 +245,7 @@ test-suite simplex-chat-test , process ==1.6.* , simple-logger ==0.1.* , simplex-chat - , simplexmq >=1.1 && <3.0 + , simplexmq >=3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* From ca366d0b47a22e49c0fadc98083b9923102c8054 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 30 Jun 2022 08:16:22 +0100 Subject: [PATCH 43/89] core: fix APINtfGetToken parsing (#760) --- src/Simplex/Chat.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 27eb69dff6..0bf1bdb750 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2285,7 +2285,7 @@ chatCommandP = <|> "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP) <|> "/_profile " *> (APIUpdateProfile <$> jsonP) <|> "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString) - <|> "/_ntf get " $> APIGetNtfToken + <|> "/_ntf get" $> APIGetNtfToken <|> "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP) <|> "/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP) <|> "/_ntf delete " *> (APIDeleteToken <$> strP) From b2c455c3016206be9fa04709c1b3974d981ad564 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 30 Jun 2022 20:35:28 +0400 Subject: [PATCH 44/89] update simplexmq (recover) (#762) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 2 +- stack.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cabal.project b/cabal.project index acf6a5435c..0989d993a1 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 0b25b57e3c561f12914ccff13169388397b815c3 + tag: c3012173e346b06b5625de5268dc157e74d29fca source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 90a5a0fc4b..dca670b18d 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."0b25b57e3c561f12914ccff13169388397b815c3" = "1d65dwrfha1lbnkyvjkn3s9f6bhbmz3cdzxkvwq830mcqnr8jb1b"; + "https://github.com/simplex-chat/simplexmq.git"."c3012173e346b06b5625de5268dc157e74d29fca" = "1cz9mqiws3gyr4mgvp9nrgkxf22p7dsziw51ldl4fcy1a5jkyf8d"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 0bf1bdb750..f031b01d8e 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -59,7 +59,7 @@ import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Client (NtfServer) +import Simplex.Messaging.Notifications.Types (NtfServer) import Simplex.Messaging.Parsers (base64P, parseAll) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..)) import qualified Simplex.Messaging.Protocol as SMP diff --git a/stack.yaml b/stack.yaml index 6223dcdc32..b26a4b94f6 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 0b25b57e3c561f12914ccff13169388397b815c3 + commit: c3012173e346b06b5625de5268dc157e74d29fca # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 815981487bfdd0e3f95e7397da7f3fe26316c230 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 1 Jul 2022 09:49:30 +0100 Subject: [PATCH 45/89] ios: register notification token/mode on app start (#761) * ios: register notification token/mode on app start * refactor * register token on start * update model on main thread --- apps/ios/Shared/AppDelegate.swift | 17 ++---- apps/ios/Shared/Model/ChatModel.swift | 5 +- apps/ios/Shared/Model/SimpleXAPI.swift | 31 +++++++++- apps/ios/Shared/SimpleXApp.swift | 4 +- .../UserSettings/NotificationsView.swift | 19 +++--- .../Views/UserSettings/SettingsView.swift | 5 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 60 +++++++------------ apps/ios/SimpleXChat/APITypes.swift | 35 +++++++---- 8 files changed, 93 insertions(+), 83 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 66184591c8..bbec769a79 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -21,17 +21,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { let token = deviceToken.map { String(format: "%02hhx", $0) }.joined() logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)") let m = ChatModel.shared - let deviceToken = DeviceToken(env: pushEnvironment, token: token) + let deviceToken = DeviceToken(pushProvider: PushProvider(env: pushEnvironment), token: token) m.deviceToken = deviceToken - let useNotifications = UserDefaults.standard.bool(forKey: "useNotifications") - if useNotifications { - Task { - do { - m.tokenStatus = try await apiRegisterToken(token: deviceToken, notificationMode: .instant) - } catch { - logger.error("apiRegisterToken error: \(responseError(error))") - } - } + if m.savedToken != nil { + registerToken(token: deviceToken) } } @@ -44,14 +37,14 @@ class AppDelegate: NSObject, UIApplicationDelegate { fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { logger.debug("AppDelegate: didReceiveRemoteNotification") print("*** userInfo", userInfo) + let m = ChatModel.shared if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], - UserDefaults.standard.bool(forKey: "useNotifications") { + m.notificationMode != .off { if let verification = ntfData["verification"] as? String, let nonce = ntfData["nonce"] as? String { if let token = ChatModel.shared.deviceToken { logger.debug("AppDelegate: didReceiveRemoteNotification: verification, confirming \(verification)") Task { - let m = ChatModel.shared do { if case .active = m.tokenStatus {} else { m.tokenStatus = .confirmed } try await apiVerifyToken(token: token, nonce: nonce, code: verification) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index be3e1fe446..41ba73c613 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -29,8 +29,9 @@ final class ChatModel: ObservableObject { @Published var userSMPServers: [String]? @Published var appOpenUrl: URL? @Published var deviceToken: DeviceToken? - @Published var tokenStatus = NtfTknStatus.new - @Published var notificationMode = NotificationMode.off + @Published var savedToken: DeviceToken? + @Published var tokenStatus: NtfTknStatus? + @Published var notificationMode = NotificationsMode.off @Published var notificationPreview: NotificationPreviewMode? = .message // current WebRTC call @Published var callInvitations: Dictionary = [:] diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a9795453eb..5d2b9ade8a 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -226,12 +226,37 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM throw r } -func apiRegisterToken(token: DeviceToken, notificationMode: NotificationMode) async throws -> NtfTknStatus { +func apiGetNtfToken() throws -> (DeviceToken?, NtfTknStatus?, NotificationsMode) { + let r = chatSendCmdSync(.apiGetNtfToken) + switch r { + case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode) + case .chatCmdError(.errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off) + default: throw r + } +} + +func apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) async throws -> NtfTknStatus { let r = await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode)) if case let .ntfTokenStatus(status) = r { return status } throw r } +func registerToken(token: DeviceToken) { + let m = ChatModel.shared + let mode = m.notificationMode + if mode != .off { + logger.debug("registerToken \(mode.rawValue)") + Task { + do { + let status = try await apiRegisterToken(token: token, notificationMode: mode) + await MainActor.run { m.tokenStatus = status } + } catch let error { + logger.error("registerToken apiRegisterToken error: \(responseError(error))") + } + } + } +} + func apiVerifyToken(token: DeviceToken, nonce: String, code: String) async throws { try await sendCommandOkResp(.apiVerifyToken(token: token, nonce: nonce, code: code)) } @@ -501,6 +526,10 @@ func startChat() throws { m.userAddress = try apiGetUserAddress() m.userSMPServers = try getUserSMPServers() m.chats = try apiGetChats() + (m.savedToken, m.tokenStatus, m.notificationMode) = try apiGetNtfToken() + if let token = m.deviceToken { + registerToken(token: token) + } withAnimation { m.onboardingStage = m.chats.isEmpty ? .step3_MakeConnection diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 5d983832a7..feb6721476 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -73,8 +73,8 @@ struct SimpleXApp: App { private func setDbContainer() { // Uncomment and run once to open DB in app documents folder: - // dbContainerGroupDefault.set(.documents) - // v3DBMigrationDefault.set(.offer) +// dbContainerGroupDefault.set(.documents) +// v3DBMigrationDefault.set(.offer) // to create database in app documents folder also uncomment: // let legacyDatabase = true let legacyDatabase = hasLegacyDatabase() diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index 3cf7d39198..46c1da8a55 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -11,9 +11,8 @@ import SimpleXChat struct NotificationsView: View { @EnvironmentObject var m: ChatModel - @State private var notificationMode: NotificationMode? + @State private var notificationMode: NotificationsMode? @State private var showAlert: NotificationAlert? - @AppStorage(DEFAULT_USE_NOTIFICATIONS) private var useNotifications = false var body: some View { List { @@ -21,7 +20,7 @@ struct NotificationsView: View { NavigationLink { List { Section { - SelectionListView(list: NotificationMode.values, selection: $notificationMode) { mode in + SelectionListView(list: NotificationsMode.values, selection: $notificationMode) { mode in showAlert = .setMode(mode: mode) } } footer: { @@ -93,7 +92,7 @@ struct NotificationsView: View { } } - private func ntfModeAlertTitle(_ mode: NotificationMode) -> LocalizedStringKey { + private func ntfModeAlertTitle(_ mode: NotificationsMode) -> LocalizedStringKey { switch mode { case .off: return "Turn off notifications?" case .periodic: return "Enable periodic notifications?" @@ -101,18 +100,16 @@ struct NotificationsView: View { } } - private func setNotificationsMode(_ mode: NotificationMode, _ token: DeviceToken) { + private func setNotificationsMode(_ mode: NotificationsMode, _ token: DeviceToken) { Task { switch mode { case .off: do { try await apiDeleteToken(token: token) - useNotifications = false m.tokenStatus = .new notificationMode = .off m.notificationMode = .off - } - catch let error { + } catch let error { DispatchQueue.main.async { let err = responseError(error) logger.error("apiDeleteToken error: \(err)") @@ -123,12 +120,10 @@ struct NotificationsView: View { do { do { m.tokenStatus = try await apiRegisterToken(token: token, notificationMode: mode) - useNotifications = true notificationMode = mode m.notificationMode = mode } catch let error { DispatchQueue.main.async { - useNotifications = notificationMode != .off let err = responseError(error) logger.error("apiRegisterToken error: \(err)") showAlert = .error(title: "Error enabling notifications", error: err) @@ -140,7 +135,7 @@ struct NotificationsView: View { } } -func ntfModeDescription(_ mode: NotificationMode) -> LocalizedStringKey { +func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { switch mode { case .off: return "**Maximum privacy**: push notifications are off.\nNo meta-data is shared with SimpleX Chat notification server." case .periodic: return "**High privacy**: new messages are checked every 20 minutes.\nYour device token is shared with SimpleX Chat notification server, but it cannot see how many connections you have or how many messages you receive." @@ -187,7 +182,7 @@ struct SelectionListView: View { } enum NotificationAlert: Identifiable { - case setMode(mode: NotificationMode) + case setMode(mode: NotificationsMode) case error(title: LocalizedStringKey, error: String) var id: String { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 97e0ba5c0f..d99f31b350 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -18,7 +18,6 @@ let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? let DEFAULT_SHOW_LA_NOTICE = "showLocalAuthenticationNotice" let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown" let DEFAULT_PERFORM_LA = "performLocalAuthentication" -let DEFAULT_USE_NOTIFICATIONS = "useNotifications" let DEFAULT_PENDING_CONNECTIONS = "pendingConnections" let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay" let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" @@ -32,7 +31,6 @@ let appDefaults: [String: Any] = [ DEFAULT_SHOW_LA_NOTICE: false, DEFAULT_LA_NOTICE_SHOWN: false, DEFAULT_PERFORM_LA: false, - DEFAULT_USE_NOTIFICATIONS: false, DEFAULT_PENDING_CONNECTIONS: true, DEFAULT_WEBRTC_POLICY_RELAY: true, DEFAULT_PRIVACY_ACCEPT_IMAGES: true, @@ -218,6 +216,9 @@ struct SettingsView: View { case .expired: icon = "bolt.slash.fill" color = .primary + case .none: + icon = "bolt" + color = .primary } return Image(systemName: icon) .padding(.trailing, 9) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 4edc81a91b..7d8778b3db 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -18,6 +18,11 @@ 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; }; + 5C14247E286DAC1D0004E3EE /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C142479286DAC1D0004E3EE /* libffi.a */; }; + 5C14247F286DAC1D0004E3EE /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14247A286DAC1D0004E3EE /* libgmpxx.a */; }; + 5C142480286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14247B286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a */; }; + 5C142481286DAC1D0004E3EE /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14247C286DAC1D0004E3EE /* libgmp.a */; }; + 5C142482286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14247D286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a */; }; 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; }; 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; }; @@ -102,11 +107,6 @@ 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; - 5CFA59CA2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */; }; - 5CFA59CB2864464A00863A68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C62864464A00863A68 /* libffi.a */; }; - 5CFA59CC2864464A00863A68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C72864464A00863A68 /* libgmpxx.a */; }; - 5CFA59CD2864464A00863A68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C82864464A00863A68 /* libgmp.a */; }; - 5CFA59CE2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */; }; 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; }; 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; @@ -190,6 +190,11 @@ 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; 5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = ""; }; 5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 5C142479286DAC1D0004E3EE /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C14247A286DAC1D0004E3EE /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C14247B286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a"; sourceTree = ""; }; + 5C14247C286DAC1D0004E3EE /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C14247D286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a"; sourceTree = ""; }; 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = ""; }; 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = ""; }; 5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = ""; }; @@ -275,11 +280,6 @@ 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; - 5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a"; sourceTree = ""; }; - 5CFA59C62864464A00863A68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CFA59C72864464A00863A68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CFA59C82864464A00863A68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a"; sourceTree = ""; }; 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = ""; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = ""; }; @@ -325,13 +325,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CFA59CE2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a in Frameworks */, - 5CFA59CA2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a in Frameworks */, - 5CFA59CC2864464A00863A68 /* libgmpxx.a in Frameworks */, + 5C14247F286DAC1D0004E3EE /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, + 5C142480286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a in Frameworks */, + 5C14247E286DAC1D0004E3EE /* libffi.a in Frameworks */, + 5C142482286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a in Frameworks */, + 5C142481286DAC1D0004E3EE /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CFA59CD2864464A00863A68 /* libgmp.a in Frameworks */, - 5CFA59CB2864464A00863A68 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -385,11 +385,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CFA59C62864464A00863A68 /* libffi.a */, - 5CFA59C82864464A00863A68 /* libgmp.a */, - 5CFA59C72864464A00863A68 /* libgmpxx.a */, - 5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */, - 5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */, + 5C142479286DAC1D0004E3EE /* libffi.a */, + 5C14247C286DAC1D0004E3EE /* libgmp.a */, + 5C14247A286DAC1D0004E3EE /* libgmpxx.a */, + 5C14247B286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj-ghc8.10.7.a */, + 5C14247D286DAC1D0004E3EE /* libHSsimplex-chat-2.2.0-B9XlcSXdcwBJ5TIVTDfRQj.a */, ); path = Libraries; sourceTree = ""; @@ -1122,8 +1122,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios"; - "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim"; MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -1164,8 +1162,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios"; - "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim"; MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -1237,14 +1233,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( - "$(inherited)", - "$(PROJECT_DIR)/Libraries/ios", - ); - "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = ( - "$(inherited)", - "$(PROJECT_DIR)/Libraries/sim", - ); MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1275,14 +1263,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( - "$(inherited)", - "$(PROJECT_DIR)/Libraries/ios", - ); - "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = ( - "$(inherited)", - "$(PROJECT_DIR)/Libraries/sim", - ); MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 8d9b06447a..5b92fd3e87 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -28,7 +28,8 @@ public enum ChatCommand { case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) - case apiRegisterToken(token: DeviceToken, notificationMode: NotificationMode) + case apiGetNtfToken + case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) case apiDeleteToken(token: DeviceToken) case apiGetNtfMessage(nonce: String, encNtfInfo: String) @@ -76,6 +77,7 @@ public enum ChatCommand { return "/_send \(ref(type, id)) json \(msg)" case let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)" case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" + case .apiGetNtfToken: return "/_ntf get " case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" @@ -124,6 +126,7 @@ public enum ChatCommand { case .apiSendMessage: return "apiSendMessage" case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem" + case .apiGetNtfToken: return "apiGetNtfToken" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" case .apiDeleteToken: return "apiDeleteToken" @@ -222,6 +225,7 @@ public enum ChatResponse: Decodable, Error { case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo) case callEnded(contact: Contact) case ntfTokenStatus(status: NtfTknStatus) + case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode) case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) case newContactConnection(connection: PendingContactConnection) case contactConnectionDeleted(connection: PendingContactConnection) @@ -284,6 +288,7 @@ public enum ChatResponse: Decodable, Error { case .callExtraInfo: return "callExtraInfo" case .callEnded: return "callEnded" case .ntfTokenStatus: return "ntfTokenStatus" + case .ntfToken: return "ntfToken" case .ntfMessages: return "ntfMessages" case .newContactConnection: return "newContactConnection" case .contactConnectionDeleted: return "contactConnectionDeleted" @@ -349,6 +354,7 @@ public enum ChatResponse: Decodable, Error { case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))" case let .callEnded(contact): return "contact: \(contact.id)" case let .ntfTokenStatus(status): return String(describing: status) + case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)" case let .ntfMessages(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))" case let .newContactConnection(connection): return String(describing: connection) case let .contactConnectionDeleted(connection): return String(describing: connection) @@ -383,33 +389,38 @@ public protocol SelectableItem: Hashable, Identifiable { static var values: [Self] { get } } -public struct DeviceToken { - var env: PushEnvironment +public struct DeviceToken: Decodable { + var pushProvider: PushProvider var token: String - public init(env: PushEnvironment, token: String) { - self.env = env + public init(pushProvider: PushProvider, token: String) { + self.pushProvider = pushProvider self.token = token } public var cmdString: String { - "\(env.pushProvider) \(token)" + "\(pushProvider) \(token)" } } public enum PushEnvironment: String { case development case production +} - public var pushProvider: String { - switch self { - case .development: return "apns_dev" - case .production: return "apns_prod" +public enum PushProvider: String, Decodable { + case apns_dev + case apns_prod + + public init(env: PushEnvironment) { + switch env { + case .development: self = .apns_dev + case .production: self = .apns_prod } } } -public enum NotificationMode: String, SelectableItem { +public enum NotificationsMode: String, Decodable, SelectableItem { case off = "OFF" case periodic = "PERIODIC" case instant = "INSTANT" @@ -424,7 +435,7 @@ public enum NotificationMode: String, SelectableItem { public var id: String { self.rawValue } - public static var values: [NotificationMode] = [.instant, .periodic, .off] + public static var values: [NotificationsMode] = [.instant, .periodic, .off] } public enum NotificationPreviewMode: String, SelectableItem { From 96866c7a5d8e99add25c2e52b800a6bfad219ea0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 1 Jul 2022 10:37:26 +0100 Subject: [PATCH 46/89] core: handle all DB errors (#763) --- src/Simplex/Chat.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index f031b01d8e..09f804f4f4 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2247,7 +2247,7 @@ withStore action = do liftEitherError ChatErrorStore $ withTransaction st (runExceptT . action) `E.catch` handleInternal where - handleInternal :: DB.SQLError -> IO (Either StoreError a) + handleInternal :: E.SomeException -> IO (Either StoreError a) handleInternal = pure . Left . SEInternalError . show chatCommandP :: Parser ChatCommand From 1e63eb3752552f7217ea33e4766d8470339d7e0b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 1 Jul 2022 10:38:12 +0100 Subject: [PATCH 47/89] ios: update the list of chats when exiting background (#764) --- apps/ios/Shared/SimpleXApp.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index feb6721476..343691adef 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -62,6 +62,11 @@ struct SimpleXApp: App { activateChat() if chatModel.chatRunning == true { ChatReceiver.shared.start() + do { + chatModel.chats = try apiGetChats() + } catch let error { + logger.error("apiGetChats: cannot update chats \(responseError(error))") + } } doAuthenticate = authenticationExpired() default: From e68cc23828997640bb27655d9f1ab20bf2990731 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Fri, 1 Jul 2022 16:12:29 +0400 Subject: [PATCH 48/89] update simplexmq (v3.0.0-beta.2) (#765) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 0989d993a1..a061ad52df 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: c3012173e346b06b5625de5268dc157e74d29fca + tag: e622e12a7fa8b9e3b5b5110a6c234cadab1188d1 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index dca670b18d..b12e017130 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."c3012173e346b06b5625de5268dc157e74d29fca" = "1cz9mqiws3gyr4mgvp9nrgkxf22p7dsziw51ldl4fcy1a5jkyf8d"; + "https://github.com/simplex-chat/simplexmq.git"."e622e12a7fa8b9e3b5b5110a6c234cadab1188d1" = "0skkxiarnbjmfkd6nq8h3cyawfwmmcr3w3g7p1v5bgf0lgq2zsx6"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index b26a4b94f6..d97956f744 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: c3012173e346b06b5625de5268dc157e74d29fca + commit: e622e12a7fa8b9e3b5b5110a6c234cadab1188d1 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From a8a0f2db03d153778c319c24958af12250db6257 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 1 Jul 2022 20:33:20 +0100 Subject: [PATCH 49/89] ios: disable notifications if not migrated (#766) * ios: disable notifications if not migrated * refactor, update text --- apps/ios/Shared/SimpleXApp.swift | 2 +- .../Shared/Views/UserSettings/NotificationsView.swift | 10 +++++++++- apps/ios/SimpleX NSE/NotificationService.swift | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 343691adef..3ef7993e84 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -81,7 +81,7 @@ struct SimpleXApp: App { // dbContainerGroupDefault.set(.documents) // v3DBMigrationDefault.set(.offer) // to create database in app documents folder also uncomment: - // let legacyDatabase = true +// let legacyDatabase = true let legacyDatabase = hasLegacyDatabase() if legacyDatabase, case .documents = dbContainerGroupDefault.get() { dbContainerGroupDefault.set(.documents) diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index 46c1da8a55..b3752befad 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -13,6 +13,7 @@ struct NotificationsView: View { @EnvironmentObject var m: ChatModel @State private var notificationMode: NotificationsMode? @State private var showAlert: NotificationAlert? + @State private var legacyDatabase = dbContainerGroupDefault.get() == .documents var body: some View { List { @@ -69,8 +70,15 @@ struct NotificationsView: View { } } } header: { - Text("Message notifications") + Text("Push notifications") + } footer: { + if legacyDatabase { + Text("Please restart the app and migrate the database to enable push notifications.") + .font(.callout) + .padding(.top, 1) + } } + .disabled(legacyDatabase) } } diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 56419b281a..2fa61c57df 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -35,6 +35,10 @@ class NotificationService: UNNotificationServiceExtension { } func receiveNtfMessages(_ request: UNNotificationRequest, _ contentHandler: @escaping (UNNotificationContent) -> Void) { + if case .documents = dbContainerGroupDefault.get() { + contentHandler(request.content) + return + } let userInfo = request.content.userInfo if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], let nonce = ntfData["nonce"] as? String, From f7f3f82090947654a92850cf651aae1e90e08952 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 1 Jul 2022 22:45:58 +0100 Subject: [PATCH 50/89] ios: fix migration, refreshing chat list; disable periodic notifications (#767) * ios: fix migration, disable refreshing chat list and periodic notifications * fix refreshing chats when exiting background * remove unused model property --- apps/ios/Shared/AppDelegate.swift | 6 ++++-- apps/ios/Shared/ContentView.swift | 19 ++++++++----------- apps/ios/Shared/Model/ChatModel.swift | 12 ++++++++++++ apps/ios/Shared/Model/SimpleXAPI.swift | 1 - apps/ios/Shared/SimpleXApp.swift | 11 ++++++++--- .../Shared/Views/ChatList/ChatListView.swift | 2 +- .../Database/MigrateToAppGroupView.swift | 9 ++++----- apps/ios/SimpleXChat/AppGroup.swift | 8 ++++++++ 8 files changed, 45 insertions(+), 23 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index bbec769a79..64f963e973 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -65,11 +65,13 @@ class AppDelegate: NSObject, UIApplicationDelegate { logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages") // TODO remove // NtfManager.shared.notifyCheckingMessages() - receiveMessages(completionHandler) + completionHandler(.noData) +// receiveMessages(completionHandler) } else if let smpQueue = ntfData["checkMessage"] as? String { // TODO check if app in background logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessage \(smpQueue)") - receiveMessages(completionHandler) + completionHandler(.noData) +// receiveMessages(completionHandler) } else { completionHandler(.noData) } diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 356292334b..914071e417 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -14,7 +14,6 @@ struct ContentView: View { @Binding var doAuthenticate: Bool @Binding var userAuthorized: Bool? @State private var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication - @State private var v3DBMigration = v3DBMigrationDefault.get() @AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @@ -23,16 +22,14 @@ struct ContentView: View { ZStack { if prefPerformLA && userAuthorized != true { Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") } - } else { - if let step = chatModel.onboardingStage { - if case .onboardingComplete = step, - chatModel.currentUser != nil { - mainView() - } else { - OnboardingView(onboarding: step) - } - } else if !v3DBMigrationDefault.get().startChat { - MigrateToAppGroupView() + } else if !chatModel.v3DBMigration.startChat { + MigrateToAppGroupView() + } else if let step = chatModel.onboardingStage { + if case .onboardingComplete = step, + chatModel.currentUser != nil { + mainView() + } else { + OnboardingView(onboarding: step) } } } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 41ba73c613..2292a60c94 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -14,6 +14,7 @@ import SimpleXChat final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? + @Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get() @Published var currentUser: User? @Published var chatRunning: Bool? @Published var chatDbChanged = false @@ -92,13 +93,21 @@ final class ChatModel: ObservableObject { func replaceChat(_ id: String, _ chat: Chat) { if let i = getChatIndex(id) { + let serverInfo = chats[i].serverInfo chats[i] = chat + chats[i].serverInfo = serverInfo } else { // invalid state, correcting chats.insert(chat, at: 0) } } + func replaceChats(with newChats: [Chat]) { + for chat in newChats { + replaceChat(chat.id, chat) + } + } + func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { // update previews if let i = getChatIndex(cInfo.id) { @@ -248,6 +257,7 @@ final class Chat: ObservableObject, Identifiable { @Published var chatItems: [ChatItem] @Published var chatStats: ChatStats @Published var serverInfo = ServerInfo(networkStatus: .unknown) + var created = Date.now struct ServerInfo: Decodable { var networkStatus: NetworkStatus @@ -305,4 +315,6 @@ final class Chat: ObservableObject, Identifiable { } var id: ChatId { get { chatInfo.id } } + + var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5d2b9ade8a..fc2a782290 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -520,7 +520,6 @@ func initializeChat(start: Bool) throws { func startChat() throws { logger.debug("startChat") let m = ChatModel.shared - // TODO set file folder once, before chat is started let justStarted = try apiStartChat() if justStarted { m.userAddress = try apiGetUserAddress() diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 3ef7993e84..a79f7bab83 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -40,7 +40,8 @@ struct SimpleXApp: App { } .onAppear() { do { - try initializeChat(start: v3DBMigrationDefault.get().startChat) + chatModel.v3DBMigration = v3DBMigrationDefault.get() + try initializeChat(start: chatModel.v3DBMigration.startChat) } catch let error { fatalError("Failed to start or load chats: \(responseError(error))") } @@ -59,11 +60,15 @@ struct SimpleXApp: App { } doAuthenticate = false case .active: - activateChat() if chatModel.chatRunning == true { ChatReceiver.shared.start() + } + let appState = appStateGroupDefault.get() + activateChat() + if appState.inactive && chatModel.chatRunning == true { do { - chatModel.chats = try apiGetChats() + let chats = try apiGetChats() + chatModel.replaceChats(with: chats) } catch let error { logger.error("apiGetChats: cannot update chats \(responseError(error))") } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index e4b3d87b18..e3087ed30a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -20,7 +20,7 @@ struct ChatListView: View { var body: some View { let v = NavigationView { List { - ForEach(filteredChats()) { chat in + ForEach(filteredChats(), id: \.viewId) { chat in ChatListNavLink(chat: chat, showChatInfo: $showChatInfo) .padding(.trailing, -16) .disabled(chatModel.chatRunning != true) diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index 3749047b3f..a53c6a2b40 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -37,7 +37,6 @@ let v3DBMigrationDefault = EnumDefault( struct MigrateToAppGroupView: View { @EnvironmentObject var chatModel: ChatModel - @State private var v3DBMigration = v3DBMigrationDefault.get() @State private var migrationError = "" @AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String? @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 @@ -46,7 +45,7 @@ struct MigrateToAppGroupView: View { ZStack(alignment: .topLeading) { Text("Database migration").font(.largeTitle) - switch v3DBMigration { + switch chatModel.v3DBMigration { case .offer: VStack(alignment: .leading, spacing: 16) { Text("To support instant push notifications the chat database has to be migrated.") @@ -128,7 +127,7 @@ struct MigrateToAppGroupView: View { default: Spacer() Text("Unexpected migration state") - Text("\(v3DBMigration.rawValue)") + Text("\(chatModel.v3DBMigration.rawValue)") Spacer() skipMigration() } @@ -174,7 +173,7 @@ struct MigrateToAppGroupView: View { } private func setV3DBMigration(_ value: V3DBMigrationState) { - v3DBMigration = value + chatModel.v3DBMigration = value v3DBMigrationDefault.set(value) } @@ -201,7 +200,7 @@ struct MigrateToAppGroupView: View { await MainActor.run { setV3DBMigration(.migrating) } dbContainerGroupDefault.set(.group) resetChatCtrl() - try initializeChat(start: false) + try await MainActor.run { try initializeChat(start: false) } try await apiImportArchive(config: config) await MainActor.run { setV3DBMigration(.migrated) } } catch let error { diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index b55c559bcd..b71d192969 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -22,6 +22,14 @@ public enum AppState: String { case bgRefresh case suspending case suspended + + public var inactive: Bool { + switch self { + case .suspending: return true + case .suspended: return true + default: return false + } + } } public enum DBContainer: String { From a8ba4ede82fc62d6788dd2032c4876b38094dc0d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 1 Jul 2022 23:12:45 +0100 Subject: [PATCH 51/89] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index a061ad52df..34e7468733 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: e622e12a7fa8b9e3b5b5110a6c234cadab1188d1 + tag: f9cd7e5416013803c899dda59110a362e853407d source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b12e017130..4fbb663a45 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."e622e12a7fa8b9e3b5b5110a6c234cadab1188d1" = "0skkxiarnbjmfkd6nq8h3cyawfwmmcr3w3g7p1v5bgf0lgq2zsx6"; + "https://github.com/simplex-chat/simplexmq.git"."f9cd7e5416013803c899dda59110a362e853407d" = "07plsai0pyagax03yph5bihsx1xh6bz700c24xswf8qyd2y510cd"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index d97956f744..9e95e644c1 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: e622e12a7fa8b9e3b5b5110a6c234cadab1188d1 + commit: f9cd7e5416013803c899dda59110a362e853407d # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 29c6d51e6a0365422ec8f123ba434549b565f07a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 2 Jul 2022 08:50:25 +0100 Subject: [PATCH 52/89] ios: fix background refresh (#769) * ios: fix background refresh * change app inactive check --- apps/ios/Shared/AppDelegate.swift | 15 +++++---------- apps/ios/Shared/Model/BGManager.swift | 11 ++++++++--- apps/ios/Shared/Model/SimpleXAPI.swift | 3 +-- apps/ios/Shared/Model/SuspendChat.swift | 9 +++++++++ apps/ios/Shared/SimpleXApp.swift | 3 --- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 64f963e973..0add3347ba 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -61,17 +61,12 @@ class AppDelegate: NSObject, UIApplicationDelegate { completionHandler(.noData) } } else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages { - // TODO check if app in background logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages") - // TODO remove - // NtfManager.shared.notifyCheckingMessages() - completionHandler(.noData) -// receiveMessages(completionHandler) - } else if let smpQueue = ntfData["checkMessage"] as? String { - // TODO check if app in background - logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessage \(smpQueue)") - completionHandler(.noData) -// receiveMessages(completionHandler) + if isAppInactive() { + receiveMessages(completionHandler) + } else { + completionHandler(.noData) + } } else { completionHandler(.noData) } diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index 6f35732d15..6468d3b08a 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -47,11 +47,16 @@ class BGManager { private func handleRefresh(_ task: BGAppRefreshTask) { logger.debug("BGManager.handleRefresh") schedule() - let completeRefresh = completionHandler { + if isAppInactive() { + let completeRefresh = completionHandler { + task.setTaskCompleted(success: true) + } + task.expirationHandler = { completeRefresh("expirationHandler") } + receiveMessages(completeRefresh) + } else { + logger.debug("BGManager.completionHandler: already active, not started") task.setTaskCompleted(success: true) } - task.expirationHandler = { completeRefresh("expirationHandler") } - receiveMessages(completeRefresh) } func completionHandler(_ complete: @escaping () -> Void) -> ((String) -> Void) { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index fc2a782290..e2f9ecd016 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -564,8 +564,7 @@ class ChatReceiver { await processReceivedMsg(msg) } if self.receiveMessages { - do { try await Task.sleep(nanoseconds: 7_500_000) } - catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") } + _ = try? await Task.sleep(nanoseconds: 7_500_000) await receiveMsgLoop() } } diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index ab552ad9f7..e2bd313e6b 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -7,6 +7,7 @@ // import Foundation +import UIKit import SimpleXChat private let suspendLockQueue = DispatchQueue(label: "chat.simplex.app.suspend.lock") @@ -37,9 +38,13 @@ func suspendBgRefresh() { } func chatSuspended() { + logger.debug("chatSuspended") suspendLockQueue.sync { if case .suspending = appStateGroupDefault.get() { appStateGroupDefault.set(.suspended) + if ChatModel.shared.chatRunning == true { + ChatReceiver.shared.stop() + } } } } @@ -50,3 +55,7 @@ func activateChat(appState: AppState = .active) { apiActivateChat() } } + +func isAppInactive() -> Bool { + UIApplication.shared.applicationState != .active && appStateGroupDefault.get().inactive +} diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index a79f7bab83..398bb70d81 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -51,9 +51,6 @@ struct SimpleXApp: App { switch (phase) { case .background: suspendChat() - if chatModel.chatRunning == true { - ChatReceiver.shared.stop() - } BGManager.shared.schedule() if userAuthorized == true { enteredBackground = ProcessInfo.processInfo.systemUptime From 3450420b804253fdc754ae2f1f8488745b593355 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 2 Jul 2022 12:35:04 +0400 Subject: [PATCH 53/89] core: pass ERR responses to view (#768) --- src/Simplex/Chat.hs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 09f804f4f4..ee7337367a 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1150,9 +1150,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage SENT msgId -> -- ? updateDirectChatItemStatus sentMsgDeliveryEvent conn msgId - -- TODO print errors - MERR _ _ -> pure () -- ? updateDirectChatItemStatus - ERR _ -> pure () + MERR _ err -> toView . CRChatError $ ChatErrorAgent err -- ? updateDirectChatItemStatus + ERR err -> toView . CRChatError $ ChatErrorAgent err -- TODO add debugging output _ -> pure () Just ct@Contact {localDisplayName = c, contactId} -> case agentMsg of @@ -1235,7 +1234,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage Just chatItemId -> do chatItem <- withStore $ \db -> updateDirectChatItemStatus db userId contactId chatItemId (agentErrToItemStatus err) toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) - ERR _ -> pure () + ERR err -> toView . CRChatError $ ChatErrorAgent err -- TODO add debugging output _ -> pure () @@ -1325,9 +1324,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage ackMsgDeliveryEvent conn msgMeta SENT msgId -> sentMsgDeliveryEvent conn msgId - -- TODO print errors - MERR _ _ -> pure () - ERR _ -> pure () + MERR _ err -> toView . CRChatError $ ChatErrorAgent err + ERR err -> toView . CRChatError $ ChatErrorAgent err -- TODO add debugging output _ -> pure () @@ -1364,8 +1362,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> throwChatError $ CEFileSend fileId err MSG meta _ _ -> withAckMessage agentConnId meta $ pure () - -- TODO print errors - ERR _ -> pure () + ERR err -> toView . CRChatError $ ChatErrorAgent err -- TODO add debugging output _ -> pure () @@ -1419,9 +1416,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage withAgent (`deleteConnection` agentConnId) RcvChunkDuplicate -> pure () RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo - -- TODO print errors - MERR _ _ -> pure () - ERR _ -> pure () + MERR _ err -> toView . CRChatError $ ChatErrorAgent err + ERR err -> toView . CRChatError $ ChatErrorAgent err -- TODO add debugging output _ -> pure () @@ -1434,9 +1430,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage XInfo p -> profileContactRequest invId p Nothing -- TODO show/log error, other events in contact request _ -> pure () - -- TODO print errors - MERR _ _ -> pure () - ERR _ -> pure () + MERR _ err -> toView . CRChatError $ ChatErrorAgent err + ERR err -> toView . CRChatError $ ChatErrorAgent err -- TODO add debugging output _ -> pure () where From 15a17f3c136a22fa26d1624c270c0576a4fb2611 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 2 Jul 2022 10:13:06 +0100 Subject: [PATCH 54/89] core: subscribe to all connections concurrently (#770) --- src/Simplex/Chat.hs | 23 ++++++++++++----------- src/Simplex/Chat/Controller.hs | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index ee7337367a..01d24ee4cc 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -146,24 +146,26 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de pure ss {smp = fromMaybe defaultSMPServers $ nonEmpty userSmpServers} _ -> pure ss -runChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m () -runChatController user subConns = race_ notificationSubscriber $ agentSubscriber user subConns - startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m (Async ()) startChatController user subConns = do asks smpAgent >>= resumeAgentClient s <- asks agentAsync - readTVarIO s >>= maybe (start s) pure + readTVarIO s >>= maybe (start s) (pure . fst) where start s = do - a <- async $ runChatController user subConns - atomically . writeTVar s $ Just a - pure a + a1 <- async $ race_ notificationSubscriber agentSubscriber + a2 <- + if subConns + then Just <$> async (subscribeUserConnections subscribeConnection user) + else pure Nothing + atomically . writeTVar s $ Just (a1, a2) + pure a1 stopChatController :: MonadUnliftIO m => ChatController -> m () stopChatController ChatController {smpAgent, agentAsync = s} = do disconnectAgentClient smpAgent - readTVarIO s >>= mapM_ uninterruptibleCancel >> atomically (writeTVar s Nothing) + readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2) + atomically (writeTVar s Nothing) withLock :: MonadUnliftIO m => TMVar () -> m a -> m a withLock lock = @@ -1000,11 +1002,10 @@ acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationI connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profile withStore' $ \db -> createAcceptedContact db userId connId cName profileId p userContactLinkId xContactId -agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m () -agentSubscriber user subConns = do +agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m () +agentSubscriber = do q <- asks $ subQ . smpAgent l <- asks chatLock - when subConns $ subscribeUserConnections subscribeConnection user forever $ do (_, connId, msg) <- atomically $ readTBQueue q u <- readTVarIO =<< asks currentUser diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 1a3980cc93..b3760fcae7 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -73,7 +73,7 @@ data ChatController = ChatController activeTo :: TVar ActiveTo, firstTime :: Bool, smpAgent :: AgentClient, - agentAsync :: TVar (Maybe (Async ())), + agentAsync :: TVar (Maybe (Async (), Maybe (Async ()))), chatStore :: SQLiteStore, chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted idsDrg :: TVar ChaChaDRG, From 01b9c16f1a9b930f5440dd771b80ad97defcd799 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 2 Jul 2022 13:35:38 +0400 Subject: [PATCH 55/89] update simplexmq (notify on errors) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 34e7468733..6f3c77ada8 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: f9cd7e5416013803c899dda59110a362e853407d + tag: 57d83ae42d877f748831469d311eb95fe248c5c8 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 4fbb663a45..805b3a5296 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."f9cd7e5416013803c899dda59110a362e853407d" = "07plsai0pyagax03yph5bihsx1xh6bz700c24xswf8qyd2y510cd"; + "https://github.com/simplex-chat/simplexmq.git"."57d83ae42d877f748831469d311eb95fe248c5c8" = "0pq64py6qpxvr9dnramhjg8ccbizmw96lfqxwmc5mnfgvij049hq"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 9e95e644c1..3ac759cb86 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: f9cd7e5416013803c899dda59110a362e853407d + commit: 57d83ae42d877f748831469d311eb95fe248c5c8 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 60642317d03adef86f7ace8759aba455657961c8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 2 Jul 2022 15:35:51 +0100 Subject: [PATCH 56/89] ios: update version 3.0 (54) --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 7d8778b3db..caa9b66c3b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1101,7 +1101,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1122,7 +1122,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 3.0; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1141,7 +1141,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1162,7 +1162,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 3.0; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1220,7 +1220,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1233,7 +1233,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 3.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1250,7 +1250,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1263,7 +1263,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 3.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; From e4c6d210c62809478dea46e0d3fefb6f9d199f7b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 2 Jul 2022 17:18:45 +0100 Subject: [PATCH 57/89] ios: fix updating chats when exiting background (#772) --- apps/ios/Shared/AppDelegate.swift | 2 +- apps/ios/Shared/Model/BGManager.swift | 2 +- apps/ios/Shared/Model/ChatModel.swift | 12 ++++++--- apps/ios/Shared/Model/SimpleXAPI.swift | 19 +++++++++++--- apps/ios/Shared/Model/SuspendChat.swift | 6 +---- apps/ios/Shared/SimpleXApp.swift | 25 ++++++++++++++----- apps/ios/Shared/Views/Chat/ChatView.swift | 2 +- .../Views/ChatList/ChatListNavLink.swift | 11 +------- 8 files changed, 49 insertions(+), 30 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 0add3347ba..895891838e 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -62,7 +62,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { } } else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages { logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages") - if isAppInactive() { + if appStateGroupDefault.get().inactive { receiveMessages(completionHandler) } else { completionHandler(.noData) diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index 6468d3b08a..d7267ff8b8 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -47,7 +47,7 @@ class BGManager { private func handleRefresh(_ task: BGAppRefreshTask) { logger.debug("BGManager.handleRefresh") schedule() - if isAppInactive() { + if appStateGroupDefault.get().inactive { let completeRefresh = completionHandler { task.setTaskCompleted(success: true) } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 2292a60c94..cc0731f2ad 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -102,9 +102,15 @@ final class ChatModel: ObservableObject { } } - func replaceChats(with newChats: [Chat]) { - for chat in newChats { - replaceChat(chat.id, chat) + func updateChats(with newChats: [ChatData]) { + for c in newChats { + if let chat = getChat(c.id) { + chat.chatInfo = c.chatInfo + chat.chatItems = c.chatItems + chat.chatStats = c.chatStats + } else { + addChat(Chat(c)) + } } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e2f9ecd016..af3ac5d5aa 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -179,9 +179,9 @@ func apiDeleteStorage() async throws { try await sendCommandOkResp(.apiDeleteStorage) } -func apiGetChats() throws -> [Chat] { +func apiGetChats() throws -> [ChatData] { let r = chatSendCmdSync(.apiGetChats) - if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } } + if case let .apiChats(chats) = r { return chats } throw r } @@ -191,6 +191,18 @@ func apiGetChat(type: ChatType, id: Int64) throws -> Chat { throw r } +func loadChat(chat: Chat) { + do { + let cInfo = chat.chatInfo + let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId) + let m = ChatModel.shared + m.updateChatInfo(chat.chatInfo) + m.chatItems = chat.chatItems + } catch let error { + logger.error("loadChat error: \(responseError(error))") + } +} + func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) async throws -> ChatItem { let chatModel = ChatModel.shared let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg) @@ -524,7 +536,8 @@ func startChat() throws { if justStarted { m.userAddress = try apiGetUserAddress() m.userSMPServers = try getUserSMPServers() - m.chats = try apiGetChats() + let chats = try apiGetChats() + m.chats = chats.map { Chat.init($0) } (m.savedToken, m.tokenStatus, m.notificationMode) = try apiGetNtfToken() if let token = m.deviceToken { registerToken(token: token) diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index e2bd313e6b..e19eb64f45 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -38,9 +38,9 @@ func suspendBgRefresh() { } func chatSuspended() { - logger.debug("chatSuspended") suspendLockQueue.sync { if case .suspending = appStateGroupDefault.get() { + logger.debug("chatSuspended") appStateGroupDefault.set(.suspended) if ChatModel.shared.chatRunning == true { ChatReceiver.shared.stop() @@ -55,7 +55,3 @@ func activateChat(appState: AppState = .active) { apiActivateChat() } } - -func isAppInactive() -> Bool { - UIApplication.shared.applicationState != .active && appStateGroupDefault.get().inactive -} diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 398bb70d81..587e9762b6 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -63,12 +63,7 @@ struct SimpleXApp: App { let appState = appStateGroupDefault.get() activateChat() if appState.inactive && chatModel.chatRunning == true { - do { - let chats = try apiGetChats() - chatModel.replaceChats(with: chats) - } catch let error { - logger.error("apiGetChats: cannot update chats \(responseError(error))") - } + updateChats() } doAuthenticate = authenticationExpired() default: @@ -107,4 +102,22 @@ struct SimpleXApp: App { return true } } + + private func updateChats() { + do { + let chats = try apiGetChats() + chatModel.updateChats(with: chats) + if let id = chatModel.chatId, + let chat = chatModel.getChat(id) { + loadChat(chat: chat) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + if chatModel.chatId == chat.id { + Task { await markChatRead(chat) } + } + } + } + } catch let error { + logger.error("apiGetChats: cannot update chats \(responseError(error))") + } + } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 79b30ebf27..6c5bd9109a 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -225,7 +225,7 @@ struct ChatView: View { } func markAllRead() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { if chatModel.chatId == chat.id { Task { await markChatRead(chat) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index bce1f0a662..eedf579a10 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -30,16 +30,7 @@ struct ChatListNavLink: View { private func chatView() -> some View { ChatView(chat: chat, showChatInfo: $showChatInfo) - .onAppear { - do { - let cInfo = chat.chatInfo - let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId) - chatModel.updateChatInfo(chat.chatInfo) - chatModel.chatItems = chat.chatItems - } catch { - logger.error("ChatListNavLink.chatView apiGetChatItems error: \(error.localizedDescription)") - } - } + .onAppear { loadChat(chat: chat) } } @ViewBuilder private func contactNavLink(_ contact: Contact) -> some View { From c61909246413c8b664108732ae118c3d62751105 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 3 Jul 2022 13:07:42 +0100 Subject: [PATCH 58/89] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 6f3c77ada8..b0bb9f2d75 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 57d83ae42d877f748831469d311eb95fe248c5c8 + tag: 4c0164c49effe8d1b3fd98aca184245a8b4cac93 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 805b3a5296..6ecd9cde90 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."57d83ae42d877f748831469d311eb95fe248c5c8" = "0pq64py6qpxvr9dnramhjg8ccbizmw96lfqxwmc5mnfgvij049hq"; + "https://github.com/simplex-chat/simplexmq.git"."4c0164c49effe8d1b3fd98aca184245a8b4cac93" = "1ni3phxal10rfrkgnhcih3ksh7nfnj5b7vkk0qs9k52hmc8g306b"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 3ac759cb86..9d6e1ba0cb 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 57d83ae42d877f748831469d311eb95fe248c5c8 + commit: 4c0164c49effe8d1b3fd98aca184245a8b4cac93 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 2c121b5731b7aa3c439cbae2724b85d6ebcaa32f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 3 Jul 2022 19:53:07 +0100 Subject: [PATCH 59/89] ios: choose notifications mode during onboarding and after DB migration (#773) --- apps/ios/Shared/Model/SimpleXAPI.swift | 6 +- apps/ios/Shared/SimpleXApp.swift | 12 +- .../Shared/Views/ChatList/ChatListView.swift | 2 +- .../Database/MigrateToAppGroupView.swift | 20 ++-- .../Views/Onboarding/CreateProfile.swift | 2 +- .../Views/Onboarding/OnboardingView.swift | 6 +- .../Onboarding/SetNotificationsMode.swift | 112 ++++++++++++++++++ .../Shared/Views/Onboarding/SimpleXInfo.swift | 2 +- .../UserSettings/NotificationsView.swift | 38 +++--- .../Views/UserSettings/SettingsView.swift | 10 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 8 +- apps/ios/SimpleXChat/APITypes.swift | 2 +- 12 files changed, 175 insertions(+), 45 deletions(-) create mode 100644 apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index af3ac5d5aa..2d0f9f775a 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -543,8 +543,10 @@ func startChat() throws { registerToken(token: token) } withAnimation { - m.onboardingStage = m.chats.isEmpty - ? .step3_MakeConnection + m.onboardingStage = m.onboardingStage == .step2_CreateProfile + ? .step3_SetNotificationsMode + : m.chats.isEmpty + ? .step4_MakeConnection : .onboardingComplete } } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 587e9762b6..83db2de40a 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -82,19 +82,21 @@ struct SimpleXApp: App { let legacyDatabase = hasLegacyDatabase() if legacyDatabase, case .documents = dbContainerGroupDefault.get() { dbContainerGroupDefault.set(.documents) - switch v3DBMigrationDefault.get() { - case .migrated: () - default: v3DBMigrationDefault.set(.offer) - } + setMigrationState(.offer) logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db") } else { dbContainerGroupDefault.set(.group) - v3DBMigrationDefault.set(.ready) + setMigrationState(.ready) logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db") logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present") } } + private func setMigrationState(_ state: V3DBMigrationState) { + if case .migrated = v3DBMigrationDefault.get() { return } + v3DBMigrationDefault.set(state) + } + private func authenticationExpired() -> Bool { if let enteredBackground = enteredBackground { return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30 diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index e3087ed30a..29850a6e89 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -34,7 +34,7 @@ struct ChatListView: View { } .onChange(of: chatModel.chats.isEmpty) { empty in if !empty { return } - withAnimation { chatModel.onboardingStage = .step3_MakeConnection } + withAnimation { chatModel.onboardingStage = .step4_MakeConnection } } .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } .onAppear() { connectViaUrl() } diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index a53c6a2b40..de99d315ef 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -43,13 +43,13 @@ struct MigrateToAppGroupView: View { var body: some View { ZStack(alignment: .topLeading) { - Text("Database migration").font(.largeTitle) + Text("Push notifications").font(.largeTitle) switch chatModel.v3DBMigration { case .offer: VStack(alignment: .leading, spacing: 16) { Text("To support instant push notifications the chat database has to be migrated.") - Text("If you need to use the chat now tap **Skip** below (you will be offered to migrate the database when you restart the app).") + Text("If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).") } .padding(.top, 56) center { @@ -109,6 +109,7 @@ struct MigrateToAppGroupView: View { do { resetChatCtrl() try initializeChat(start: true) + chatModel.onboardingStage = .step3_SetNotificationsMode setV3DBMigration(.ready) } catch let error { dbContainerGroupDefault.set(.documents) @@ -117,7 +118,7 @@ struct MigrateToAppGroupView: View { } deleteOldArchive() } label: { - Text("Start using chat") + Text("Start chat") .font(.title) .frame(maxWidth: .infinity) } @@ -165,16 +166,16 @@ struct MigrateToAppGroupView: View { fatalError("Failed to start or load chats: \(responseError(error))") } } label: { - Text("Skip and start using chat") + Text("Do it later") .frame(maxWidth: .infinity, alignment: .trailing) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) } - private func setV3DBMigration(_ value: V3DBMigrationState) { - chatModel.v3DBMigration = value - v3DBMigrationDefault.set(value) + private func setV3DBMigration(_ state: V3DBMigrationState) { + chatModel.v3DBMigration = state + v3DBMigrationDefault.set(state) } func migrateDatabaseToV3() { @@ -242,6 +243,9 @@ func deleteOldArchive() { struct MigrateToGroupView_Previews: PreviewProvider { static var previews: some View { - MigrateToAppGroupView() + let chatModel = ChatModel() + chatModel.v3DBMigration = .migrated + return MigrateToAppGroupView() + .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 511ff82b36..8b2fcd627e 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -96,7 +96,7 @@ struct CreateProfile: View { do { m.currentUser = try apiCreateActiveUser(profile) try startChat() - withAnimation { m.onboardingStage = .step3_MakeConnection } + withAnimation { m.onboardingStage = .step3_SetNotificationsMode } } catch { fatalError("Failed to create user or start chat: \(responseError(error))") diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index 04fcbf42e5..0b01893834 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -15,7 +15,8 @@ struct OnboardingView: View { switch onboarding { case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) case .step2_CreateProfile: CreateProfile() - case .step3_MakeConnection: MakeConnection() + case .step3_SetNotificationsMode: SetNotificationsMode() + case .step4_MakeConnection: MakeConnection() case .onboardingComplete: EmptyView() } } @@ -24,7 +25,8 @@ struct OnboardingView: View { enum OnboardingStage { case step1_SimpleXInfo case step2_CreateProfile - case step3_MakeConnection + case step3_SetNotificationsMode + case step4_MakeConnection case onboardingComplete } diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift new file mode 100644 index 0000000000..87803512b8 --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -0,0 +1,112 @@ +// +// NotificationsModeView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 03/07/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct SetNotificationsMode: View { + @EnvironmentObject var m: ChatModel + @State private var notificationMode = NotificationsMode.instant + @State private var showAlert: NotificationAlert? + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("Push notifications").font(.largeTitle) + + Text("Send notifications:") + ForEach(NotificationsMode.values) { mode in + NtfModeSelector(mode: mode, selection: $notificationMode) + } + + Spacer() + + Button { + if let token = m.deviceToken { + setNotificationsMode(token, notificationMode) + } else { + AlertManager.shared.showAlertMsg(title: "No device token!") + } + m.onboardingStage = m.chats.isEmpty + ? .step4_MakeConnection + : .onboardingComplete + } label: { + if case .off = notificationMode { + Text("Use chat") + } else { + Text("Enable notifications") + } + } + .font(.title) + .frame(maxWidth: .infinity) + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + } + } + + private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { + switch mode { + case .off: + m.tokenStatus = .new + m.notificationMode = .off + default: + Task { + do { + let status = try await apiRegisterToken(token: token, notificationMode: mode) + await MainActor.run { + m.tokenStatus = status + m.notificationMode = mode + } + } catch let error { + AlertManager.shared.showAlertMsg( + title: "Error enabling notifications", + message: "\(responseError(error))" + ) + } + } + } + } +} + +struct NtfModeSelector: View { + var mode: NotificationsMode + @Binding var selection: NotificationsMode + @State private var tapped = false + + var body: some View { + ZStack { + VStack(alignment: .leading, spacing: 4) { + Text(mode.label) + .font(.headline) + .foregroundColor(selection == mode ? .accentColor : .secondary) + Text(ntfModeDescription(mode)) + .lineLimit(10) + .font(.subheadline) + } + .padding(12) + } + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 18)) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2) + ) + ._onButtonGesture { down in + tapped = down + if down { selection = mode } + } perform: {} + } +} + +struct NotificationsModeView_Previews: PreviewProvider { + static var previews: some View { + SetNotificationsMode() + } +} diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 2d05852640..b53c871c68 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -77,7 +77,7 @@ struct OnboardingActionButton: View { if m.currentUser == nil { actionButton("Create your profile", onboarding: .step2_CreateProfile) } else { - actionButton("Make a private connection", onboarding: .step3_MakeConnection) + actionButton("Make a private connection", onboarding: .step4_MakeConnection) } } diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index b3752befad..71c6e2a6ce 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -89,7 +89,7 @@ struct NotificationsView: View { title: Text(ntfModeAlertTitle(mode)), message: Text(ntfModeDescription(mode)), primaryButton: .default(Text(mode == .off ? "Turn off" : "Enable")) { - setNotificationsMode(mode, token) + setNotificationsMode(token, mode) }, secondaryButton: .cancel() { notificationMode = m.notificationMode @@ -108,17 +108,19 @@ struct NotificationsView: View { } } - private func setNotificationsMode(_ mode: NotificationsMode, _ token: DeviceToken) { + private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { Task { switch mode { case .off: do { try await apiDeleteToken(token: token) - m.tokenStatus = .new - notificationMode = .off - m.notificationMode = .off + await MainActor.run { + m.tokenStatus = .new + notificationMode = .off + m.notificationMode = .off + } } catch let error { - DispatchQueue.main.async { + await MainActor.run { let err = responseError(error) logger.error("apiDeleteToken error: \(err)") showAlert = .error(title: "Error deleting token", error: err) @@ -126,16 +128,17 @@ struct NotificationsView: View { } default: do { - do { - m.tokenStatus = try await apiRegisterToken(token: token, notificationMode: mode) + let status = try await apiRegisterToken(token: token, notificationMode: mode) + await MainActor.run { + m.tokenStatus = status notificationMode = mode m.notificationMode = mode - } catch let error { - DispatchQueue.main.async { - let err = responseError(error) - logger.error("apiRegisterToken error: \(err)") - showAlert = .error(title: "Error enabling notifications", error: err) - } + } + } catch let error { + await MainActor.run { + let err = responseError(error) + logger.error("apiRegisterToken error: \(err)") + showAlert = .error(title: "Error enabling notifications", error: err) } } } @@ -145,9 +148,9 @@ struct NotificationsView: View { func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { switch mode { - case .off: return "**Maximum privacy**: push notifications are off.\nNo meta-data is shared with SimpleX Chat notification server." - case .periodic: return "**High privacy**: new messages are checked every 20 minutes.\nYour device token is shared with SimpleX Chat notification server, but it cannot see how many connections you have or how many messages you receive." - case .instant: return "**Medium privacy** (recommended): notifications are sent instantly.\nYour device token and notifications are sent to SimpleX Chat notification server, but it cannot access the message content, size or who is it from." + case .off: return "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." + case .periodic: return "**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." + case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who is it from." } } @@ -171,6 +174,7 @@ struct SelectionListView: View { .contentShape(Rectangle()) .listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground)) .onTapGesture { + if selection == item { return } if let f = onSelection { f(item) } else { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index d99f31b350..40827bdf49 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -200,13 +200,13 @@ struct SettingsView: View { switch (chatModel.tokenStatus) { case .new: icon = "bolt" - color = .primary + color = .secondary case .registered: icon = "bolt.fill" - color = .primary + color = .secondary case .invalid: icon = "bolt.slash" - color = .primary + color = .secondary case .confirmed: icon = "bolt.fill" color = .yellow @@ -215,10 +215,10 @@ struct SettingsView: View { color = .green case .expired: icon = "bolt.slash.fill" - color = .primary + color = .secondary case .none: icon = "bolt" - color = .primary + color = .secondary } return Image(systemName: icon) .padding(.trailing, 9) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index caa9b66c3b..3e05ad8857 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; }; 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; }; + 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; @@ -227,6 +228,7 @@ 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = ""; }; 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = ""; }; 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = ""; }; + 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNotificationsMode.swift; sourceTree = ""; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = ""; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = ""; }; @@ -489,9 +491,10 @@ children = ( 5CB0BA8D2827126500B3292C /* OnboardingView.swift */, 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */, - 5CB0BA91282713FD00B3292C /* CreateProfile.swift */, - 5CB0BA952827143500B3292C /* MakeConnection.swift */, 5CB0BA992827FD8800B3292C /* HowItWorks.swift */, + 5CB0BA91282713FD00B3292C /* CreateProfile.swift */, + 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */, + 5CB0BA952827143500B3292C /* MakeConnection.swift */, ); path = Onboarding; sourceTree = ""; @@ -827,6 +830,7 @@ 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */, 3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */, 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */, + 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */, 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 5b92fd3e87..d654ea61b3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -427,7 +427,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem { public var label: LocalizedStringKey { switch self { - case .off: return "Off" + case .off: return "Off (Local)" case .periodic: return "Periodically" case .instant: return "Instantly" } From 687e3be9ac521666c801e863f321c065531a9940 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 4 Jul 2022 11:15:25 +0100 Subject: [PATCH 60/89] iOS: update call invitations when exiting background (#771) * core: communicate call invitations state between NSE and app via db * enable tests * delete calls, encoding * load calls on start * remove line * remove table alias Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- apps/ios/Shared/SimpleXApp.swift | 1 + package.yaml | 1 + simplex-chat.cabal | 2 + src/Simplex/Chat.hs | 49 ++++++++++++++---- src/Simplex/Chat/Call.hs | 50 +++++++++++++++++-- src/Simplex/Chat/Controller.hs | 4 +- .../Chat/Migrations/M20220702_calls.hs | 22 ++++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 12 +++++ src/Simplex/Chat/Store.hs | 40 ++++++++++++++- src/Simplex/Chat/View.hs | 3 +- tests/ChatClient.hs | 3 +- tests/ChatTests.hs | 2 + tests/SchemaDump.hs | 3 +- 13 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20220702_calls.hs diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 83db2de40a..6b543a6577 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -63,6 +63,7 @@ struct SimpleXApp: App { let appState = appStateGroupDefault.get() activateChat() if appState.inactive && chatModel.chatRunning == true { + // TODO refresh call invitation updateChats() } doAuthenticate = authenticationExpired() diff --git a/package.yaml b/package.yaml index afac726977..31cc92667b 100644 --- a/package.yaml +++ b/package.yaml @@ -78,6 +78,7 @@ tests: dependencies: - simplex-chat - async == 2.2.* + - deepseq == 1.4.* - hspec == 2.7.* - network == 3.1.* - stm == 2.5.* diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 0b3fe1c97e..2f65a11637 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -40,6 +40,7 @@ library Simplex.Chat.Migrations.M20220404_files_status_fields Simplex.Chat.Migrations.M20220514_profiles_user_id Simplex.Chat.Migrations.M20220626_auto_reply + Simplex.Chat.Migrations.M20220702_calls Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.Protocol @@ -233,6 +234,7 @@ test-suite simplex-chat-test , composition ==1.0.* , containers ==0.6.* , cryptonite >=0.27 && <0.30 + , deepseq ==1.4.* , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 01d24ee4cc..820c454cf1 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -27,6 +27,7 @@ import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Char (isSpace) +import Data.Either (fromRight) import Data.Fixed (div') import Data.Functor (($>)) import Data.Int (Int64) @@ -149,6 +150,7 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m (Async ()) startChatController user subConns = do asks smpAgent >>= resumeAgentClient + restoreCalls s <- asks agentAsync readTVarIO s >>= maybe (start s) (pure . fst) where @@ -160,6 +162,11 @@ startChatController user subConns = do else pure Nothing atomically . writeTVar s $ Just (a1, a2) pure a1 + restoreCalls = do + savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db user) + let callsMap = M.fromList $ map (\(call@Call {contactId}) -> (contactId, call)) savedCalls + calls <- asks currentCalls + atomically $ writeTVar calls callsMap stopChatController :: MonadUnliftIO m => ChatController -> m () stopChatController ChatController {smpAgent, agentAsync = s} = do @@ -478,7 +485,7 @@ processChatCommand = \case callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair} msg <- sendDirectContactMessage ct (XCallInv callId invitation) ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndCall CISCallPending 0) Nothing Nothing - let call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState} + let call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} call_ <- atomically $ TM.lookupInsert contactId call' calls forM_ call_ $ \call -> updateCallItemStatus userId ct call WCSDisconnected Nothing toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci @@ -537,6 +544,16 @@ processChatCommand = \case SndMessage {msgId} <- sendDirectContactMessage ct (XCallEnd callId) updateCallItemStatus userId ct call WCSDisconnected $ Just msgId pure Nothing + APIGetCallInvitations -> withUser $ \user@User {userId} -> do + invs <- mapMaybe callInvitation <$> withStore' (`getCalls` user) + CRCallInvitations <$> mapM (rcvCallInvitation userId) invs + where + callInvitation Call {contactId, callState, callTs} = case callState of + CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callTs, peerCallType, sharedKey) + _ -> Nothing + rcvCallInvitation userId (contactId, callTs, peerCallType, sharedKey) = do + contact <- withStore (\db -> getContact db userId contactId) + pure RcvCallInvitation {contact, callType = peerCallType, sharedKey, callTs} APICallStatus contactId receivedStatus -> withCurrentCall contactId $ \userId ct call -> updateCallItemStatus userId ct call receivedStatus Nothing $> Just call @@ -861,7 +878,7 @@ processChatCommand = \case ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) unless cancelled $ cancelRcvFileTransfer user ft withCurrentCall :: ContactId -> (UserId -> Contact -> Call -> m (Maybe Call)) -> m ChatResponse - withCurrentCall ctId action = withUser $ \User {userId} -> do + withCurrentCall ctId action = withUser $ \user@User {userId} -> do ct <- withStore $ \db -> getContact db userId ctId calls <- asks currentCalls withChatLock $ @@ -870,9 +887,13 @@ processChatCommand = \case Just call@Call {contactId} | ctId == contactId -> do call_ <- action userId ct call - atomically $ case call_ of - Just call' -> TM.insert ctId call' calls - _ -> TM.delete ctId calls + case call_ of + Just call' -> do + unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId + atomically $ TM.insert ctId call' calls + _ -> do + withStore' $ \db -> deleteCalls db user ctId + atomically $ TM.delete ctId calls pure CRCmdOk | otherwise -> throwChatError $ CECallContact contactId forwardFile :: ChatName -> FileTransferId -> (ChatName -> FilePath -> ChatCommand) -> m ChatResponse @@ -1713,14 +1734,15 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage ci <- saveCallItem CISCallPending let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> (snd <$> dhKeyPair)) callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey} - call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState} + call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} calls <- asks currentCalls - -- theoretically, the new call invitation for the current contant can mark the in-progress call as ended + -- theoretically, the new call invitation for the current contact can mark the in-progress call as ended -- (and replace it in ChatController) -- practically, this should not happen + withStore' $ \db -> createCall db user call' $ chatItemTs' ci call_ <- atomically (TM.lookupInsert contactId call' calls) forM_ call_ $ \call -> updateCallItemStatus userId ct call WCSDisconnected Nothing - toView . CRCallInvitation ct callType sharedKey $ chatItemTs' ci + toView . CRCallInvitation $ RcvCallInvitation {contact = ct, callType, sharedKey, callTs = chatItemTs' ci} toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci where saveCallItem status = saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvCall status 0) Nothing @@ -1789,9 +1811,13 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage | contactId /= ctId' || callId /= callId' -> messageError $ eventName <> ": wrong contact or callId" | otherwise -> do (call_, aciContent_) <- action call - atomically $ case call_ of - Just call' -> TM.insert ctId' call' calls - _ -> TM.delete ctId' calls + case call_ of + Just call' -> do + unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId' + atomically $ TM.insert ctId' call' calls + _ -> do + withStore' $ \db -> deleteCalls db user ctId' + atomically $ TM.delete ctId' calls forM_ aciContent_ $ \aciContent -> updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId @@ -2279,6 +2305,7 @@ chatCommandP = <|> "/_call extra @" *> (APISendCallExtraInfo <$> A.decimal <* A.space <*> jsonP) <|> "/_call end @" *> (APIEndCall <$> A.decimal) <|> "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP) + <|> "/_call get" $> APIGetCallInvitations <|> "/_profile " *> (APIUpdateProfile <$> jsonP) <|> "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString) <|> "/_ntf get" $> APIGetNtfToken diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs index c40351aef4..54ce2dad9c 100644 --- a/src/Simplex/Chat/Call.hs +++ b/src/Simplex/Chat/Call.hs @@ -12,20 +12,33 @@ import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Lazy.Char8 as LB import Data.Int (Int64) import Data.Text (Text) +import Data.Text.Encoding (encodeUtf8) +import Data.Time.Clock (UTCTime) +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Generics (Generic) +import Simplex.Chat.Types (Contact, ContactId) +import Simplex.Chat.Util (safeDecodeUtf8) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (dropPrefix, enumJSON) +import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON) data Call = Call - { contactId :: Int64, + { contactId :: ContactId, callId :: CallId, chatItemId :: Int64, - callState :: CallState + callState :: CallState, + callTs :: UTCTime } +isRcvInvitation :: Call -> Bool +isRcvInvitation Call {callState} = case callState of + CallInvitationReceived {} -> True + _ -> False + data CallStateTag = CSTCallInvitationSent | CSTCallInvitationReceived @@ -75,6 +88,21 @@ data CallState peerCallSession :: WebRTCSession, sharedKey :: Maybe C.Key } + deriving (Generic) + +-- database representation +instance FromJSON CallState where + parseJSON = J.genericParseJSON $ singleFieldJSON fstToLower + +instance ToJSON CallState where + toJSON = J.genericToJSON $ singleFieldJSON fstToLower + toEncoding = J.genericToEncoding $ singleFieldJSON fstToLower + +instance ToField CallState where + toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode + +instance FromField CallState where + fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8 newtype CallId = CallId ByteString deriving (Eq, Show) @@ -91,6 +119,22 @@ instance ToJSON CallId where toJSON = strToJSON toEncoding = strToJEncoding +instance FromField CallId where fromField f = CallId <$> fromField f + +instance ToField CallId where toField (CallId m) = toField m + +data RcvCallInvitation = RcvCallInvitation + { contact :: Contact, + callType :: CallType, + sharedKey :: Maybe C.Key, + callTs :: UTCTime + } + deriving (Show, Generic) + +instance ToJSON RcvCallInvitation where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + data CallType = CallType { media :: CallMedia, capabilities :: CallCapabilities diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b3760fcae7..cc21642441 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -126,6 +126,7 @@ data ChatCommand | APISendCallAnswer ContactId WebRTCSession | APISendCallExtraInfo ContactId WebRTCExtraInfo | APIEndCall ContactId + | APIGetCallInvitations | APICallStatus ContactId WebRTCCallStatus | APIUpdateProfile Profile | APIParseMarkdown Text @@ -269,11 +270,12 @@ data ChatResponse | CRPendingSubSummary {pendingSubStatus :: [PendingSubStatus]} | CRSndFileSubError {sndFileTransfer :: SndFileTransfer, chatError :: ChatError} | CRRcvFileSubError {rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} - | CRCallInvitation {contact :: Contact, callType :: CallType, sharedKey :: Maybe C.Key, callTs :: UTCTime} + | CRCallInvitation {callInvitation :: RcvCallInvitation} | CRCallOffer {contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool} | CRCallAnswer {contact :: Contact, answer :: WebRTCSession} | CRCallExtraInfo {contact :: Contact, extraInfo :: WebRTCExtraInfo} | CRCallEnded {contact :: Contact} + | CRCallInvitations {callInvitations :: [RcvCallInvitation]} | CRUserContactLinkSubscribed | CRUserContactLinkSubError {chatError :: ChatError} | CRNtfTokenStatus {status :: NtfTknStatus} diff --git a/src/Simplex/Chat/Migrations/M20220702_calls.hs b/src/Simplex/Chat/Migrations/M20220702_calls.hs new file mode 100644 index 0000000000..4cbf3dbad6 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220702_calls.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220702_calls where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220702_calls :: Query +m20220702_calls = + [sql| +CREATE TABLE calls ( -- stores call invitations state for communicating state between NSE and app when call notification comes + call_id INTEGER PRIMARY KEY, + contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, + shared_call_id BLOB NOT NULL, + chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, + call_state BLOB NOT NULL, + call_ts TEXT NOT NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 55429b04a5..b99291d104 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -369,3 +369,15 @@ CREATE UNIQUE INDEX idx_messages_group_shared_msg_id ON messages( shared_msg_id ); CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id); +CREATE TABLE calls( + -- stores call invitations state for communicating state between NSE and app when call notification comes + call_id INTEGER PRIMARY KEY, + contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, + shared_call_id BLOB NOT NULL, + chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, + call_state BLOB NOT NULL, + call_ts TEXT NOT NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 3c994c47de..7fda746f23 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -160,6 +160,9 @@ module Simplex.Chat.Store updateGroupChatItemsRead, getSMPServers, overwriteSMPServers, + createCall, + deleteCalls, + getCalls, getPendingContactConnection, deletePendingContactConnection, withTransaction, @@ -193,6 +196,7 @@ import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), SQLError, import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.QQ (sql) import GHC.Generics (Generic) +import Simplex.Chat.Call import Simplex.Chat.Markdown import Simplex.Chat.Messages import Simplex.Chat.Migrations.M20220101_initial @@ -207,6 +211,7 @@ import Simplex.Chat.Migrations.M20220321_chat_item_edited import Simplex.Chat.Migrations.M20220404_files_status_fields import Simplex.Chat.Migrations.M20220514_profiles_user_id import Simplex.Chat.Migrations.M20220626_auto_reply +import Simplex.Chat.Migrations.M20220702_calls import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -232,7 +237,8 @@ schemaMigrations = ("20220321_chat_item_edited", m20220321_chat_item_edited), ("20220404_files_status_fields", m20220404_files_status_fields), ("20220514_profiles_user_id", m20220514_profiles_user_id), - ("20220626_auto_reply", m20220626_auto_reply) + ("20220626_auto_reply", m20220626_auto_reply), + ("20220702_calls", m20220702_calls) ] -- | The list of migrations in ascending order by date @@ -3661,6 +3667,38 @@ overwriteSMPServers db User {userId} smpServers = (host, port, keyHash, userId, currentTs, currentTs) pure $ Right () +createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () +createCall db User {userId} Call {contactId, callId, chatItemId, callState} callTs = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + INSERT INTO calls + (contact_id, shared_call_id, chat_item_id, call_state, call_ts, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?) + |] + (contactId, callId, chatItemId, callState, callTs, userId, currentTs, currentTs) + +deleteCalls :: DB.Connection -> User -> ContactId -> IO () +deleteCalls db User {userId} contactId = do + DB.execute db "DELETE FROM calls WHERE user_id = ? AND contact_id = ?" (userId, contactId) + +getCalls :: DB.Connection -> User -> IO [Call] +getCalls db User {userId} = do + map toCall + <$> DB.query + db + [sql| + SELECT + contact_id, shared_call_id, chat_item_id, call_state, call_ts + FROM calls + WHERE user_id = ? + |] + (Only userId) + where + toCall :: (ContactId, CallId, ChatItemId, CallState, UTCTime) -> Call + toCall (contactId, callId, chatItemId, callState, callTs) = Call {contactId, callId, chatItemId, callState, callTs} + -- | Saves unique local display name based on passed displayName, suffixed with _N if required. -- This function should be called inside transaction. withLocalDisplayName :: forall a. DB.Connection -> UserId -> Text -> (Text -> IO a) -> IO (Either StoreError a) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 670a1cdbd2..e1dd53fa05 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -152,11 +152,12 @@ responseToView testView = \case ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] CRRcvFileSubError RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] - CRCallInvitation {contact, callType, sharedKey} -> viewCallInvitation contact callType sharedKey + CRCallInvitation RcvCallInvitation {contact, callType, sharedKey} -> viewCallInvitation contact callType sharedKey CRCallOffer {contact, callType, offer, sharedKey} -> viewCallOffer contact callType offer sharedKey CRCallAnswer {contact, answer} -> viewCallAnswer contact answer CRCallExtraInfo {contact} -> ["call extra info from " <> ttyContact' contact] CRCallEnded {contact} -> ["call with " <> ttyContact' contact <> " ended"] + CRCallInvitations _ -> [] CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"] CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"] CRNewContactConnection _ -> [] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 202191d8a7..4eb48b1b00 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -14,7 +14,7 @@ import Control.Concurrent.STM import Control.Exception (bracket, bracket_) import Control.Monad.Except import Data.List (dropWhileEnd, find) -import Data.Maybe (fromJust) +import Data.Maybe (fromJust, isNothing) import qualified Data.Text as T import Network.Socket import Simplex.Chat @@ -118,6 +118,7 @@ startTestChat_ st cfg opts dbFilePrefix user = do ct <- newChatTerminal t cc <- newChatController st (Just user) cfg opts {dbFilePrefix} Nothing -- no notifications chatAsync <- async . runSimplexChat opts user cc . const $ runChatTerminal ct + atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry termQ <- newTQueueIO termAsync <- async $ readTerminalOutput t termQ pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ} diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 43e3b1fbfe..51d4219ef7 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -2093,6 +2093,8 @@ testNegotiateCall :: IO () testNegotiateCall = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob + -- just for testing db query + alice ##> "/_call get" -- alice invite bob to call alice ##> ("/_call invite @2 " <> serialize testCallType) alice <## "ok" diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index bbf9e15588..923d735314 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -3,6 +3,7 @@ module SchemaDump where import ChatClient (withTmpFiles) +import Control.DeepSeq import Control.Monad (void) import Simplex.Chat.Store (createStore) import System.Process (readCreateProcess, shell) @@ -24,7 +25,7 @@ testVerifySchemaDump = void $ createStore testDB False void $ readCreateProcess (shell $ "touch " <> schema) "" savedSchema <- readFile schema - savedSchema `seq` pure () + savedSchema `deepseq` pure () void $ readCreateProcess (shell $ "sqlite3 " <> testDB <> " '.schema --indent' > " <> schema) "" currentSchema <- readFile schema savedSchema `shouldBe` currentSchema From 03ab4612a587418faaa6a5237271cc5a83b3909a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 4 Jul 2022 11:31:30 +0100 Subject: [PATCH 61/89] ios: text correction --- apps/ios/Shared/Views/UserSettings/NotificationsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index 71c6e2a6ce..4b8acc4c05 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -150,7 +150,7 @@ func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { switch mode { case .off: return "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." case .periodic: return "**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." - case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who is it from." + case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." } } From c323d6c61f351b44c579d53d2ec5151fa628147c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 4 Jul 2022 11:53:15 +0100 Subject: [PATCH 62/89] ios: update version v3.0 (55) --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 3e05ad8857..5a38c5827b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1105,7 +1105,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1145,7 +1145,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1224,7 +1224,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1254,7 +1254,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; From ab848e8c13e3d0d1e2ee2767dd48f6825c4072ff Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 5 Jul 2022 15:15:15 +0400 Subject: [PATCH 63/89] ios: refresh call invitations and report call on start and activation; core: restore calls on activation (#776) --- apps/ios/Shared/Model/ChatModel.swift | 2 +- apps/ios/Shared/Model/NtfManager.swift | 2 +- apps/ios/Shared/Model/SimpleXAPI.swift | 44 +++++++++++++------ apps/ios/Shared/SimpleXApp.swift | 11 ++++- .../Shared/Views/Call/CallController.swift | 10 ++--- apps/ios/Shared/Views/Call/CallManager.swift | 10 ++--- .../Shared/Views/Call/IncomingCallView.swift | 6 +-- apps/ios/SimpleXChat/APITypes.swift | 10 ++++- apps/ios/SimpleXChat/CallTypes.swift | 20 +++------ apps/ios/SimpleXChat/Notifications.swift | 4 +- src/Simplex/Chat.hs | 23 ++++++---- src/Simplex/Chat/Call.hs | 3 +- src/Simplex/Chat/Store.hs | 1 + 13 files changed, 89 insertions(+), 57 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index cc0731f2ad..7ebad52128 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -35,7 +35,7 @@ final class ChatModel: ObservableObject { @Published var notificationMode = NotificationsMode.off @Published var notificationPreview: NotificationPreviewMode? = .message // current WebRTC call - @Published var callInvitations: Dictionary = [:] + @Published var callInvitations: Dictionary = [:] @Published var activeCall: Call? @Published var callCommand: WCallCommand? @Published var showCallView = false diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 8a189ef726..307f1f8a61 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -184,7 +184,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { addNotification(createMessageReceivedNtf(cInfo, cItem)) } - func notifyCallInvitation(_ invitation: CallInvitation) { + func notifyCallInvitation(_ invitation: RcvCallInvitation) { logger.debug("NtfManager.notifyCallInvitation") addNotification(createCallInvitationNtf(invitation)) } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 2d0f9f775a..b28f4f0a84 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -476,6 +476,12 @@ func apiEndCall(_ contact: Contact) async throws { try await sendCommandOkResp(.apiEndCall(contact: contact)) } +func apiGetCallInvitations() throws -> [RcvCallInvitation] { + let r = chatSendCmdSync(.apiGetCallInvitations) + if case let .callInvitations(invs) = r { return invs } + throw r +} + func apiCallStatus(_ contact: Contact, _ status: String) async throws { if let callStatus = WebRTCCallStatus.init(rawValue: status) { try await sendCommandOkResp(.apiCallStatus(contact: contact, callStatus: callStatus)) @@ -538,6 +544,7 @@ func startChat() throws { m.userSMPServers = try getUserSMPServers() let chats = try apiGetChats() m.chats = chats.map { Chat.init($0) } + try refreshCallInvitations() (m.savedToken, m.tokenStatus, m.notificationMode) = try apiGetNtfToken() if let token = m.deviceToken { registerToken(token: token) @@ -694,19 +701,9 @@ func processReceivedMsg(_ res: ChatResponse) async { let fileName = cItem.file?.filePath { removeFile(fileName) } - case let .callInvitation(contact, callType, sharedKey, callTs): - let uuid = UUID() - var invitation = CallInvitation(contact: contact, callkitUUID: uuid, peerMedia: callType.media, sharedKey: sharedKey, callTs: callTs) - m.callInvitations[contact.id] = invitation - CallController.shared.reportNewIncomingCall(invitation: invitation) { error in - if let error = error { - invitation.callkitUUID = nil - m.callInvitations[contact.id] = invitation - logger.error("reportNewIncomingCall error: \(error.localizedDescription)") - } else { - logger.debug("reportNewIncomingCall success") - } - } + case let .callInvitation(invitation): + m.callInvitations[invitation.contact.id] = invitation + activateCall(invitation) // This will be called from notification service extension // CXProvider.reportNewIncomingVoIPPushPayload([ @@ -790,6 +787,27 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) { m.updateNetworkStatus(contact.id, .error(err)) } +func refreshCallInvitations() throws { + let m = ChatModel.shared + let callInvitations = try apiGetCallInvitations() + m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv } + if let inv = callInvitations.last { + activateCall(inv) + } +} + +func activateCall(_ callInvitation: RcvCallInvitation) { + let m = ChatModel.shared + CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in + if let error = error { + m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil + logger.error("reportNewIncomingCall error: \(error.localizedDescription)") + } else { + logger.debug("reportNewIncomingCall success") + } + } +} + private struct UserResponse: Decodable { var user: User? var error: String? diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 6b543a6577..5347851a74 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -63,8 +63,8 @@ struct SimpleXApp: App { let appState = appStateGroupDefault.get() activateChat() if appState.inactive && chatModel.chatRunning == true { - // TODO refresh call invitation updateChats() + updateCallInvitations() } doAuthenticate = authenticationExpired() default: @@ -123,4 +123,13 @@ struct SimpleXApp: App { logger.error("apiGetChats: cannot update chats \(responseError(error))") } } + + private func updateCallInvitations() { + do { + try refreshCallInvitations() + } + catch let error { + logger.error("apiGetCallInvitations: cannot update call invitations \(responseError(error))") + } + } } diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index df103c02ea..23e0609df3 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -18,7 +18,7 @@ class CallController: NSObject, ObservableObject { // private let provider = CXProvider(configuration: CallController.configuration) // private let controller = CXCallController() private let callManager = CallManager() - @Published var activeCallInvitation: CallInvitation? + @Published var activeCallInvitation: RcvCallInvitation? // PKPushRegistry will be used from notification service extension // let registry = PKPushRegistry(queue: nil) @@ -120,7 +120,7 @@ class CallController: NSObject, ObservableObject { // } // } - func reportNewIncomingCall(invitation: CallInvitation, completion: @escaping (Error?) -> Void) { + func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) { logger.debug("CallController.reportNewIncomingCall") if !UserDefaults.standard.bool(forKey: DEFAULT_EXPERIMENTAL_CALLS) { return } // if CallController.useCallKit, let uuid = invitation.callkitUUID { @@ -142,7 +142,7 @@ class CallController: NSObject, ObservableObject { // } // } - func reportCallRemoteEnded(invitation: CallInvitation) { + func reportCallRemoteEnded(invitation: RcvCallInvitation) { // if CallController.useCallKit, let uuid = invitation.callkitUUID { // provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded) // } else if invitation.contact.id == activeCallInvitation?.contact.id { @@ -172,7 +172,7 @@ class CallController: NSObject, ObservableObject { } } - func answerCall(invitation: CallInvitation) { + func answerCall(invitation: RcvCallInvitation) { callManager.answerIncomingCall(invitation: invitation) if invitation.contact.id == self.activeCallInvitation?.contact.id { self.activeCallInvitation = nil @@ -193,7 +193,7 @@ class CallController: NSObject, ObservableObject { // } } - func endCall(invitation: CallInvitation) { + func endCall(invitation: RcvCallInvitation) { callManager.endCall(invitation: invitation) { if invitation.contact.id == self.activeCallInvitation?.contact.id { DispatchQueue.main.async { diff --git a/apps/ios/Shared/Views/Call/CallManager.swift b/apps/ios/Shared/Views/Call/CallManager.swift index 493a3a1d64..61bd3b9b31 100644 --- a/apps/ios/Shared/Views/Call/CallManager.swift +++ b/apps/ios/Shared/Views/Call/CallManager.swift @@ -34,7 +34,7 @@ class CallManager { return false } - func answerIncomingCall(invitation: CallInvitation) { + func answerIncomingCall(invitation: RcvCallInvitation) { let m = ChatModel.shared m.callInvitations.removeValue(forKey: invitation.contact.id) m.activeCall = Call( @@ -42,13 +42,13 @@ class CallManager { contact: invitation.contact, callkitUUID: invitation.callkitUUID, callState: .invitationAccepted, - localMedia: invitation.peerMedia, + localMedia: invitation.callType.media, sharedKey: invitation.sharedKey ) m.showCallView = true let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) logger.debug("answerIncomingCall useRelay \(useRelay)") - m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true, relay: useRelay) + m.callCommand = .start(media: invitation.callType.media, aesKey: invitation.sharedKey, useWorker: true, relay: useRelay) } func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) { @@ -86,7 +86,7 @@ class CallManager { } } - func endCall(invitation: CallInvitation, completed: @escaping () -> Void) { + func endCall(invitation: RcvCallInvitation, completed: @escaping () -> Void) { ChatModel.shared.callInvitations.removeValue(forKey: invitation.contact.id) Task { do { @@ -98,7 +98,7 @@ class CallManager { } } - private func getCallInvitation(_ callUUID: UUID) -> CallInvitation? { + private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? { if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) { return invitation } diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift index 45651b89b1..22a14e4d5b 100644 --- a/apps/ios/Shared/Views/Call/IncomingCallView.swift +++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift @@ -26,10 +26,10 @@ struct IncomingCallView: View { } } - private func incomingCall(_ invitation: CallInvitation) -> some View { + private func incomingCall(_ invitation: RcvCallInvitation) -> some View { VStack(alignment: .leading, spacing: 6) { HStack { - Image(systemName: invitation.peerMedia == .video ? "video.fill" : "phone.fill").foregroundColor(.green) + Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green) Text(invitation.callTypeText) } HStack { @@ -81,7 +81,7 @@ struct IncomingCallView: View { struct IncomingCallView_Previews: PreviewProvider { static var previews: some View { - CallController.shared.activeCallInvitation = CallInvitation.sampleData + CallController.shared.activeCallInvitation = RcvCallInvitation.sampleData return IncomingCallView() } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index d654ea61b3..6fa970ae62 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -52,6 +52,7 @@ public enum ChatCommand { case apiSendCallAnswer(contact: Contact, answer: WebRTCSession) case apiSendCallExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo) case apiEndCall(contact: Contact) + case apiGetCallInvitations case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case receiveFile(fileId: Int64) @@ -100,6 +101,7 @@ public enum ChatCommand { case let .apiSendCallAnswer(contact, answer): return "/_call answer @\(contact.apiId) \(encodeJSON(answer))" case let .apiSendCallExtraInfo(contact, extraInfo): return "/_call extra @\(contact.apiId) \(encodeJSON(extraInfo))" case let .apiEndCall(contact): return "/_call end @\(contact.apiId)" + case .apiGetCallInvitations: return "/_call get" case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" case let .receiveFile(fileId): return "/freceive \(fileId)" @@ -149,6 +151,7 @@ public enum ChatCommand { case .apiSendCallAnswer: return "apiSendCallAnswer" case .apiSendCallExtraInfo: return "apiSendCallExtraInfo" case .apiEndCall: return "apiEndCall" + case .apiGetCallInvitations: return "apiGetCallInvitations" case .apiCallStatus: return "apiCallStatus" case .apiChatRead: return "apiChatRead" case .receiveFile: return "receiveFile" @@ -219,11 +222,12 @@ public enum ChatResponse: Decodable, Error { case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileRcvCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndGroupFileCancelled(chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer]) - case callInvitation(contact: Contact, callType: CallType, sharedKey: String?, callTs: Date) + case callInvitation(callInvitation: RcvCallInvitation) case callOffer(contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) case callAnswer(contact: Contact, answer: WebRTCSession) case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo) case callEnded(contact: Contact) + case callInvitations(callInvitations: [RcvCallInvitation]) case ntfTokenStatus(status: NtfTknStatus) case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode) case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) @@ -287,6 +291,7 @@ public enum ChatResponse: Decodable, Error { case .callAnswer: return "callAnswer" case .callExtraInfo: return "callExtraInfo" case .callEnded: return "callEnded" + case .callInvitations: return "callInvitations" case .ntfTokenStatus: return "ntfTokenStatus" case .ntfToken: return "ntfToken" case .ntfMessages: return "ntfMessages" @@ -348,11 +353,12 @@ public enum ChatResponse: Decodable, Error { case let .sndFileCancelled(chatItem, _): return String(describing: chatItem) case let .sndFileRcvCancelled(chatItem, _): return String(describing: chatItem) case let .sndGroupFileCancelled(chatItem, _, _): return String(describing: chatItem) - case let .callInvitation(contact, callType, sharedKey, _): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")" + case let .callInvitation(inv): return String(describing: inv) case let .callOffer(contact, callType, offer, sharedKey, askConfirmation): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))" case let .callAnswer(contact, answer): return "contact: \(contact.id)\nanswer: \(String(describing: answer))" case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))" case let .callEnded(contact): return "contact: \(contact.id)" + case let .callInvitations(invs): return String(describing: invs) case let .ntfTokenStatus(status): return String(describing: status) case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)" case let .ntfMessages(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))" diff --git a/apps/ios/SimpleXChat/CallTypes.swift b/apps/ios/SimpleXChat/CallTypes.swift index 01f05ac4fd..1d5fa360ca 100644 --- a/apps/ios/SimpleXChat/CallTypes.swift +++ b/apps/ios/SimpleXChat/CallTypes.swift @@ -37,32 +37,24 @@ public struct WebRTCExtraInfo: Codable { public var rtcIceCandidates: String } -public struct CallInvitation { - public init(contact: Contact, callkitUUID: UUID? = nil, peerMedia: CallMediaType, sharedKey: String? = nil, callTs: Date) { - self.contact = contact - self.callkitUUID = callkitUUID - self.peerMedia = peerMedia - self.sharedKey = sharedKey - self.callTs = callTs - } - +public struct RcvCallInvitation: Decodable { public var contact: Contact - public var callkitUUID: UUID? - public var peerMedia: CallMediaType + public var callkitUUID: UUID? = UUID() + public var callType: CallType public var sharedKey: String? public var callTs: Date public var callTypeText: LocalizedStringKey { get { - switch peerMedia { + switch callType.media { case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call" case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call" } } } - public static let sampleData = CallInvitation( + public static let sampleData = RcvCallInvitation( contact: Contact.sampleData, - peerMedia: .audio, + callType: CallType(media: .audio, capabilities: CallCapabilities(encryption: false)), callTs: .now ) } diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 9413e6381b..535dc2c5bb 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -50,8 +50,8 @@ public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UN ) } -public func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent { - let text = invitation.peerMedia == .video +public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutableNotificationContent { + let text = invitation.callType.media == .video ? NSLocalizedString("Incoming video call", comment: "notification") : NSLocalizedString("Incoming audio call", comment: "notification") return createNotification( diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 820c454cf1..3fb05ddb63 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -150,7 +150,7 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m (Async ()) startChatController user subConns = do asks smpAgent >>= resumeAgentClient - restoreCalls + restoreCalls user s <- asks agentAsync readTVarIO s >>= maybe (start s) (pure . fst) where @@ -162,11 +162,13 @@ startChatController user subConns = do else pure Nothing atomically . writeTVar s $ Just (a1, a2) pure a1 - restoreCalls = do - savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db user) - let callsMap = M.fromList $ map (\(call@Call {contactId}) -> (contactId, call)) savedCalls - calls <- asks currentCalls - atomically $ writeTVar calls callsMap + +restoreCalls :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m () +restoreCalls user = do + savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db user) + let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls + calls <- asks currentCalls + atomically $ writeTVar calls callsMap stopChatController :: MonadUnliftIO m => ChatController -> m () stopChatController ChatController {smpAgent, agentAsync = s} = do @@ -213,7 +215,9 @@ processChatCommand = \case APIStopChat -> do ask >>= stopChatController pure CRChatStopped - APIActivateChat -> withAgent activateAgent $> CRCmdOk + APIActivateChat -> do + withUser $ \user -> restoreCalls user + withAgent activateAgent $> CRCmdOk APISuspendChat t -> withAgent (`suspendAgent` t) $> CRCmdOk ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk SetFilesFolder filesFolder' -> do @@ -544,8 +548,9 @@ processChatCommand = \case SndMessage {msgId} <- sendDirectContactMessage ct (XCallEnd callId) updateCallItemStatus userId ct call WCSDisconnected $ Just msgId pure Nothing - APIGetCallInvitations -> withUser $ \user@User {userId} -> do - invs <- mapMaybe callInvitation <$> withStore' (`getCalls` user) + APIGetCallInvitations -> withUser $ \User {userId} -> do + calls <- asks currentCalls >>= readTVarIO + let invs = mapMaybe callInvitation $ M.elems calls CRCallInvitations <$> mapM (rcvCallInvitation userId) invs where callInvitation Call {contactId, callState, callTs} = case callState of diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs index 54ce2dad9c..de4336493a 100644 --- a/src/Simplex/Chat/Call.hs +++ b/src/Simplex/Chat/Call.hs @@ -33,6 +33,7 @@ data Call = Call callState :: CallState, callTs :: UTCTime } + deriving (Show) isRcvInvitation :: Call -> Bool isRcvInvitation Call {callState} = case callState of @@ -88,7 +89,7 @@ data CallState peerCallSession :: WebRTCSession, sharedKey :: Maybe C.Key } - deriving (Generic) + deriving (Show, Generic) -- database representation instance FromJSON CallState where diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 7fda746f23..3f79794cb7 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -3693,6 +3693,7 @@ getCalls db User {userId} = do contact_id, shared_call_id, chat_item_id, call_state, call_ts FROM calls WHERE user_id = ? + ORDER BY call_ts ASC |] (Only userId) where From f7a27ff91b5e8dfc5b557ca0bc1239e274ae6b38 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 5 Jul 2022 15:24:51 +0400 Subject: [PATCH 64/89] ios: make calls non expirmental feature, hide experimental features (#777) --- .../Shared/Views/Call/CallController.swift | 1 - apps/ios/Shared/Views/Chat/ChatView.swift | 3 +-- .../Views/UserSettings/CallSettings.swift | 7 +++--- .../Views/UserSettings/SettingsView.swift | 25 ++++++++----------- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index 23e0609df3..5c0e23360c 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -122,7 +122,6 @@ class CallController: NSObject, ObservableObject { func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) { logger.debug("CallController.reportNewIncomingCall") - if !UserDefaults.standard.bool(forKey: DEFAULT_EXPERIMENTAL_CALLS) { return } // if CallController.useCallKit, let uuid = invitation.callkitUUID { // let update = CXCallUpdate() // update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.displayName) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 6c5bd9109a..bafe833368 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -14,7 +14,6 @@ private let memberImageSize: CGFloat = 34 struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.colorScheme) var colorScheme - @AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false @ObservedObject var chat: Chat @Binding var showChatInfo: Bool @State private var composeState = ComposeState() @@ -108,7 +107,7 @@ struct ChatView: View { } } ToolbarItem(placement: .navigationBarTrailing) { - if enableCalls, case let .direct(contact) = cInfo { + if case let .direct(contact) = cInfo { HStack { callButton(contact, .audio, imageName: "phone") callButton(contact, .video, imageName: "video") diff --git a/apps/ios/Shared/Views/UserSettings/CallSettings.swift b/apps/ios/Shared/Views/UserSettings/CallSettings.swift index 516b237538..7bc79dd0b3 100644 --- a/apps/ios/Shared/Views/UserSettings/CallSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/CallSettings.swift @@ -21,10 +21,9 @@ struct CallSettings: View { Section("Limitations") { VStack(alignment: .leading, spacing: 8) { textListItem("1.", "Do NOT use SimpleX for emergency calls.") - textListItem("2.", "Pre-arrange the calls, as notifications arrive with a delay (we are improving it).") - textListItem("3.", "The microphone does not work when the app is in the background.") - textListItem("4.", "To prevent the call interruption, enable Do Not Disturb mode.") - textListItem("5.", "If the video fails to connect, flip the camera to resolve it.") + textListItem("2.", "The microphone does not work when the app is in the background.") + textListItem("3.", "To prevent the call interruption, enable Do Not Disturb mode.") + textListItem("4.", "If the video fails to connect, flip the camera to resolve it.") } .font(.callout) .padding(.vertical, 8) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 40827bdf49..ab2b8836fd 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -48,7 +48,6 @@ struct SettingsView: View { @EnvironmentObject var chatModel: ChatModel @Binding var showSettings: Bool @AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true - @AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false var body: some View { let user: User = chatModel.currentUser! @@ -99,13 +98,11 @@ struct SettingsView: View { Text("Notifications") } } - if enableCalls { - NavigationLink { - CallSettings() - .navigationTitle("Your calls") - } label: { - settingsRow("video") { Text("Audio & video calls") } - } + NavigationLink { + CallSettings() + .navigationTitle("Your calls") + } label: { + settingsRow("video") { Text("Audio & video calls") } } NavigationLink { PrivacySettings() @@ -176,12 +173,12 @@ struct SettingsView: View { Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)") .padding(.leading, indent) } - NavigationLink { - ExperimentalFeaturesView() - .navigationTitle("Experimental features") - } label: { - settingsRow("gauge") { Text("Experimental features") } - } +// NavigationLink { +// ExperimentalFeaturesView() +// .navigationTitle("Experimental features") +// } label: { +// settingsRow("gauge") { Text("Experimental features") } +// } Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") } } From 935d5bfdd63afdfaf80d7c8dae147d12c9a6e676 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 5 Jul 2022 15:25:29 +0400 Subject: [PATCH 65/89] android: use RcvCallInvitation type in CallInvitation event (#778) --- .../main/java/chat/simplex/app/model/ChatModel.kt | 4 ++-- .../main/java/chat/simplex/app/model/NtfManager.kt | 4 ++-- .../main/java/chat/simplex/app/model/SimpleXAPI.kt | 10 ++++------ .../chat/simplex/app/views/call/CallManager.kt | 14 +++++++------- .../simplex/app/views/call/IncomingCallActivity.kt | 8 ++++---- .../app/views/call/IncomingCallAlertView.kt | 12 ++++++------ .../java/chat/simplex/app/views/call/WebRTC.kt | 6 +++--- 7 files changed, 28 insertions(+), 30 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 8f27073ca4..25276fc6bf 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -45,8 +45,8 @@ class ChatModel(val controller: ChatController) { // current WebRTC call val callManager = CallManager(this) - val callInvitations = mutableStateMapOf() - val activeCallInvitation = mutableStateOf(null) + val callInvitations = mutableStateMapOf() + val activeCallInvitation = mutableStateOf(null) val activeCall = mutableStateOf(null) val callCommand = mutableStateOf(null) val showCallView = mutableStateOf(false) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt index dc9ebe5127..c64b1c8d24 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -102,7 +102,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference } } - fun notifyCallInvitation(invitation: CallInvitation) { + fun notifyCallInvitation(invitation: RcvCallInvitation) { if (isAppOnForeground(context)) return val contactId = invitation.contact.id Log.d(TAG, "notifyCallInvitation $contactId") @@ -124,7 +124,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference .setSound(soundUri) } val text = generalGetString( - if (invitation.peerMedia == CallMediaType.Video) { + if (invitation.callType.media == CallMediaType.Video) { if (invitation.sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call } else { if (invitation.sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index f1f66fbeba..10e40f30da 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -566,10 +566,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager removeFile(appContext, fileName) } } - is CR.CallInvitation -> { - val invitation = CallInvitation(r.contact, r.callType.media, r.sharedKey, r.callTs) - chatModel.callManager.reportNewIncomingCall(invitation) - } + is CR.CallInvitation -> + chatModel.callManager.reportNewIncomingCall(r.callInvitation) is CR.CallOffer -> { // TODO askConfirmation? // TODO check encryption is compatible @@ -1022,7 +1020,7 @@ sealed class CR { @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndGroupFileCancelled") class SndGroupFileCancelled(val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List): CR() - @Serializable @SerialName("callInvitation") class CallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant): CR() + @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() @Serializable @SerialName("callOffer") class CallOffer(val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() @Serializable @SerialName("callAnswer") class CallAnswer(val contact: Contact, val answer: WebRTCSession): CR() @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() @@ -1139,7 +1137,7 @@ sealed class CR { is SndFileRcvCancelled -> json.encodeToString(chatItem) is SndFileStart -> json.encodeToString(chatItem) is SndGroupFileCancelled -> json.encodeToString(chatItem) - is CallInvitation -> "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}" + is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}" is CallOffer -> "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}" is CallAnswer -> "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}" is CallExtraInfo -> "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}" diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt index 78f60ab1e8..226bb708fe 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt @@ -9,7 +9,7 @@ import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.minutes class CallManager(val chatModel: ChatModel) { - fun reportNewIncomingCall(invitation: CallInvitation) { + fun reportNewIncomingCall(invitation: RcvCallInvitation) { Log.d(TAG, "CallManager.reportNewIncomingCall") with (chatModel) { callInvitations[invitation.contact.id] = invitation @@ -24,7 +24,7 @@ class CallManager(val chatModel: ChatModel) { } } - fun acceptIncomingCall(invitation: CallInvitation) { + fun acceptIncomingCall(invitation: RcvCallInvitation) { ModalManager.shared.closeModals() val call = chatModel.activeCall.value if (call == null) { @@ -42,17 +42,17 @@ class CallManager(val chatModel: ChatModel) { } } - private fun justAcceptIncomingCall(invitation: CallInvitation) { + private fun justAcceptIncomingCall(invitation: RcvCallInvitation) { with (chatModel) { activeCall.value = Call( contact = invitation.contact, callState = CallState.InvitationAccepted, - localMedia = invitation.peerMedia, + localMedia = invitation.callType.media, sharedKey = invitation.sharedKey ) showCallView.value = true val useRelay = controller.appPrefs.webrtcPolicyRelay.get() - callCommand.value = WCallCommand.Start (media = invitation.peerMedia, aesKey = invitation.sharedKey, relay = useRelay) + callCommand.value = WCallCommand.Start (media = invitation.callType.media, aesKey = invitation.sharedKey, relay = useRelay) callInvitations.remove(invitation.contact.id) if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { activeCallInvitation.value = null @@ -77,7 +77,7 @@ class CallManager(val chatModel: ChatModel) { } } - fun endCall(invitation: CallInvitation) { + fun endCall(invitation: RcvCallInvitation) { with (chatModel) { callInvitations.remove(invitation.contact.id) if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { @@ -92,7 +92,7 @@ class CallManager(val chatModel: ChatModel) { } } - fun reportCallRemoteEnded(invitation: CallInvitation) { + fun reportCallRemoteEnded(invitation: RcvCallInvitation) { if (chatModel.activeCallInvitation.value?.contact?.id == invitation.contact.id) { chatModel.activeCallInvitation.value = null chatModel.controller.ntfManager.cancelCallNotification() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt index ff1b424f34..283b6be684 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt @@ -112,7 +112,7 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) { } @Composable -fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel, activity: IncomingCallActivity) { +fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel, activity: IncomingCallActivity) { val cm = chatModel.callManager val cxt = LocalContext.current val scope = rememberCoroutineScope() @@ -141,7 +141,7 @@ fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel @Composable fun IncomingCallLockScreenAlertLayout( - invitation: CallInvitation, + invitation: RcvCallInvitation, callOnLockScreen: CallOnLockScreen?, rejectCall: () -> Unit, ignoreCall: () -> Unit, @@ -210,9 +210,9 @@ fun PreviewIncomingCallLockScreenAlert() { .background(MaterialTheme.colors.background) .fillMaxSize()) { IncomingCallLockScreenAlertLayout( - invitation = CallInvitation( + invitation = RcvCallInvitation( contact = Contact.sampleData, - peerMedia = CallMediaType.Audio, + callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)), sharedKey = null, callTs = Clock.System.now() ), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt index c0960220cf..631649211c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt @@ -24,7 +24,7 @@ import chat.simplex.app.views.usersettings.ProfilePreview import kotlinx.datetime.Clock @Composable -fun IncomingCallAlertView(invitation: CallInvitation, chatModel: ChatModel) { +fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) { val cm = chatModel.callManager val cxt = LocalContext.current val scope = rememberCoroutineScope() @@ -40,7 +40,7 @@ fun IncomingCallAlertView(invitation: CallInvitation, chatModel: ChatModel) { @Composable fun IncomingCallAlertLayout( - invitation: CallInvitation, + invitation: RcvCallInvitation, rejectCall: () -> Unit, ignoreCall: () -> Unit, acceptCall: () -> Unit @@ -60,10 +60,10 @@ fun IncomingCallAlertLayout( } @Composable -fun IncomingCallInfo(invitation: CallInvitation) { +fun IncomingCallInfo(invitation: RcvCallInvitation) { @Composable fun CallIcon(icon: ImageVector, descr: String) = Icon(icon, descr, tint = SimplexGreen) Row { - if (invitation.peerMedia == CallMediaType.Video) CallIcon(Icons.Filled.Videocam, stringResource(R.string.icon_descr_video_call)) + if (invitation.callType.media == CallMediaType.Video) CallIcon(Icons.Filled.Videocam, stringResource(R.string.icon_descr_video_call)) else CallIcon(Icons.Filled.Phone, stringResource(R.string.icon_descr_audio_call)) Spacer(Modifier.width(4.dp)) Text(invitation.callTypeText) @@ -94,9 +94,9 @@ private fun CallButton(text: String, icon: ImageVector, color: Color, action: () fun PreviewIncomingCallAlertLayout() { SimpleXTheme { IncomingCallAlertLayout( - invitation = CallInvitation( + invitation = RcvCallInvitation( contact = Contact.sampleData, - peerMedia = CallMediaType.Audio, + callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)), sharedKey = null, callTs = Clock.System.now() ), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt index 2c8919f3b8..afef4ca823 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt @@ -90,12 +90,12 @@ sealed class WCallResponse { @Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String) @Serializable class WebRTCExtraInfo(val rtcIceCandidates: String) @Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities) -@Serializable class CallInvitation(val contact: Contact, val peerMedia: CallMediaType, val sharedKey: String?, val callTs: Instant) { - val callTypeText: String get() = generalGetString(when(peerMedia) { +@Serializable class RcvCallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String?, val callTs: Instant) { + val callTypeText: String get() = generalGetString(when(callType.media) { CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call }) - val callTitle: String get() = generalGetString(when(peerMedia) { + val callTitle: String get() = generalGetString(when(callType.media) { CallMediaType.Video -> R.string.incoming_video_call CallMediaType.Audio -> R.string.incoming_audio_call }) From 65fae747c3eee4a536c92e7f04cf1e29b7e0f57d Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 5 Jul 2022 15:27:42 +0400 Subject: [PATCH 66/89] android: make calls non expirmental feature, hide experimental features (#779) --- .../java/chat/simplex/app/views/call/CallManager.kt | 1 - .../java/chat/simplex/app/views/chat/ChatView.kt | 9 +++------ .../simplex/app/views/usersettings/SettingsView.kt | 13 ++++--------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt index 226bb708fe..441b97d6bd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt @@ -13,7 +13,6 @@ class CallManager(val chatModel: ChatModel) { Log.d(TAG, "CallManager.reportNewIncomingCall") with (chatModel) { callInvitations[invitation.contact.id] = invitation - if (!chatModel.controller.appPrefs.experimentalCalls.get()) return if (Clock.System.now() - invitation.callTs <= 3.minutes) { activeCallInvitation.value = invitation controller.ntfManager.notifyCallInvitation(invitation) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index b0d59c9030..6d94fd8c74 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -45,7 +45,6 @@ fun ChatView(chatModel: ChatModel) { val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } val user = chatModel.currentUser.value val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() - val enableCalls = chatModel.controller.appPrefs.experimentalCalls.get() val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) } val attachmentOption = remember { mutableStateOf(null) } val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) @@ -86,7 +85,6 @@ fun ChatView(chatModel: ChatModel) { attachmentBottomSheetState, chatModel.chatItems, useLinkPreviews = useLinkPreviews, - enableCalls = enableCalls, back = { chatModel.chatId.value = null }, info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } }, openDirectChat = { contactId -> @@ -141,7 +139,6 @@ fun ChatLayout( attachmentBottomSheetState: ModalBottomSheetState, chatItems: List, useLinkPreviews: Boolean, - enableCalls: Boolean = false, back: () -> Unit, info: () -> Unit, openDirectChat: (Long) -> Unit, @@ -169,7 +166,7 @@ fun ChatLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { Scaffold( - topBar = { ChatInfoToolbar(chat, enableCalls, back, info, startCall) }, + topBar = { ChatInfoToolbar(chat, back, info, startCall) }, bottomBar = composeView, modifier = Modifier.navigationBarsWithImePadding() ) { contentPadding -> @@ -183,7 +180,7 @@ fun ChatLayout( } @Composable -fun ChatInfoToolbar(chat: Chat, enableCalls: Boolean, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit) { +fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit) { @Composable fun toolbarButton(icon: ImageVector, @StringRes textId: Int, modifier: Modifier = Modifier.padding(0.dp), onClick: () -> Unit) { IconButton(onClick, modifier = modifier) { Icon(icon, stringResource(textId), tint = MaterialTheme.colors.primary) @@ -200,7 +197,7 @@ fun ChatInfoToolbar(chat: Chat, enableCalls: Boolean, back: () -> Unit, info: () ) { val cInfo = chat.chatInfo toolbarButton(Icons.Outlined.ArrowBackIos, R.string.back, onClick = back) - if (cInfo is ChatInfo.Direct && enableCalls) { + if (cInfo is ChatInfo.Direct) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { Box(Modifier.width(85.dp), contentAlignment = Alignment.CenterStart) { toolbarButton(Icons.Outlined.Phone, R.string.icon_descr_audio_call) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 505ada39ae..582a387e9a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -45,7 +45,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { runServiceInBackground = chatModel.runServiceInBackground, setRunServiceInBackground = ::setRunServiceInBackground, setPerformLA = setPerformLA, - enableCalls = remember { mutableStateOf(chatModel.controller.appPrefs.experimentalCalls.get()) }, showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } }, showSettingsModal = { modalView -> { ModalManager.shared.showCustomModal { close -> ModalView(close = close, modifier = Modifier, @@ -69,7 +68,6 @@ fun SettingsLayout( runServiceInBackground: MutableState, setRunServiceInBackground: (Boolean) -> Unit, setPerformLA: (Boolean) -> Unit, - enableCalls: MutableState, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), @@ -103,10 +101,8 @@ fun SettingsLayout( spacer() SettingsSectionView(stringResource(R.string.settings_section_title_settings)) { - if (enableCalls.value) { - SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it) }) - divider() - } + SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it) }) + divider() SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }) divider() PrivateNotificationsItem(runServiceInBackground, setRunServiceInBackground) @@ -133,8 +129,8 @@ fun SettingsLayout( divider() InstallTerminalAppItem(uriHandler) divider() - SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) }) - divider() +// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) }) +// divider() AppVersionItem() } } @@ -302,7 +298,6 @@ fun PreviewSettingsLayout() { runServiceInBackground = remember { mutableStateOf(true) }, setRunServiceInBackground = {}, setPerformLA = {}, - enableCalls = remember { mutableStateOf(true) }, showModal = { {} }, showSettingsModal = { {} }, showCustomModal = { {} }, From 111acb081333b47e937146bba9d12dd32aa27765 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 08:46:04 +0100 Subject: [PATCH 67/89] core: use NTF scheme for notification server address (#774) * core: use NTF scheme for notification server address * simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 8 ++++---- stack.yaml | 2 +- tests/ChatClient.hs | 1 + tests/ProtocolTests.hs | 9 ++------- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/cabal.project b/cabal.project index b0bb9f2d75..f6ee24247e 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 4c0164c49effe8d1b3fd98aca184245a8b4cac93 + tag: abd694bd5af57b596f48214fe3c0d0e8982fc793 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 6ecd9cde90..6fb28658c4 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."4c0164c49effe8d1b3fd98aca184245a8b4cac93" = "1ni3phxal10rfrkgnhcih3ksh7nfnj5b7vkk0qs9k52hmc8g306b"; + "https://github.com/simplex-chat/simplexmq.git"."abd694bd5af57b596f48214fe3c0d0e8982fc793" = "0j8m4hcms8ffkbpspds8crvcs1gslgrk4xqp6xvgb0hhv499541n"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 3fb05ddb63..7a9f901c8f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -5,6 +5,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -60,9 +61,8 @@ import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Types (NtfServer) import Simplex.Messaging.Parsers (base64P, parseAll) -import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..)) +import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..), NtfServer) import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (ifM, liftEitherError, tryError, unlessM, whenM, (<$?>)) @@ -104,7 +104,7 @@ _defaultSMPServers = ] _defaultNtfServers :: [NtfServer] -_defaultNtfServers = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"] +_defaultNtfServers = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"] maxImageSize :: Integer maxImageSize = 236700 @@ -1126,7 +1126,7 @@ processAgentMessage (Just User {userId}) "" agentMessage = case agentMessage of SUSPENDED -> toView CRChatSuspended _ -> pure () where - serverEvent srv@SMP.ProtocolServer {host, port} conns event str = do + serverEvent srv@(SMPServer host port _) conns event str = do cs <- withStore' $ \db -> getConnectionsContacts db userId conns toView $ event srv cs showToast ("server " <> str) (safeDecodeUtf8 . strEncode $ SrvLoc host port) diff --git a/stack.yaml b/stack.yaml index 9d6e1ba0cb..dbc8d7c173 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 4c0164c49effe8d1b3fd98aca184245a8b4cac93 + commit: abd694bd5af57b596f48214fe3c0d0e8982fc793 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 4eb48b1b00..b45adf8cf6 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -274,6 +274,7 @@ serverCfg = certificateFile = "tests/fixtures/tls/server.crt", logStatsInterval = Just 86400, logStatsStartTime = 0, + serverStatsFile = Nothing, smpServerVRange = supportedSMPServerVRange } diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 1be897fec0..e7ffcc2d07 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -15,7 +15,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Protocol (ProtocolServer (..), smpClientVRange) +import Simplex.Messaging.Protocol (smpClientVRange) import Simplex.Messaging.Version import Test.Hspec @@ -23,12 +23,7 @@ protocolTests :: Spec protocolTests = decodeChatMessageTest srv :: SMPServer -srv = - ProtocolServer - { host = "smp.simplex.im", - port = "5223", - keyHash = C.KeyHash "\215m\248\251" - } +srv = SMPServer "smp.simplex.im" "5223" (C.KeyHash "\215m\248\251") queue :: SMPQueueUri queue = From 36dc66d5d53450807bd5b6ef876c1afa1bdf412a Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:48:38 +0400 Subject: [PATCH 68/89] core: use NTF scheme for notification server address (Terminal.hs) --- src/Simplex/Chat/Terminal.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 27014b091e..45686804e9 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -28,7 +28,7 @@ terminalChatConfig = "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im" ], - ntf = ["smp://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"] + ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"] } } From ab6301c3e909bb6421bd55e697a71cfe08b07d2a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:52:10 +0100 Subject: [PATCH 69/89] ios: notification preview mode, show connection entity notification (#781) * ios: notification preview mode, show connection entity notification * prepare connection entity notification as best attempt --- apps/ios/Shared/Model/ChatModel.swift | 2 +- apps/ios/Shared/Model/NtfManager.swift | 15 +--- .../UserSettings/NotificationsView.swift | 14 ++- .../ios/SimpleX NSE/NotificationService.swift | 29 +++---- apps/ios/SimpleXChat/APITypes.swift | 4 +- apps/ios/SimpleXChat/AppGroup.swift | 7 ++ apps/ios/SimpleXChat/ChatTypes.swift | 7 ++ apps/ios/SimpleXChat/Notifications.swift | 86 ++++++++++++++++--- 8 files changed, 115 insertions(+), 49 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 7ebad52128..92170ecbb3 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -33,7 +33,7 @@ final class ChatModel: ObservableObject { @Published var savedToken: DeviceToken? @Published var tokenStatus: NtfTknStatus? @Published var notificationMode = NotificationsMode.off - @Published var notificationPreview: NotificationPreviewMode? = .message + @Published var notificationPreview: NotificationPreviewMode? = ntfPreviewModeGroupDefault.get() // current WebRTC call @Published var callInvitations: Dictionary = [:] @Published var activeCall: Call? diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 307f1f8a61..de78a60780 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -136,12 +136,11 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: NSLocalizedString("Incoming call", comment: "notification") ), - // TODO remove UNNotificationCategory( - identifier: ntfCategoryCheckingMessages, + identifier: ntfCategoryConnectionEvent, actions: [], intentIdentifiers: [], - hiddenPreviewsBodyPlaceholder: NSLocalizedString("Checking new messages...", comment: "notification") + hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification") ) ]) } @@ -189,16 +188,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { addNotification(createCallInvitationNtf(invitation)) } - // TODO remove - func notifyCheckingMessages() { - logger.debug("NtfManager.notifyCheckingMessages") - let content = createNotification( - categoryIdentifier: ntfCategoryCheckingMessages, - title: NSLocalizedString("Checking new messages...", comment: "notification") - ) - addNotification(content) - } - private func addNotification(_ content: UNMutableNotificationContent) { if !granted { return } let trigger = UNTimeIntervalNotificationTrigger(timeInterval: ntfTimeInterval, repeats: false) diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index 4b8acc4c05..4e4001b24e 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -55,9 +55,19 @@ struct NotificationsView: View { NavigationLink { List { Section { - SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) + SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) { previewMode in + ntfPreviewModeGroupDefault.set(previewMode) + m.notificationPreview = previewMode + } } footer: { - + VStack(alignment: .leading, spacing: 1) { + Text("You can set lock screen notification preview via settings.") + Button("Open Settings") { + DispatchQueue.main.async { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + } + } } } .navigationTitle("Show preview") diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 2fa61c57df..c937786210 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -45,23 +45,14 @@ class NotificationService: UNNotificationServiceExtension { let encNtfInfo = ntfData["message"] as? String, let _ = startChat() { if let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - if let content = receiveMessageForNotification() { - contentHandler(content) - } else if let connEntity = ntfMsgInfo.connEntity { - switch connEntity { - case let .rcvDirectMsgConnection(_, contact): - () - case let .rcvGroupMsgConnection(_, groupInfo, groupMember): - () - case let .sndFileConnection(_, sndFileTransfer): - () - case let .rcvFileConnection(_, rcvFileTransfer): - () - case let .userContactConnection(_, userContact): - () - } - contentHandler(request.content) - } + if let connEntity = ntfMsgInfo.connEntity { + bestAttemptContent = createConnectionEventNtf(connEntity) + } + if let content = receiveMessageForNotification() { + contentHandler(content) + } else if let content = bestAttemptContent { + contentHandler(content) + } } } } @@ -70,8 +61,8 @@ class NotificationService: UNNotificationServiceExtension { logger.debug("NotificationService.serviceExtensionTimeWillExpire") // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = self.contentHandler, let bestAttemptContent = self.bestAttemptContent { - contentHandler(bestAttemptContent) + if let contentHandler = self.contentHandler, let content = bestAttemptContent { + contentHandler(content) } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 6fa970ae62..b2622a539e 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -452,8 +452,8 @@ public enum NotificationPreviewMode: String, SelectableItem { public var label: LocalizedStringKey { switch self { case .hidden: return "Hidden" - case .contact: return "Contact" - case .message: return "Message" + case .contact: return "Contact name" + case .message: return "Message text" } } diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index b71d192969..a1ee992750 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -12,6 +12,7 @@ import SwiftUI let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" +let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" let APP_GROUP_NAME = "group.chat.simplex.app" @@ -51,6 +52,12 @@ public let dbContainerGroupDefault = EnumDefault( public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START) +public let ntfPreviewModeGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NTF_PREVIEW_MODE, + withDefault: .message +) + public class DateDefault { var defaults: UserDefaults var key: String diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 1b97a14a0c..04bf5a50dd 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -426,6 +426,13 @@ public struct GroupMember: Decodable { } } + public var chatViewName: String { + get { + let p = memberProfile + return p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") + } + } + public static let sampleData = GroupMember( groupMemberId: 1, memberId: "abcd", diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 535dc2c5bb..8f27063242 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -14,37 +14,59 @@ public let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST" public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED" public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED" public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION" +public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT" public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE" -// TODO remove -public let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES" public let appNotificationId = "chat.simplex.app.notification" +let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification") + public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent { - createNotification( + let hideContent = ntfPreviewModeGroupDefault.get() == .hidden + return createNotification( categoryIdentifier: ntfCategoryContactRequest, - title: String.localizedStringWithFormat(NSLocalizedString("%@ wants to connect!", comment: "notification title"), contactRequest.displayName), - body: String.localizedStringWithFormat(NSLocalizedString("Accept contact request from %@?", comment: "notification body"), contactRequest.chatViewName), + title: String.localizedStringWithFormat( + NSLocalizedString("%@ wants to connect!", comment: "notification title"), + hideContent ? NSLocalizedString("Somebody", comment: "notification title") : contactRequest.displayName + ), + body: String.localizedStringWithFormat( + NSLocalizedString("Accept contact request from %@?", comment: "notification body"), + hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName + ), targetContentIdentifier: nil, userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId] ) } public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent { - createNotification( + let hideContent = ntfPreviewModeGroupDefault.get() == .hidden + return createNotification( categoryIdentifier: ntfCategoryContactConnected, - title: String.localizedStringWithFormat(NSLocalizedString("%@ is connected!", comment: "notification title"), contact.displayName), - body: String.localizedStringWithFormat(NSLocalizedString("You can now send messages to %@", comment: "notification body"), contact.chatViewName), + title: String.localizedStringWithFormat( + NSLocalizedString("%@ is connected!", comment: "notification title"), + hideContent ? NSLocalizedString("A new contact", comment: "notification title") : contact.displayName + ), + body: String.localizedStringWithFormat( + NSLocalizedString("You can now send messages to %@", comment: "notification body"), + hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName + ), targetContentIdentifier: contact.id // userInfo: ["chatId": contact.id, "contactId": contact.apiId] ) } public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { - createNotification( + let previewMode = ntfPreviewModeGroupDefault.get() + var title: String + if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { + title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: previewMode == .hidden) + } else { + title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):" + } + return createNotification( categoryIdentifier: ntfCategoryMessageReceived, - title: "\(cInfo.chatViewName):", - body: hideSecrets(cItem), + title: title, + body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"), targetContentIdentifier: cInfo.id // userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id] ) @@ -54,15 +76,55 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl let text = invitation.callType.media == .video ? NSLocalizedString("Incoming video call", comment: "notification") : NSLocalizedString("Incoming audio call", comment: "notification") + let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( categoryIdentifier: ntfCategoryCallInvitation, - title: "\(invitation.contact.chatViewName):", + title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):", body: text, targetContentIdentifier: nil, userInfo: ["chatId": invitation.contact.id] ) } +public func createConnectionEventNtf(_ connEntity: ConnectionEntity) -> UNMutableNotificationContent { + let hideContent = ntfPreviewModeGroupDefault.get() == .hidden + var title: String + var body: String? = nil + var targetContentIdentifier: String? = nil + switch connEntity { + case let .rcvDirectMsgConnection(_, contact): + if let contact = contact { + title = hideContent ? contactHidden : "\(contact.chatViewName):" + targetContentIdentifier = contact.id + } else { + title = NSLocalizedString("New contact:", comment: "notification") + } + body = NSLocalizedString("message received", comment: "notification") + case let .rcvGroupMsgConnection(_, groupInfo, groupMember): + title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: hideContent) + body = NSLocalizedString("message received", comment: "notification") + targetContentIdentifier = groupInfo.id + case .sndFileConnection: + title = NSLocalizedString("Sent file event", comment: "notification") + case .rcvFileConnection: + title = NSLocalizedString("Received file event", comment: "notification") + case .userContactConnection: + title = NSLocalizedString("New contact request", comment: "notification") + } + return createNotification( + categoryIdentifier: ntfCategoryCallInvitation, + title: title, + body: body, + targetContentIdentifier: targetContentIdentifier + ) +} + +private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember, hideContent: Bool) -> String { + hideContent + ? NSLocalizedString("Group message:", comment: "notification") + : "#\(groupInfo.displayName) \(groupMember.chatViewName):" +} + public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil, targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent { let content = UNMutableNotificationContent() From 6b89eb872be4d4c52fce33fcca0677572c509ef6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:52:25 +0100 Subject: [PATCH 70/89] mobile: update webrtc ICE servers (#782) * mobile: update webrtc ICE servers * update webrtc package version --- apps/android/app/src/main/assets/www/call.js | 4 ++-- packages/simplex-chat-webrtc/package.json | 2 +- packages/simplex-chat-webrtc/src/call.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/android/app/src/main/assets/www/call.js b/apps/android/app/src/main/assets/www/call.js index 3a0a547239..ed33745c01 100644 --- a/apps/android/app/src/main/assets/www/call.js +++ b/apps/android/app/src/main/assets/www/call.js @@ -24,8 +24,8 @@ var TransformOperation; let activeCall; const processCommand = (function () { const defaultIceServers = [ - { urls: ["stun:stun.simplex.chat:5349"] }, - { urls: ["turn:turn.simplex.chat:5349"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, + { urls: ["stun:stun.simplex.im:5349"] }, + { urls: ["turn:turn.simplex.im:5349"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, ]; function getCallConfig(encodedInsertableStreams, iceServers, relay) { return { diff --git a/packages/simplex-chat-webrtc/package.json b/packages/simplex-chat-webrtc/package.json index 057d2f7095..880a7eaa3a 100644 --- a/packages/simplex-chat-webrtc/package.json +++ b/packages/simplex-chat-webrtc/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/webrtc", - "version": "0.0.5", + "version": "0.1.0", "description": "WebRTC call in browser and webview for SimpleX Chat clients", "main": "dist/call.js", "types": "dist/call.d.ts", diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index d9a4bcccf6..37ddc02b64 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -217,8 +217,8 @@ const processCommand = (function () { } const defaultIceServers: RTCIceServer[] = [ - {urls: ["stun:stun.simplex.chat:5349"]}, - {urls: ["turn:turn.simplex.chat:5349"], username: "private", credential: "yleob6AVkiNI87hpR94Z"}, + {urls: ["stun:stun.simplex.im:5349"]}, + {urls: ["turn:turn.simplex.im:5349"], username: "private", credential: "yleob6AVkiNI87hpR94Z"}, ] function getCallConfig(encodedInsertableStreams: boolean, iceServers?: RTCIceServer[], relay?: boolean): CallConfig { From 95f518a582546d4d2fce3068873e617350ace83b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 14:07:27 +0100 Subject: [PATCH 71/89] ios: stopped state for DB management, suspend quicker/instantly on app termination (#783) * ios: stopped state for DB management, suspend quicker/instantly on app termination * update terminateChat --- apps/ios/Shared/AppDelegate.swift | 5 +++ apps/ios/Shared/Model/SuspendChat.swift | 34 +++++++++++++++---- .../Shared/Views/Database/DatabaseView.swift | 3 ++ apps/ios/SimpleXChat/AppGroup.swift | 1 + 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 895891838e..b06fac9ee0 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -75,6 +75,11 @@ class AppDelegate: NSObject, UIApplicationDelegate { } } + func applicationWillTerminate(_ application: UIApplication) { + logger.debug("AppDelegate: applicationWillTerminate") + terminateChat() + } + private func receiveMessages(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { let complete = BGManager.shared.completionHandler { logger.debug("AppDelegate: completed BGManager.receiveMessages") diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index e19eb64f45..2c0261ae8f 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -16,6 +16,8 @@ let appSuspendTimeout: Int = 15 // seconds let bgSuspendTimeout: Int = 5 // seconds +let terminationTimeout: Int = 3 // seconds + private func _suspendChat(timeout: Int) { appStateGroupDefault.set(.suspending) apiSuspendChat(timeoutMicroseconds: timeout * 1000000) @@ -25,7 +27,9 @@ private func _suspendChat(timeout: Int) { func suspendChat() { suspendLockQueue.sync { - _suspendChat(timeout: appSuspendTimeout) + if appStateGroupDefault.get() != .stopped { + _suspendChat(timeout: appSuspendTimeout) + } } } @@ -37,18 +41,36 @@ func suspendBgRefresh() { } } +func terminateChat() { + suspendLockQueue.sync { + switch appStateGroupDefault.get() { + case .suspending: + // suspend instantly if already suspending + _chatSuspended() + apiSuspendChat(timeoutMicroseconds: 0) + case .stopped: () + default: + _suspendChat(timeout: terminationTimeout) + } + } +} + func chatSuspended() { suspendLockQueue.sync { if case .suspending = appStateGroupDefault.get() { - logger.debug("chatSuspended") - appStateGroupDefault.set(.suspended) - if ChatModel.shared.chatRunning == true { - ChatReceiver.shared.stop() - } + _chatSuspended() } } } +private func _chatSuspended() { + logger.debug("_chatSuspended") + appStateGroupDefault.set(.suspended) + if ChatModel.shared.chatRunning == true { + ChatReceiver.shared.stop() + } +} + func activateChat(appState: AppState = .active) { suspendLockQueue.sync { appStateGroupDefault.set(appState) diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 9611089914..1b46b43e1b 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -223,6 +223,7 @@ struct DatabaseView: View { try await apiStopChat() ChatReceiver.shared.stop() await MainActor.run { m.chatRunning = false } + appStateGroupDefault.set(.stopped) } catch let error { await MainActor.run { runChat = true @@ -307,6 +308,7 @@ struct DatabaseView: View { do { try initializeChat(start: true) m.chatDbChanged = false + appStateGroupDefault.set(.active) } catch let error { fatalError("Error starting chat \(responseError(error))") } @@ -318,6 +320,7 @@ struct DatabaseView: View { m.chatRunning = true ChatReceiver.shared.start() chatLastStartGroupDefault.set(Date.now) + appStateGroupDefault.set(.active) } catch let error { runChat = false alert = .error(title: "Error starting chat", error: responseError(error)) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index a1ee992750..708907bd36 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -23,6 +23,7 @@ public enum AppState: String { case bgRefresh case suspending case suspended + case stopped public var inactive: Bool { switch self { From fb54841d767bde9aaca8906abe1bbc7043cc371e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 14:17:50 +0100 Subject: [PATCH 72/89] update simplexmq (suspendAgent without delay) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index f6ee24247e..e3aeadb4a5 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: abd694bd5af57b596f48214fe3c0d0e8982fc793 + tag: cc798145d2e26be8986747dfe3e5524b45699fab source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 6fb28658c4..4e1ad844ef 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."abd694bd5af57b596f48214fe3c0d0e8982fc793" = "0j8m4hcms8ffkbpspds8crvcs1gslgrk4xqp6xvgb0hhv499541n"; + "https://github.com/simplex-chat/simplexmq.git"."cc798145d2e26be8986747dfe3e5524b45699fab" = "1w907i65hq468xss822if1z9iwq4ajcszyfm5sgc8wc2pp5v54zb"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index dbc8d7c173..fc139618a4 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: abd694bd5af57b596f48214fe3c0d0e8982fc793 + commit: cc798145d2e26be8986747dfe3e5524b45699fab # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From eb35d81abad7e3eca8cf74011289b9b5bc600b7e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 15:01:41 +0100 Subject: [PATCH 73/89] ios: make qr code pages scrollable (for small screens) (#784) --- .../Shared/Views/NewChat/AddContactView.swift | 36 +++---- .../Views/UserSettings/UserAddress.swift | 98 ++++++++++--------- 2 files changed, 69 insertions(+), 65 deletions(-) diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift index 99f62e65c8..a965d72dbb 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift @@ -12,25 +12,27 @@ import CoreImage.CIFilterBuiltins struct AddContactView: View { var connReqInvitation: String var body: some View { - VStack(alignment: .leading) { - Text("One-time invitation link") - .font(.title) - .padding(.vertical) - Text("Your contact can scan it from the app") - .padding(.bottom) - QRCode(uri: connReqInvitation) - .padding(.bottom) - Text("If you can't meet in person, **show QR code in the video call**, or share the link.") - .padding(.bottom) - Button { - showShareSheet(items: [connReqInvitation]) - } label: { - Label("Share invitation link", systemImage: "square.and.arrow.up") + ScrollView { + VStack(alignment: .leading) { + Text("One-time invitation link") + .font(.title) + .padding(.vertical) + Text("Your contact can scan it from the app") + .padding(.bottom) + QRCode(uri: connReqInvitation) + .padding(.bottom) + Text("If you can't meet in person, **show QR code in the video call**, or share the link.") + .padding(.bottom) + Button { + showShareSheet(items: [connReqInvitation]) + } label: { + Label("Share invitation link", systemImage: "square.and.arrow.up") + } + .frame(maxWidth: .infinity, alignment: .center) } - .frame(maxWidth: .infinity, alignment: .center) + .padding() + .frame(maxHeight: .infinity, alignment: .top) } - .padding() - .frame(maxHeight: .infinity, alignment: .top) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddress.swift b/apps/ios/Shared/Views/UserSettings/UserAddress.swift index c1ff708233..16e4994d33 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddress.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddress.swift @@ -13,61 +13,63 @@ struct UserAddress: View { @State private var deleteAddressAlert = false var body: some View { - VStack (alignment: .leading) { - Text("You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.") - .padding(.bottom) - if let userAdress = chatModel.userAddress { - QRCode(uri: userAdress) - HStack { - Button { - showShareSheet(items: [userAdress]) - } label: { - Label("Share link", systemImage: "square.and.arrow.up") - } - .padding() + ScrollView { + VStack (alignment: .leading) { + Text("You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.") + .padding(.bottom) + if let userAdress = chatModel.userAddress { + QRCode(uri: userAdress) + HStack { + Button { + showShareSheet(items: [userAdress]) + } label: { + Label("Share link", systemImage: "square.and.arrow.up") + } + .padding() - Button(role: .destructive) { deleteAddressAlert = true } label: { - Label("Delete address", systemImage: "trash") - } - .padding() - .alert(isPresented: $deleteAddressAlert) { - Alert( - title: Text("Delete address?"), - message: Text("All your contacts will remain connected"), - primaryButton: .destructive(Text("Delete")) { - Task { - do { - try await apiDeleteUserAddress() - DispatchQueue.main.async { - chatModel.userAddress = nil + Button(role: .destructive) { deleteAddressAlert = true } label: { + Label("Delete address", systemImage: "trash") + } + .padding() + .alert(isPresented: $deleteAddressAlert) { + Alert( + title: Text("Delete address?"), + message: Text("All your contacts will remain connected"), + primaryButton: .destructive(Text("Delete")) { + Task { + do { + try await apiDeleteUserAddress() + DispatchQueue.main.async { + chatModel.userAddress = nil + } + } catch let error { + logger.error("UserAddress apiDeleteUserAddress: \(error.localizedDescription)") } - } catch let error { - logger.error("UserAddress apiDeleteUserAddress: \(error.localizedDescription)") } - } - }, secondaryButton: .cancel() - ) - } - } - .frame(maxWidth: .infinity) - } else { - Button { - Task { - do { - let userAddress = try await apiCreateUserAddress() - DispatchQueue.main.async { - chatModel.userAddress = userAddress - } - } catch let error { - logger.error("UserAddress apiCreateUserAddress: \(error.localizedDescription)") + }, secondaryButton: .cancel() + ) } } - } label: { Label("Create address", systemImage: "qrcode") } - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity) + } else { + Button { + Task { + do { + let userAddress = try await apiCreateUserAddress() + DispatchQueue.main.async { + chatModel.userAddress = userAddress + } + } catch let error { + logger.error("UserAddress apiCreateUserAddress: \(error.localizedDescription)") + } + } + } label: { Label("Create address", systemImage: "qrcode") } + .frame(maxWidth: .infinity) + } } + .padding() + .frame(maxHeight: .infinity, alignment: .top) } - .padding() - .frame(maxHeight: .infinity, alignment: .top) } } From 6eb76315eb5e83f8bb81be679257accfd31533c2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 15:22:01 +0100 Subject: [PATCH 74/89] ios: NSE debug logging --- apps/ios/SimpleX NSE/NotificationService.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index c937786210..8decba34d1 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -26,15 +26,17 @@ class NotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler receiveNtfMessages(request, contentHandler) case .suspending: + logger.debug("NotificationService: app is suspending") self.contentHandler = contentHandler receiveNtfMessages(request, contentHandler) default: - print("userInfo", request.content.userInfo) + logger.debug("NotificationService: app state is \(appState.rawValue, privacy: .public)") contentHandler(request.content) } } func receiveNtfMessages(_ request: UNNotificationRequest, _ contentHandler: @escaping (UNNotificationContent) -> Void) { + logger.debug("NotificationService: receiveNtfMessages") if case .documents = dbContainerGroupDefault.get() { contentHandler(request.content) return @@ -44,13 +46,17 @@ class NotificationService: UNNotificationServiceExtension { let nonce = ntfData["nonce"] as? String, let encNtfInfo = ntfData["message"] as? String, let _ = startChat() { + logger.debug("NotificationService: receiveNtfMessages: chat is started") if let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)") if let connEntity = ntfMsgInfo.connEntity { bestAttemptContent = createConnectionEventNtf(connEntity) } if let content = receiveMessageForNotification() { + logger.debug("NotificationService: receiveMessageForNotification: has message") contentHandler(content) } else if let content = bestAttemptContent { + logger.debug("NotificationService: receiveMessageForNotification: no message") contentHandler(content) } } From 8966a0e22f712aeac354d6ef8725f51d308bbdcd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 16:06:35 +0100 Subject: [PATCH 75/89] ios: NSE waits up to 10 sec until app is suspended (#785) * ios: NSE waits up to 10 sec until app is suspended * show default notification if the app is no longer suspending/suspended --- .../ios/SimpleX NSE/NotificationService.swift | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 8decba34d1..dcee1bf91a 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -12,23 +12,37 @@ import SimpleXChat let logger = Logger() +let suspendingDelay: UInt64 = 2_000_000_000 + class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? - var bestAttemptContent: UNMutableNotificationContent? + var bestAttemptContent: UNNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("NotificationService.didReceive") - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + bestAttemptContent = request.content + self.contentHandler = contentHandler let appState = appStateGroupDefault.get() switch appState { case .suspended: logger.debug("NotificationService: app is suspended") - self.contentHandler = contentHandler receiveNtfMessages(request, contentHandler) case .suspending: logger.debug("NotificationService: app is suspending") - self.contentHandler = contentHandler - receiveNtfMessages(request, contentHandler) + Task { + var state = appState + for _ in 1...5 { + _ = try await Task.sleep(nanoseconds: suspendingDelay) + state = appStateGroupDefault.get() + if state == .suspended || state != .suspending { break } + } + logger.debug("NotificationService: app state is \(state.rawValue, privacy: .public)") + if state.inactive { + receiveNtfMessages(request, contentHandler) + } else { + contentHandler(request.content) + } + } default: logger.debug("NotificationService: app state is \(appState.rawValue, privacy: .public)") contentHandler(request.content) From 51ea478acb036ef1a0f99cf9cc0a0d76496c63a6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 16:53:08 +0100 Subject: [PATCH 76/89] ios: update version v3.0 (56) --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 5a38c5827b..016dfe5025 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1105,7 +1105,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 55; + CURRENT_PROJECT_VERSION = 56; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1145,7 +1145,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 55; + CURRENT_PROJECT_VERSION = 56; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1224,7 +1224,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 55; + CURRENT_PROJECT_VERSION = 56; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1254,7 +1254,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 55; + CURRENT_PROJECT_VERSION = 56; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; From d3dd6e5d1c0f4bdcdc1eea75186a55d61cac2028 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 17:20:33 +0100 Subject: [PATCH 77/89] fix android terminal scroll (#786) --- .../main/java/chat/simplex/app/views/TerminalView.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 52340e2ac6..46709dd1c8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -17,8 +18,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.chat.ComposeState -import chat.simplex.app.views.chat.SendMsgView +import chat.simplex.app.views.chat.* import chat.simplex.app.views.helpers.* import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsWithImePadding @@ -82,6 +82,10 @@ fun TerminalLayout( @Composable fun TerminalLog(terminalItems: List) { val listState = rememberLazyListState() + val keyboardState by getKeyboardState() + val ciListState = rememberSaveable(stateSaver = CIListStateSaver) { + mutableStateOf(CIListState(false, terminalItems.count(), keyboardState)) + } val scope = rememberCoroutineScope() LazyColumn(state = listState) { items(terminalItems) { item -> @@ -101,8 +105,9 @@ fun TerminalLog(terminalItems: List) { ) } val len = terminalItems.count() - if (len > 1) { + if (len > 1 && (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount)) { scope.launch { + ciListState.value = CIListState(true, len, keyboardState) listState.animateScrollToItem(len - 1) } } From e290309cd11ceb20a8f4dfe586671b3fb7f8c9ff Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 6 Jul 2022 21:45:29 +0400 Subject: [PATCH 78/89] core: add optional parentTempDirectory to ArchiveConfig (#788) * core: add optional parentTempDirectory to ArchiveConfig * swift * brackets * Update src/Simplex/Chat/Archive.hs Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * logs Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/SimpleXChat/APITypes.swift | 4 ++- src/Simplex/Chat/Archive.hs | 39 +++++++++++++++++++++++------ src/Simplex/Chat/Controller.hs | 2 +- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index b2622a539e..317135dd97 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -383,10 +383,12 @@ struct ComposedMessage: Encodable { public struct ArchiveConfig: Encodable { var archivePath: String var disableCompression: Bool? + var parentTempDirectory: String? - public init(archivePath: String, disableCompression: Bool? = nil) { + public init(archivePath: String, disableCompression: Bool? = nil, parentTempDirectory: String? = nil) { self.archivePath = archivePath self.disableCompression = disableCompression + self.parentTempDirectory = parentTempDirectory } } diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index a15ca1bb2d..ce8f820f6e 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -24,32 +24,57 @@ archiveFilesFolder :: String archiveFilesFolder = "simplex_v1_files" exportArchive :: ChatMonad m => ArchiveConfig -> m () -exportArchive ArchiveConfig {archivePath, disableCompression} = - withSystemTempDirectory "simplex-chat." $ \dir -> do +exportArchive cfg@ArchiveConfig {archivePath, disableCompression} = do + liftIO . print $ "exportArchive 1" + withTempDir cfg "simplex-chat." $ \dir -> do + liftIO . print $ "exportArchive 2, dir = " <> dir StorageFiles {chatDb, agentDb, filesPath} <- storageFiles + liftIO . print $ "exportArchive 3" copyFile chatDb $ dir archiveChatDbFile + liftIO . print $ "exportArchive 4" copyFile agentDb $ dir archiveAgentDbFile - forM_ filesPath $ \fp -> + liftIO . print $ "exportArchive 5" + forM_ filesPath $ \fp -> do + liftIO . print $ "exportArchive 6, fp = " <> fp copyDirectoryFiles fp $ dir archiveFilesFolder + liftIO . print $ "exportArchive 7" let method = if disableCompression == Just True then Z.Store else Z.Deflate + liftIO . print $ "exportArchive 8, method = " <> show method Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir + liftIO . print $ "exportArchive 9" importArchive :: ChatMonad m => ArchiveConfig -> m () -importArchive ArchiveConfig {archivePath} = - withSystemTempDirectory "simplex-chat." $ \dir -> do +importArchive cfg@ArchiveConfig {archivePath} = do + liftIO . print $ "importArchive 1" + withTempDir cfg "simplex-chat." $ \dir -> do + liftIO . print $ "importArchive 2, dir = " <> dir Z.withArchive archivePath $ Z.unpackInto dir + liftIO . print $ "importArchive 3" StorageFiles {chatDb, agentDb, filesPath} <- storageFiles + liftIO . print $ "importArchive 4" backup chatDb + liftIO . print $ "importArchive 5" backup agentDb + liftIO . print $ "importArchive 6" copyFile (dir archiveChatDbFile) chatDb + liftIO . print $ "importArchive 7" copyFile (dir archiveAgentDbFile) agentDb + liftIO . print $ "importArchive 8" let filesDir = dir archiveFilesFolder - forM_ filesPath $ \fp -> - whenM (doesDirectoryExist filesDir) $ + liftIO . print $ "importArchive 9, filesDir = " <> filesDir + forM_ filesPath $ \fp -> do + liftIO . print $ "importArchive 10, fp = " <> fp + whenM (doesDirectoryExist filesDir) $ do + liftIO . print $ "importArchive 11" copyDirectoryFiles filesDir fp where backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak" +withTempDir :: ChatMonad m => ArchiveConfig -> (String -> (FilePath -> m ()) -> m ()) +withTempDir cfg = case parentTempDirectory cfg of + Just tmpDir -> withTempDirectory tmpDir + _ -> withSystemTempDirectory + copyDirectoryFiles :: MonadIO m => FilePath -> FilePath -> m () copyDirectoryFiles fromDir toDir = do createDirectoryIfMissing False toDir diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index cc21642441..f0cee565d4 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -292,7 +292,7 @@ instance ToJSON ChatResponse where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR" -data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool} +data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath} deriving (Show, Generic, FromJSON) data ContactSubStatus = ContactSubStatus From 36aae92c554104ff386e18c9ff6eeee5c7e91254 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 21:33:32 +0100 Subject: [PATCH 79/89] android: toggle speaker in audio call (#789) --- .../chat/simplex/app/views/call/CallView.kt | 49 ++++++++++++++++++- .../chat/simplex/app/views/call/WebRTC.kt | 1 + .../app/src/main/res/values-ru/strings.xml | 2 + .../app/src/main/res/values/strings.xml | 2 + 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt index 036228cb6a..b6f529600b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt @@ -1,6 +1,8 @@ package chat.simplex.app.views.call import android.Manifest +import android.content.Context +import android.media.AudioManager import android.util.Log import android.view.ViewGroup import android.webkit.* @@ -35,6 +37,7 @@ import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.app.views.helpers.withApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @@ -44,6 +47,8 @@ fun ActiveCallView(chatModel: ChatModel) { val call = chatModel.activeCall.value if (call != null) withApi { chatModel.callManager.endCall(call) } }) + val cxt = LocalContext.current + val scope = rememberCoroutineScope() Box(Modifier.fillMaxSize()) { WebRTCView(chatModel.callCommand) { apiMsg -> Log.d(TAG, "received from WebRTCView: $apiMsg") @@ -79,6 +84,10 @@ fun ActiveCallView(chatModel: ChatModel) { } is WCallResponse.Connected -> { chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) + scope.launch { + delay(2000L) + setCallSound(cxt, call) + } } is WCallResponse.Ended -> { chatModel.activeCall.value = call.copy(callState = CallState.Ended) @@ -117,21 +126,43 @@ fun ActiveCallView(chatModel: ChatModel) { @Composable private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) { + var cxt = LocalContext.current ActiveCallOverlayLayout( call = call, dismiss = { withApi { chatModel.callManager.endCall(call) } }, toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) }, toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) }, + toggleSound = { + var call = chatModel.activeCall.value + if (call != null) { + call = call.copy(soundSpeaker = !call.soundSpeaker) + chatModel.activeCall.value = call + setCallSound(cxt, call) + } + }, flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) } ) } +private fun setCallSound(cxt: Context, call: Call) { + Log.d(TAG, "setCallSound: set audio mode") + val am = cxt.getSystemService(Context.AUDIO_SERVICE) as AudioManager + if (call.soundSpeaker) { + am.mode = AudioManager.MODE_NORMAL + am.isSpeakerphoneOn = true + } else { + am.mode = AudioManager.MODE_IN_CALL + am.isSpeakerphoneOn = false + } +} + @Composable private fun ActiveCallOverlayLayout( call: Call, dismiss: () -> Unit, toggleAudio: () -> Unit, toggleVideo: () -> Unit, + toggleSound: () -> Unit, flipCamera: () -> Unit ) { Column(Modifier.padding(16.dp)) { @@ -174,6 +205,11 @@ private fun ActiveCallOverlayLayout( Box(Modifier.padding(start = 32.dp)) { ToggleAudioButton(call, toggleAudio) } + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { + Box(Modifier.padding(end = 32.dp)) { + ToggleSoundButton(call, toggleSound) + } + } } } } @@ -194,12 +230,21 @@ private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: In @Composable private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) { if (call.audioEnabled) { - ControlButton(call, Icons.Outlined.Mic, R.string.icon_descr_video_off, toggleAudio) + ControlButton(call, Icons.Outlined.Mic, R.string.icon_descr_audio_off, toggleAudio) } else { ControlButton(call, Icons.Outlined.MicOff, R.string.icon_descr_audio_on, toggleAudio) } } +@Composable +private fun ToggleSoundButton(call: Call, toggleSound: () -> Unit) { + if (call.soundSpeaker) { + ControlButton(call, Icons.Outlined.VolumeUp, R.string.icon_descr_speaker_off, toggleSound) + } else { + ControlButton(call, Icons.Outlined.VolumeDown, R.string.icon_descr_speaker_on, toggleSound) + } +} + @Composable fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { @Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) = @@ -393,6 +438,7 @@ fun PreviewActiveCallOverlayVideo() { dismiss = {}, toggleAudio = {}, toggleVideo = {}, + toggleSound = {}, flipCamera = {} ) } @@ -413,6 +459,7 @@ fun PreviewActiveCallOverlayAudio() { dismiss = {}, toggleAudio = {}, toggleVideo = {}, + toggleSound = {}, flipCamera = {} ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt index afef4ca823..bb079e8008 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt @@ -18,6 +18,7 @@ data class Call( val sharedKey: String? = null, val audioEnabled: Boolean = true, val videoEnabled: Boolean = localMedia == CallMediaType.Video, + val soundSpeaker: Boolean = localMedia == CallMediaType.Video, var localCamera: VideoCamera = VideoCamera.User, val connectionInfo: ConnectionInfo? = null ) { diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 16269f27a7..c6a916f6c3 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -404,6 +404,8 @@ Включить видео Выключить звук Включить звук + Выключить спикер + Включить спикер Перевернуть камеру diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 284fd2ca86..e327f0febb 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -406,6 +406,8 @@ Video on Audio off Audio on + Speaker off + Speaker on Flip camera From 2ae3748489560a87c3ffa737067a16c1cb97b7ee Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 6 Jul 2022 22:10:14 +0100 Subject: [PATCH 80/89] android: update version v3.0 (37) --- apps/android/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index d0b5c8a8d8..baef53847a 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 36 - versionName "2.2" + versionCode 37 + versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { From 2b7de2a7a6feb2c9ec2c18852b13e500905ea3d2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 7 Jul 2022 19:05:03 +0100 Subject: [PATCH 81/89] android: only start service when app is in the background, change service icon (#790) * android: only start service when app is in the background, change service icon * update version v3.0 (38) * set flag --- apps/android/app/build.gradle | 2 +- apps/android/app/src/main/cpp/simplex-api.c | 17 +++++++- .../main/java/chat/simplex/app/SimplexApp.kt | 11 ++--- .../java/chat/simplex/app/SimplexService.kt | 8 ++-- .../java/chat/simplex/app/model/SimpleXAPI.kt | 38 +++++++++++------- .../chat/simplex/app/views/WelcomeView.kt | 1 - .../res/drawable-hdpi/ntf_service_icon.png | Bin 0 -> 1553 bytes .../res/drawable-mdpi/ntf_service_icon.png | Bin 0 -> 1272 bytes .../res/drawable-xhdpi/ntf_service_icon.png | Bin 0 -> 1840 bytes .../res/drawable-xxhdpi/ntf_service_icon.png | Bin 0 -> 2489 bytes .../res/drawable-xxxhdpi/ntf_service_icon.png | Bin 0 -> 2812 bytes .../ios/SimpleX NSE/NotificationService.swift | 1 + 12 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 apps/android/app/src/main/res/drawable-hdpi/ntf_service_icon.png create mode 100644 apps/android/app/src/main/res/drawable-mdpi/ntf_service_icon.png create mode 100644 apps/android/app/src/main/res/drawable-xhdpi/ntf_service_icon.png create mode 100644 apps/android/app/src/main/res/drawable-xxhdpi/ntf_service_icon.png create mode 100644 apps/android/app/src/main/res/drawable-xxxhdpi/ntf_service_icon.png diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index baef53847a..9d5670a4ec 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 37 + versionCode 38 versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/apps/android/app/src/main/cpp/simplex-api.c b/apps/android/app/src/main/cpp/simplex-api.c index e9178df5c4..5be1d1fadd 100644 --- a/apps/android/app/src/main/cpp/simplex-api.c +++ b/apps/android/app/src/main/cpp/simplex-api.c @@ -24,9 +24,11 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass // from simplex-chat typedef void* chat_ctrl; -extern chat_ctrl chat_init(const char * path); +extern chat_ctrl chat_init(const char *path); extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); extern char *chat_recv_msg(chat_ctrl ctrl); +extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); +extern char *chat_parse_markdown(const char *str); JNIEXPORT jlong JNICALL Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) { @@ -48,3 +50,16 @@ JNIEXPORT jstring JNICALL Java_chat_simplex_app_SimplexAppKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) { return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller)); } + +JNIEXPORT jstring JNICALL +Java_chat_simplex_app_SimplexAppKt_chatRecvMsgWait(JNIEnv *env, __unused jclass clazz, jlong controller, jint wait) { + return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait)); +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_app_SimplexAppKt_chatParseMarkdown(JNIEnv *env, __unused jclass clazz, jstring str) { + const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str)); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 73ecd073db..69cae0dd2a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -24,8 +24,10 @@ external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long external fun chatInit(path: String): ChatCtrl -external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String -external fun chatRecvMsg(ctrl: ChatCtrl) : String +external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String +external fun chatRecvMsg(ctrl: ChatCtrl): String +external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String +external fun chatParseMarkdown(str: String): String class SimplexApp: Application(), LifecycleEventObserver { val chatController: ChatController by lazy { @@ -55,7 +57,6 @@ class SimplexApp: Application(), LifecycleEventObserver { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo } else { chatController.startChat(user) - SimplexService.start(applicationContext) chatController.showBackgroundServiceNoticeIfNeeded() } } @@ -66,9 +67,9 @@ class SimplexApp: Application(), LifecycleEventObserver { withApi { when (event) { Lifecycle.Event.ON_STOP -> - if (!appPreferences.runServiceInBackground.get()) SimplexService.stop(applicationContext) + if (appPreferences.runServiceInBackground.get()) SimplexService.start(applicationContext) Lifecycle.Event.ON_START -> - SimplexService.start(applicationContext) + SimplexService.stop(applicationContext) Lifecycle.Event.ON_RESUME -> if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { chatController.showBackgroundServiceNoticeIfNeeded() diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt index 94ae6a7371..db62727d2c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt @@ -20,6 +20,7 @@ class SimplexService: Service() { private var wakeLock: PowerManager.WakeLock? = null private var isServiceStarted = false private var isStartingService = false + private var isStoppingService = false private var notificationManager: NotificationManager? = null private var serviceNotification: Notification? = null private val chatController by lazy { (application as SimplexApp).chatController } @@ -71,7 +72,6 @@ class SimplexService: Service() { } else { Log.w(TAG, "Starting foreground service") chatController.startChat(user) - chatController.startReceiver() isServiceStarted = true saveServiceState(self, ServiceState.STARTED) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { @@ -88,6 +88,8 @@ class SimplexService: Service() { private fun stopService() { Log.d(TAG, "Stopping foreground service") + if (!isServiceStarted || isStoppingService) return + isStoppingService = true try { wakeLock?.let { while (it.isHeld) it.release() // release all, in case acquired more than once @@ -98,7 +100,7 @@ class SimplexService: Service() { } catch (e: Exception) { Log.d(TAG, "Service stopped without being started: ${e.message}") } - + isStoppingService = false isServiceStarted = false saveServiceState(this, ServiceState.STOPPED) } @@ -121,7 +123,7 @@ class SimplexService: Service() { PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) } return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.ntf_icon) + .setSmallIcon(R.drawable.ntf_service_icon) .setColor(0x88FFFF) .setContentTitle(title) .setContentText(text) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 10e40f30da..598112224b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -117,8 +117,11 @@ class AppPreferences(val context: Context) { } } +private const val MESSAGE_TIMEOUT: Int = 15_000_000 + open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) { val chatModel = ChatModel(this) + private var receiverStarted = false init { chatModel.runServiceInBackground.value = appPrefs.runServiceInBackground.get() @@ -142,6 +145,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager chatModel.currentUser.value = user chatModel.userCreated.value = true chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete + startReceiver() Log.d(TAG, "chat started") } catch (e: Error) { Log.e(TAG, "failed starting chat $e") @@ -149,8 +153,9 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - fun startReceiver() { + private fun startReceiver() { Log.d(TAG, "ChatController startReceiver") + if (receiverStarted) return thread(name="receiver") { GlobalScope.launch { withContext(Dispatchers.IO) { recvMspLoop() } } } @@ -176,18 +181,23 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - suspend fun recvMsg(): CR { + private suspend fun recvMsg(): CR? { return withContext(Dispatchers.IO) { - val json = chatRecvMsg(ctrl) - val r = APIResponse.decodeStr(json).resp - Log.d(TAG, "chatRecvMsg: ${r.responseType}") - if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json") - r + val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) + if (json == "") { + null + } else { + val r = APIResponse.decodeStr(json).resp + Log.d(TAG, "chatRecvMsg: ${r.responseType}") + if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json") + r + } } } - suspend fun recvMspLoop() { - processReceivedMsg(recvMsg()) + private suspend fun recvMspLoop() { + val msg = recvMsg() + if (msg != null) processReceivedMsg(msg) recvMspLoop() } @@ -206,7 +216,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager throw Error("user not created ${r.responseType} ${r.details}") } - suspend fun apiStartChat(): Boolean { + private suspend fun apiStartChat(): Boolean { val r = sendCmd(CC.StartChat()) when (r) { is CR.ChatStarted -> return true @@ -215,13 +225,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - suspend fun apiSetFilesFolder(filesFolder: String) { + private suspend fun apiSetFilesFolder(filesFolder: String) { val r = sendCmd(CC.SetFilesFolder(filesFolder)) if (r is CR.CmdOk) return throw Error("failed to set files folder: ${r.responseType} ${r.details}") } - suspend fun apiGetChats(): List { + private suspend fun apiGetChats(): List { val r = sendCmd(CC.ApiGetChats()) if (r is CR.ApiChats ) return r.chats throw Error("failed getting the list of chats: ${r.responseType} ${r.details}") @@ -256,7 +266,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager return null } - suspend fun getUserSMPServers(): List? { + private suspend fun getUserSMPServers(): List? { val r = sendCmd(CC.GetUserSMPServers()) if (r is CR.UserSMPServers) return r.smpServers Log.e(TAG, "getUserSMPServers bad response: ${r.responseType} ${r.details}") @@ -383,7 +393,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager return false } - suspend fun apiGetUserAddress(): String? { + private suspend fun apiGetUserAddress(): String? { val r = sendCmd(CC.ShowMyAddress()) if (r is CR.UserContactLink) return r.connReqContact if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index 833977b982..862e476841 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -114,7 +114,6 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String) { Profile(displayName, fullName, null) ) chatModel.controller.startChat(user) - SimplexService.start(chatModel.controller.appContext) chatModel.controller.showBackgroundServiceNoticeIfNeeded() chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete } diff --git a/apps/android/app/src/main/res/drawable-hdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-hdpi/ntf_service_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2bdab389b1f96ec138e654709ecf2660213ab5f3 GIT binary patch literal 1553 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6Xz}5igjtE6@fgk&_zXnda-upao=eFt9KR zF)#yJj6lf1D8&FW4aj1!W0Z!o-P$u5SfJv5KpF%*fEc6>LNhI3f~!zjz>Hvnl&)dY z0lJBSv%n*=n1O*?7=#%aX3ddcU|^b*84^(v;p=0SoS&uf-VSw#k=7U$=bf}CY%Ze#KQE)+Ga;hEB z9<8@;8M`+oFo+?&An=1ftYd8;zN-SJBJ;x%fjvCsDvwMv&F#DQ z26Tv*Ui^A>g1N<9*8d0MSQkH7((g9OzgW5E{zdJ^{DYZE!FgI=wtm=I7FRELzwYSD z>ivs?l_rQTNcyz4d+Qg@3m+z3xl?v#a+I>wgo(3KYdOv^pOpF-eX;#=mr%x4u`T`I z<}(&6OuQtd>iKKE!0oeL_ZRg~=zVcE@%hJZs~xW!fA^1y-Syz~i3{(Wqy$rL?{9d! ze0R@Jzib!b(mz>Avz~GP%Dv&d?3d{8%r&v$Tc@u6<*jh_JKH5=2Yv5FhTPMBOJq7_ z++S#1wy0E|<#ec-$5zFXjGMm?WV83Y^cMX5`h!LAr3@3Rj=usscIZ8b-C!*gW21WL z^~qO1qe>30XTED``q1Ka!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD3OyI;hE;^%b*2hb1<+l z3NbJPS&Tr)z$nE4G7ZRLuw#^lv)$S=8Cam=en1)oJb)Oa4ni|6V1lcVSip>6gOsLN z?z#%3I14-?iy0WWg+Z8+Vb&a=ub3ufhD4M^`1)8S=jZArrsOB3>Q&?xfOIj~R9FF- zxv3?I3Kh9IdBs*0wn|_XRzNmLSYJs2tfVB{Rw=?aK*2e`C{@8s&p^*W$&O1wLBXad zCCw_x#SN+*$g@?-C@Cqh($_C9FV`zK*2^zS*Eh7ZwA42+(l;{F1**_3uFNY*tkBIX zR)!b?Gsh*hIJqdZpd>RtPXT0ZVp4u-iLH_n$Rap^xU(cP4PjGWG1OZ?59)(t^bPe4 z^s#A6t;oco4I~562KE=kIvbE-R*^xe#rZj8&$PP_iJpJkCAO4YM4(ro?GW|avskbwEr{B(rhsu8_ zzjTk-9#l7Fk9ov}9a~s0uwUd{IQ#wH{RgFGJzaNAU3d7=E|V;Z*7-NQvdap zZOaSowB?o`X89$SCx>!0a5lkfC?7X0Y)foliT_Xh$!s~P&to6HZ& zRxtd&`OK_d@;}d)gS(ziG<TK)_2*BP&lX|DLcbm5K>6I$ L)z4*}Q$iB}%9ESS literal 0 HcmV?d00001 diff --git a/apps/android/app/src/main/res/drawable-xhdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-xhdpi/ntf_service_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..950a783161530e65694e4ad2320436f3c210e410 GIT binary patch literal 1840 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G|oWRD45dJguM!v-tY$DUh!@P+6=(yL$VrXxO!M_+&;qhK7+4sE z7?^=9Mj&Khlwtsx24peVF-pVPZta;2EKqSjAPoW@Knzj`p_vvi!BrS6U`DV(N@J}T zmjEfw0*}aI1_o|n5N2eUHAjMhfoW1^NJL45ua8x7ey(0(N`6wRUPW#JNEd@mg%yyQ zn_7~nP?4LHS8P>bs{~eI1!RMS^_3LBN=mYAl_Got6rA&mQWebf4D<|??6?#Z6l{u8 z(yW49+@RWlJX@uVl9B=|ef{$Ca=mh6z5JqdeM3u2OML?)eIp}XpbFjM%Dj@q3f;V7 zWr!g#b6ir3lZ!G7N;32F6hP)CCgqow*eWT3EP?}wJ4-Ut5H{r%L%jv`pgu@O-%!s$ zADgz+icB2ZKr#?*V1GfZvjG`q6&aLToS#z)a+aOBkqy`e6hVahAQ~dDX+Rc4*Wh22 znF-m&{c!2iNs|Mk_1$vjXo&WkYW*%M8KlJG-AhPqYqDSc3dwxR_QP>Fi-GwaSX{| zeLKy%BiK=-ZSo7YaPe42mxE3RkFri@6?6$)!y=fIBjU1QbI7($!c7X(xUy0gPHjzi z$*iNZMIf?;b-U5j{8$w`v)w1N9(_MQce2lU;aKc7=C_y5jLPrd0nniE7k zxKvmtF?lvFaR^et&HeA&ShJtke75h?Lp8?p&zt|gT7G=@kNU%J<&4Zf{PBrNk~e9F)T}#?ek`7n$8`FD z%t!MrkHkJmf7*Ta$Cg>e>Nohpn)<(1)$WSWId|x9V(F2`ZU++!A~$s?H}|RW>!2}F?HqEpQT3Z zz3a_FKCN11sC};L?ee2bFKu{!>(O?B>ZjgsU)(z)w@56Zs(0X z6u-`p%i<1qZOk04T^2^>8PCdW-ri8}$onGs-Rj}%s~47DU2b+P{EXe<>W^pFlrb}Y zOcgr~H2ULqp(3VRO(Ba~UmW94Qrdl5Mr8e`y8XN?OLtyPd>;R&fVrFJiCyV_$qOPC z@oU5O|FY_?U9_LYWZBUgPyf%il$Hjayu)uH*8fp-^W@6T{Ru}t2+hB^yQNiJqq`(- z$!Vp@7j{2)Ss=@LzVXaEuWP60eT%#P;@$U}?_AaK(|;^dKcctsV`(S<%|BU(z4!Dz zIlRu%#YE#PQ&06g-FvqlYkgSeE8)K{_CR*trE`^YH(kkL=6A?A7MvqDrJ?$N=3AFL zYIl^fw%)SntX4=n&++bacmCvD*PDy{fP}dRIbPc5G3$xP;)@mm8Y0&ohaX5Tn9aod=GBYsIz12HD-|-mVQ>v#D`)8l z+S2ZSF1L#%bNk7=EniYEx4V9StT{t*eQ@umliU3dl`8R{c(+7Jb2|fXp5@O2cANEN zo~_l(`EPQ6mX1+J+09>H@n=*`8#_I-QTX`zQX~^^Uay SoW^@V^@FFYpUXO@geCwu4ZErU literal 0 HcmV?d00001 diff --git a/apps/android/app/src/main/res/drawable-xxhdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-xxhdpi/ntf_service_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..81da12bb9256d7144ca32305ba43692536ba5260 GIT binary patch literal 2489 zcmcImeLT}^8~@F=$V-PzlgQ3eI>JVew{2F-OB5xcv7On*yu@s6!xV*bjyO@%3#BI| z$H@@U9EMh(rnl1`@0=VCQE{l$B<7{F(Nms2oj;%Z_ql)9b$_qxdws9_ci(^fj{EG| zuA^zB2>^hOr-!Sr(t4<-#xmvG|7F#7rGbPKya@nMnXNTTgDTgE5D#B(064e-0FsXZ zfI!!W+C?jPyOl`}Ge$75qqsQCDJ>-f#q zEKWGe#>vSEg|Yhh{kSdV22rXA%ElUv`WlVFkNgLkDzZph*w>;QPE`yO=*!@;qGMHU z;T>#o3kv@Oe(rCPK>QkEDflH}Hz$&z{3HtsHXj9+VoUN>ePVnf`HX0yYb28q!&P>}AtJ7cOdCvxw#Z!BFamaH?lrX<>tgSL!7RoVuklT!0_m0i7{& z7fP6t{lPB5@M%*vY}OHr6$@J;60wesj)b*kLb9LNG1`+C5(-+Um9t8^ub);?kK-hY zM@83D#JAO{E3X!%&|x@mHTsa>jyCG`?hBHtery4zYDoJbOVZZwj_}Qd7P9xXSllKd zJb$ws3_Io&l(Uyd0X7+26phP5DO!A(F=#_Zad#c*^gUNa&29F9XJvt73dy^EH@0E! z#;k(w&aQmLV1Ggd2-dmw2DRF#9yDZGSy0w;=lXM|P4UPU=rY(b(YjM35wF$Flkd_= zNqzi0jQ)37E^ZA|nZ ztt?MV_g4JBNCvs06Nsh(sF#owSab94Y*67?@T2S7V@LtfZPzCA^7611Js_TWfL`X2 z6@%|3Wd&t%(rozMp`da6$y{!f-|Qw5p=Q!YE@;KCnAn)--0OR)VQ6!OeIN^& z)>#3I22V!#m;E``G@hiP9YpR4u(R*9Z-Lk8Nt??ngJfp5zTJq4zDr&9hv3b72y`b> z;cT!_9_AI}c_DD^L{PU5HSo#LPl?#-*}<-3>$B#5pRBg~;T*G+Ob&N`jxjQmrV}$- zDWWv>>Z_ZRrXNvWBTS%gUBit}6_%0d394#U`nTU^$5*XLRm{x9u4ox3A?eK=NcGbl zar1Ssg*&AQd%Tk~@a_TV*9eW5ux3ID$?51|t#SCp#!)9OW53Y<9b-oWg(Z9Zc<#Xv zFVyFqwH;PE5B^1lINRRj{5Wi{xz|O3_{>qqlKWSQpzu9eZu-o97LoZa=WdPp&z_p7 z?_;&<=;(CJC=`c(7Uu9BIGV*^1Q{arUI`Pl@4uO3I7SKOk&+t^OpMrkdt?(islh`} zuSS2^<2v-m{Avb0yNjaz9%eWy+xN0?YdqmhCe?1Zyyj+)Nw>a!Xy)aPr+v^mU;kB? zA!mow71;F4ZSiC>tf$G89s-xl`aH|H-%?%-t#>I6uCgrDLXfQ9hOiBZiBWrQ9-k03 zxlC+PQW%z~3wWpod^xhN}qS>Gg0Pr{{>SIA<%pl2n)$EcKR+ za6^h77TMjvmUJ06(*4+C+({i5HFZAt{&K6v1r4$h`K&OJO}7){gC z`Ji68cX)l&2lb>Ay{|1~iR~!CRgeZt+P<)1~CzyImdMyhBq?_ZM?} zEh<2zfYInJp+42v*u292oHR3|3XS B`#%5x literal 0 HcmV?d00001 diff --git a/apps/android/app/src/main/res/drawable-xxxhdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-xxxhdpi/ntf_service_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4ba82576b5df9e5ba50a3be56f28296c87454725 GIT binary patch literal 2812 zcmdT`iCa?J7C$0}qEb2JfL%>rmX&~*6Cp*~;Oxyb4NDObal*g?C)CnPGqar27gjHG zs_^PH8)dlV%}6sd$xKC48%(e=>1uhQy4CmYyMMsj=R0Suz1Huy*4k_D^PQ9J>%B!A zY77McK-+Dr>o$c9Ro?0g6wl#OrlUfDDR?hD05lxaoFl3!=4Pa=+q?iE$r1q4GXdZa zg(>|x0PIBpz{^koz?1@jL43v4pRo#uD>=|D!pjR-rO?#@kctkVs?byv7jTFQfIsU1 zpopXxpi0m;OK7F)cU|cuxwH={ASmkgz=S|APYj6`iy(&6!pI0#Y`hWxU|AT28cR+f znz3SI;usi~v-xKPMxiUsNOQB#kc4Pw^FS|OGZz}2Y-W!@AyDQxsF|4=mL48~+2*?W zD_t>jHjhk5h{qt2OePb-v_;V96r_!llM@n!MxxQy3WPNyF)o3~vW{a|dq|PUG8o2}$|A=Exl&`vaSTOiI467T=PZA7e6RBdV&Fd!e{lRa(S;U6qx;1Z zNo1VO*IYlCen2a$#`w^wWJN=jZBxSkX8VfAB9%S;p~qh~@UvC1gE%M_`Rz2|pvRd3 zX8`~*>*k8z&Qf`Ok`S}BNB`iIwf6+D=hY(j7V+Kb5I^~>~vmBXiBZi;p9c@U8L zoG{zYFlUV(W$xZ^xO!!64j2jskLn<(R^Y-#ka7=%Im~id^Q+`mq$m)_8WZKFvhdMv zoQRZ&&Yj8g(XVfei>4CCv$8;^H>_L)&HO(Rh5n)Y_B9*rBpNjd-+()x?0enUBWS3p zZ+)QccPRVql26`lsiHTJwLCri{0hbg;iUe1S`kL|CZ?v+C>m`&=j~`EEHBlhBQGzH z-P6^z$MOSV>Gk618rBu<2b$Tk3v)Mg9$C)ilOWIAU=;^PWJX#-R|{RdY3`u5fs275 zfoLgNqs6!BgzGyPG2u95rpM`pkCufE??@n-W(Y|n66a!I!|=n7f3Is}PKb{a za?k1HhlfbC*{c2EUYov zL1GZUaOJ@>lrq(39Rq+2O2wTJG(m`%a)gBmQ+<$y>D)AX8-=7_nigPGCuWmmxe=yos7cLzFk@G z)g@EMg5mgOtIKA1yJS9&n*I}pN0}Ru4?O}SI4x~nd!6UB-wUit52Ec|EM{kEEW>8} zif7AXiKa1Hbwf-U%yXlEqi_om@@~UGeb9ZCCjx63C|4LZq2*AP`Kxb|tZCY9Oh>MY z)f84%`-|RN$n3G|aNcO`ZMxbEsq}K#uJL*G@=E>Mlb?EdG)Y_w(0&p=5F&}F#wMRI z!_~~GO{&dse;K>l3Y!G=vS)W;GMai~T0!qdboO;DbgP~!C_{vZJX?Q&vo-Q(mmEVe;(z*B7w3RS$cHx;w)t zrk79IYIDN--02Fkp(JW>hwqhwp8Zf!E*uY z5gkDpqWa0>#~1fnIry|v?c29;?DX?T`pb5-1V8I9pJ^yhGU7xHLx;q0J5@i< zB*hMAt9$?P;}=bhx^_oDBwH2#QU5rTZc;UfVjz$&N2CHuQC>KLT&BT$wIcktsS8ThEI46^hYhu_=21h8(wwe9?C_YJeiE!LwCHA?cyTh9eUE&H?j42s_OyC zna%o_N~WFebhqzGSttmKA4A-_KDZGFh8in74$zmOpRROEnF8$)_?qD zEm!EVG{v~h=DNm;2Mb@CSmShMM0$OV7&9}v3*C$yhY8wz8*9TY(>E`02B5^{2!Hh% z-9gw%K=qzZsoYksIh)u1{nnMBrK0LTdI?rLSiA+1c67bCqZp(_nW`K~yBf4z-wyHfgjmxxCQ$ z5WhVQhD-0NpE~craDMVUGeheShTJF^5|9-plDhV()D7^{z{wIf$NyBCtcFi@j#oi= zATcO_c!NzyjT^coILqaTbAB<5Ha0Yjl)iXzrLg=mKbC8hvLk$b@d4ZJhHIUv+8uHi z*@6kUvGGgk>BbRj7zzVQXg!AOmpFSR2MA;np*GD(MhBvPg YaA&aDJI^jy`J?Ez$=mhf#?bwL0=FBZXaE2J literal 0 HcmV?d00001 diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index dcee1bf91a..0f93abd8b4 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -179,6 +179,7 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r { return NtfMessages(connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages) } + logger.debug("apiGetNtfMessage ignored response: \(String.init(describing: r), privacy: .public)") return nil } From fdad58b0ee2fef47e01341b93b832c6d9e6f2dd1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 7 Jul 2022 23:00:42 +0100 Subject: [PATCH 82/89] android: update service (#792) --- .../android/app/src/main/java/chat/simplex/app/SimplexApp.kt | 1 + .../app/src/main/java/chat/simplex/app/SimplexService.kt | 5 +++-- .../app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 69cae0dd2a..30df816f37 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -74,6 +74,7 @@ class SimplexApp: Application(), LifecycleEventObserver { if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { chatController.showBackgroundServiceNoticeIfNeeded() } + else -> {} } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt index db62727d2c..6163e6d97b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt @@ -7,6 +7,7 @@ import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.* +import chat.simplex.app.model.AppPreferences import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.onboarding.OnboardingStage import kotlinx.coroutines.Dispatchers @@ -48,7 +49,6 @@ class SimplexService: Service() { val text = getString(R.string.simplex_service_notification_text) notificationManager = createNotificationChannel() serviceNotification = createNotification(title, text) - startForeground(SIMPLEX_SERVICE_ID, serviceNotification) } @@ -88,7 +88,7 @@ class SimplexService: Service() { private fun stopService() { Log.d(TAG, "Stopping foreground service") - if (!isServiceStarted || isStoppingService) return + if (isStoppingService) return isStoppingService = true try { wakeLock?.let { @@ -215,6 +215,7 @@ class SimplexService: Service() { suspend fun stop(context: Context) = serviceAction(context, Action.STOP) private suspend fun serviceAction(context: Context, action: Action) { + if (!AppPreferences(context).runServiceInBackground.get()) { return } Log.d(TAG, "SimplexService serviceAction: ${action.name}") withContext(Dispatchers.IO) { Intent(context, SimplexService::class.java).also { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 598112224b..88225a0ae7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -113,7 +113,6 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" - } } From 7ad173c5dcd23d43babfc57399b8ca1430a6c320 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 8 Jul 2022 14:03:24 +0100 Subject: [PATCH 83/89] android: version 3.0 (39) --- apps/android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 9d5670a4ec..4d36129a38 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 38 + versionCode 39 versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From ba9a6f3ab65377ca3d93f9ba0aedcb8d93ce5505 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Fri, 8 Jul 2022 17:16:28 +0400 Subject: [PATCH 84/89] android: database export & import (#787) * android: database export & import wip * fix import * import, delete * disabled during in progress * footer * ChatArchiveView * refactor * disable settings * more chat running interactions * more chat running interactions * fixes * rename * fixes * fix * change ts format * chatWasStopped model variable * remove logs * reset chatWasStopped * chat was stopped preference * fixes * unconditional chatRunning * remove intermediary view * refactor * mkInstantPreference * refactor * refactor * refactor * DatabaseItem * remove todos * refactor * refactor * refactor * translations * translations * refactor import * refactor import --- .../java/chat/simplex/app/MainActivity.kt | 3 +- .../main/java/chat/simplex/app/SimplexApp.kt | 2 +- .../java/chat/simplex/app/model/ChatModel.kt | 2 + .../java/chat/simplex/app/model/SimpleXAPI.kt | 67 ++- .../app/views/chatlist/ChatListNavLinkView.kt | 48 +- .../app/views/chatlist/ChatListView.kt | 34 +- .../app/views/chatlist/ChatPreviewView.kt | 6 +- .../app/views/database/ChatArchiveView.kt | 145 ++++++ .../app/views/database/DatabaseView.kt | 471 ++++++++++++++++++ .../app/views/usersettings/SettingsView.kt | 99 +++- .../app/src/main/res/values-ru/strings.xml | 44 ++ .../app/src/main/res/values/strings.xml | 44 ++ apps/ios/SimpleXChat/APITypes.swift | 4 +- src/Simplex/Chat/Archive.hs | 30 +- 14 files changed, 906 insertions(+), 93 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index a724d0fdaf..e0039a67bb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -305,7 +305,8 @@ fun MainPage( if (chatModel.showCallView.value) ActiveCallView(chatModel) else { showAdvertiseLAAlert = true - if (chatModel.chatId.value == null) ChatListView(chatModel, setPerformLA = { setPerformLA(it) }) + val stopped = chatModel.chatRunning.value == false + if (chatModel.chatId.value == null) ChatListView(chatModel, setPerformLA, stopped) else ChatView(chatModel) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 30df816f37..e0eded442a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -67,7 +67,7 @@ class SimplexApp: Application(), LifecycleEventObserver { withApi { when (event) { Lifecycle.Event.ON_STOP -> - if (appPreferences.runServiceInBackground.get()) SimplexService.start(applicationContext) + if (appPreferences.runServiceInBackground.get() && chatModel.chatRunning.value != false) SimplexService.start(applicationContext) Lifecycle.Event.ON_START -> SimplexService.stop(applicationContext) Lifecycle.Event.ON_RESUME -> diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 25276fc6bf..b911edc4a0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -23,6 +23,8 @@ class ChatModel(val controller: ChatController) { val onboardingStage = mutableStateOf(null) val currentUser = mutableStateOf(null) val userCreated = mutableStateOf(null) + val chatRunning = mutableStateOf(null) + val chatDbChanged = mutableStateOf(false) val chats = mutableStateListOf() val chatId = mutableStateOf(null) val chatItems = mutableStateListOf() diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 88225a0ae7..a9b9ec8aa7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -81,6 +81,9 @@ class AppPreferences(val context: Context) { val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true) val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) + val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) + val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null) + val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) private fun mkIntPreference(prefName: String, default: Int) = Preference( @@ -100,6 +103,15 @@ class AppPreferences(val context: Context) { set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply() ) + private fun mkDatePreference(prefName: String, default: Instant?): Preference = + Preference( + get = { + val pref = sharedPreferences.getString(prefName, default?.toEpochMilliseconds()?.toString()) + pref?.let { Instant.fromEpochMilliseconds(pref.toLong()) } + }, + set = fun(value) = sharedPreferences.edit().putString(prefName, value?.toEpochMilliseconds()?.toString()).apply() + ) + companion object { private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion" @@ -113,6 +125,9 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" + private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName" + private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime" + private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" } } @@ -130,12 +145,12 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager suspend fun startChat(user: User) { Log.d(TAG, "user: $user") try { - val chatStarted = apiStartChat() + val justStarted = apiStartChat() apiSetFilesFolder(getAppFilesDirectory(appContext)) chatModel.userAddress.value = apiGetUserAddress() chatModel.userSMPServers.value = getUserSMPServers() val chats = apiGetChats() - if (chatStarted) { + if (justStarted) { chatModel.chats.clear() chatModel.chats.addAll(chats) } else { @@ -144,6 +159,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager chatModel.currentUser.value = user chatModel.userCreated.value = true chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete + chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now()) + chatModel.chatRunning.value = true startReceiver() Log.d(TAG, "chat started") } catch (e: Error) { @@ -215,7 +232,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager throw Error("user not created ${r.responseType} ${r.details}") } - private suspend fun apiStartChat(): Boolean { + suspend fun apiStartChat(): Boolean { val r = sendCmd(CC.StartChat()) when (r) { is CR.ChatStarted -> return true @@ -224,12 +241,38 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } + suspend fun apiStopChat(): Boolean { + val r = sendCmd(CC.ApiStopChat()) + when (r) { + is CR.ChatStopped -> return true + else -> throw Error("failed stopping chat: ${r.responseType} ${r.details}") + } + } + private suspend fun apiSetFilesFolder(filesFolder: String) { val r = sendCmd(CC.SetFilesFolder(filesFolder)) if (r is CR.CmdOk) return throw Error("failed to set files folder: ${r.responseType} ${r.details}") } + suspend fun apiExportArchive(config: ArchiveConfig) { + val r = sendCmd(CC.ApiExportArchive(config)) + if (r is CR.CmdOk) return + throw Error("failed to export archive: ${r.responseType} ${r.details}") + } + + suspend fun apiImportArchive(config: ArchiveConfig) { + val r = sendCmd(CC.ApiImportArchive(config)) + if (r is CR.CmdOk) return + throw Error("failed to import archive: ${r.responseType} ${r.details}") + } + + suspend fun apiDeleteStorage() { + val r = sendCmd(CC.ApiDeleteStorage()) + if (r is CR.CmdOk) return + throw Error("failed to delete storage: ${r.responseType} ${r.details}") + } + private suspend fun apiGetChats(): List { val r = sendCmd(CC.ApiGetChats()) if (r is CR.ApiChats ) return r.chats @@ -844,7 +887,11 @@ sealed class CC { class ShowActiveUser: CC() class CreateActiveUser(val profile: Profile): CC() class StartChat: CC() + class ApiStopChat: CC() class SetFilesFolder(val filesFolder: String): CC() + class ApiExportArchive(val config: ArchiveConfig): CC() + class ApiImportArchive(val config: ArchiveConfig): CC() + class ApiDeleteStorage: CC() class ApiGetChats: CC() class ApiGetChat(val type: ChatType, val id: Long): CC() class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent): CC() @@ -878,7 +925,11 @@ sealed class CC { is ShowActiveUser -> "/u" is CreateActiveUser -> "/u ${profile.displayName} ${profile.fullName}" is StartChat -> "/_start" + is ApiStopChat -> "/_stop" is SetFilesFolder -> "/_files_folder $filesFolder" + is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" + is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" + is ApiDeleteStorage -> "/_db delete" is ApiGetChats -> "/_get chats pcc=on" is ApiGetChat -> "/_get chat ${chatRef(type, id)} count=100" is ApiSendMessage -> "/_send ${chatRef(type, id)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" @@ -913,7 +964,11 @@ sealed class CC { is ShowActiveUser -> "showActiveUser" is CreateActiveUser -> "createActiveUser" is StartChat -> "startChat" + is ApiStopChat -> "apiStopChat" is SetFilesFolder -> "setFilesFolder" + is ApiExportArchive -> "apiExportArchive" + is ApiImportArchive -> "apiImportArchive" + is ApiDeleteStorage -> "apiDeleteStorage" is ApiGetChats -> "apiGetChats" is ApiGetChat -> "apiGetChat" is ApiSendMessage -> "apiSendMessage" @@ -955,6 +1010,9 @@ sealed class CC { @Serializable class ComposedMessage(val filePath: String?, val quotedItemId: Long?, val msgContent: MsgContent) +@Serializable +class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null) + val json = Json { prettyPrint = true ignoreUnknownKeys = true @@ -988,6 +1046,7 @@ sealed class CR { @Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR() @Serializable @SerialName("chatStarted") class ChatStarted: CR() @Serializable @SerialName("chatRunning") class ChatRunning: CR() + @Serializable @SerialName("chatStopped") class ChatStopped: CR() @Serializable @SerialName("apiChats") class ApiChats(val chats: List): CR() @Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR() @Serializable @SerialName("userSMPServers") class UserSMPServers(val smpServers: List): CR() @@ -1046,6 +1105,7 @@ sealed class CR { is ActiveUser -> "activeUser" is ChatStarted -> "chatStarted" is ChatRunning -> "chatRunning" + is ChatStopped -> "chatStopped" is ApiChats -> "apiChats" is ApiChat -> "apiChat" is UserSMPServers -> "userSMPServers" @@ -1105,6 +1165,7 @@ sealed class CR { is ActiveUser -> json.encodeToString(user) is ChatStarted -> noDetails() is ChatRunning -> noDetails() + is ChatStopped -> noDetails() is ApiChats -> json.encodeToString(chats) is ApiChat -> json.encodeToString(chat) is UserSMPServers -> json.encodeToString(smpServers) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 0fc8193047..db30a923c4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -1,6 +1,7 @@ package chat.simplex.app.views.chatlist import android.content.res.Configuration +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -28,6 +29,7 @@ import kotlinx.datetime.Clock fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { val showMenu = remember { mutableStateOf(false) } var showMarkRead by remember { mutableStateOf(false) } + val stopped = chatModel.chatRunning.value == false LaunchedEffect(chat.id, chat.chatStats.unreadCount > 0) { showMenu.value = false delay(500L) @@ -36,31 +38,35 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { when (chat.chatInfo) { is ChatInfo.Direct -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat) }, + chatLinkPreview = { ChatPreviewView(chat, stopped) }, click = { openOrPendingChat(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) }, - showMenu + showMenu, + stopped ) is ChatInfo.Group -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat) }, + chatLinkPreview = { ChatPreviewView(chat, stopped) }, click = { openOrPendingChat(chat.chatInfo, chatModel) }, dropdownMenuItems = { GroupMenuItems(chat, chatModel, showMenu, showMarkRead) }, - showMenu + showMenu, + stopped ) is ChatInfo.ContactRequest -> ChatListNavLinkLayout( chatLinkPreview = { ContactRequestView(chat.chatInfo) }, click = { contactRequestAlertDialog(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactRequestMenuItems(chat.chatInfo, chatModel, showMenu) }, - showMenu + showMenu, + stopped ) is ChatInfo.ContactConnection -> ChatListNavLinkLayout( chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) }, click = { contactConnectionAlertDialog(chat.chatInfo.contactConnection, chatModel) }, dropdownMenuItems = { ContactConnectionMenuItems(chat.chatInfo, chatModel, showMenu) }, - showMenu + showMenu, + stopped ) } } @@ -286,17 +292,12 @@ fun ChatListNavLinkLayout( chatLinkPreview: @Composable () -> Unit, click: () -> Unit, dropdownMenuItems: (@Composable () -> Unit)?, - showMenu: MutableState + showMenu: MutableState, + stopped: Boolean ) { - Surface( - modifier = Modifier - .fillMaxWidth() - .combinedClickable( - onClick = click, - onLongClick = { showMenu.value = true } - ) - .height(88.dp) - ) { + var modifier = Modifier.fillMaxWidth().height(88.dp) + if (!stopped) modifier = modifier.combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) + Surface(modifier) { Row( modifier = Modifier .fillMaxWidth() @@ -345,12 +346,14 @@ fun PreviewChatListNavLinkDirect() { ) ), chatStats = Chat.ChatStats() - ) + ), + stopped = false ) }, click = {}, dropdownMenuItems = null, - showMenu = remember { mutableStateOf(false) } + showMenu = remember { mutableStateOf(false) }, + stopped = false ) } } @@ -378,12 +381,14 @@ fun PreviewChatListNavLinkGroup() { ) ), chatStats = Chat.ChatStats() - ) + ), + stopped = false ) }, click = {}, dropdownMenuItems = null, - showMenu = remember { mutableStateOf(false) } + showMenu = remember { mutableStateOf(false) }, + stopped = false ) } } @@ -403,7 +408,8 @@ fun PreviewChatListNavLinkContactRequest() { }, click = {}, dropdownMenuItems = null, - showMenu = remember { mutableStateOf(false) } + showMenu = remember { mutableStateOf(false) }, + stopped = false ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index a5ff2008aa..a3fc3ada33 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.outlined.Menu import androidx.compose.material.icons.outlined.PersonAdd import androidx.compose.runtime.* @@ -20,6 +21,8 @@ import chat.simplex.app.R import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.ToolbarDark import chat.simplex.app.ui.theme.ToolbarLight +import chat.simplex.app.views.helpers.AlertManager +import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.newchat.NewChatSheet import chat.simplex.app.views.onboarding.MakeConnection import chat.simplex.app.views.usersettings.SettingsView @@ -64,7 +67,7 @@ fun scaffoldController(): ScaffoldController { } @Composable -fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { +fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { val scaffoldCtrl = scaffoldController() LaunchedEffect(chatModel.clearOverlays.value) { if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse() @@ -82,7 +85,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { .fillMaxSize() .background(MaterialTheme.colors.background) ) { - ChatListToolbar(scaffoldCtrl) + ChatListToolbar(scaffoldCtrl, stopped) Divider() if (chatModel.chats.isNotEmpty()) { ChatList(chatModel) @@ -103,7 +106,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { } @Composable -fun ChatListToolbar(scaffoldCtrl: ScaffoldController) { +fun ChatListToolbar(scaffoldCtrl: ScaffoldController, stopped: Boolean) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -127,13 +130,24 @@ fun ChatListToolbar(scaffoldCtrl: ScaffoldController) { fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(5.dp) ) - IconButton(onClick = { scaffoldCtrl.toggleSheet() }) { - Icon( - Icons.Outlined.PersonAdd, - stringResource(R.string.add_contact), - tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(10.dp) - ) + if (!stopped) { + IconButton(onClick = { scaffoldCtrl.toggleSheet() }) { + Icon( + Icons.Outlined.PersonAdd, + stringResource(R.string.add_contact), + tint = MaterialTheme.colors.primary, + modifier = Modifier.padding(10.dp) + ) + } + } else { + IconButton(onClick = { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_is_stopped_indication), generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)) }) { + Icon( + Icons.Filled.Report, + generalGetString(R.string.chat_is_stopped_indication), + tint = Color.Red, + modifier = Modifier.padding(10.dp) + ) + } } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 4bbabd9143..1ce4177e25 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -26,7 +26,7 @@ import chat.simplex.app.views.helpers.ChatInfoImage import chat.simplex.app.views.helpers.badgeLayout @Composable -fun ChatPreviewView(chat: Chat) { +fun ChatPreviewView(chat: Chat, stopped: Boolean) { Row { val cInfo = chat.chatInfo ChatInfoImage(cInfo, size = 72.dp) @@ -80,7 +80,7 @@ fun ChatPreviewView(chat: Chat) { color = MaterialTheme.colors.onPrimary, fontSize = 11.sp, modifier = Modifier - .background(MaterialTheme.colors.primary, shape = CircleShape) + .background(if (stopped) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape) .badgeLayout() .padding(horizontal = 3.dp) .padding(vertical = 1.dp) @@ -131,6 +131,6 @@ fun ChatStatusImage(chat: Chat) { @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData) + ChatPreviewView(Chat.sampleData, stopped = false) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt new file mode 100644 index 0000000000..f3c4fb5a66 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt @@ -0,0 +1,145 @@ +package chat.simplex.app.views.database + +import android.content.Context +import android.content.res.Configuration +import android.net.Uri +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +import chat.simplex.app.TAG +import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.usersettings.SettingsActionItem +import chat.simplex.app.views.usersettings.SettingsSectionView +import kotlinx.datetime.* +import java.io.BufferedOutputStream +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + +@Composable +fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) { + val context = LocalContext.current + val archivePath = "${getFilesDirectory(context)}/$archiveName" + val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, archivePath) + ChatArchiveLayout( + title, + archiveTime, + saveArchive = { saveArchiveLauncher.launch(archivePath.substringAfterLast("/")) }, + deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) } + ) +} + +@Composable +fun ChatArchiveLayout( + title: String, + archiveTime: Instant, + saveArchive: () -> Unit, + deleteArchiveAlert: () -> Unit +) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + @Composable fun divider() = Divider(Modifier.padding(horizontal = 8.dp)) + Text( + title, + Modifier.padding(start = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + + SettingsSectionView(stringResource(R.string.chat_database_section)) { + SettingsActionItem( + Icons.Outlined.IosShare, + stringResource(R.string.save_archive), + saveArchive, + textColor = MaterialTheme.colors.primary + ) + divider() + SettingsActionItem( + Icons.Outlined.Delete, + stringResource(R.string.delete_archive), + deleteArchiveAlert, + textColor = Color.Red + ) + } + val archiveTs = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US).format(Date.from(archiveTime.toJavaInstant())) + SettingsSectionFooter( + String.format(generalGetString(R.string.archive_created_on_ts), archiveTs) + ) + } +} + +@Composable +private fun rememberSaveArchiveLauncher(cxt: Context, chatArchivePath: String): ManagedActivityResultLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument(), + onResult = { destination -> + try { + destination?.let { + val contentResolver = cxt.contentResolver + contentResolver.openOutputStream(destination)?.let { stream -> + val outputStream = BufferedOutputStream(stream) + val file = File(chatArchivePath) + outputStream.write(file.readBytes()) + outputStream.close() + Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show() + } + } + } catch (e: Error) { + Toast.makeText(cxt, generalGetString(R.string.error_saving_file), Toast.LENGTH_SHORT).show() + Log.e(TAG, "rememberSaveArchiveLauncher error saving archive $e") + } + } + ) + +private fun deleteArchiveAlert(m: ChatModel, archivePath: String) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.delete_chat_archive_question), + confirmText = generalGetString(R.string.delete_verb), + onConfirm = { + val fileDeleted = File(archivePath).delete() + if (fileDeleted) { + m.controller.appPrefs.chatArchiveName.set(null) + m.controller.appPrefs.chatArchiveTime.set(null) + ModalManager.shared.closeModal() + } else { + Log.e(TAG, "deleteArchiveAlert delete() error") + } + } + ) +} + +@Preview(showBackground = true) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +) +@Composable +fun PreviewChatArchiveLayout() { + SimpleXTheme { + ChatArchiveLayout( + title = "New database archive", + archiveTime = Clock.System.now(), + saveArchive = {}, + deleteArchiveAlert = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt new file mode 100644 index 0000000000..e3d55c164b --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -0,0 +1,471 @@ +package chat.simplex.app.views.database + +import android.content.Context +import android.content.res.Configuration +import android.net.Uri +import android.os.FileUtils +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Report +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.TAG +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.usersettings.* +import kotlinx.datetime.* +import java.io.* +import java.text.SimpleDateFormat +import java.util.* + +@Composable +fun DatabaseView( + m: ChatModel, + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) +) { + val context = LocalContext.current + val progressIndicator = remember { mutableStateOf(false) } + val runChat = remember { mutableStateOf(false) } + val prefs = m.controller.appPrefs + val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) } + val chatArchiveTime = remember { mutableStateOf(prefs.chatArchiveTime.get()) } + val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } + val chatArchiveFile = remember { mutableStateOf(null) } + val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, chatArchiveFile) + val importArchiveLauncher = rememberGetContentLauncher { uri: Uri? -> + if (uri != null) { + importArchiveAlert(m, context, uri, progressIndicator) + } + } + LaunchedEffect(m.chatRunning) { + runChat.value = m.chatRunning.value ?: true + } + Box( + Modifier.fillMaxSize(), + ) { + DatabaseLayout( + progressIndicator.value, + runChat.value, + m.chatDbChanged.value, + importArchiveLauncher, + chatArchiveName, + chatArchiveTime, + chatLastStart, + startChat = { startChat(m, runChat) }, + stopChatAlert = { stopChatAlert(m, runChat) }, + exportArchive = { exportArchive(context, m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) }, + deleteChatAlert = { deleteChatAlert(m, progressIndicator) }, + showSettingsModal + ) + if (progressIndicator.value) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = HighOrLowlight, + strokeWidth = 2.5.dp + ) + } + } + } +} + +@Composable +fun DatabaseLayout( + progressIndicator: Boolean, + runChat: Boolean, + chatDbChanged: Boolean, + importArchiveLauncher: ManagedActivityResultLauncher, + chatArchiveName: MutableState, + chatArchiveTime: MutableState, + chatLastStart: MutableState, + startChat: () -> Unit, + stopChatAlert: () -> Unit, + exportArchive: () -> Unit, + deleteChatAlert: () -> Unit, + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) +) { + val stopped = !runChat + val operationsDisabled = !stopped || progressIndicator || chatDbChanged + + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + @Composable fun divider() = Divider(Modifier.padding(horizontal = 8.dp)) + Text( + stringResource(R.string.your_chat_database), + Modifier.padding(start = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + + SettingsSectionView(stringResource(R.string.run_chat_section)) { + RunChatSetting(runChat, stopped, chatDbChanged, startChat, stopChatAlert) + } + Spacer(Modifier.height(30.dp)) + + SettingsSectionView(stringResource(R.string.chat_database_section)) { + SettingsActionItem( + Icons.Outlined.IosShare, + stringResource(R.string.export_database), + exportArchive, + textColor = MaterialTheme.colors.primary, + disabled = operationsDisabled + ) + divider() + SettingsActionItem( + Icons.Outlined.FileDownload, + stringResource(R.string.import_database), + { importArchiveLauncher.launch("application/zip") }, + textColor = Color.Red, + disabled = operationsDisabled + ) + divider() + val chatArchiveNameVal = chatArchiveName.value + val chatArchiveTimeVal = chatArchiveTime.value + val chatLastStartVal = chatLastStart.value + if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) { + val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal) + SettingsActionItem( + Icons.Outlined.Inventory2, + title, + click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) }, + disabled = operationsDisabled + ) + divider() + } + SettingsActionItem( + Icons.Outlined.DeleteForever, + stringResource(R.string.delete_database), + deleteChatAlert, + textColor = Color.Red, + disabled = operationsDisabled + ) + } + SettingsSectionFooter( + if (chatDbChanged) { + stringResource(R.string.restart_the_app_to_use_new_chat_database) + } else { + if (stopped) { + stringResource(R.string.you_must_use_the_most_recent_version_of_database) + } else { + stringResource(R.string.stop_chat_to_enable_database_actions) + } + } + ) + } +} + +@Composable +fun RunChatSetting( + runChat: Boolean, + stopped: Boolean, + chatDbChanged: Boolean, + startChat: () -> Unit, + stopChatAlert: () -> Unit +) { + SettingsItemView() { + Row(verticalAlignment = Alignment.CenterVertically) { + val chatRunningText = if (stopped) stringResource(R.string.chat_is_stopped) else stringResource(R.string.chat_is_running) + Icon( + if (stopped) Icons.Filled.Report else Icons.Filled.PlayArrow, + chatRunningText, + tint = if (chatDbChanged) HighOrLowlight else if (stopped) Color.Red else MaterialTheme.colors.primary + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + chatRunningText, + Modifier.padding(end = 24.dp), + color = if (chatDbChanged) HighOrLowlight else Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + Switch( + checked = runChat, + onCheckedChange = { runChatSwitch -> + if (runChatSwitch) { + startChat() + } else { + stopChatAlert() + } + }, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ), + enabled = !chatDbChanged + ) + } + } +} + +@Composable +fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { + return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive) +} + +@Composable +fun SettingsSectionFooter(text: String) { + Text(text, color = HighOrLowlight, modifier = Modifier.padding(start = 16.dp, top = 5.dp).fillMaxWidth(0.9F), fontSize = 12.sp) +} + +private fun startChat(m: ChatModel, runChat: MutableState) { + withApi { + try { + m.controller.apiStartChat() + runChat.value = true + m.chatRunning.value = true + m.controller.appPrefs.chatLastStart.set(Clock.System.now()) + } catch (e: Error) { + runChat.value = false + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_starting_chat), e.toString()) + } + } +} + +private fun stopChatAlert(m: ChatModel, runChat: MutableState) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.stop_chat_question), + text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database), + confirmText = generalGetString(R.string.stop_chat_confirmation), + onConfirm = { stopChat(m, runChat) }, + onDismiss = { runChat.value = true } + ) +} + +private fun stopChat(m: ChatModel, runChat: MutableState) { + withApi { + try { + m.controller.apiStopChat() + runChat.value = false + m.chatRunning.value = false + } catch (e: Error) { + runChat.value = true + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_starting_chat), e.toString()) + } + } +} + +private fun exportArchive( + context: Context, + m: ChatModel, + progressIndicator: MutableState, + chatArchiveName: MutableState, + chatArchiveTime: MutableState, + chatArchiveFile: MutableState, + saveArchiveLauncher: ManagedActivityResultLauncher +) { + progressIndicator.value = true + withApi { + try { + val archiveFile = exportChatArchive(m, context, chatArchiveName, chatArchiveTime, chatArchiveFile) + chatArchiveFile.value = archiveFile + saveArchiveLauncher.launch(archiveFile.substringAfterLast("/")) + progressIndicator.value = false + } catch (e: Error) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_exporting_chat_database), e.toString()) + progressIndicator.value = false + } + } +} + +private suspend fun exportChatArchive( + m: ChatModel, + context: Context, + chatArchiveName: MutableState, + chatArchiveTime: MutableState, + chatArchiveFile: MutableState +): String { + val archiveTime = Clock.System.now() + val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant())) + val archiveName = "simplex-chat.$ts.zip" + val archivePath = "${getFilesDirectory(context)}/$archiveName" + val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString()) + m.controller.apiExportArchive(config) + deleteOldArchive(m, context) + m.controller.appPrefs.chatArchiveName.set(archiveName) + chatArchiveName.value = archiveName + m.controller.appPrefs.chatArchiveTime.set(archiveTime) + chatArchiveTime.value = archiveTime + chatArchiveFile.value = archivePath + return archivePath +} + +private fun deleteOldArchive(m: ChatModel, context: Context) { + val chatArchiveName = m.controller.appPrefs.chatArchiveName.get() + if (chatArchiveName != null) { + val file = File("${getFilesDirectory(context)}/$chatArchiveName") + val fileDeleted = file.delete() + if (fileDeleted) { + m.controller.appPrefs.chatArchiveName.set(null) + m.controller.appPrefs.chatArchiveTime.set(null) + } else { + Log.e(TAG, "deleteOldArchive file.delete() error") + } + } +} + +@Composable +private fun rememberSaveArchiveLauncher(cxt: Context, chatArchiveFile: MutableState): ManagedActivityResultLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument(), + onResult = { destination -> + try { + destination?.let { + val filePath = chatArchiveFile.value + if (filePath != null) { + val contentResolver = cxt.contentResolver + contentResolver.openOutputStream(destination)?.let { stream -> + val outputStream = BufferedOutputStream(stream) + val file = File(filePath) + outputStream.write(file.readBytes()) + outputStream.close() + Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show() + } + } else { + Toast.makeText(cxt, generalGetString(R.string.file_not_found), Toast.LENGTH_SHORT).show() + } + } + } catch (e: Error) { + Toast.makeText(cxt, generalGetString(R.string.error_saving_file), Toast.LENGTH_SHORT).show() + Log.e(TAG, "rememberSaveArchiveLauncher error saving archive $e") + } finally { + chatArchiveFile.value = null + } + } + ) + +private fun importArchiveAlert(m: ChatModel, context: Context, importedArchiveUri: Uri, progressIndicator: MutableState) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.import_database_question), + text = generalGetString(R.string.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one), + confirmText = generalGetString(R.string.import_database_confirmation), + onConfirm = { importArchive(m, context, importedArchiveUri, progressIndicator) } + ) +} + +private fun importArchive(m: ChatModel, context: Context, importedArchiveUri: Uri, progressIndicator: MutableState) { + progressIndicator.value = true + val archivePath = saveArchiveFromUri(context, importedArchiveUri) + if (archivePath != null) { + withApi { + try { + m.controller.apiDeleteStorage() + try { + val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString()) + m.controller.apiImportArchive(config) + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database)) + } + } catch (e: Error) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_importing_database), e.toString()) + } + } + } catch (e: Error) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_database), e.toString()) + } + } finally { + File(archivePath).delete() + } + } + } +} + +private fun saveArchiveFromUri(context: Context, importedArchiveUri: Uri): String? { + return try { + val inputStream = context.contentResolver.openInputStream(importedArchiveUri) + val archiveName = getFileName(context, importedArchiveUri) + if (inputStream != null && archiveName != null) { + val archivePath = "${context.cacheDir}/$archiveName" + val destFile = File(archivePath) + FileUtils.copy(inputStream, FileOutputStream(destFile)) + archivePath + } else { + Log.e(TAG, "saveArchiveFromUri null inputStream") + null + } + } catch (e: Exception) { + Log.e(TAG, "saveArchiveFromUri error: ${e.message}") + null + } +} + +private fun deleteChatAlert(m: ChatModel, progressIndicator: MutableState) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.delete_chat_profile_question), + text = generalGetString(R.string.delete_chat_profile_action_cannot_be_undone_warning), + confirmText = generalGetString(R.string.delete_verb), + onConfirm = { deleteChat(m, progressIndicator) } + ) +} + +private fun deleteChat(m: ChatModel, progressIndicator: MutableState) { + progressIndicator.value = true + withApi { + try { + m.controller.apiDeleteStorage() + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile)) + } + } catch (e: Error) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_database), e.toString()) + } + } + } +} + +private fun operationEnded(m: ChatModel, progressIndicator: MutableState, alert: () -> Unit) { + m.chatDbChanged.value = true + progressIndicator.value = false + alert.invoke() +} + +@Preview(showBackground = true) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +) +@Composable +fun PreviewDatabaseLayout() { + SimpleXTheme { + DatabaseLayout( + progressIndicator = false, + runChat = true, + chatDbChanged = false, + importArchiveLauncher = rememberGetContentLauncher {}, + chatArchiveName = remember { mutableStateOf("dummy_archive") }, + chatArchiveTime = remember { mutableStateOf(Clock.System.now()) }, + chatLastStart = remember { mutableStateOf(Clock.System.now()) }, + startChat = {}, + stopChatAlert = {}, + exportArchive = {}, + deleteChatAlert = {}, + showSettingsModal = { {} } + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 582a387e9a..6e42d149ed 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -23,12 +24,14 @@ import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.TerminalView +import chat.simplex.app.views.database.DatabaseView import chat.simplex.app.views.helpers.* import chat.simplex.app.views.onboarding.SimpleXInfo @Composable fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { val user = chatModel.currentUser.value + val stopped = chatModel.chatRunning.value == false fun setRunServiceInBackground(on: Boolean) { chatModel.controller.appPrefs.runServiceInBackground.set(on) @@ -42,6 +45,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { if (user != null) { SettingsLayout( profile = user.profile, + stopped, runServiceInBackground = chatModel.runServiceInBackground, setRunServiceInBackground = ::setRunServiceInBackground, setPerformLA = setPerformLA, @@ -65,6 +69,7 @@ val simplexTeamUri = @Composable fun SettingsLayout( profile: Profile, + stopped: Boolean, runServiceInBackground: MutableState, setRunServiceInBackground: (Boolean) -> Unit, setPerformLA: (Boolean) -> Unit, @@ -92,40 +97,42 @@ fun SettingsLayout( Spacer(Modifier.height(30.dp)) SettingsSectionView(stringResource(R.string.settings_section_title_you)) { - SettingsItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp) { - ProfilePreview(profile) + SettingsItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, disabled = stopped) { + ProfilePreview(profile, stopped = stopped) } divider() - SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { UserAddressView(it) }) + SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { UserAddressView(it) }, disabled = stopped) + divider() + DatabaseItem(showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } spacer() SettingsSectionView(stringResource(R.string.settings_section_title_settings)) { - SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it) }) + SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it) }, disabled = stopped) divider() - SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }) + SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped) divider() - PrivateNotificationsItem(runServiceInBackground, setRunServiceInBackground) + PrivateNotificationsItem(runServiceInBackground, setRunServiceInBackground, stopped) divider() - SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showModal { SMPServersView(it) }) + SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showModal { SMPServersView(it) }, disabled = stopped) } spacer() SettingsSectionView(stringResource(R.string.settings_section_title_help)) { - SettingsActionItem(Icons.Outlined.HelpOutline, stringResource(R.string.how_to_use_simplex_chat), showModal { HelpView(it) }) + SettingsActionItem(Icons.Outlined.HelpOutline, stringResource(R.string.how_to_use_simplex_chat), showModal { HelpView(it) }, disabled = stopped) divider() SettingsActionItem(Icons.Outlined.Info, stringResource(R.string.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }) divider() SettingsActionItem(Icons.Outlined.TextFormat, stringResource(R.string.markdown_in_messages), showModal { MarkdownHelpView() }) divider() - SettingsActionItem(Icons.Outlined.Tag, stringResource(R.string.chat_with_the_founder), { uriHandler.openUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary) + SettingsActionItem(Icons.Outlined.Tag, stringResource(R.string.chat_with_the_founder), { uriHandler.openUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped) divider() SettingsActionItem(Icons.Outlined.Email, stringResource(R.string.send_us_an_email), { uriHandler.openUri("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary) } spacer() SettingsSectionView(stringResource(R.string.settings_section_title_develop)) { - ChatConsoleItem(showTerminal) + ChatConsoleItem(showTerminal, stopped) divider() InstallTerminalAppItem(uriHandler) divider() @@ -139,19 +146,49 @@ fun SettingsLayout( @Composable fun SettingsSectionView(title: String, content: (@Composable () -> Unit)) { Column { - Text(title, color = HighOrLowlight, style = MaterialTheme.typography.body2, - modifier = Modifier.padding(start = 16.dp, bottom = 5.dp), fontSize = 12.sp) + Text( + title, color = HighOrLowlight, style = MaterialTheme.typography.body2, + modifier = Modifier.padding(start = 16.dp, bottom = 5.dp), fontSize = 12.sp + ) Surface(color = if (isSystemInDarkTheme()) GroupDark else MaterialTheme.colors.background) { Column(Modifier.padding(horizontal = 6.dp)) { content() } } } } +@Composable private fun DatabaseItem(openDatabaseView: () -> Unit, stopped: Boolean) { + SettingsItemView(openDatabaseView) { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row { + Icon( + Icons.Outlined.Archive, + contentDescription = stringResource(R.string.database_export_and_import), + tint = HighOrLowlight, + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text(stringResource(R.string.database_export_and_import)) + } + if (stopped) { + Icon( + Icons.Filled.Report, + contentDescription = stringResource(R.string.chat_is_stopped), + tint = Color.Red, + modifier = Modifier.padding(end = 6.dp) + ) + } + } + } +} + @Composable private fun PrivateNotificationsItem( runServiceInBackground: MutableState, - setRunServiceInBackground: (Boolean) -> Unit + setRunServiceInBackground: (Boolean) -> Unit, + stopped: Boolean ) { - SettingsItemView() { + SettingsItemView(disabled = stopped) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( Icons.Outlined.Bolt, @@ -164,7 +201,8 @@ fun SettingsLayout( Modifier .padding(end = 24.dp) .fillMaxWidth() - .weight(1f) + .weight(1f), + color = if (stopped) HighOrLowlight else Color.Unspecified ) Switch( checked = runServiceInBackground.value, @@ -173,7 +211,8 @@ fun SettingsLayout( checkedThumbColor = MaterialTheme.colors.primary, uncheckedThumbColor = HighOrLowlight ), - modifier = Modifier.padding(end = 6.dp) + modifier = Modifier.padding(end = 6.dp), + enabled = !stopped ) } } @@ -207,15 +246,18 @@ fun SettingsLayout( } } -@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) { - SettingsItemView(showTerminal) { +@Composable private fun ChatConsoleItem(showTerminal: () -> Unit, stopped: Boolean) { + SettingsItemView(showTerminal, disabled = stopped) { Icon( painter = painterResource(id = R.drawable.ic_outline_terminal), contentDescription = stringResource(R.string.chat_console), tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) - Text(stringResource(R.string.chat_console)) + Text( + stringResource(R.string.chat_console), + color = if (stopped) HighOrLowlight else Color.Unspecified + ) } } @@ -237,7 +279,7 @@ fun SettingsLayout( } } -@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary) { +@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary, stopped: Boolean = false) { ProfileImage(size = size, image = profileOf.image, color = color) Spacer(Modifier.padding(horizontal = 4.dp)) Column { @@ -245,19 +287,23 @@ fun SettingsLayout( profileOf.displayName, style = MaterialTheme.typography.caption, fontWeight = FontWeight.Bold, + color = if (stopped) HighOrLowlight else Color.Unspecified + ) + Text( + profileOf.fullName, + color = if (stopped) HighOrLowlight else Color.Unspecified ) - Text(profileOf.fullName) } } @Composable -fun SettingsItemView(click: (() -> Unit)? = null, height: Dp = 46.dp, content: (@Composable () -> Unit)) { +fun SettingsItemView(click: (() -> Unit)? = null, height: Dp = 46.dp, disabled: Boolean = false, content: (@Composable () -> Unit)) { val modifier = Modifier .padding(start = 8.dp) .fillMaxWidth() .height(height) Row( - if (click == null) modifier else modifier.clickable(onClick = click), + if (click == null || disabled) modifier else modifier.clickable(onClick = click), verticalAlignment = Alignment.CenterVertically ) { content() @@ -265,11 +311,11 @@ fun SettingsItemView(click: (() -> Unit)? = null, height: Dp = 46.dp, content: ( } @Composable -fun SettingsActionItem(icon: ImageVector, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified) { - SettingsItemView(click) { +fun SettingsActionItem(icon: ImageVector, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified, disabled: Boolean = false) { + SettingsItemView(click, disabled = disabled) { Icon(icon, text, tint = HighOrLowlight) Spacer(Modifier.padding(horizontal = 4.dp)) - Text(text, color = textColor) + Text(text, color = if (disabled) HighOrLowlight else textColor) } } @@ -295,6 +341,7 @@ fun PreviewSettingsLayout() { SimpleXTheme { SettingsLayout( profile = Profile.sampleData, + stopped = false, runServiceInBackground = remember { mutableStateOf(true) }, setRunServiceInBackground = {}, setPerformLA = {}, diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index c6a916f6c3..0bd6e6901d 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -244,6 +244,7 @@ Настройки Ваш SimpleX адрес + Экспорт и импорт архива чата Информация о SimpleX Chat Как использовать Форматирование сообщений @@ -439,4 +440,47 @@ УСТРОЙСТВО ЧАТЫ Экспериментальные функции + + + Архив чата + ЗАПУСТИТЬ ЧАТ + Чат запущен + Чат остановлен + АРХИВ ЧАТА + Экспорт архива чата + Импорт архива чата + Новый архив чата + Старый архив чата + Удалить данные чата + Ошибка при запуске чата + Остановить чат? + Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен. + Остановить + Ошибка при остановке чата + Ошибка экспорта архива чата + Импортировать архив чата? + Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными.\nЭто действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. + Импортировать + Ошибка при удалении данных чата + Ошибка при импорте архива чата + Архив чата импортирован + Перезапустите приложение, чтобы использовать импортированный архив. + Удалить профиль? + Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. + Данные чата удалены + Перезапустите приложение, чтобы создать новый профиль. + Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе вы можете перестать получать сообщения от некоторых контактов. + Остановите чат, чтобы разблокировать операции с архивом. + Перезапустите приложение, чтобы использовать новый архив чата. + + + Чат остановлен + Вы можете стартовать чат через Настройки приложения или перезапустив приложение. + + + Архив чата + Сохранить архив + Удалить архив + Дата создания %1$s + Удалить архив чата? diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index e327f0febb..eb30fbb86c 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -250,6 +250,7 @@ Your settings Your SimpleX contact address + Database export & import About SimpleX Chat How to use it Markdown help @@ -441,4 +442,47 @@ DEVICE CHATS Experimental features + + + Your chat database + RUN CHAT + Chat is running + Chat is stopped + CHAT DATABASE + Export database + Import database + New database archive + Old database archive + Delete database + Error starting chat + Stop chat? + Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. + Stop + Error stopping chat + Error exporting chat database + Import chat database? + Your current chat database will be DELETED and REPLACED with the imported one.\nThis action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. + Import + Error deleting chat database + Error importing chat database + Chat database imported + Restart the app to use imported chat database. + Delete chat profile? + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. + Chat database deleted + Restart the app to create a new chat profile. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. + Stop chat to enable database actions. + Restart the app to use new chat database. + + + Chat is stopped + You can start chat via app Settings / Database or by restarting the app. + + + Chat archive + Save archive + Delete archive + Created on %1$s + Delete chat archive? diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 317135dd97..b2622a539e 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -383,12 +383,10 @@ struct ComposedMessage: Encodable { public struct ArchiveConfig: Encodable { var archivePath: String var disableCompression: Bool? - var parentTempDirectory: String? - public init(archivePath: String, disableCompression: Bool? = nil, parentTempDirectory: String? = nil) { + public init(archivePath: String, disableCompression: Bool? = nil) { self.archivePath = archivePath self.disableCompression = disableCompression - self.parentTempDirectory = parentTempDirectory } } diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index ce8f820f6e..8f7a92e675 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -24,48 +24,28 @@ archiveFilesFolder :: String archiveFilesFolder = "simplex_v1_files" exportArchive :: ChatMonad m => ArchiveConfig -> m () -exportArchive cfg@ArchiveConfig {archivePath, disableCompression} = do - liftIO . print $ "exportArchive 1" +exportArchive cfg@ArchiveConfig {archivePath, disableCompression} = withTempDir cfg "simplex-chat." $ \dir -> do - liftIO . print $ "exportArchive 2, dir = " <> dir StorageFiles {chatDb, agentDb, filesPath} <- storageFiles - liftIO . print $ "exportArchive 3" copyFile chatDb $ dir archiveChatDbFile - liftIO . print $ "exportArchive 4" copyFile agentDb $ dir archiveAgentDbFile - liftIO . print $ "exportArchive 5" - forM_ filesPath $ \fp -> do - liftIO . print $ "exportArchive 6, fp = " <> fp + forM_ filesPath $ \fp -> copyDirectoryFiles fp $ dir archiveFilesFolder - liftIO . print $ "exportArchive 7" let method = if disableCompression == Just True then Z.Store else Z.Deflate - liftIO . print $ "exportArchive 8, method = " <> show method Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir - liftIO . print $ "exportArchive 9" importArchive :: ChatMonad m => ArchiveConfig -> m () -importArchive cfg@ArchiveConfig {archivePath} = do - liftIO . print $ "importArchive 1" +importArchive cfg@ArchiveConfig {archivePath} = withTempDir cfg "simplex-chat." $ \dir -> do - liftIO . print $ "importArchive 2, dir = " <> dir Z.withArchive archivePath $ Z.unpackInto dir - liftIO . print $ "importArchive 3" StorageFiles {chatDb, agentDb, filesPath} <- storageFiles - liftIO . print $ "importArchive 4" backup chatDb - liftIO . print $ "importArchive 5" backup agentDb - liftIO . print $ "importArchive 6" copyFile (dir archiveChatDbFile) chatDb - liftIO . print $ "importArchive 7" copyFile (dir archiveAgentDbFile) agentDb - liftIO . print $ "importArchive 8" let filesDir = dir archiveFilesFolder - liftIO . print $ "importArchive 9, filesDir = " <> filesDir - forM_ filesPath $ \fp -> do - liftIO . print $ "importArchive 10, fp = " <> fp - whenM (doesDirectoryExist filesDir) $ do - liftIO . print $ "importArchive 11" + forM_ filesPath $ \fp -> + whenM (doesDirectoryExist filesDir) $ copyDirectoryFiles filesDir fp where backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak" From b58c880d4c49338e6462288fd7b081d1a17470bf Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Fri, 8 Jul 2022 20:03:21 +0400 Subject: [PATCH 85/89] android: version 3.0 (40) --- apps/android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 4d36129a38..ed044fca89 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 39 + versionCode 40 versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From b1c732f3ccadc8ca35be87f36f405383aab3b5be Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:42:38 +0100 Subject: [PATCH 86/89] ios: translations (#793) --- .../app/src/main/res/values-ru/strings.xml | 10 +- .../Shared/Views/Database/DatabaseView.swift | 10 +- .../en.xcloc/Localized Contents/en.xliff | 665 +++++++++++++----- .../SimpleX NSE/en.lproj/Localizable.strings | Bin 3612 -> 0 bytes .../en.xcloc/contents.json | 4 +- .../ru.xcloc/Localized Contents/ru.xliff | 665 +++++++++++++----- .../SimpleX NSE/en.lproj/Localizable.strings | Bin 3612 -> 0 bytes .../ru.xcloc/contents.json | 4 +- apps/ios/ru.lproj/Localizable.strings | 298 +++++++- 9 files changed, 1242 insertions(+), 414 deletions(-) delete mode 100644 apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings delete mode 100644 apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 0bd6e6901d..9df9a051a9 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -442,7 +442,7 @@ Экспериментальные функции - Архив чата + Данные чата ЗАПУСТИТЬ ЧАТ Чат запущен Чат остановлен @@ -457,25 +457,25 @@ Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен. Остановить Ошибка при остановке чата - Ошибка экспорта архива чата + Ошибка при экспорте архива чата Импортировать архив чата? Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными.\nЭто действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. Импортировать Ошибка при удалении данных чата Ошибка при импорте архива чата Архив чата импортирован - Перезапустите приложение, чтобы использовать импортированный архив. + Перезапустите приложение, чтобы использовать импортированные данные чата. Удалить профиль? Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. Данные чата удалены Перезапустите приложение, чтобы создать новый профиль. Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе вы можете перестать получать сообщения от некоторых контактов. - Остановите чат, чтобы разблокировать операции с архивом. + Остановите чат, чтобы разблокировать операции с архивом чата. Перезапустите приложение, чтобы использовать новый архив чата. Чат остановлен - Вы можете стартовать чат через Настройки приложения или перезапустив приложение. + Вы можете запустить чат через Настройки приложения или перезапустив приложение. Архив чата diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 1b46b43e1b..d904cd05ca 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -170,7 +170,7 @@ struct DatabaseView: View { if let fileURL = importedArchivePath { return Alert( title: Text("Import chat database?"), - message: Text("Your current chat database will be DELETED and REPLACED with the imported one.\n") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."), + message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."), primaryButton: .destructive(Text("Import")) { importArchive(fileURL) }, @@ -182,9 +182,7 @@ struct DatabaseView: View { case .archiveImported: return Alert( title: Text("Chat database imported"), - message: Text("Restart the app to use imported chat database"), - primaryButton: .default(Text("Ok")), - secondaryButton: .cancel() + message: Text("Restart the app to use imported chat database") ) case .deleteChat: @@ -199,9 +197,7 @@ struct DatabaseView: View { case .chatDeleted: return Alert( title: Text("Chat database deleted"), - message: Text("Restart the app to create a new chat profile"), - primaryButton: .default(Text("Ok")), - secondaryButton: .cancel() + message: Text("Restart the app to create a new chat profile") ) case .deleteLegacyDatabase: return Alert( diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 5f1e5c140e..06630b7512 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2,7 +2,7 @@
- +
@@ -30,6 +30,11 @@ #secret# No comment provided by engineer. + + %@ + %@ + No comment provided by engineer. + %@ / %@ %@ / %@ @@ -80,11 +85,26 @@ **Create link / QR code** for your contact to use. No comment provided by engineer. + + **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + No comment provided by engineer. + + + **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + No comment provided by engineer. + **Paste received link** or open it in the browser and tap **Open in mobile app**. **Paste received link** or open it in the browser and tap **Open in mobile app**. No comment provided by engineer. + + **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + No comment provided by engineer. + **Scan QR code**: to connect to your contact in person or via video call. **Scan QR code**: to connect to your contact in person or via video call. @@ -120,6 +140,11 @@ : No comment provided by engineer. + + A new contact + A new contact + notification title + About SimpleX About SimpleX @@ -206,11 +231,41 @@ Cancel No comment provided by engineer. + + Chat archive + Chat archive + No comment provided by engineer. + Chat console Chat console No comment provided by engineer. + + Chat database + Chat database + No comment provided by engineer. + + + Chat database deleted + Chat database deleted + No comment provided by engineer. + + + Chat database imported + Chat database imported + No comment provided by engineer. + + + Chat is running + Chat is running + No comment provided by engineer. + + + Chat is stopped + Chat is stopped + No comment provided by engineer. + Chat with the developers Chat with the developers @@ -221,16 +276,6 @@ Chats back button to return to chats list - - Check messages - Check messages - No comment provided by engineer. - - - Checking new messages... - Checking new messages... - notification - Choose file Choose file @@ -346,6 +391,11 @@ Contact and all messages will be deleted - this cannot be undone! No comment provided by engineer. + + Contact hidden: + Contact hidden: + notification + Contact is connected Contact is connected @@ -356,6 +406,11 @@ Contact is not connected yet! No comment provided by engineer. + + Contact name + Contact name + No comment provided by engineer. + Copy Copy @@ -386,11 +441,26 @@ Create your profile No comment provided by engineer. + + Created on %@ + Created on %@ + No comment provided by engineer. + Currently maximum supported file size is %@. Currently maximum supported file size is %@. No comment provided by engineer. + + Database export & import + Database export & import + No comment provided by engineer. + + + Database will be migrated when the app restarts + Database will be migrated when the app restarts + No comment provided by engineer. + Decentralized Decentralized @@ -416,6 +486,21 @@ Delete address? No comment provided by engineer. + + Delete archive + Delete archive + No comment provided by engineer. + + + Delete chat archive? + Delete chat archive? + No comment provided by engineer. + + + Delete chat profile? + Delete chat profile? + No comment provided by engineer. + Delete contact Delete contact @@ -426,6 +511,11 @@ Delete contact? No comment provided by engineer. + + Delete database + Delete database + No comment provided by engineer. + Delete for everyone Delete for everyone @@ -446,6 +536,16 @@ Delete message? No comment provided by engineer. + + Delete old database + Delete old database + No comment provided by engineer. + + + Delete old database? + Delete old database? + No comment provided by engineer. + Delete pending connection Delete pending connection @@ -491,19 +591,59 @@ Do NOT use SimpleX for emergency calls. No comment provided by engineer. + + Do it later + Do it later + No comment provided by engineer. + Edit Edit No comment provided by engineer. + + Enable + Enable + No comment provided by engineer. + Enable SimpleX Lock Enable SimpleX Lock authentication reason - - Enable notifications? (BETA) - Enable notifications? (BETA) + + Enable instant notifications? + Enable instant notifications? + No comment provided by engineer. + + + Enable notifications + Enable notifications + No comment provided by engineer. + + + Enable periodic notifications? + Enable periodic notifications? + No comment provided by engineer. + + + Error accessing database file + Error accessing database file + No comment provided by engineer. + + + Error deleting chat database + Error deleting chat database + No comment provided by engineer. + + + Error deleting database + Error deleting database + No comment provided by engineer. + + + Error deleting old database + Error deleting old database No comment provided by engineer. @@ -511,9 +651,19 @@ Error deleting token No comment provided by engineer. - - Error registering token - Error registering token + + Error enabling notifications + Error enabling notifications + No comment provided by engineer. + + + Error exporting chat database + Error exporting chat database + No comment provided by engineer. + + + Error importing chat database + Error importing chat database No comment provided by engineer. @@ -521,6 +671,16 @@ Error saving SMP servers No comment provided by engineer. + + Error starting chat + Error starting chat + No comment provided by engineer. + + + Error stopping chat + Error stopping chat + No comment provided by engineer. + Error: %@ Error: %@ @@ -531,9 +691,29 @@ Error: URL is invalid No comment provided by engineer. - - Experimental features - Experimental features + + Error: no database file + Error: no database file + No comment provided by engineer. + + + Export database + Export database + No comment provided by engineer. + + + Export error: + Export error: + No comment provided by engineer. + + + Exported database archive. + Exported database archive. + No comment provided by engineer. + + + Exporting database archive... + Exporting database archive... No comment provided by engineer. @@ -551,11 +731,21 @@ Group deletion is not supported No comment provided by engineer. + + Group message: + Group message: + notification + Help Help No comment provided by engineer. + + Hidden + Hidden + No comment provided by engineer. + How SimpleX works How SimpleX works @@ -596,6 +786,11 @@ If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link. No comment provided by engineer. + + If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). + If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). + No comment provided by engineer. + Ignore Ignore @@ -611,6 +806,21 @@ Immune to spam and abuse No comment provided by engineer. + + Import + Import + No comment provided by engineer. + + + Import chat database? + Import chat database? + No comment provided by engineer. + + + Import database + Import database + No comment provided by engineer. + In person or via a video call – the most secure way to connect. In person or via a video call – the most secure way to connect. @@ -636,6 +846,11 @@ Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instantly + Instantly + No comment provided by engineer. + Invalid connection link Invalid connection link @@ -701,6 +916,31 @@ We will be adding server redundancy to prevent lost messages. Message delivery error No comment provided by engineer. + + Message text + Message text + No comment provided by engineer. + + + Migrating database archive... + Migrating database archive... + No comment provided by engineer. + + + Migration error: + Migration error: + No comment provided by engineer. + + + Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). + Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). + No comment provided by engineer. + + + Migration is completed + Migration is completed + No comment provided by engineer. + Most likely this contact has deleted the connection with you. Most likely this contact has deleted the connection with you. @@ -711,16 +951,51 @@ We will be adding server redundancy to prevent lost messages. New contact request notification + + New contact: + New contact: + notification + + + New database archive + New database archive + No comment provided by engineer. + New message New message notification + + No device token! + No device token! + No comment provided by engineer. + + + Notifications + Notifications + No comment provided by engineer. + Notifications are disabled! Notifications are disabled! No comment provided by engineer. + + Off (Local) + Off (Local) + No comment provided by engineer. + + + Old database + Old database + No comment provided by engineer. + + + Old database archive + Old database archive + No comment provided by engineer. + One-time invitation link One-time invitation link @@ -776,6 +1051,11 @@ We will be adding server redundancy to prevent lost messages. People can connect to you only via the links you share. No comment provided by engineer. + + Periodically + Periodically + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Please check that you used the correct link or ask your contact to send you another one. @@ -786,9 +1066,9 @@ We will be adding server redundancy to prevent lost messages. Please check your network connection and try again. No comment provided by engineer. - - Pre-arrange the calls, as notifications arrive with a delay (we are improving it). - Pre-arrange the calls, as notifications arrive with a delay (we are improving it). + + Please restart the app and migrate the database to enable push notifications. + Please restart the app and migrate the database to enable push notifications. No comment provided by engineer. @@ -806,6 +1086,11 @@ We will be adding server redundancy to prevent lost messages. Profile image No comment provided by engineer. + + Push notifications + Push notifications + No comment provided by engineer. + Read Read @@ -821,6 +1106,11 @@ We will be adding server redundancy to prevent lost messages. Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). No comment provided by engineer. + + Received file event + Received file event + notification + Reject Reject @@ -841,9 +1131,19 @@ We will be adding server redundancy to prevent lost messages. Reply No comment provided by engineer. - - Retry - Retry + + Restart the app to create a new chat profile + Restart the app to create a new chat profile + No comment provided by engineer. + + + Restart the app to use imported chat database + Restart the app to use imported chat database + No comment provided by engineer. + + + Run chat + Run chat No comment provided by engineer. @@ -866,6 +1166,11 @@ We will be adding server redundancy to prevent lost messages. Save (and notify contacts) No comment provided by engineer. + + Save archive + Save archive + No comment provided by engineer. + Saved SMP servers will be removed Saved SMP servers will be removed @@ -886,6 +1191,21 @@ We will be adding server redundancy to prevent lost messages. Send link previews No comment provided by engineer. + + Send notifications + Send notifications + No comment provided by engineer. + + + Send notifications: + Send notifications: + No comment provided by engineer. + + + Sent file event + Sent file event + notification + Server connected Server connected @@ -916,6 +1236,11 @@ We will be adding server redundancy to prevent lost messages. Show pending connections No comment provided by engineer. + + Show preview + Show preview + No comment provided by engineer. + SimpleX Lock SimpleX Lock @@ -926,11 +1251,51 @@ We will be adding server redundancy to prevent lost messages. SimpleX Lock turned on No comment provided by engineer. + + SimpleX encrypted message or connection event + SimpleX encrypted message or connection event + notification + Skipped messages Skipped messages No comment provided by engineer. + + Somebody + Somebody + notification title + + + Start chat + Start chat + No comment provided by engineer. + + + Start migration + Start migration + No comment provided by engineer. + + + Stop + Stop + No comment provided by engineer. + + + Stop chat to enable database actions + Stop chat to enable database actions + No comment provided by engineer. + + + Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. + Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. + No comment provided by engineer. + + + Stop chat? + Stop chat? + No comment provided by engineer. + Take picture Take picture @@ -956,13 +1321,6 @@ We will be adding server redundancy to prevent lost messages. The app can notify you when you receive messages or contact requests - please open settings to enable. No comment provided by engineer. - - The app can receive background notifications every 20 minutes to check the new messages. -*Please note*: if you confirm, your device token will be sent to SimpleX Chat notifications server. - The app can receive background notifications every 20 minutes to check the new messages. -*Please note*: if you confirm, your device token will be sent to SimpleX Chat notifications server. - No comment provided by engineer. - The connection you accepted will be cancelled! The connection you accepted will be cancelled! @@ -973,6 +1331,11 @@ We will be adding server redundancy to prevent lost messages. The contact you shared this link with will NOT be able to connect! No comment provided by engineer. + + The created archive is available via app Settings / Database / Old database archive. + The created archive is available via app Settings / Database / Old database archive. + No comment provided by engineer. + The microphone does not work when the app is in the background. The microphone does not work when the app is in the background. @@ -983,6 +1346,11 @@ We will be adding server redundancy to prevent lost messages. The next generation of private messaging No comment provided by engineer. + + The old database was not removed during the migration, it can be deleted. + The old database was not removed during the migration, it can be deleted. + No comment provided by engineer. + The profile is only shared with your contacts. The profile is only shared with your contacts. @@ -993,6 +1361,11 @@ We will be adding server redundancy to prevent lost messages. The sender will NOT be notified No comment provided by engineer. + + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. + No comment provided by engineer. + To ask any questions and to receive SimpleX Chat updates. To ask any questions and to receive SimpleX Chat updates. @@ -1030,6 +1403,11 @@ You will be prompted to complete authentication before this feature is enabled.< You will be prompted to complete authentication before this feature is enabled. No comment provided by engineer. + + To support instant push notifications the chat database has to be migrated. + To support instant push notifications the chat database has to be migrated. + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Trying to connect to the server used to receive messages from this contact (error: %@). @@ -1040,6 +1418,16 @@ You will be prompted to complete authentication before this feature is enabled.< Trying to connect to the server used to receive messages from this contact. No comment provided by engineer. + + Turn off + Turn off + No comment provided by engineer. + + + Turn off notifications? + Turn off notifications? + No comment provided by engineer. + Turn on Turn on @@ -1050,6 +1438,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unexpected error: %@ No comment provided by engineer. + + Unexpected migration state + Unexpected migration state + No comment provided by engineer. + Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection. @@ -1067,6 +1460,11 @@ To connect, please ask your contact to create another connection link and check Use SimpleX Chat servers? No comment provided by engineer. + + Use chat + Use chat + No comment provided by engineer. + Using SimpleX Chat servers. Using SimpleX Chat servers. @@ -1117,11 +1515,21 @@ To connect, please ask your contact to create another connection link and check You can now send messages to %@ notification body + + You can set lock screen notification preview via settings. + You can set lock screen notification preview via settings. + No comment provided by engineer. + You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. No comment provided by engineer. + + You can start chat via app Settings / Database or by restarting the app + You can start chat via app Settings / Database or by restarting the app + No comment provided by engineer. + You can use markdown to format messages: You can use markdown to format messages: @@ -1142,6 +1550,11 @@ To connect, please ask your contact to create another connection link and check You invited your contact No comment provided by engineer. + + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. + No comment provided by engineer. + You will be connected when your connection request is accepted, please wait or check later! You will be connected when your connection request is accepted, please wait or check later! @@ -1177,6 +1590,11 @@ To connect, please ask your contact to create another connection link and check Your chat address No comment provided by engineer. + + Your chat database + Your chat database + No comment provided by engineer. + Your chat profile Your chat profile @@ -1209,6 +1627,11 @@ You can cancel this connection and remove the contact (and try later with a new Your contact sent a file that is larger than currently supported maximum size (%@). No comment provided by engineer. + + Your current chat database will be DELETED and REPLACED with the imported one. + Your current chat database will be DELETED and REPLACED with the imported one. + No comment provided by engineer. + Your privacy Your privacy @@ -1376,11 +1799,21 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. + + message received + message received + notification + missed call missed call call status + + new message + new message + notification + no e2e encryption no e2e encryption @@ -1426,6 +1859,11 @@ SimpleX servers cannot see your profile. strike No comment provided by engineer. + + this contact + this contact + notification title + unknown unknown @@ -1485,7 +1923,7 @@ SimpleX servers cannot see your profile.
- +
@@ -1517,7 +1955,7 @@ SimpleX servers cannot see your profile.
- +
@@ -1537,161 +1975,4 @@ SimpleX servers cannot see your profile.
- -
- -
- - - %@ is connected! - %@ is connected! - notification title - - - %@ wants to connect! - %@ wants to connect! - notification title - - - %d skipped message(s) - %d skipped message(s) - integrity error chat item - - - **e2e encrypted** audio call - **e2e encrypted** audio call - No comment provided by engineer. - - - **e2e encrypted** video call - **e2e encrypted** video call - No comment provided by engineer. - - - Accept contact request from %@? - Accept contact request from %@? - notification body - - - Incoming audio call - Incoming audio call - notification - - - Incoming video call - Incoming video call - notification - - - You can now send messages to %@ - You can now send messages to %@ - notification body - - - accepted call - accepted call - call status - - - audio call (not e2e encrypted) - audio call (not e2e encrypted) - No comment provided by engineer. - - - bad message ID - bad message ID - integrity error chat item - - - bad message hash - bad message hash - integrity error chat item - - - call error - call error - call status - - - call in progress - call in progress - call status - - - calling… - calling… - call status - - - connecting call… - connecting call… - call status - - - connecting… - connecting… - chat list item title - - - connection established - connection established - chat list item title (it should not be shown - - - connection:%@ - connection:%@ - connection information - - - deleted - deleted - deleted chat item - - - duplicate message - duplicate message - integrity error chat item - - - ended call %@ - ended call %@ - call status - - - invited to connect - invited to connect - chat list item title - - - missed call - missed call - call status - - - rejected call - rejected call - call status - - - via contact address link - via contact address link - chat list item description - - - via one-time link - via one-time link - chat list item description - - - video call (not e2e encrypted) - video call (not e2e encrypted) - No comment provided by engineer. - - - you shared one-time link - you shared one-time link - chat list item description - - -
diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 34577cd8ef60dbd981023e5ff3479c5e5059ce17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3612 zcmcIn+m6#P5S{1!3WEe%Rzmv@LV(1}egcnelU|_hmL{dzp9jvdcNjYtH%UQJyKX$5 zbLQ^-{p()t#K~SBrIw8>@ma{suOIQ&`Bg1jDY3qjd%2O9@=8A8*G4LQFK}{?r^Kqp zcOfqz;p788LnPlr;|=sqWpM`EJ&?(z2A3wIHk%ICtZuP-llW}-nV4sP4Ci24c;3qEGorNs z_5qC5V>S3Nh7MMh@e;?n*uGVQ+1$kf*FR zR$!={F04MuDY(?NlGm<{V>{#Y#$7V#VBLj`PjN0urnuUBw!R;;TCEGQqc?kZ+ON$T z?bv<$(@<|c17^`0@jIcqSvjFoIhiAxx_9<*)>?0}2(yT>XJ?p052w!>=Zh%6$DK+o zO1&QN=$)nS@;ji?o8f(qwl;BOfm^UWNIb@hQnHZ~oLaJvUfW|D^A=c~w z4)&ga&BzsnpcjU+=$4OJSbh!d9P|kFYKC^*w#+lbq*F%u`Olc})|BRg zA@=Q$!Qg!PhL7H5yJDV`NwKtoyFAQjR>0w%lDDvCZ=t7WKIhm!>~6-pNAY(KI$xE0 z+%L@!<4^v3*5q8uTW=R{yE&dY*_c^>^|tS~Ur{Ni=ZfY1foIS7%j{u4Y4$Js?;-6h g*XoJ+=PT|~K0G~J#Hly)@uw9z-|0jje=@NC1Bhlq2><{9 diff --git a/apps/ios/SimpleX Localizations/en.xcloc/contents.json b/apps/ios/SimpleX Localizations/en.xcloc/contents.json index 3e0830ec05..4429d3639d 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/en.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "en", "toolInfo" : { - "toolBuildNumber" : "13E113", + "toolBuildNumber" : "13F100", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "13.3" + "toolVersion" : "13.4.1" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 4dbb579d1c..7f9beb6e79 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2,7 +2,7 @@
- +
@@ -30,6 +30,11 @@ #секрет# No comment provided by engineer. + + %@ + %@ + No comment provided by engineer. + %@ / %@ %@ / %@ @@ -80,11 +85,26 @@ **Создать ссылку / QR код** для вашего контакта. No comment provided by engineer. + + **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + **Более конфиденциально**: проверять новые сообщения каждые 20 минут. Токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и сообщений. + No comment provided by engineer. + + + **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + **Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat, проверять сообщения периодически в фоновом режиме (зависит от того насколько часто вы используете приложение). + No comment provided by engineer. + **Paste received link** or open it in the browser and tap **Open in mobile app**. **Вставить полученную ссылку**, или откройте её в браузере и нажмите **Open in mobile app**. No comment provided by engineer. + + **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + **Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они. + No comment provided by engineer. + **Scan QR code**: to connect to your contact in person or via video call. **Сканировать QR код**: соединиться с вашим контактом при встрече или во время видеозвонка. @@ -120,6 +140,11 @@ : No comment provided by engineer. + + A new contact + Новый контакт + notification title + About SimpleX О SimpleX @@ -206,11 +231,41 @@ Отменить No comment provided by engineer. + + Chat archive + Архив чата + No comment provided by engineer. + Chat console Консоль No comment provided by engineer. + + Chat database + Архив чата + No comment provided by engineer. + + + Chat database deleted + Данные чата удалены + No comment provided by engineer. + + + Chat database imported + Архив чата импортирован + No comment provided by engineer. + + + Chat is running + Чат запущен + No comment provided by engineer. + + + Chat is stopped + Чат остановлен + No comment provided by engineer. + Chat with the developers Соединиться с разработчиками @@ -221,16 +276,6 @@ Чаты back button to return to chats list - - Check messages - Проверять сообщения - No comment provided by engineer. - - - Checking new messages... - Проверяются новые сообщения... - notification - Choose file Выбрать файл @@ -346,6 +391,11 @@ Контакт и все сообщения будут удалены - это действие нельзя отменить! No comment provided by engineer. + + Contact hidden: + Контакт скрыт: + notification + Contact is connected Соединение с контактом установлено @@ -356,6 +406,11 @@ Соединение еще не установлено! No comment provided by engineer. + + Contact name + Имена контактов + No comment provided by engineer. + Copy Скопировать @@ -386,11 +441,26 @@ Создать профиль No comment provided by engineer. + + Created on %@ + Дата создания %@ + No comment provided by engineer. + Currently maximum supported file size is %@. Максимальный размер файла - %@. No comment provided by engineer. + + Database export & import + Экспорт и импорт архива чата + No comment provided by engineer. + + + Database will be migrated when the app restarts + Данные чата будут мигрированы при перезапуске + No comment provided by engineer. + Decentralized Децентрализованный @@ -416,6 +486,21 @@ Удалить адрес? No comment provided by engineer. + + Delete archive + Удалить архив + No comment provided by engineer. + + + Delete chat archive? + Удалить архив чата? + No comment provided by engineer. + + + Delete chat profile? + Удалить профиль? + No comment provided by engineer. + Delete contact Удалить контакт @@ -426,6 +511,11 @@ Удалить контакт? No comment provided by engineer. + + Delete database + Удалить данные чата + No comment provided by engineer. + Delete for everyone Удалить для всех @@ -446,6 +536,16 @@ Удалить сообщение? No comment provided by engineer. + + Delete old database + Удалить предыдущую версию данных + No comment provided by engineer. + + + Delete old database? + Удалить предыдущую версию данных? + No comment provided by engineer. + Delete pending connection Удалить соединение @@ -491,19 +591,59 @@ Не используйте SimpleX для экстренных звонков No comment provided by engineer. + + Do it later + Отложить + No comment provided by engineer. + Edit Редактировать No comment provided by engineer. + + Enable + Включить + No comment provided by engineer. + Enable SimpleX Lock Включить блокировку SimpleX authentication reason - - Enable notifications? (BETA) - Включить уведомления? (БЕТА) + + Enable instant notifications? + Включить мгновенные уведомления? + No comment provided by engineer. + + + Enable notifications + Включить уведомления + No comment provided by engineer. + + + Enable periodic notifications? + Включить периодические уведомления? + No comment provided by engineer. + + + Error accessing database file + Ошибка при доступе к данным чата + No comment provided by engineer. + + + Error deleting chat database + Ошибка при удалении данных чата + No comment provided by engineer. + + + Error deleting database + Ошибка при удалении данных чата + No comment provided by engineer. + + + Error deleting old database + Ошибка при удалении предыдущей версии данных чата No comment provided by engineer. @@ -511,9 +651,19 @@ Ошибка удаления токена No comment provided by engineer. - - Error registering token - Ошибка регистрации токена + + Error enabling notifications + Ошибка при включении уведомлений + No comment provided by engineer. + + + Error exporting chat database + Ошибка при экспорте архива чата + No comment provided by engineer. + + + Error importing chat database + Ошибка при импорте архива чата No comment provided by engineer. @@ -521,6 +671,16 @@ Ошибка при сохранении SMP серверов No comment provided by engineer. + + Error starting chat + Ошибка при запуске чата + No comment provided by engineer. + + + Error stopping chat + Ошибка при остановке чата + No comment provided by engineer. + Error: %@ Ошибка: %@ @@ -531,9 +691,29 @@ Ошибка: неверная ссылка No comment provided by engineer. - - Experimental features - Экспериментальные функции + + Error: no database file + Ошибка: данные чата не найдены + No comment provided by engineer. + + + Export database + Экспорт архива чата + No comment provided by engineer. + + + Export error: + Ошибка при экспорте: + No comment provided by engineer. + + + Exported database archive. + Архив чата экспортирован. + No comment provided by engineer. + + + Exporting database archive... + Архив чата экспортируется... No comment provided by engineer. @@ -551,11 +731,21 @@ Удаление групп не поддерживается No comment provided by engineer. + + Group message: + Групповое сообщение: + notification + Help Помощь No comment provided by engineer. + + Hidden + Скрытое + No comment provided by engineer. + How SimpleX works Как SimpleX работает @@ -596,6 +786,11 @@ Если вы не можете встретиться лично, вы можете **сосканировать QR код во время видеозвонка**, или ваш контакт может отправить вам ссылку. No comment provided by engineer. + + If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). + Если сейчас вам нужно использовать чат, нажмите **Отложить** внизу (вы сможете мигрировать данные чата при следующем запуске приложения). + No comment provided by engineer. + Ignore Не отвечать @@ -611,6 +806,21 @@ Защищен от спама No comment provided by engineer. + + Import + Импортировать + No comment provided by engineer. + + + Import chat database? + Импортировать архив чата? + No comment provided by engineer. + + + Import database + Импорт архива чата + No comment provided by engineer. + In person or via a video call – the most secure way to connect. При встрече или в видеозвонке – самый безопасный способ установить соединение @@ -636,6 +846,11 @@ [SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instantly + Мгновенно + No comment provided by engineer. + Invalid connection link Ошибка в ссылке контакта @@ -701,6 +916,31 @@ We will be adding server redundancy to prevent lost messages. Ошибка доставки сообщения No comment provided by engineer. + + Message text + Текст сообщения + No comment provided by engineer. + + + Migrating database archive... + Данные чата перемещаются... + No comment provided by engineer. + + + Migration error: + Ошибка при перемещении данных: + No comment provided by engineer. + + + Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). + Ошибка при перемещении данных. Нажмите **Отложить** внизу чтобы продолжить использовать предыдущую версию данных. Пожалуйста, сообщите об этой ошибке разработчикам через чат или email [chat@simplex.chat](mailto:chat@simplex.chat). + No comment provided by engineer. + + + Migration is completed + Перемещение данных завершено + No comment provided by engineer. + Most likely this contact has deleted the connection with you. Скорее всего, этот контакт удалил соединение с вами. @@ -711,16 +951,51 @@ We will be adding server redundancy to prevent lost messages. Новый запрос на соединение notification + + New contact: + Новый контакт: + notification + + + New database archive + Новый архив чата + No comment provided by engineer. + New message Новое сообщение notification + + No device token! + Отсутствует токен устройства! + No comment provided by engineer. + + + Notifications + Уведомления + No comment provided by engineer. + Notifications are disabled! Уведомления выключены No comment provided by engineer. + + Off (Local) + Выключить (Локальные) + No comment provided by engineer. + + + Old database + Предыдущая версия данных чата + No comment provided by engineer. + + + Old database archive + Старый архив чата + No comment provided by engineer. + One-time invitation link Одноразовая ссылка @@ -776,6 +1051,11 @@ We will be adding server redundancy to prevent lost messages. С вами можно соединиться только через созданные вами ссылки. No comment provided by engineer. + + Periodically + Периодически + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Пожалуйста, проверьте, что вы использовали правильную ссылку или попросите, чтобы ваш контакт отправил вам другую ссылку. @@ -786,9 +1066,9 @@ We will be adding server redundancy to prevent lost messages. Пожалуйста, проверьте ваше соединение с сетью и попробуйте еще раз. No comment provided by engineer. - - Pre-arrange the calls, as notifications arrive with a delay (we are improving it). - Назначайте звонки заранее, поскольку уведомления приходят с задержкой (мы улучшаем это) + + Please restart the app and migrate the database to enable push notifications. + Пожалуйста, перезапустите приложение и переместите данные чата, чтобы включить доставку уведомлений. No comment provided by engineer. @@ -806,6 +1086,11 @@ We will be adding server redundancy to prevent lost messages. Аватар No comment provided by engineer. + + Push notifications + Доставка уведомлений + No comment provided by engineer. + Read Прочитано @@ -821,6 +1106,11 @@ We will be adding server redundancy to prevent lost messages. Узнайте больше из нашего [GitHub репозитория](https://github.com/simplex-chat/simplex-chat#readme). No comment provided by engineer. + + Received file event + Загрузка файла + notification + Reject Отклонить @@ -841,9 +1131,19 @@ We will be adding server redundancy to prevent lost messages. Ответить No comment provided by engineer. - - Retry - Повторить + + Restart the app to create a new chat profile + Перезапустите приложение, чтобы создать новый профиль. + No comment provided by engineer. + + + Restart the app to use imported chat database + Перезапустите приложение, чтобы использовать импортированные данные чата. + No comment provided by engineer. + + + Run chat + Запустить chat No comment provided by engineer. @@ -866,6 +1166,11 @@ We will be adding server redundancy to prevent lost messages. Сохранить (и уведомить контакты) No comment provided by engineer. + + Save archive + Сохранить архив + No comment provided by engineer. + Saved SMP servers will be removed Сохраненные SMP серверы будут удалены @@ -886,6 +1191,21 @@ We will be adding server redundancy to prevent lost messages. Отправлять картинки ссылок No comment provided by engineer. + + Send notifications + Отправлять уведомления + No comment provided by engineer. + + + Send notifications: + Отправлять уведомления: + No comment provided by engineer. + + + Sent file event + Отправка файла + notification + Server connected Установлено соединение с сервером @@ -916,6 +1236,11 @@ We will be adding server redundancy to prevent lost messages. Показать ожидаемые соединения No comment provided by engineer. + + Show preview + Показывать уведомления + No comment provided by engineer. + SimpleX Lock Блокировка SimpleX @@ -926,11 +1251,51 @@ We will be adding server redundancy to prevent lost messages. Блокировка SimpleX включена No comment provided by engineer. + + SimpleX encrypted message or connection event + SimpleX: зашифрованное сообщение или соединение контакта + notification + Skipped messages Пропущенные сообщения No comment provided by engineer. + + Somebody + Контакт + notification title + + + Start chat + Запустить чат + No comment provided by engineer. + + + Start migration + Запустить перемещение данных + No comment provided by engineer. + + + Stop + Остановить + No comment provided by engineer. + + + Stop chat to enable database actions + Остановите чат, чтобы разблокировать операции с архивом чата + No comment provided by engineer. + + + Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. + Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен. + No comment provided by engineer. + + + Stop chat? + Остановить чат? + No comment provided by engineer. + Take picture Сделать фото @@ -956,13 +1321,6 @@ We will be adding server redundancy to prevent lost messages. Приложение может посылать вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках. No comment provided by engineer. - - The app can receive background notifications every 20 minutes to check the new messages. -*Please note*: if you confirm, your device token will be sent to SimpleX Chat notifications server. - Приложение может получать скрытые уведомления каждые 20 минут чтобы проверить новые сообщения. -*Обратите внимание*: если вы подтвердите, токен вашего устройства будет послан на сервер SimpleX Chat. - No comment provided by engineer. - The connection you accepted will be cancelled! Подтвержденное соединение будет отменено! @@ -973,6 +1331,11 @@ We will be adding server redundancy to prevent lost messages. Контакт, которому вы отправили эту ссылку, не сможет соединиться! No comment provided by engineer. + + The created archive is available via app Settings / Database / Old database archive. + Созданный архив доступен через Настройки приложения. + No comment provided by engineer. + The microphone does not work when the app is in the background. Микрофон не работает, когда приложение в фоновом режиме. @@ -983,6 +1346,11 @@ We will be adding server redundancy to prevent lost messages. Новое поколение приватных сообщений No comment provided by engineer. + + The old database was not removed during the migration, it can be deleted. + Предыдущая версия данных чата не удалена при перемещении, её можно удалить. + No comment provided by engineer. + The profile is only shared with your contacts. Профиль отправляется только вашим контактам. @@ -993,6 +1361,11 @@ We will be adding server redundancy to prevent lost messages. Отправитель не будет уведомлён No comment provided by engineer. + + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. + Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. + No comment provided by engineer. + To ask any questions and to receive SimpleX Chat updates. Чтобы задать вопросы и получать уведомления о SimpleX Chat. @@ -1030,6 +1403,11 @@ You will be prompted to complete authentication before this feature is enabled.< Вам будет нужно пройти аутентификацию для включения блокировки. No comment provided by engineer. + + To support instant push notifications the chat database has to be migrated. + Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены. + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: %@). @@ -1040,6 +1418,16 @@ You will be prompted to complete authentication before this feature is enabled.< Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта. No comment provided by engineer. + + Turn off + Выключить + No comment provided by engineer. + + + Turn off notifications? + Выключить уведомления? + No comment provided by engineer. + Turn on Включить @@ -1050,6 +1438,11 @@ You will be prompted to complete authentication before this feature is enabled.< Неожиданная ошибка: %@ No comment provided by engineer. + + Unexpected migration state + Неожиданная ошибка при перемещении данных чата + No comment provided by engineer. + Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection. @@ -1067,6 +1460,11 @@ To connect, please ask your contact to create another connection link and check Использовать серверы предосталенные SimpleX Chat? No comment provided by engineer. + + Use chat + Использовать чат + No comment provided by engineer. + Using SimpleX Chat servers. Используются серверы, предоставленные SimpleX Chat. @@ -1117,11 +1515,21 @@ To connect, please ask your contact to create another connection link and check Вы теперь можете отправлять сообщения %@ notification body + + You can set lock screen notification preview via settings. + Вы можете установить просмотр уведомлений на экране блокировки в настройках. + No comment provided by engineer. + You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились. No comment provided by engineer. + + You can start chat via app Settings / Database or by restarting the app + Вы можете запустить чат через Настройки приложения или перезапустив приложение. + No comment provided by engineer. + You can use markdown to format messages: Вы можете форматировать сообщения: @@ -1142,6 +1550,11 @@ To connect, please ask your contact to create another connection link and check Вы пригласили ваш контакт No comment provided by engineer. + + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. + Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, инача вы можете перестать получать сообщения от каких то контактов. + No comment provided by engineer. + You will be connected when your connection request is accepted, please wait or check later! Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже! @@ -1177,6 +1590,11 @@ To connect, please ask your contact to create another connection link and check Ваш SimpleX адрес No comment provided by engineer. + + Your chat database + Данные чата + No comment provided by engineer. + Your chat profile Ваш профиль @@ -1209,6 +1627,11 @@ You can cancel this connection and remove the contact (and try later with a new Ваш контакт отправил файл, размер которого превышает максимальный размер (%@). No comment provided by engineer. + + Your current chat database will be DELETED and REPLACED with the imported one. + Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными. + No comment provided by engineer. + Your privacy Конфиденциальность @@ -1376,11 +1799,21 @@ SimpleX серверы не могут получить доступ к ваше курсив No comment provided by engineer. + + message received + получено сообщение + notification + missed call пропущенный звонок call status + + new message + новое сообщение + notification + no e2e encryption нет e2e шифрования @@ -1426,6 +1859,11 @@ SimpleX серверы не могут получить доступ к ваше зачеркнуть No comment provided by engineer. + + this contact + этот контакт + notification title + unknown неизвестно @@ -1485,7 +1923,7 @@ SimpleX серверы не могут получить доступ к ваше
- +
@@ -1517,7 +1955,7 @@ SimpleX серверы не могут получить доступ к ваше
- +
@@ -1537,161 +1975,4 @@ SimpleX серверы не могут получить доступ к ваше
- -
- -
- - - %@ is connected! - соединение с %@ установлено! - notification title - - - %@ wants to connect! - %@ хочет соединиться! - notification title - - - %d skipped message(s) - %d пропущенных сообщений - integrity error chat item - - - **e2e encrypted** audio call - **e2e зашифрованный** аудиозвонок - No comment provided by engineer. - - - **e2e encrypted** video call - **e2e зашифрованный** видеозвонок - No comment provided by engineer. - - - Accept contact request from %@? - Принять запрос на соединение от %@? - notification body - - - Incoming audio call - Входящий аудиозвонок - notification - - - Incoming video call - Входящий видеозвонок - notification - - - You can now send messages to %@ - Вы можете отправлять сообщения %@ - notification body - - - accepted call - принятный звонок - call status - - - audio call (not e2e encrypted) - аудиозвонок (не e2e зашифрованный) - No comment provided by engineer. - - - bad message ID - ошибка ID сообщения - integrity error chat item - - - bad message hash - ошибка хэш сообщения - integrity error chat item - - - call error - ошибка звонка - call status - - - call in progress - активный звонок - call status - - - calling… - входящий звонок… - call status - - - connecting call… - звонок соединяется… - call status - - - connecting… - соединяется… - chat list item title - - - connection established - соединение установлено - chat list item title (it should not be shown - - - connection:%@ - connection:%@ - connection information - - - deleted - удалено - deleted chat item - - - duplicate message - повторное сообщение - integrity error chat item - - - ended call %@ - завершённый звонок %@ - call status - - - invited to connect - приглашение соединиться - chat list item title - - - missed call - пропущенный звонок - call status - - - rejected call - отклонённый звонок - call status - - - via contact address link - через ссылку-контакт - chat list item description - - - via one-time link - через одноразовую ссылку - chat list item description - - - video call (not e2e encrypted) - видеозвонок (не e2e зашифрованный) - No comment provided by engineer. - - - you shared one-time link - вы создали одноразовую ссылку - chat list item description - - -
diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 34577cd8ef60dbd981023e5ff3479c5e5059ce17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3612 zcmcIn+m6#P5S{1!3WEe%Rzmv@LV(1}egcnelU|_hmL{dzp9jvdcNjYtH%UQJyKX$5 zbLQ^-{p()t#K~SBrIw8>@ma{suOIQ&`Bg1jDY3qjd%2O9@=8A8*G4LQFK}{?r^Kqp zcOfqz;p788LnPlr;|=sqWpM`EJ&?(z2A3wIHk%ICtZuP-llW}-nV4sP4Ci24c;3qEGorNs z_5qC5V>S3Nh7MMh@e;?n*uGVQ+1$kf*FR zR$!={F04MuDY(?NlGm<{V>{#Y#$7V#VBLj`PjN0urnuUBw!R;;TCEGQqc?kZ+ON$T z?bv<$(@<|c17^`0@jIcqSvjFoIhiAxx_9<*)>?0}2(yT>XJ?p052w!>=Zh%6$DK+o zO1&QN=$)nS@;ji?o8f(qwl;BOfm^UWNIb@hQnHZ~oLaJvUfW|D^A=c~w z4)&ga&BzsnpcjU+=$4OJSbh!d9P|kFYKC^*w#+lbq*F%u`Olc})|BRg zA@=Q$!Qg!PhL7H5yJDV`NwKtoyFAQjR>0w%lDDvCZ=t7WKIhm!>~6-pNAY(KI$xE0 z+%L@!<4^v3*5q8uTW=R{yE&dY*_c^>^|tS~Ur{Ni=ZfY1foIS7%j{u4Y4$Js?;-6h g*XoJ+=PT|~K0G~J#Hly)@uw9z-|0jje=@NC1Bhlq2><{9 diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json index 50ec87db4c..ab6a47f150 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ru", "toolInfo" : { - "toolBuildNumber" : "13E113", + "toolBuildNumber" : "13F100", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "13.3" + "toolVersion" : "13.4.1" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index ec3d6440db..b44ef5fb88 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -40,9 +40,18 @@ /* No comment provided by engineer. */ "**e2e encrypted** video call" = "**e2e зашифрованный** видеозвонок"; +/* No comment provided by engineer. */ +"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Более конфиденциально**: проверять новые сообщения каждые 20 минут. Токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и сообщений."; + +/* No comment provided by engineer. */ +"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat, проверять сообщения периодически в фоновом режиме (зависит от того насколько часто вы используете приложение)."; + /* No comment provided by engineer. */ "**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Вставить полученную ссылку**, или откройте её в браузере и нажмите **Open in mobile app**."; +/* No comment provided by engineer. */ +"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они."; + /* No comment provided by engineer. */ "**Scan QR code**: to connect to your contact in person or via video call." = "**Сканировать QR код**: соединиться с вашим контактом при встрече или во время видеозвонка."; @@ -52,6 +61,9 @@ /* No comment provided by engineer. */ "#secret#" = "#секрет#"; +/* No comment provided by engineer. */ +"%@" = "%@"; + /* No comment provided by engineer. */ "%@ / %@" = "%@ / %@"; @@ -79,6 +91,9 @@ /* No comment provided by engineer. */ "6" = "6"; +/* notification title */ +"A new contact" = "Новый контакт"; + /* No comment provided by engineer. */ "About SimpleX" = "О SimpleX"; @@ -158,21 +173,33 @@ /* No comment provided by engineer. */ "Cancel" = "Отменить"; +/* No comment provided by engineer. */ +"Chat archive" = "Архив чата"; + /* No comment provided by engineer. */ "Chat console" = "Консоль"; +/* No comment provided by engineer. */ +"Chat database" = "Архив чата"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "Данные чата удалены"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Архив чата импортирован"; + +/* No comment provided by engineer. */ +"Chat is running" = "Чат запущен"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "Чат остановлен"; + /* No comment provided by engineer. */ "Chat with the developers" = "Соединиться с разработчиками"; /* back button to return to chats list */ "Chats" = "Чаты"; -/* No comment provided by engineer. */ -"Check messages" = "Проверять сообщения"; - -/* notification */ -"Checking new messages..." = "Проверяются новые сообщения..."; - /* No comment provided by engineer. */ "Choose file" = "Выбрать файл"; @@ -269,12 +296,18 @@ /* No comment provided by engineer. */ "contact has no e2e encryption" = "у контакта нет e2e шифрования"; +/* notification */ +"Contact hidden:" = "Контакт скрыт:"; + /* notification */ "Contact is connected" = "Соединение с контактом установлено"; /* No comment provided by engineer. */ "Contact is not connected yet!" = "Соединение еще не установлено!"; +/* No comment provided by engineer. */ +"Contact name" = "Имена контактов"; + /* No comment provided by engineer. */ "Copy" = "Скопировать"; @@ -293,9 +326,18 @@ /* No comment provided by engineer. */ "Create your profile" = "Создать профиль"; +/* No comment provided by engineer. */ +"Created on %@" = "Дата создания %@"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Максимальный размер файла - %@."; +/* No comment provided by engineer. */ +"Database export & import" = "Экспорт и импорт архива чата"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "Данные чата будут мигрированы при перезапуске"; + /* No comment provided by engineer. */ "Decentralized" = "Децентрализованный"; @@ -308,6 +350,15 @@ /* No comment provided by engineer. */ "Delete address?" = "Удалить адрес?"; +/* No comment provided by engineer. */ +"Delete archive" = "Удалить архив"; + +/* No comment provided by engineer. */ +"Delete chat archive?" = "Удалить архив чата?"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "Удалить профиль?"; + /* No comment provided by engineer. */ "Delete contact" = "Удалить контакт"; @@ -317,6 +368,9 @@ /* No comment provided by engineer. */ "Delete contact?" = "Удалить контакт?"; +/* No comment provided by engineer. */ +"Delete database" = "Удалить данные чата"; + /* No comment provided by engineer. */ "Delete for everyone" = "Удалить для всех"; @@ -329,6 +383,12 @@ /* No comment provided by engineer. */ "Delete message?" = "Удалить сообщение?"; +/* No comment provided by engineer. */ +"Delete old database" = "Удалить предыдущую версию данных"; + +/* No comment provided by engineer. */ +"Delete old database?" = "Удалить предыдущую версию данных?"; + /* No comment provided by engineer. */ "Delete pending connection" = "Удалить соединение"; @@ -356,6 +416,9 @@ /* No comment provided by engineer. */ "Display name" = "Имя профиля"; +/* No comment provided by engineer. */ +"Do it later" = "Отложить"; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "Не используйте SimpleX для экстренных звонков"; @@ -369,7 +432,16 @@ "Edit" = "Редактировать"; /* No comment provided by engineer. */ -"Enable notifications? (BETA)" = "Включить уведомления? (БЕТА)"; +"Enable" = "Включить"; + +/* No comment provided by engineer. */ +"Enable instant notifications?" = "Включить мгновенные уведомления?"; + +/* No comment provided by engineer. */ +"Enable notifications" = "Включить уведомления"; + +/* No comment provided by engineer. */ +"Enable periodic notifications?" = "Включить периодические уведомления?"; /* authentication reason */ "Enable SimpleX Lock" = "Включить блокировку SimpleX"; @@ -381,23 +453,59 @@ "ended call %@" = "завершённый звонок %@"; /* No comment provided by engineer. */ -"Enter one SMP server per line:" = "Введите SMP серверы, каждый на отдельной строке:"; +"Error accessing database file" = "Ошибка при доступе к данным чата"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "Ошибка при удалении данных чата"; + +/* No comment provided by engineer. */ +"Error deleting database" = "Ошибка при удалении данных чата"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "Ошибка при удалении предыдущей версии данных чата"; /* No comment provided by engineer. */ "Error deleting token" = "Ошибка удаления токена"; /* No comment provided by engineer. */ -"Error registering token" = "Ошибка регистрации токена"; +"Error enabling notifications" = "Ошибка при включении уведомлений"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "Ошибка при экспорте архива чата"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "Ошибка при импорте архива чата"; /* No comment provided by engineer. */ "Error saving SMP servers" = "Ошибка при сохранении SMP серверов"; +/* No comment provided by engineer. */ +"Error starting chat" = "Ошибка при запуске чата"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "Ошибка при остановке чата"; + /* No comment provided by engineer. */ "Error: %@" = "Ошибка: %@"; +/* No comment provided by engineer. */ +"Error: no database file" = "Ошибка: данные чата не найдены"; + /* No comment provided by engineer. */ "Error: URL is invalid" = "Ошибка: неверная ссылка"; +/* No comment provided by engineer. */ +"Export database" = "Экспорт архива чата"; + +/* No comment provided by engineer. */ +"Export error:" = "Ошибка при экспорте:"; + +/* No comment provided by engineer. */ +"Exported database archive." = "Архив чата экспортирован."; + +/* No comment provided by engineer. */ +"Exporting database archive..." = "Архив чата экспортируется..."; + /* No comment provided by engineer. */ "File will be received when your contact is online, please wait or check later!" = "Файл будет принят, когда ваш контакт будет в сети, подождите или проверьте позже!"; @@ -407,9 +515,15 @@ /* No comment provided by engineer. */ "Group deletion is not supported" = "Удаление групп не поддерживается"; +/* notification */ +"Group message:" = "Групповое сообщение:"; + /* No comment provided by engineer. */ "Help" = "Помощь"; +/* No comment provided by engineer. */ +"Hidden" = "Скрытое"; + /* No comment provided by engineer. */ "How it works" = "Как это работает"; @@ -434,6 +548,9 @@ /* No comment provided by engineer. */ "If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Если вы не можете встретиться лично, вы можете **сосканировать QR код во время видеозвонка**, или ваш контакт может отправить вам ссылку."; +/* No comment provided by engineer. */ +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Если сейчас вам нужно использовать чат, нажмите **Отложить** внизу (вы сможете мигрировать данные чата при следующем запуске приложения)."; + /* No comment provided by engineer. */ "Ignore" = "Не отвечать"; @@ -443,6 +560,15 @@ /* No comment provided by engineer. */ "Immune to spam and abuse" = "Защищен от спама"; +/* No comment provided by engineer. */ +"Import" = "Импортировать"; + +/* No comment provided by engineer. */ +"Import chat database?" = "Импортировать архив чата?"; + +/* No comment provided by engineer. */ +"Import database" = "Импорт архива чата"; + /* No comment provided by engineer. */ "In person or via a video call – the most secure way to connect." = "При встрече или в видеозвонке – самый безопасный способ установить соединение"; @@ -458,6 +584,9 @@ /* No comment provided by engineer. */ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "[SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"Instantly" = "Мгновенно"; + /* No comment provided by engineer. */ "Invalid connection link" = "Ошибка в ссылке контакта"; @@ -497,6 +626,24 @@ /* No comment provided by engineer. */ "Message delivery error" = "Ошибка доставки сообщения"; +/* notification */ +"message received" = "получено сообщение"; + +/* No comment provided by engineer. */ +"Message text" = "Текст сообщения"; + +/* No comment provided by engineer. */ +"Migrating database archive..." = "Данные чата перемещаются..."; + +/* No comment provided by engineer. */ +"Migration error:" = "Ошибка при перемещении данных:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Ошибка при перемещении данных. Нажмите **Отложить** внизу чтобы продолжить использовать предыдущую версию данных. Пожалуйста, сообщите об этой ошибке разработчикам через чат или email [chat@simplex.chat](mailto:chat@simplex.chat)."; + +/* No comment provided by engineer. */ +"Migration is completed" = "Перемещение данных завершено"; + /* call status */ "missed call" = "пропущенный звонок"; @@ -506,15 +653,39 @@ /* notification */ "New contact request" = "Новый запрос на соединение"; +/* notification */ +"New contact:" = "Новый контакт:"; + +/* No comment provided by engineer. */ +"New database archive" = "Новый архив чата"; + +/* notification */ +"new message" = "новое сообщение"; + /* notification */ "New message" = "Новое сообщение"; +/* No comment provided by engineer. */ +"No device token!" = "Отсутствует токен устройства!"; + /* No comment provided by engineer. */ "no e2e encryption" = "нет e2e шифрования"; +/* No comment provided by engineer. */ +"Notifications" = "Уведомления"; + /* No comment provided by engineer. */ "Notifications are disabled!" = "Уведомления выключены"; +/* No comment provided by engineer. */ +"Off (Local)" = "Выключить (Локальные)"; + +/* No comment provided by engineer. */ +"Old database" = "Предыдущая версия данных чата"; + +/* No comment provided by engineer. */ +"Old database archive" = "Старый архив чата"; + /* No comment provided by engineer. */ "One-time invitation link" = "Одноразовая ссылка"; @@ -536,6 +707,9 @@ /* No comment provided by engineer. */ "Paste" = "Вставить"; +/* No comment provided by engineer. */ +"Paste image" = "Вставить изображение"; + /* No comment provided by engineer. */ "Paste received link" = "Вставить полученную ссылку"; @@ -551,6 +725,9 @@ /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "С вами можно соединиться только через созданные вами ссылки."; +/* No comment provided by engineer. */ +"Periodically" = "Периодически"; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Пожалуйста, проверьте, что вы использовали правильную ссылку или попросите, чтобы ваш контакт отправил вам другую ссылку."; @@ -558,7 +735,7 @@ "Please check your network connection and try again." = "Пожалуйста, проверьте ваше соединение с сетью и попробуйте еще раз."; /* No comment provided by engineer. */ -"Pre-arrange the calls, as notifications arrive with a delay (we are improving it)." = "Назначайте звонки заранее, поскольку уведомления приходят с задержкой (мы улучшаем это)"; +"Please restart the app and migrate the database to enable push notifications." = "Пожалуйста, перезапустите приложение и переместите данные чата, чтобы включить доставку уведомлений."; /* No comment provided by engineer. */ "Privacy & security" = "Конфиденциальность"; @@ -569,6 +746,9 @@ /* No comment provided by engineer. */ "Profile image" = "Аватар"; +/* No comment provided by engineer. */ +"Push notifications" = "Доставка уведомлений"; + /* No comment provided by engineer. */ "Read" = "Прочитано"; @@ -584,6 +764,9 @@ /* No comment provided by engineer. */ "received confirmation…" = "получено подтверждение…"; +/* notification */ +"Received file event" = "Загрузка файла"; + /* reject incoming call via notification */ "Reject" = "Отклонить"; @@ -600,7 +783,13 @@ "Reply" = "Ответить"; /* No comment provided by engineer. */ -"Retry" = "Повторить"; +"Restart the app to create a new chat profile" = "Перезапустите приложение, чтобы создать новый профиль."; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "Перезапустите приложение, чтобы использовать импортированные данные чата."; + +/* No comment provided by engineer. */ +"Run chat" = "Запустить chat"; /* No comment provided by engineer. */ "Save" = "Сохранить"; @@ -608,6 +797,9 @@ /* No comment provided by engineer. */ "Save (and notify contacts)" = "Сохранить (и уведомить контакты)"; +/* No comment provided by engineer. */ +"Save archive" = "Сохранить архив"; + /* No comment provided by engineer. */ "Saved SMP servers will be removed" = "Сохраненные SMP серверы будут удалены"; @@ -623,6 +815,15 @@ /* No comment provided by engineer. */ "Send link previews" = "Отправлять картинки ссылок"; +/* No comment provided by engineer. */ +"Send notifications" = "Отправлять уведомления"; + +/* No comment provided by engineer. */ +"Send notifications:" = "Отправлять уведомления:"; + +/* notification */ +"Sent file event" = "Отправка файла"; + /* No comment provided by engineer. */ "Server connected" = "Установлено соединение с сервером"; @@ -641,6 +842,12 @@ /* No comment provided by engineer. */ "Show pending connections" = "Показать ожидаемые соединения"; +/* No comment provided by engineer. */ +"Show preview" = "Показывать уведомления"; + +/* notification */ +"SimpleX encrypted message or connection event" = "SimpleX: зашифрованное сообщение или соединение контакта"; + /* No comment provided by engineer. */ "SimpleX Lock" = "Блокировка SimpleX"; @@ -653,9 +860,33 @@ /* No comment provided by engineer. */ "SMP servers" = "SMP серверы"; +/* No comment provided by engineer. */ +"SMP servers (one per line)" = "SMP серверы (один на строке)"; + +/* notification title */ +"Somebody" = "Контакт"; + +/* No comment provided by engineer. */ +"Start chat" = "Запустить чат"; + +/* No comment provided by engineer. */ +"Start migration" = "Запустить перемещение данных"; + /* No comment provided by engineer. */ "starting…" = "инициализация…"; +/* No comment provided by engineer. */ +"Stop" = "Остановить"; + +/* No comment provided by engineer. */ +"Stop chat to enable database actions" = "Остановите чат, чтобы разблокировать операции с архивом чата"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен."; + +/* No comment provided by engineer. */ +"Stop chat?" = "Остановить чат?"; + /* No comment provided by engineer. */ "strike" = "зачеркнуть"; @@ -674,27 +905,36 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; -/* No comment provided by engineer. */ -"The app can receive background notifications every 20 minutes to check the new messages.\n*Please note*: if you confirm, your device token will be sent to SimpleX Chat notifications server." = "Приложение может получать скрытые уведомления каждые 20 минут чтобы проверить новые сообщения.\n*Обратите внимание*: если вы подтвердите, токен вашего устройства будет послан на сервер SimpleX Chat."; - /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Подтвержденное соединение будет отменено!"; /* No comment provided by engineer. */ "The contact you shared this link with will NOT be able to connect!" = "Контакт, которому вы отправили эту ссылку, не сможет соединиться!"; +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "Созданный архив доступен через Настройки приложения."; + /* No comment provided by engineer. */ "The microphone does not work when the app is in the background." = "Микрофон не работает, когда приложение в фоновом режиме."; /* No comment provided by engineer. */ "The next generation of private messaging" = "Новое поколение приватных сообщений"; +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить."; + /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Профиль отправляется только вашим контактам."; /* No comment provided by engineer. */ "The sender will NOT be notified" = "Отправитель не будет уведомлён"; +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; + +/* notification title */ +"this contact" = "этот контакт"; + /* No comment provided by engineer. */ "To ask any questions and to receive SimpleX Chat updates." = "Чтобы задать вопросы и получать уведомления о SimpleX Chat."; @@ -716,18 +956,30 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Чтобы защитить вашу информацию, включите блокировку SimpleX Chat.\nВам будет нужно пройти аутентификацию для включения блокировки."; +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены."; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: %@)."; /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact." = "Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта."; +/* No comment provided by engineer. */ +"Turn off" = "Выключить"; + +/* No comment provided by engineer. */ +"Turn off notifications?" = "Выключить уведомления?"; + /* No comment provided by engineer. */ "Turn on" = "Включить"; /* No comment provided by engineer. */ "Unexpected error: %@" = "Неожиданная ошибка: %@"; +/* No comment provided by engineer. */ +"Unexpected migration state" = "Неожиданная ошибка при перемещении данных чата"; + /* connection info */ "unknown" = "неизвестно"; @@ -737,6 +989,9 @@ /* authentication reason */ "Unlock" = "Разблокировать"; +/* No comment provided by engineer. */ +"Use chat" = "Использовать чат"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?"; @@ -794,9 +1049,15 @@ /* notification body */ "You can now send messages to %@" = "Вы теперь можете отправлять сообщения %@"; +/* No comment provided by engineer. */ +"You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках."; + /* No comment provided by engineer. */ "You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." = "Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились."; +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "Вы можете запустить чат через Настройки приложения или перезапустив приложение."; + /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Вы можете форматировать сообщения:"; @@ -809,6 +1070,9 @@ /* No comment provided by engineer. */ "You invited your contact" = "Вы пригласили ваш контакт"; +/* No comment provided by engineer. */ +"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, инача вы можете перестать получать сообщения от каких то контактов."; + /* chat list item description */ "you shared one-time link" = "вы создали ссылку"; @@ -827,6 +1091,9 @@ /* No comment provided by engineer. */ "Your chat address" = "Ваш SimpleX адрес"; +/* No comment provided by engineer. */ +"Your chat database" = "Данные чата"; + /* No comment provided by engineer. */ "Your chat profile" = "Ваш профиль"; @@ -845,6 +1112,9 @@ /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт отправил файл, размер которого превышает максимальный размер (%@)."; +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными."; + /* No comment provided by engineer. */ "Your privacy" = "Конфиденциальность"; From 06cb564eae319a258d6178b4170251c9f3dc9698 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 9 Jul 2022 09:29:56 +0100 Subject: [PATCH 87/89] ios: ensure that device token is registered once (#794) --- apps/ios/Shared/Model/ChatModel.swift | 1 + apps/ios/Shared/Model/SimpleXAPI.swift | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 92170ecbb3..f56f1423e9 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -31,6 +31,7 @@ final class ChatModel: ObservableObject { @Published var appOpenUrl: URL? @Published var deviceToken: DeviceToken? @Published var savedToken: DeviceToken? + @Published var tokenRegistered = false @Published var tokenStatus: NtfTknStatus? @Published var notificationMode = NotificationsMode.off @Published var notificationPreview: NotificationPreviewMode? = ntfPreviewModeGroupDefault.get() diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index b28f4f0a84..bd4384b505 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -238,12 +238,14 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM throw r } -func apiGetNtfToken() throws -> (DeviceToken?, NtfTknStatus?, NotificationsMode) { +func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) { let r = chatSendCmdSync(.apiGetNtfToken) switch r { case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode) case .chatCmdError(.errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off) - default: throw r + default: + logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)") + return (nil, nil, .off) } } @@ -256,7 +258,8 @@ func apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) a func registerToken(token: DeviceToken) { let m = ChatModel.shared let mode = m.notificationMode - if mode != .off { + if mode != .off && !m.tokenRegistered { + m.tokenRegistered = true logger.debug("registerToken \(mode.rawValue)") Task { do { @@ -545,7 +548,7 @@ func startChat() throws { let chats = try apiGetChats() m.chats = chats.map { Chat.init($0) } try refreshCallInvitations() - (m.savedToken, m.tokenStatus, m.notificationMode) = try apiGetNtfToken() + (m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken() if let token = m.deviceToken { registerToken(token: token) } From bf390dabd4182259cecd0ed979b4622e7df8f4ca Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 9 Jul 2022 10:32:24 +0100 Subject: [PATCH 88/89] mobile: update version/build v3.0 (ios: 57, android: 41) --- apps/android/app/build.gradle | 2 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index ed044fca89..ca49f575c9 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 40 + versionCode 41 versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 016dfe5025..c5004171b4 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1105,7 +1105,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 56; + CURRENT_PROJECT_VERSION = 57; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1145,7 +1145,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 56; + CURRENT_PROJECT_VERSION = 57; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1224,7 +1224,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 56; + CURRENT_PROJECT_VERSION = 57; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1254,7 +1254,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 56; + CURRENT_PROJECT_VERSION = 57; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; From 36f97b2ea9539c5e65db7a5d9417d15e55afa8d0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 9 Jul 2022 13:04:18 +0100 Subject: [PATCH 89/89] v3.0.0 --- cabal.project | 2 +- package.yaml | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- stack.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cabal.project b/cabal.project index e3aeadb4a5..2f4de58dd1 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: cc798145d2e26be8986747dfe3e5524b45699fab + tag: e75846aa38dd26fa70e3faa38ec780edf245e022 source-repository-package type: git diff --git a/package.yaml b/package.yaml index 31cc92667b..6eb0b0574e 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 2.2.0 +version: 3.0.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 4e1ad844ef..45bf4a4a86 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."cc798145d2e26be8986747dfe3e5524b45699fab" = "1w907i65hq468xss822if1z9iwq4ajcszyfm5sgc8wc2pp5v54zb"; + "https://github.com/simplex-chat/simplexmq.git"."e75846aa38dd26fa70e3faa38ec780edf245e022" = "1mjr5bpjnz6pw9w4qy2r548xlgw89rxbmj36zb4vwq4jghj3gmcz"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 2f65a11637..292e4dffde 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 2.2.0 +version: 3.0.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/stack.yaml b/stack.yaml index fc139618a4..86ec49a705 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: cc798145d2e26be8986747dfe3e5524b45699fab + commit: e75846aa38dd26fa70e3faa38ec780edf245e022 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7