Merge pull request #727 from simplex-chat/master
merge v2.2.1 to stable
This commit is contained in:
13
README.md
13
README.md
@@ -63,12 +63,14 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
|
||||
## News and updates
|
||||
|
||||
[Jun 4, 2022. v2.2: the new Privacy and Security settings](./20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
|
||||
[May 11, 2022. v2.0 released - sending images and files in mobile apps](./blog/20220511-simplex-chat-v2-images-files.md)
|
||||
|
||||
[Apr 04, 2022. Instant notifications for SimpleX Chat mobile apps](./blog/20220404-simplex-chat-instant-notifications.md)
|
||||
|
||||
[Mar 08, 2022 Mobile apps for iOS and Android released](./blog/20220308-simplex-chat-mobile-apps.md)
|
||||
|
||||
[Jan 12, 2022. SimpleX v1 released: the only messaging and application platform without user identities](./20220112-simplex-chat-v1-released.md)
|
||||
|
||||
[All updates](./blog)
|
||||
|
||||
## Make a private connection
|
||||
@@ -113,12 +115,13 @@ See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/p
|
||||
- ✅ Private instant notifications for Android using background service.
|
||||
- ✅ Haskell chat bot templates.
|
||||
- ✅ v2.0 - supporting images and files in mobile apps.
|
||||
- 🏗 End-to-end encrypted audio and video calls via the mobile apps.
|
||||
- 🏗 Automatic chat history deletion.
|
||||
- ✅ Manual chat history deletion.
|
||||
- 🚀 End-to-end encrypted audio and video calls via the mobile apps (enable via Experimental Features).
|
||||
- 🏗 Privacy preserving instant notifications for iOS using Apple Push Notification service (in progress).
|
||||
- 🏗 Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (in progress).
|
||||
- 🏗 Chat database portability and encryption.
|
||||
- Groups support for mobile apps.
|
||||
- Chat database portability and encryption.
|
||||
- Disappearing messages, with mutual agreement.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- SMP protocol improvements:
|
||||
- SMP queue redundancy and rotation.
|
||||
|
||||
@@ -12,21 +12,22 @@ struct ContentView: View {
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var callController = CallController.shared
|
||||
@Binding var doAuthenticate: Bool
|
||||
@Binding var enteredBackground: Double?
|
||||
@State private var userAuthorized: Bool?
|
||||
@State private var laFailed: Bool = false
|
||||
@Binding var userAuthorized: Bool?
|
||||
@State private var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
|
||||
@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
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if userAuthorized == true {
|
||||
if prefPerformLA && userAuthorized != true {
|
||||
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
|
||||
} else {
|
||||
if let step = chatModel.onboardingStage {
|
||||
if case .onboardingComplete = step,
|
||||
let user = chatModel.currentUser {
|
||||
chatModel.currentUser != nil {
|
||||
ZStack(alignment: .top) {
|
||||
ChatListView(user: user)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.onAppear {
|
||||
NtfManager.shared.requestAuthorization(onDeny: {
|
||||
alertManager.showAlert(notificationAlert())
|
||||
@@ -47,54 +48,39 @@ struct ContentView: View {
|
||||
OnboardingView(onboarding: step)
|
||||
}
|
||||
}
|
||||
} else if prefPerformLA && laFailed {
|
||||
retryAuthView()
|
||||
}
|
||||
}
|
||||
.onChange(of: doAuthenticate) { doAuth in
|
||||
if doAuth, authenticationExpired() {
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
.onAppear { if doAuthenticate { runAuthenticate() } }
|
||||
.onChange(of: doAuthenticate) { _ in if doAuthenticate { runAuthenticate() } }
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
}
|
||||
|
||||
private func retryAuthView() -> some View {
|
||||
Button {
|
||||
laFailed = false
|
||||
runAuthenticate()
|
||||
} label: { Label("Retry", systemImage: "arrow.counterclockwise") }
|
||||
}
|
||||
|
||||
private func runAuthenticate() {
|
||||
if !prefPerformLA {
|
||||
userAuthorized = true
|
||||
} else {
|
||||
chatModel.showChatInfo = false
|
||||
DispatchQueue.main.async() {
|
||||
userAuthorized = false
|
||||
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
userAuthorized = true
|
||||
case .failed:
|
||||
laFailed = true
|
||||
AlertManager.shared.showAlert(laFailedAlert())
|
||||
case .unavailable:
|
||||
userAuthorized = true
|
||||
prefPerformLA = false
|
||||
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
|
||||
}
|
||||
}
|
||||
} else if showChatInfo {
|
||||
showChatInfo = false
|
||||
DispatchQueue.main.async {
|
||||
justAuthenticate()
|
||||
}
|
||||
} else {
|
||||
justAuthenticate()
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticationExpired() -> Bool {
|
||||
if let enteredBackground = enteredBackground {
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
|
||||
} else {
|
||||
return true
|
||||
private func justAuthenticate() {
|
||||
userAuthorized = false
|
||||
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
userAuthorized = true
|
||||
case .failed:
|
||||
AlertManager.shared.showAlert(laFailedAlert())
|
||||
case .unavailable:
|
||||
userAuthorized = true
|
||||
prefPerformLA = false
|
||||
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import WebKit
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var currentUser: User?
|
||||
@Published var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// current chat
|
||||
|
||||
@@ -11,7 +11,6 @@ import UIKit
|
||||
import Dispatch
|
||||
import BackgroundTasks
|
||||
import SwiftUI
|
||||
import CallKit
|
||||
|
||||
private var chatController: chat_ctrl?
|
||||
|
||||
@@ -680,7 +679,7 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||
}
|
||||
withCall(contact) { call in
|
||||
m.callCommand = .end
|
||||
CallController.shared.reportCallRemoteEnded(call: call)
|
||||
// CallController.shared.reportCallRemoteEnded(call: call)
|
||||
}
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
|
||||
@@ -17,8 +17,8 @@ struct SimpleXApp: App {
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@State private var userAuthorized: Bool? = nil
|
||||
@State private var doAuthenticate: Bool = false
|
||||
@State private var userAuthorized: Bool?
|
||||
@State private var doAuthenticate = false
|
||||
@State private var enteredBackground: Double? = nil
|
||||
|
||||
init() {
|
||||
@@ -30,7 +30,7 @@ struct SimpleXApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
return WindowGroup {
|
||||
ContentView(doAuthenticate: $doAuthenticate, enteredBackground: $enteredBackground)
|
||||
ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized)
|
||||
.environmentObject(chatModel)
|
||||
.onOpenURL { url in
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
@@ -45,14 +45,24 @@ struct SimpleXApp: App {
|
||||
switch (phase) {
|
||||
case .background:
|
||||
BGManager.shared.schedule()
|
||||
if userAuthorized == true {
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
doAuthenticate = false
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
case .active:
|
||||
doAuthenticate = true
|
||||
doAuthenticate = authenticationExpired()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticationExpired() -> Bool {
|
||||
if let enteredBackground = enteredBackground {
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,9 @@ struct ActiveCallView: View {
|
||||
case let .connection(state):
|
||||
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
|
||||
case .connected = callStatus {
|
||||
if case .outgoing = call.direction {
|
||||
CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||
}
|
||||
// if case .outgoing = call.direction {
|
||||
// CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||
// }
|
||||
call.callState = .connected
|
||||
// CallKit doesn't work well with WKWebView
|
||||
// This is a hack to enable microphone in WKWebView after CallKit takes over it
|
||||
|
||||
@@ -7,90 +7,91 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CallKit
|
||||
//import CallKit
|
||||
import AVFoundation
|
||||
|
||||
class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
//class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
class CallController: NSObject, ObservableObject {
|
||||
static let useCallKit = false
|
||||
static let shared = CallController()
|
||||
private let provider = CXProvider(configuration: CallController.configuration)
|
||||
private let controller = CXCallController()
|
||||
// private let provider = CXProvider(configuration: CallController.configuration)
|
||||
// private let controller = CXCallController()
|
||||
private let callManager = CallManager()
|
||||
@Published var activeCallInvitation: CallInvitation?
|
||||
|
||||
// PKPushRegistry will be used from notification service extension
|
||||
// let registry = PKPushRegistry(queue: nil)
|
||||
|
||||
static let configuration: CXProviderConfiguration = {
|
||||
let configuration = CXProviderConfiguration()
|
||||
configuration.supportsVideo = true
|
||||
configuration.supportedHandleTypes = [.generic]
|
||||
configuration.includesCallsInRecents = true // TODO disable or add option
|
||||
configuration.maximumCallsPerCallGroup = 1
|
||||
return configuration
|
||||
}()
|
||||
// static let configuration: CXProviderConfiguration = {
|
||||
// let configuration = CXProviderConfiguration()
|
||||
// configuration.supportsVideo = true
|
||||
// configuration.supportedHandleTypes = [.generic]
|
||||
// configuration.includesCallsInRecents = true // TODO disable or add option
|
||||
// configuration.maximumCallsPerCallGroup = 1
|
||||
// return configuration
|
||||
// }()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
self.provider.setDelegate(self, queue: nil)
|
||||
// self.provider.setDelegate(self, queue: nil)
|
||||
// self.registry.delegate = self
|
||||
// self.registry.desiredPushTypes = [.voIP]
|
||||
}
|
||||
|
||||
func providerDidReset(_ provider: CXProvider) {
|
||||
}
|
||||
// func providerDidReset(_ provider: CXProvider) {
|
||||
// }
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
logger.debug("CallController.provider CXStartCallAction")
|
||||
if callManager.startOutgoingCall(callUUID: action.callUUID) {
|
||||
action.fulfill()
|
||||
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
logger.debug("CallController.provider CXAnswerCallAction")
|
||||
if callManager.answerIncomingCall(callUUID: action.callUUID) {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
logger.debug("CallController.provider CXEndCallAction")
|
||||
callManager.endCall(callUUID: action.callUUID) { ok in
|
||||
if ok {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
||||
print("timed out", #function)
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
print("received", #function)
|
||||
// do {
|
||||
// try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers)
|
||||
// logger.debug("audioSession category set")
|
||||
// try audioSession.setActive(true)
|
||||
// logger.debug("audioSession activated")
|
||||
// } catch {
|
||||
// print(error)
|
||||
// logger.error("failed activating audio session")
|
||||
// func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
// logger.debug("CallController.provider CXStartCallAction")
|
||||
// if callManager.startOutgoingCall(callUUID: action.callUUID) {
|
||||
// action.fulfill()
|
||||
// provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
}
|
||||
// }
|
||||
|
||||
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
print("received", #function)
|
||||
}
|
||||
// func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
// logger.debug("CallController.provider CXAnswerCallAction")
|
||||
// if callManager.answerIncomingCall(callUUID: action.callUUID) {
|
||||
// action.fulfill()
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
// }
|
||||
|
||||
// func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
// logger.debug("CallController.provider CXEndCallAction")
|
||||
// callManager.endCall(callUUID: action.callUUID) { ok in
|
||||
// if ok {
|
||||
// action.fulfill()
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
||||
// print("timed out", #function)
|
||||
// action.fulfill()
|
||||
// }
|
||||
|
||||
// func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
// print("received", #function)
|
||||
//// do {
|
||||
//// try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers)
|
||||
//// logger.debug("audioSession category set")
|
||||
//// try audioSession.setActive(true)
|
||||
//// logger.debug("audioSession activated")
|
||||
//// } catch {
|
||||
//// print(error)
|
||||
//// logger.error("failed activating audio session")
|
||||
//// }
|
||||
// }
|
||||
|
||||
// func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
// print("received", #function)
|
||||
// }
|
||||
|
||||
// func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||
//
|
||||
@@ -121,48 +122,49 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
func reportNewIncomingCall(invitation: CallInvitation, 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)
|
||||
update.hasVideo = invitation.peerMedia == .video
|
||||
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
} else {
|
||||
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
// let update = CXCallUpdate()
|
||||
// update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.displayName)
|
||||
// update.hasVideo = invitation.peerMedia == .video
|
||||
// provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
// } else {
|
||||
NtfManager.shared.notifyCallInvitation(invitation)
|
||||
if invitation.callTs.timeIntervalSinceNow >= -180 {
|
||||
activeCallInvitation = invitation
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
|
||||
}
|
||||
}
|
||||
// func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
// if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
// provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
|
||||
// }
|
||||
// }
|
||||
|
||||
func reportCallRemoteEnded(invitation: CallInvitation) {
|
||||
if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
} else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
// } else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
activeCallInvitation = nil
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
func reportCallRemoteEnded(call: Call) {
|
||||
if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
}
|
||||
}
|
||||
// func reportCallRemoteEnded(call: Call) {
|
||||
// if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
// }
|
||||
// }
|
||||
|
||||
func startCall(_ contact: Contact, _ media: CallMediaType) {
|
||||
logger.debug("CallController.startCall")
|
||||
let uuid = callManager.newOutgoingCall(contact, media)
|
||||
if CallController.useCallKit {
|
||||
let handle = CXHandle(type: .generic, value: contact.displayName)
|
||||
let action = CXStartCallAction(call: uuid, handle: handle)
|
||||
action.isVideo = media == .video
|
||||
requestTransaction(with: action)
|
||||
} else if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
// if CallController.useCallKit {
|
||||
// let handle = CXHandle(type: .generic, value: contact.displayName)
|
||||
// let action = CXStartCallAction(call: uuid, handle: handle)
|
||||
// action.isVideo = media == .video
|
||||
// requestTransaction(with: action)
|
||||
// } else if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
logger.debug("CallController.startCall: call started")
|
||||
} else {
|
||||
logger.error("CallController.startCall: no active call")
|
||||
@@ -177,9 +179,9 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
}
|
||||
|
||||
func endCall(callUUID: UUID) {
|
||||
if CallController.useCallKit {
|
||||
requestTransaction(with: CXEndCallAction(call: callUUID))
|
||||
} else {
|
||||
// if CallController.useCallKit {
|
||||
// requestTransaction(with: CXEndCallAction(call: callUUID))
|
||||
// } else {
|
||||
callManager.endCall(callUUID: callUUID) { ok in
|
||||
if ok {
|
||||
logger.debug("CallController.endCall: call ended")
|
||||
@@ -187,7 +189,7 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
logger.error("CallController.endCall: no actove call pr call invitation to end")
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
func endCall(invitation: CallInvitation) {
|
||||
@@ -204,15 +206,15 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
callManager.endCall(call: call, completed: completed)
|
||||
}
|
||||
|
||||
private func requestTransaction(with action: CXAction) {
|
||||
let t = CXTransaction()
|
||||
t.addAction(action)
|
||||
controller.request(t) { error in
|
||||
if let error = error {
|
||||
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
|
||||
} else {
|
||||
logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
// private func requestTransaction(with action: CXAction) {
|
||||
// let t = CXTransaction()
|
||||
// t.addAction(action)
|
||||
// controller.request(t) { error in
|
||||
// if let error = error {
|
||||
// logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
|
||||
// } else {
|
||||
// logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ struct ChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var showChatInfo: Bool
|
||||
@State var alert: ChatInfoViewAlert? = nil
|
||||
@State var deletingContact: Contact?
|
||||
|
||||
@@ -98,7 +99,7 @@ struct ChatInfoView: View {
|
||||
try await apiDeleteChat(type: .direct, id: contact.apiId)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.removeChat(contact.id)
|
||||
chatModel.showChatInfo = false
|
||||
showChatInfo = false
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
||||
@@ -117,7 +118,7 @@ struct ChatInfoView: View {
|
||||
Task {
|
||||
await clearChat(chat)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.showChatInfo = false
|
||||
showChatInfo = false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -129,6 +130,6 @@ struct ChatInfoView: View {
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var showChatInfo = true
|
||||
return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct ChatView: View {
|
||||
@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()
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@@ -97,12 +98,12 @@ struct ChatView: View {
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Button {
|
||||
chatModel.showChatInfo = true
|
||||
showChatInfo = true
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $chatModel.showChatInfo) {
|
||||
ChatInfoView(chat: chat)
|
||||
.sheet(isPresented: $showChatInfo) {
|
||||
ChatInfoView(chat: chat, showChatInfo: $showChatInfo)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -269,7 +270,8 @@ struct ChatView_Previews: PreviewProvider {
|
||||
ChatItem.getSample(8, .directSnd, .now, "👍👍👍👍"),
|
||||
ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
|
||||
]
|
||||
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
@State var showChatInfo = false
|
||||
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo)
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SwiftUI
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var chat: Chat
|
||||
@Binding var showChatInfo: Bool
|
||||
@State private var showContactRequestDialog = false
|
||||
|
||||
var body: some View {
|
||||
@@ -27,7 +28,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
|
||||
private func chatView() -> some View {
|
||||
ChatView(chat: chat)
|
||||
ChatView(chat: chat, showChatInfo: $showChatInfo)
|
||||
.onAppear {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
@@ -278,19 +279,20 @@ struct ChatListNavLink: View {
|
||||
struct ChatListNavLink_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var chatId: String? = "@1"
|
||||
@State var showChatInfo = false
|
||||
return Group {
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.contactRequest,
|
||||
chatItems: []
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 80))
|
||||
}
|
||||
|
||||
@@ -10,18 +10,17 @@ import SwiftUI
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Binding var showChatInfo: Bool
|
||||
// not really used in this view
|
||||
@State private var showSettings = false
|
||||
@State private var searchText = ""
|
||||
@AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true
|
||||
|
||||
var user: User
|
||||
|
||||
var body: some View {
|
||||
let v = NavigationView {
|
||||
List {
|
||||
ForEach(filteredChats()) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
ChatListNavLink(chat: chat, showChatInfo: $showChatInfo)
|
||||
.padding(.trailing, -16)
|
||||
}
|
||||
}
|
||||
@@ -92,10 +91,11 @@ struct ChatListView_Previews: PreviewProvider {
|
||||
)
|
||||
|
||||
]
|
||||
@State var showChatInfo = false
|
||||
return Group {
|
||||
ChatListView(user: User.sampleData)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.environmentObject(chatModel)
|
||||
ChatListView(user: User.sampleData)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.environmentObject(ChatModel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -966,7 +966,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -989,7 +989,7 @@
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
|
||||
MARKETING_VERSION = 2.2;
|
||||
MARKETING_VERSION = 2.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1009,7 +1009,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1032,7 +1032,7 @@
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
|
||||
MARKETING_VERSION = 2.2;
|
||||
MARKETING_VERSION = 2.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1091,7 +1091,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1112,7 +1112,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
MARKETING_VERSION = 2.2;
|
||||
MARKETING_VERSION = 2.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1131,7 +1131,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1152,7 +1152,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
MARKETING_VERSION = 2.2;
|
||||
MARKETING_VERSION = 2.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
46
blog/20220604-simplex-chat-new-privacy-security-settings.md
Normal file
46
blog/20220604-simplex-chat-new-privacy-security-settings.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# SimpleX Chat v2.2 - the first messaging platform without user identities - 100% private by design!
|
||||
|
||||
**Published:** June 4, 2022
|
||||
|
||||
See [v2 announcement](./20220511-simplex-chat-v2-images-files.md) for more information about SimpleX platform and how it protects your privacy by avoiding user identities of any kind in its design - SimpleX, unlike any other messaging platfom, has no identity keys or any numbers that identify its users.
|
||||
|
||||
## New Privacy and Security settings in version 2.2
|
||||
|
||||
<img src="./images/20220604-privacy-settings.png" width="480">
|
||||
|
||||
### Protect your chats
|
||||
|
||||
To protect your chats you can enable SimpleX Lock. Every time you open the chat after it was in the background for 30 second, you will need to pass biometric or pin code authentication to use the app (provided it is enabled for your device).
|
||||
|
||||
### Save data and avoid sharing you are online
|
||||
|
||||
In case you want to save your mobile data or to avoid showing to your contacts that you are online, you can disable automatic image downloads. For many users it is more convenient to have images downloaded automatically, so it is enabled by default.
|
||||
|
||||
Low resolution image previews would still be shown, the senders have no way to see if you received them or not.
|
||||
|
||||
### Avoid visiting websites of the links you send
|
||||
|
||||
When you receive the links that include link previews, it is fully private - these previews are generated by the sender, and they do not expose your IP address in any way.
|
||||
|
||||
When you send the links, the app automatically downloads the link description and the picture from the website of the link. While it is convenient, it exposes your IP address to the website. To avoid it you can disable sending link previews.
|
||||
|
||||
### Identify any lost messages in the chat
|
||||
|
||||
The app tracks the integrity of the messages you receive by cheching their sequential numbers and by validating that the hash of the previous message matches the hash included in the message – each conversation, effectively, is two blockchains that only you and your contact have access to.
|
||||
|
||||
In case some of the messages are lost, you would see it in the chat. It can happen because of one the following reasons:
|
||||
|
||||
- the messages have expired on the server after 30 days not being delivered.
|
||||
- the messages were removed when the server was restarted. We will add server redundancy later this year to avoid message loss in this case, for now if you see an indication that some messages were lost in the chat, you can check with your contact what it was.
|
||||
- some other app error. Please notify us via chat - we will investigate possible root causes.
|
||||
- the connection is compromised. This is very unlikely, but not an impossible scenario.
|
||||
|
||||
### There is more
|
||||
|
||||
You can discover additional features we are currently testing in Experimental Features - they will be announced later!
|
||||
|
||||
## More information
|
||||
|
||||
See [v1 announcement](./20220112-simplex-chat-v1-released.md) for information on how SimpleX protects the security of the messages.
|
||||
|
||||
Read about SimpleX design in [whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md).
|
||||
@@ -1,8 +1,10 @@
|
||||
# Blog
|
||||
|
||||
May 24, 2022 [Clearing messages for better conversation privacy](./20220524-simplex-chat-better-privacy.md)
|
||||
Jun 4, 2022 [v2.2: the new Privacy and Security settings](./20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
|
||||
May 11, 2022 [Sending images and files in mobile apps](./20220511-simplex-chat-v2-images-files.md)
|
||||
May 24, 2022 [v2.1: clearing messages for better conversation privacy](./20220524-simplex-chat-better-privacy.md)
|
||||
|
||||
May 11, 2022 [v2.0 released - sending images and files in mobile apps](./20220511-simplex-chat-v2-images-files.md)
|
||||
|
||||
Apr 04, 2022 [Instant notifications for SimpleX Chat mobile apps](./20220404-simplex-chat-instant-notifications.md)
|
||||
|
||||
|
||||
BIN
blog/images/20220604-privacy-settings.png
Normal file
BIN
blog/images/20220604-privacy-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 389 KiB |
Reference in New Issue
Block a user