Merge pull request #727 from simplex-chat/master

merge v2.2.1 to stable
This commit is contained in:
Evgeny Poberezkin
2022-06-04 15:06:04 +01:00
committed by GitHub
15 changed files with 242 additions and 190 deletions

View File

@@ -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.

View File

@@ -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())
}
}
}

View File

@@ -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

View File

@@ -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)")

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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")
// }
// }
// }
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}

View File

@@ -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())
}
}

View File

@@ -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;

View 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).

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB