Merge branch 'ios-notifications' into ep/ios-file-provider

This commit is contained in:
Evgeny Poberezkin 2022-06-09 15:20:43 +01:00
commit c7b5d73512
35 changed files with 848 additions and 269 deletions

View File

@ -24,7 +24,24 @@
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**! - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows. - 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows.
## Why privacy of communications matter ## Contents
- [Why privacy matters](#why-privacy-matters)
- [SimpleX approach to privacy and security](#simplex-approach-to-privacy-and-security)
- [Complete privacy](#complete-privacy-of-your-identity-profile-contacts-and-metadata)
- [Protection against spam and abuse](#the-best-protection-against-spam-and-abuse)
- [Ownership and security of your data](#complete-ownership-control-and-security-of-your-data)
- [Users own SimpleX network](#users-own-simplex-network)
- [Frequently asked questions](#frequently-asked-questions)
- [News and updates](#news-and-updates)
- [Make a private connection](#make-a-private-connection)
- [Quick installation of a terminal app](#zap-quick-installation-of-a-terminal-app)
- [SimpleX Platform design](#simplex-platform-design)
- [For developers](#for-developers)
- [Roadmap](#roadmap)
- [Disclaimer, License](#disclaimer)
## Why privacy matters
Everyone should care about privacy and security of their communications - innocuous conversations can put you in danger even if there is nothing to hide. Everyone should care about privacy and security of their communications - innocuous conversations can put you in danger even if there is nothing to hide.
@ -32,9 +49,9 @@ One of the most shocking stories is the experience of [Mohamedou Ould Salahi](ht
It is not enough to use an end-to-end encrypted messenger, we all should use the messengers that protect the privacy of our personal networks - who we are connected with. It is not enough to use an end-to-end encrypted messenger, we all should use the messengers that protect the privacy of our personal networks - who we are connected with.
## SimpleX unique approach to privacy and security ## SimpleX approach to privacy and security
### Full privacy of your identity, profile, contacts and metadata ### Complete privacy of your identity, profile, contacts and metadata
**Unlike any other existing messaging platform, SimpleX has no identifiers assigned to the users** - not even random numbers. This protects the privacy of who are you communicating with, hiding it from SimpleX platform servers and from any observers. [Read more](./docs/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). **Unlike any other existing messaging platform, SimpleX has no identifiers assigned to the users** - not even random numbers. This protects the privacy of who are you communicating with, hiding it from SimpleX platform servers and from any observers. [Read more](./docs/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
@ -50,25 +67,24 @@ SimpleX stores all user data on client devices, the messages are only held tempo
You can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers. [Read more](./docs/SIMPLEX.md#users-own-simplex-network). You can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers. [Read more](./docs/SIMPLEX.md#users-own-simplex-network).
## For developers ## Frequently asked questions
We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours. 1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release annoucement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works.
You already can: 2. _Why should I not just use Signal?_ This [post](https://github.com/dessalines/essays/blob/master/why_not_signal.md) shows why Signal cannot be considered a private messenger. Signal is a centralised platform that uses phone numbers to identify its users and their contacts.
- use SimpleX Chat library to integrate chat functionality into your apps. 3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identites?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has.
- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon).
If you are considering developing with SimpleX platform please get in touch for any advice and support.
## News and updates ## News and updates
[Jun 4, 2022. v2.2: the new Privacy and Security settings](./blog/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) [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) [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) [All updates](./blog)
## Make a private connection ## Make a private connection
@ -103,6 +119,17 @@ Only the client devices have information about users, their contacts and groups.
See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information on platform objectives and technical design. See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information on platform objectives and technical design.
## For developers
We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours.
You already can:
- use SimpleX Chat library to integrate chat functionality into your apps.
- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon).
If you are considering developing with SimpleX platform please get in touch for any advice and support.
## Roadmap ## Roadmap
- ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies. - ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies.
@ -113,12 +140,13 @@ See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/p
- ✅ Private instant notifications for Android using background service. - ✅ Private instant notifications for Android using background service.
- ✅ Haskell chat bot templates. - ✅ Haskell chat bot templates.
- ✅ v2.0 - supporting images and files in mobile apps. - ✅ v2.0 - supporting images and files in mobile apps.
- 🏗 End-to-end encrypted audio and video calls via the mobile apps. - ✅ Manual chat history deletion.
- 🏗 Automatic 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). - 🏗 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 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. - Groups support for mobile apps.
- Chat database portability and encryption. - Disappearing messages, with mutual agreement.
- Web widgets for custom interactivity in the chats. - Web widgets for custom interactivity in the chats.
- SMP protocol improvements: - SMP protocol improvements:
- SMP queue redundancy and rotation. - SMP queue redundancy and rotation.
@ -139,3 +167,13 @@ You are likely to discover some bugs - we would really appreciate if you use it
## License ## License
[AGPL v3](./LICENSE) [AGPL v3](./LICENSE)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
&nbsp;
[![Android app](https://github.com/simplex-chat/.github/blob/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/f_droid.svg" alt="F-Droid" height="41">](https://app.simplex.chat)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/website/raw/master/simplex.apk)

View File

@ -12,21 +12,22 @@ struct ContentView: View {
@ObservedObject var alertManager = AlertManager.shared @ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared @ObservedObject var callController = CallController.shared
@Binding var doAuthenticate: Bool @Binding var doAuthenticate: Bool
@Binding var enteredBackground: Double? @Binding var userAuthorized: Bool?
@State private var userAuthorized: Bool? @State private var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
@State private var laFailed: Bool = false
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false @AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
var body: some View { var body: some View {
ZStack { ZStack {
if userAuthorized == true { if prefPerformLA && userAuthorized != true {
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
} else {
if let step = chatModel.onboardingStage { if let step = chatModel.onboardingStage {
if case .onboardingComplete = step, if case .onboardingComplete = step,
let user = chatModel.currentUser { chatModel.currentUser != nil {
ZStack(alignment: .top) { ZStack(alignment: .top) {
ChatListView(user: user) ChatListView(showChatInfo: $showChatInfo)
.onAppear { .onAppear {
NtfManager.shared.requestAuthorization(onDeny: { NtfManager.shared.requestAuthorization(onDeny: {
alertManager.showAlert(notificationAlert()) alertManager.showAlert(notificationAlert())
@ -47,38 +48,33 @@ struct ContentView: View {
OnboardingView(onboarding: step) 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! } .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
} }
private func retryAuthView() -> some View {
Button {
laFailed = false
runAuthenticate()
} label: { Label("Retry", systemImage: "arrow.counterclockwise") }
}
private func runAuthenticate() { private func runAuthenticate() {
if !prefPerformLA { if !prefPerformLA {
userAuthorized = true userAuthorized = true
} else if showChatInfo {
showChatInfo = false
DispatchQueue.main.async {
justAuthenticate()
}
} else { } else {
chatModel.showChatInfo = false justAuthenticate()
DispatchQueue.main.async() { }
}
private func justAuthenticate() {
userAuthorized = false userAuthorized = false
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
switch (laResult) { switch (laResult) {
case .success: case .success:
userAuthorized = true userAuthorized = true
case .failed: case .failed:
laFailed = true
AlertManager.shared.showAlert(laFailedAlert()) AlertManager.shared.showAlert(laFailedAlert())
case .unavailable: case .unavailable:
userAuthorized = true userAuthorized = true
@ -87,16 +83,6 @@ struct ContentView: View {
} }
} }
} }
}
}
private func authenticationExpired() -> Bool {
if let enteredBackground = enteredBackground {
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
} else {
return true
}
}
func laNoticeAlert() -> Alert { func laNoticeAlert() -> Alert {
Alert( Alert(

View File

@ -15,7 +15,6 @@ import SimpleXChat
final class ChatModel: ObservableObject { final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage? @Published var onboardingStage: OnboardingStage?
@Published var currentUser: User? @Published var currentUser: User?
@Published var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
// list of chat "previews" // list of chat "previews"
@Published var chats: [Chat] = [] @Published var chats: [Chat] = []
// current chat // current chat

View File

@ -55,8 +55,8 @@ struct SimpleXApp: App {
@ObservedObject var alertManager = AlertManager.shared @ObservedObject var alertManager = AlertManager.shared
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State private var userAuthorized: Bool? = nil @State private var userAuthorized: Bool?
@State private var doAuthenticate: Bool = false @State private var doAuthenticate = false
@State private var enteredBackground: Double? = nil @State private var enteredBackground: Double? = nil
init() { init() {
@ -74,7 +74,7 @@ struct SimpleXApp: App {
var body: some Scene { var body: some Scene {
return WindowGroup { return WindowGroup {
ContentView(doAuthenticate: $doAuthenticate, enteredBackground: $enteredBackground) ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized)
.environmentObject(chatModel) .environmentObject(chatModel)
.onOpenURL { url in .onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)") logger.debug("ContentView.onOpenURL: \(url)")
@ -91,11 +91,13 @@ struct SimpleXApp: App {
switch (phase) { switch (phase) {
case .background: case .background:
BGManager.shared.schedule() BGManager.shared.schedule()
doAuthenticate = false if userAuthorized == true {
enteredBackground = ProcessInfo.processInfo.systemUptime enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
machMessenger.stop() machMessenger.stop()
case .active: case .active:
doAuthenticate = true doAuthenticate = authenticationExpired()
machMessenger.start() machMessenger.start()
default: default:
break break
@ -103,4 +105,12 @@ struct SimpleXApp: App {
} }
} }
} }
private func authenticationExpired() -> Bool {
if let enteredBackground = enteredBackground {
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
} else {
return true
}
}
} }

View File

@ -13,6 +13,7 @@ struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared @ObservedObject var alertManager = AlertManager.shared
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@Binding var showChatInfo: Bool
@State var alert: ChatInfoViewAlert? = nil @State var alert: ChatInfoViewAlert? = nil
@State var deletingContact: Contact? @State var deletingContact: Contact?
@ -99,7 +100,7 @@ struct ChatInfoView: View {
try await apiDeleteChat(type: .direct, id: contact.apiId) try await apiDeleteChat(type: .direct, id: contact.apiId)
DispatchQueue.main.async { DispatchQueue.main.async {
chatModel.removeChat(contact.id) chatModel.removeChat(contact.id)
chatModel.showChatInfo = false showChatInfo = false
} }
} catch let error { } catch let error {
logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
@ -118,7 +119,7 @@ struct ChatInfoView: View {
Task { Task {
await clearChat(chat) await clearChat(chat)
DispatchQueue.main.async { DispatchQueue.main.async {
chatModel.showChatInfo = false showChatInfo = false
} }
} }
}, },
@ -130,6 +131,6 @@ struct ChatInfoView: View {
struct ChatInfoView_Previews: PreviewProvider { struct ChatInfoView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
@State var showChatInfo = true @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

@ -16,6 +16,7 @@ struct ChatView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false @AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@Binding var showChatInfo: Bool
@State private var composeState = ComposeState() @State private var composeState = ComposeState()
@State private var deletingItem: ChatItem? = nil @State private var deletingItem: ChatItem? = nil
@FocusState private var keyboardVisible: Bool @FocusState private var keyboardVisible: Bool
@ -98,12 +99,12 @@ struct ChatView: View {
} }
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
Button { Button {
chatModel.showChatInfo = true showChatInfo = true
} label: { } label: {
ChatInfoToolbar(chat: chat) ChatInfoToolbar(chat: chat)
} }
.sheet(isPresented: $chatModel.showChatInfo) { .sheet(isPresented: $showChatInfo) {
ChatInfoView(chat: chat) ChatInfoView(chat: chat, showChatInfo: $showChatInfo)
} }
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
@ -270,7 +271,8 @@ struct ChatView_Previews: PreviewProvider {
ChatItem.getSample(8, .directSnd, .now, "👍👍👍👍"), 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.") 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) .environmentObject(chatModel)
} }
} }

View File

@ -12,6 +12,7 @@ import SimpleXChat
struct ChatListNavLink: View { struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@State var chat: Chat @State var chat: Chat
@Binding var showChatInfo: Bool
@State private var showContactRequestDialog = false @State private var showContactRequestDialog = false
var body: some View { var body: some View {
@ -28,7 +29,7 @@ struct ChatListNavLink: View {
} }
private func chatView() -> some View { private func chatView() -> some View {
ChatView(chat: chat) ChatView(chat: chat, showChatInfo: $showChatInfo)
.onAppear { .onAppear {
do { do {
let cInfo = chat.chatInfo let cInfo = chat.chatInfo
@ -279,19 +280,20 @@ struct ChatListNavLink: View {
struct ChatListNavLink_Previews: PreviewProvider { struct ChatListNavLink_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
@State var chatId: String? = "@1" @State var chatId: String? = "@1"
@State var showChatInfo = false
return Group { return Group {
ChatListNavLink(chat: Chat( ChatListNavLink(chat: Chat(
chatInfo: ChatInfo.sampleData.direct, chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
)) ), showChatInfo: $showChatInfo)
ChatListNavLink(chat: Chat( ChatListNavLink(chat: Chat(
chatInfo: ChatInfo.sampleData.direct, chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
)) ), showChatInfo: $showChatInfo)
ChatListNavLink(chat: Chat( ChatListNavLink(chat: Chat(
chatInfo: ChatInfo.sampleData.contactRequest, chatInfo: ChatInfo.sampleData.contactRequest,
chatItems: [] chatItems: []
)) ), showChatInfo: $showChatInfo)
} }
.previewLayout(.fixed(width: 360, height: 80)) .previewLayout(.fixed(width: 360, height: 80))
} }

View File

@ -11,18 +11,17 @@ import SimpleXChat
struct ChatListView: View { struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@Binding var showChatInfo: Bool
// not really used in this view // not really used in this view
@State private var showSettings = false @State private var showSettings = false
@State private var searchText = "" @State private var searchText = ""
@AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true @AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true
var user: User
var body: some View { var body: some View {
let v = NavigationView { let v = NavigationView {
List { List {
ForEach(filteredChats()) { chat in ForEach(filteredChats()) { chat in
ChatListNavLink(chat: chat) ChatListNavLink(chat: chat, showChatInfo: $showChatInfo)
.padding(.trailing, -16) .padding(.trailing, -16)
} }
} }
@ -93,10 +92,11 @@ struct ChatListView_Previews: PreviewProvider {
) )
] ]
@State var showChatInfo = false
return Group { return Group {
ChatListView(user: User.sampleData) ChatListView(showChatInfo: $showChatInfo)
.environmentObject(chatModel) .environmentObject(chatModel)
ChatListView(user: User.sampleData) ChatListView(showChatInfo: $showChatInfo)
.environmentObject(ChatModel()) .environmentObject(ChatModel())
} }
} }

View File

@ -23,8 +23,10 @@
<false/> <false/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string>
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
<string>voip</string>
</array> </array>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<string>YES</string> <string>YES</string>

View File

@ -56,6 +56,11 @@
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; }; 5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; };
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; }; 5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.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 */; }; 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
@ -95,11 +100,6 @@
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 */; }; 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; };
5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */ = {isa = PBXBuildFile; fileRef = 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */; }; 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 */; }; 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; };
5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* AppGroup.swift */; }; 5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* AppGroup.swift */; };
5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; }; 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; };
@ -263,11 +263,14 @@
5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; }; 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; }; 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; };
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; }; 5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
5C6F2A3728522C9A00103588 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C6F2A3828522C9A00103588 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C6F2A3928522C9A00103588 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
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 = "<group>"; };
5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a"; sourceTree = "<group>"; };
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; }; 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; }; 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; }; 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
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 = "<group>"; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; }; 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; };
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; }; 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; };
@ -329,11 +332,6 @@
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a"; sourceTree = "<group>"; };
64A6907D28376BB90076573F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64A6907E28376BB90076573F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64A6907F28376BB90076573F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
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 = "<group>"; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; }; 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; }; 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
@ -392,13 +390,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5C69D5B62852379F009B27A4 /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CE2BA85284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a 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 */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5CE2BA89284532AD00EC33A6 /* libgmpxx.a in Frameworks */, 5C69D5B52852379F009B27A4 /* libffi.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; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -490,11 +488,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
64A6907D28376BB90076573F /* libffi.a */, 5C6F2A3828522C9A00103588 /* libffi.a */,
64A6907F28376BB90076573F /* libgmp.a */, 5C6F2A3728522C9A00103588 /* libgmp.a */,
64A6907E28376BB90076573F /* libgmpxx.a */, 5C6F2A3928522C9A00103588 /* libgmpxx.a */,
64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */, 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */,
64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */, 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@ -503,11 +501,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */, 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */,
5C1CAA162847C5C8009E5C72 /* UniformTypeIdentifiers.framework */,
5CDCAD6028187D7900503DA2 /* libz.tbd */, 5CDCAD6028187D7900503DA2 /* libz.tbd */,
5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */, 5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */,
5C764E7C279C71DB000C6508 /* libz.tbd */,
5C764E7B279C71D4000C6508 /* libiconv.tbd */,
5C1CAA162847C5C8009E5C72 /* UniformTypeIdentifiers.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1523,7 +1519,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1546,7 +1542,7 @@
); );
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios"; "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim"; "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_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1566,7 +1562,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1589,7 +1585,7 @@
); );
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios"; "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim"; "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_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1647,7 +1643,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1668,7 +1664,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
MARKETING_VERSION = 2.2; MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1686,7 +1682,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1707,7 +1703,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
MARKETING_VERSION = 2.2; MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1741,6 +1737,10 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Libraries",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/ios", "$(PROJECT_DIR)/Libraries/ios",
@ -1788,6 +1788,10 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Libraries",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/ios", "$(PROJECT_DIR)/Libraries/ios",

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

View File

@ -3,7 +3,7 @@ packages: .
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 964daf5442e1069634762450bc28cfd69a2968a1 tag: c1348aa54fba292d34339d6b111572cb1c74b546
source-repository-package source-repository-package
type: git type: git

View File

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

View File

@ -77,7 +77,7 @@ fi
chmod +x $BIN_PATH chmod +x $BIN_PATH
echo "$APP_NAME installed sucesfully!" echo "$APP_NAME installed successfully!"
if [ -z "$(command -v $APP_NAME)" ]; then if [ -z "$(command -v $APP_NAME)" ]; then
if [ -n "$($SHELL -c 'echo $ZSH_VERSION')" ]; then if [ -n "$($SHELL -c 'echo $ZSH_VERSION')" ]; then

View File

@ -39,6 +39,17 @@ dependencies:
- time == 1.9.* - time == 1.9.*
- unliftio == 0.2.* - unliftio == 0.2.*
- unliftio-core == 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: library:
source-dirs: src source-dirs: src

15
scripts/nix/README.md Normal file
View File

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

View File

@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."964daf5442e1069634762450bc28cfd69a2968a1" = "1vsbiawqlvi6v48ws2rmg5cmp5qphnry3ymg6458p2w8wdm2gsng"; "https://github.com/simplex-chat/simplexmq.git"."c1348aa54fba292d34339d6b111572cb1c74b546" = "103hw1h1agy42krf11d98bv3c1w0q0wi2z7r2ll0gmp5xv1r4rf0";
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";

View File

@ -17,9 +17,20 @@ build-type: Simple
extra-source-files: extra-source-files:
README.md 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 library
exposed-modules: exposed-modules:
Simplex.Chat Simplex.Chat
Simplex.Chat.Archive
Simplex.Chat.Bot Simplex.Chat.Bot
Simplex.Chat.Call Simplex.Chat.Call
Simplex.Chat.Controller Simplex.Chat.Controller
@ -83,6 +94,7 @@ library
, time ==1.9.* , time ==1.9.*
, unliftio ==0.2.* , unliftio ==0.2.*
, unliftio-core ==0.2.* , unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010 default-language: Haskell2010
executable simplex-bot executable simplex-bot
@ -121,6 +133,7 @@ executable simplex-bot
, time ==1.9.* , time ==1.9.*
, unliftio ==0.2.* , unliftio ==0.2.*
, unliftio-core ==0.2.* , unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010 default-language: Haskell2010
executable simplex-bot-advanced executable simplex-bot-advanced
@ -159,6 +172,7 @@ executable simplex-bot-advanced
, time ==1.9.* , time ==1.9.*
, unliftio ==0.2.* , unliftio ==0.2.*
, unliftio-core ==0.2.* , unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010 default-language: Haskell2010
executable simplex-chat executable simplex-chat
@ -200,6 +214,7 @@ executable simplex-chat
, unliftio ==0.2.* , unliftio ==0.2.*
, unliftio-core ==0.2.* , unliftio-core ==0.2.*
, websockets ==0.12.* , websockets ==0.12.*
, zip ==1.7.*
default-language: Haskell2010 default-language: Haskell2010
test-suite simplex-chat-test test-suite simplex-chat-test
@ -247,4 +262,5 @@ test-suite simplex-chat-test
, time ==1.9.* , time ==1.9.*
, unliftio ==0.2.* , unliftio ==0.2.*
, unliftio-core ==0.2.* , unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010 default-language: Haskell2010

View File

@ -41,6 +41,7 @@ import qualified Data.Text as T
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime) import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime)
import Data.Word (Word32) import Data.Word (Word32)
import Simplex.Chat.Archive
import Simplex.Chat.Call import Simplex.Chat.Call
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Chat.Markdown import Simplex.Chat.Markdown
@ -49,7 +50,7 @@ import Simplex.Chat.Options (ChatOpts (..), smpServersP)
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Store import Simplex.Chat.Store
import Simplex.Chat.Types 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
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), defaultAgentConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), defaultAgentConfig)
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
@ -59,10 +60,10 @@ import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Client (NtfServer) import Simplex.Messaging.Notifications.Client (NtfServer)
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), PushProvider (..)) import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), PushProvider (..))
import Simplex.Messaging.Parsers (base64P, parseAll) 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.Protocol as SMP
import qualified Simplex.Messaging.TMap as TM 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.Exit (exitFailure, exitSuccess)
import System.FilePath (combine, splitExtensions, takeFileName) import System.FilePath (combine, splitExtensions, takeFileName)
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout)
@ -134,7 +135,8 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de
rcvFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty
currentCalls <- atomically TM.empty currentCalls <- atomically TM.empty
filesFolder <- newTVarIO Nothing 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 where
resolveServers :: InitialAgentServers -> IO InitialAgentServers resolveServers :: InitialAgentServers -> IO InitialAgentServers
resolveServers ss@InitialAgentServers {smp = defaultSMPServers} = case nonEmpty smpServers of 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 :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m (Async ())
startChatController user = do startChatController user = do
asks smpAgent >>= resumeAgentClient
s <- asks agentAsync s <- asks agentAsync
readTVarIO s >>= maybe (start s) pure readTVarIO s >>= maybe (start s) pure
where where
@ -194,13 +197,23 @@ processChatCommand = \case
StartChat -> withUser' $ \user -> StartChat -> withUser' $ \user ->
asks agentAsync >>= readTVarIO >>= \case asks agentAsync >>= readTVarIO >>= \case
Just _ -> pure CRChatRunning 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 ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk
SetFilesFolder filesFolder' -> withUser $ \_ -> do SetFilesFolder filesFolder' -> withUser $ \_ -> do
createDirectoryIfMissing True filesFolder' createDirectoryIfMissing True filesFolder'
ff <- asks filesFolder ff <- asks filesFolder
atomically . writeTVar ff $ Just filesFolder' atomically . writeTVar ff $ Just filesFolder'
pure CRCmdOk 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) APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore $ \st -> getChatPreviews st user withPCC)
APIGetChat (ChatRef cType cId) pagination -> withUser $ \user -> case cType of APIGetChat (ChatRef cType cId) pagination -> withUser $ \user -> case cType of
CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\st -> getDirectChat st user cId pagination) CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\st -> getDirectChat st user cId pagination)
@ -770,6 +783,10 @@ processChatCommand = \case
CTDirect -> withStore $ \st -> getContactIdByName st userId name CTDirect -> withStore $ \st -> getContactIdByName st userId name
CTGroup -> withStore $ \st -> getGroupIdByName st user name CTGroup -> withStore $ \st -> getGroupIdByName st user name
_ -> throwChatError $ CECommandError "not supported" _ -> 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 -> ChatRef -> ByteString -> m Int64
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of
CTDirect -> withStore $ \st -> getDirectChatItemIdByText st userId cId SMDSnd (safeDecodeUtf8 msg) CTDirect -> withStore $ \st -> getDirectChatItemIdByText st userId cId SMDSnd (safeDecodeUtf8 msg)
@ -1115,7 +1132,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
allowAgentConnection conn confId $ XInfo profile allowAgentConnection conn confId $ XInfo profile
INFO connInfo -> INFO connInfo ->
saveConnInfo conn connInfo saveConnInfo conn connInfo
MSG meta msgBody -> do MSG meta _msgFlags msgBody -> do
_ <- saveRcvMSG conn (ConnectionId connId) meta msgBody _ <- saveRcvMSG conn (ConnectionId connId) meta msgBody
withAckMessage agentConnId meta $ pure () withAckMessage agentConnId meta $ pure ()
ackMsgDeliveryEvent conn meta ackMsgDeliveryEvent conn meta
@ -1128,7 +1145,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
-- TODO add debugging output -- TODO add debugging output
_ -> pure () _ -> pure ()
Just ct@Contact {localDisplayName = c, contactId} -> case agentMsg of 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 msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody
withAckMessage agentConnId msgMeta $ withAckMessage agentConnId msgMeta $
case chatMsgEvent of case chatMsgEvent of
@ -1268,7 +1285,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
when (connStatus == ConnReady) $ do when (connStatus == ConnReady) $ do
notifyMemberConnected gInfo m notifyMemberConnected gInfo m
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
MSG msgMeta msgBody -> do MSG msgMeta _msgFlags msgBody -> do
msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody
withAckMessage agentConnId msgMeta $ withAckMessage agentConnId msgMeta $
case chatMsgEvent of case chatMsgEvent of
@ -1327,7 +1344,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
ci <- withStore $ \st -> getChatItemByFileId st user fileId ci <- withStore $ \st -> getChatItemByFileId st user fileId
toView $ CRSndFileRcvCancelled ci ft toView $ CRSndFileRcvCancelled ci ft
_ -> throwChatError $ CEFileSend fileId err _ -> throwChatError $ CEFileSend fileId err
MSG meta _ -> MSG meta _ _ ->
withAckMessage agentConnId meta $ pure () withAckMessage agentConnId meta $ pure ()
-- TODO print errors -- TODO print errors
ERR _ -> pure () ERR _ -> pure ()
@ -1351,7 +1368,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
updateCIFileStatus st user fileId CIFSRcvTransfer updateCIFileStatus st user fileId CIFSRcvTransfer
getChatItemByFileId st user fileId getChatItemByFileId st user fileId
toView $ CRRcvFileStart ci 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 parseFileChunk msgBody >>= \case
FileChunkCancel -> FileChunkCancel ->
unless cancelled $ do unless cancelled $ do
@ -1894,7 +1911,7 @@ sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentCo
sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m () sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m ()
sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do
chunkBytes <- readFileChunk ft chunkNo 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 withStore $ \st -> updateSndFileChunkMsg st ft chunkNo msgId
readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString
@ -1988,7 +2005,7 @@ cancelSndFileTransfer ft@SndFileTransfer {agentConnId = AgentConnId acId, fileSt
updateSndFileStatus st ft FSCancelled updateSndFileStatus st ft FSCancelled
deleteSndFileChunks st ft deleteSndFileChunks st ft
withAgent $ \a -> do withAgent $ \a -> do
void (sendMessage a acId $ smpEncode FileChunkCancel) `catchError` \_ -> pure () void (sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunkCancel) `catchError` \_ -> pure ()
deleteConnection a acId deleteConnection a acId
closeFileHandle :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m () closeFileHandle :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m ()
@ -2016,7 +2033,7 @@ sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connS
sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> ConnOrGroupId -> m SndMessage sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> ConnOrGroupId -> m SndMessage
sendDirectMessage conn chatMsgEvent connOrGroupId = do sendDirectMessage conn chatMsgEvent connOrGroupId = do
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId
deliverMessage conn msgBody msgId deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
pure msg pure msg
createSndMessage :: ChatMonad m => ChatMsgEvent -> ConnOrGroupId -> m SndMessage createSndMessage :: ChatMonad m => ChatMsgEvent -> ConnOrGroupId -> m SndMessage
@ -2029,9 +2046,10 @@ createSndMessage chatMsgEvent connOrGroupId = do
directMessage :: ChatMsgEvent -> ByteString directMessage :: ChatMsgEvent -> ByteString
directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent} directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent}
deliverMessage :: ChatMonad m => Connection -> MsgBody -> MessageId -> m () deliverMessage :: ChatMonad m => Connection -> CMEventTag -> MsgBody -> MessageId -> m ()
deliverMessage conn@Connection {connId} msgBody msgId = do deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgBody let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
withStore $ \st -> createSndMsgDelivery st sndMsgDelivery msgId withStore $ \st -> createSndMsgDelivery st sndMsgDelivery msgId
@ -2051,10 +2069,12 @@ sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do
forM_ (filter memberCurrent members) $ \m@GroupMember {groupMemberId} -> forM_ (filter memberCurrent members) $ \m@GroupMember {groupMemberId} ->
case memberConn m of case memberConn m of
Nothing -> withStore $ \st -> createPendingGroupMessage st groupMemberId msgId introId_ Nothing -> withStore $ \st -> createPendingGroupMessage st groupMemberId msgId introId_
Just conn@Connection {connStatus} -> Just conn@Connection {connStatus}
if not (connStatus == ConnSndReady || connStatus == ConnReady) | connStatus == ConnSndReady || connStatus == ConnReady -> do
then unless (connStatus == ConnDeleted) $ withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_) let tag = toCMEventTag chatMsgEvent
else (deliverMessage conn msgBody msgId >> postDeliver) `catchError` const (pure ()) (deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ())
| connStatus == ConnDeleted -> pure ()
| otherwise -> withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_)
pure msg pure msg
sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m () sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m ()
@ -2062,7 +2082,7 @@ sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do
pendingMessages <- withStore $ \st -> getPendingGroupMessages st groupMemberId pendingMessages <- withStore $ \st -> getPendingGroupMessages st groupMemberId
-- TODO ensure order - pending messages interleave with user input messages -- TODO ensure order - pending messages interleave with user input messages
forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} -> do forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} -> do
deliverMessage conn msgBody msgId deliverMessage conn cmEventTag msgBody msgId
withStore (\st -> deletePendingGroupMessage st groupMemberId msgId) withStore (\st -> deletePendingGroupMessage st groupMemberId msgId)
when (cmEventTag == XGrpMemFwd_) $ case introId_ of when (cmEventTag == XGrpMemFwd_) $ case introId_ of
Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
@ -2212,8 +2232,12 @@ chatCommandP =
("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile) ("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile)
<|> ("/user" <|> "/u") $> ShowActiveUser <|> ("/user" <|> "/u") $> ShowActiveUser
<|> "/_start" $> StartChat <|> "/_start" $> StartChat
<|> "/_stop" $> APIStopChat
<|> "/_resubscribe all" $> ResubscribeAllConnections <|> "/_resubscribe all" $> ResubscribeAllConnections
<|> "/_files_folder " *> (SetFilesFolder <$> filePath) <|> "/_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 chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False))
<|> "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP) <|> "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP)
<|> "/_get items count=" *> (APIGetChatItems <$> A.decimal) <|> "/_get items count=" *> (APIGetChatItems <$> A.decimal)

View File

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

View File

@ -76,6 +76,7 @@ data ChatController = ChatController
smpAgent :: AgentClient, smpAgent :: AgentClient,
agentAsync :: TVar (Maybe (Async ())), agentAsync :: TVar (Maybe (Async ())),
chatStore :: SQLiteStore, chatStore :: SQLiteStore,
chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted
idsDrg :: TVar ChaChaDRG, idsDrg :: TVar ChaChaDRG,
inputQ :: TBQueue String, inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe CorrId, ChatResponse), outputQ :: TBQueue (Maybe CorrId, ChatResponse),
@ -100,8 +101,12 @@ data ChatCommand
= ShowActiveUser = ShowActiveUser
| CreateActiveUser Profile | CreateActiveUser Profile
| StartChat | StartChat
| APIStopChat
| ResubscribeAllConnections | ResubscribeAllConnections
| SetFilesFolder FilePath | SetFilesFolder FilePath
| APIExportArchive ArchiveConfig
| APIImportArchive ArchiveConfig
| APIDeleteStorage
| APIGetChats {pendingConnections :: Bool} | APIGetChats {pendingConnections :: Bool}
| APIGetChat ChatRef ChatPagination | APIGetChat ChatRef ChatPagination
| APIGetChatItems Int | APIGetChatItems Int
@ -178,6 +183,7 @@ data ChatResponse
= CRActiveUser {user :: User} = CRActiveUser {user :: User}
| CRChatStarted | CRChatStarted
| CRChatRunning | CRChatRunning
| CRChatStopped
| CRApiChats {chats :: [AChat]} | CRApiChats {chats :: [AChat]}
| CRApiChat {chat :: AChat} | CRApiChat {chat :: AChat}
| CRLastMessages {chatItems :: [AChatItem]} | CRLastMessages {chatItems :: [AChatItem]}
@ -279,6 +285,9 @@ instance ToJSON ChatResponse where
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR" toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool}
deriving (Show, Generic, FromJSON)
data ContactSubStatus = ContactSubStatus data ContactSubStatus = ContactSubStatus
{ contact :: Contact, { contact :: Contact,
contactError :: Maybe ChatError contactError :: Maybe ChatError
@ -329,6 +338,8 @@ data ChatErrorType
= CENoActiveUser = CENoActiveUser
| CEActiveUserExists | CEActiveUserExists
| CEChatNotStarted | CEChatNotStarted
| CEChatNotStopped
| CEChatStoreChanged
| CEInvalidConnReq | CEInvalidConnReq
| CEInvalidChatMessage {message :: String} | CEInvalidChatMessage {message :: String}
| CEContactNotReady {contact :: Contact} | CEContactNotReady {contact :: Contact}

View File

@ -26,10 +26,12 @@ simplexChatCore cfg@ChatConfig {dbPoolSize, yesToMigrations} opts sendToast chat
st <- createStore f dbPoolSize yesToMigrations st <- createStore f dbPoolSize yesToMigrations
u <- getCreateActiveUser st u <- getCreateActiveUser st
cc <- newChatController st (Just u) cfg opts sendToast 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 :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
runSimplexChat u cc chat = do runSimplexChat ChatOpts {maintenance} u cc chat
| maintenance = wait =<< async (chat u cc)
| otherwise = do
a1 <- async $ chat u cc a1 <- async $ chat u cc
a2 <- runReaderT (startChatController u) cc a2 <- runReaderT (startChatController u) cc
waitEither_ a1 a2 waitEither_ a1 a2

View File

@ -32,12 +32,12 @@ import GHC.Generics (Generic)
import Simplex.Chat.Markdown import Simplex.Chat.Markdown
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Types 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.Agent.Protocol (AgentErrorType, AgentMsgId, MsgErrorType (..), MsgMeta (..))
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, singleFieldJSON, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, singleFieldJSON, sumTypeJSON)
import Simplex.Messaging.Protocol (MsgBody) import Simplex.Messaging.Protocol (MsgBody)
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
deriving (Show, Generic) deriving (Show, Generic)

View File

@ -54,7 +54,8 @@ mobileChatOpts =
logAgent = False, logAgent = False,
chatCmd = "", chatCmd = "",
chatCmdDelay = 3, chatCmdDelay = 3,
chatServerPort = Nothing chatServerPort = Nothing,
maintenance = True
} }
defaultMobileConfig :: ChatConfig defaultMobileConfig :: ChatConfig

View File

@ -25,7 +25,8 @@ data ChatOpts = ChatOpts
logAgent :: Bool, logAgent :: Bool,
chatCmd :: String, chatCmd :: String,
chatCmdDelay :: Int, chatCmdDelay :: Int,
chatServerPort :: Maybe String chatServerPort :: Maybe String,
maintenance :: Bool
} }
chatOpts :: FilePath -> FilePath -> Parser ChatOpts chatOpts :: FilePath -> FilePath -> Parser ChatOpts
@ -88,7 +89,13 @@ chatOpts appDir defaultDbFileName = do
<> help "Run chat server on specified port" <> help "Run chat server on specified port"
<> value Nothing <> 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 where
defaultDbFilePath = combine appDir defaultDbFileName defaultDbFilePath = combine appDir defaultDbFileName

View File

@ -31,10 +31,10 @@ import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Simplex.Chat.Call import Simplex.Chat.Call
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8) import Simplex.Chat.Util (safeDecodeUtf8)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (fromTextField_) import Simplex.Messaging.Parsers (fromTextField_)
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
data ConnectionEntity data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact} = RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
@ -437,6 +437,16 @@ instance FromField CMEventTag where fromField = fromTextField_ cmEventTagT
instance ToField CMEventTag where toField = toField . serializeCMEventTag 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 -> Either String ChatMessage
appToChatMessage AppMessage {msgId, event, params} = do appToChatMessage AppMessage {msgId, event, params} = do
eventTag <- strDecode $ encodeUtf8 event eventTag <- strDecode $ encodeUtf8 event

View File

@ -206,7 +206,6 @@ import Simplex.Chat.Migrations.M20220404_files_status_fields
import Simplex.Chat.Migrations.M20220514_profiles_user_id import Simplex.Chat.Migrations.M20220514_profiles_user_id
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (eitherToMaybe)
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) 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, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) 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.Encoding.String (StrEncoding (strEncode))
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer) import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer)
import Simplex.Messaging.Util (liftIOEither, (<$$>)) import Simplex.Messaging.Util (eitherToMaybe, liftIOEither, (<$$>))
import UnliftIO.STM import UnliftIO.STM
schemaMigrations :: [(String, Query)] schemaMigrations :: [(String, Query)]

View File

@ -1,6 +1,5 @@
module Simplex.Chat.Util where module Simplex.Chat.Util where
import Control.Monad (when)
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding (decodeUtf8With)
@ -9,15 +8,3 @@ safeDecodeUtf8 :: ByteString -> Text
safeDecodeUtf8 = decodeUtf8With onError safeDecodeUtf8 = decodeUtf8With onError
where where
onError _ _ = Just '?' 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

View File

@ -53,7 +53,8 @@ responseToView :: Bool -> ChatResponse -> [StyledString]
responseToView testView = \case responseToView testView = \case
CRActiveUser User {profile} -> viewUserProfile profile CRActiveUser User {profile} -> viewUserProfile profile
CRChatStarted -> ["chat started"] CRChatStarted -> ["chat started"]
CRChatRunning -> [] CRChatRunning -> ["chat is running"]
CRChatStopped -> ["chat stopped"]
CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] 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] CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat]
CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft]
@ -721,6 +722,8 @@ viewChatError = \case
CENoActiveUser -> ["error: active user is required"] CENoActiveUser -> ["error: active user is required"]
CEActiveUserExists -> ["error: active user already exists"] CEActiveUserExists -> ["error: active user already exists"]
CEChatNotStarted -> ["error: chat not started"] CEChatNotStarted -> ["error: chat not started"]
CEChatNotStopped -> ["error: chat not stopped"]
CEChatStoreChanged -> ["error: chat store changed"]
CEInvalidConnReq -> viewInvalidConnReq CEInvalidConnReq -> viewInvalidConnReq
CEInvalidChatMessage e -> ["chat message error: " <> sShow e] CEInvalidChatMessage e -> ["chat message error: " <> sShow e]
CEContactNotReady c -> [ttyContact' c <> ": not ready"] CEContactNotReady c -> [ttyContact' c <> ": not ready"]

View File

@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq # - ../simplexmq
- github: simplex-chat/simplexmq - github: simplex-chat/simplexmq
commit: 964daf5442e1069634762450bc28cfd69a2968a1 commit: c1348aa54fba292d34339d6b111572cb1c74b546
# - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
- github: simplex-chat/aeson - github: simplex-chat/aeson
commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7
@ -59,8 +59,10 @@ extra-deps:
# extra-deps: [] # extra-deps: []
# Override default flag values for local packages and 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 databases containing global packages
# extra-package-dbs: [] # extra-package-dbs: []

View File

@ -30,6 +30,7 @@ import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server (runSMPServerBlocking)
import Simplex.Messaging.Server.Env.STM import Simplex.Messaging.Server.Env.STM
import Simplex.Messaging.Transport import Simplex.Messaging.Transport
import Simplex.Messaging.Version
import System.Directory (createDirectoryIfMissing, removePathForcibly) import System.Directory (createDirectoryIfMissing, removePathForcibly)
import qualified System.Terminal as C import qualified System.Terminal as C
import System.Terminal.Internal (VirtualTerminal (..), VirtualTerminalSettings (..), withVirtualTerminal) import System.Terminal.Internal (VirtualTerminal (..), VirtualTerminalSettings (..), withVirtualTerminal)
@ -42,8 +43,8 @@ testDBPrefix = "tests/tmp/test"
serverPort :: ServiceName serverPort :: ServiceName
serverPort = "5001" serverPort = "5001"
opts :: ChatOpts testOpts :: ChatOpts
opts = testOpts =
ChatOpts ChatOpts
{ dbFilePrefix = undefined, { dbFilePrefix = undefined,
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"], smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"],
@ -51,7 +52,8 @@ opts =
logAgent = False, logAgent = False,
chatCmd = "", chatCmd = "",
chatCmdDelay = 3, chatCmdDelay = 3,
chatServerPort = Nothing chatServerPort = Nothing,
maintenance = False
} }
termSettings :: VirtualTerminalSettings termSettings :: VirtualTerminalSettings
@ -74,34 +76,46 @@ data TestCC = TestCC
aCfg :: AgentConfig aCfg :: AgentConfig
aCfg = agentConfig defaultChatConfig aCfg = agentConfig defaultChatConfig
cfg :: ChatConfig testAgentCfg :: AgentConfig
cfg = testAgentCfg = aCfg {reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}}
testCfg :: ChatConfig
testCfg =
defaultChatConfig defaultChatConfig
{ agentConfig = { agentConfig = testAgentCfg,
aCfg {reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}},
testView = True testView = True
} }
createTestChat :: String -> Profile -> IO TestCC testAgentCfgV1 :: AgentConfig
createTestChat dbPrefix profile = do 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 let dbFilePrefix = testDBPrefix <> dbPrefix
st <- createStore (dbFilePrefix <> "_chat.db") 1 False st <- createStore (dbFilePrefix <> "_chat.db") 1 False
Right user <- runExceptT $ createUser st profile True Right user <- runExceptT $ createUser st profile True
startTestChat_ st dbFilePrefix user startTestChat_ st cfg opts dbFilePrefix user
startTestChat :: String -> IO TestCC startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC
startTestChat dbPrefix = do startTestChat cfg opts dbPrefix = do
let dbFilePrefix = testDBPrefix <> dbPrefix let dbFilePrefix = testDBPrefix <> dbPrefix
st <- createStore (dbFilePrefix <> "_chat.db") 1 False st <- createStore (dbFilePrefix <> "_chat.db") 1 False
Just user <- find activeUser <$> getUsers st Just user <- find activeUser <$> getUsers st
startTestChat_ st dbFilePrefix user startTestChat_ st cfg opts dbFilePrefix user
startTestChat_ :: SQLiteStore -> FilePath -> User -> IO TestCC startTestChat_ :: SQLiteStore -> ChatConfig -> ChatOpts -> FilePath -> User -> IO TestCC
startTestChat_ st dbFilePrefix user = do startTestChat_ st cfg opts dbFilePrefix user = do
t <- withVirtualTerminal termSettings pure t <- withVirtualTerminal termSettings pure
ct <- newChatTerminal t ct <- newChatTerminal t
cc <- newChatController st (Just user) cfg opts {dbFilePrefix} Nothing -- no notifications 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 termQ <- newTQueueIO
termAsync <- async $ readTerminalOutput t termQ termAsync <- async $ readTerminalOutput t termQ
pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ} pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ}
@ -113,10 +127,34 @@ stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do
uninterruptibleCancel chatAsync uninterruptibleCancel chatAsync
withNewTestChat :: String -> Profile -> (TestCC -> IO a) -> IO a withNewTestChat :: String -> Profile -> (TestCC -> IO a) -> IO a
withNewTestChat dbPrefix profile = bracket (createTestChat dbPrefix profile) (\cc -> cc <// 100000 >> stopTestChat cc) 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 = 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 <// 100000 >> stopTestChat cc)
withTestChatV1 :: String -> (TestCC -> IO a) -> IO a
withTestChatV1 = withTestChatCfg testCfgV1
withTestChat :: String -> (TestCC -> IO a) -> IO a withTestChat :: String -> (TestCC -> IO a) -> IO a
withTestChat dbPrefix = bracket (startTestChat dbPrefix) (\cc -> cc <// 100000 >> stopTestChat cc) 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 = withTestChatCfgOpts testCfg
withTestChatCfgOpts :: ChatConfig -> ChatOpts -> String -> (TestCC -> IO a) -> IO a
withTestChatCfgOpts cfg opts dbPrefix = bracket (startTestChat cfg opts dbPrefix) (\cc -> cc <// 100000 >> stopTestChat cc)
readTerminalOutput :: VirtualTerminal -> TQueue String -> IO () readTerminalOutput :: VirtualTerminal -> TQueue String -> IO ()
readTerminalOutput t termQ = do readTerminalOutput t termQ = do
@ -147,8 +185,8 @@ withTmpFiles =
(createDirectoryIfMissing False "tests/tmp") (createDirectoryIfMissing False "tests/tmp")
(removePathForcibly "tests/tmp") (removePathForcibly "tests/tmp")
testChatN :: [Profile] -> ([TestCC] -> IO ()) -> IO () testChatN :: ChatConfig -> ChatOpts -> [Profile] -> ([TestCC] -> IO ()) -> IO ()
testChatN ps test = withTmpFiles $ do testChatN cfg opts ps test = withTmpFiles $ do
tcs <- getTestCCs (zip ps [1 ..]) [] tcs <- getTestCCs (zip ps [1 ..]) []
test tcs test tcs
concurrentlyN_ $ map (<// 100000) tcs concurrentlyN_ $ map (<// 100000) tcs
@ -156,7 +194,7 @@ testChatN ps test = withTmpFiles $ do
where where
getTestCCs :: [(Profile, Int)] -> [TestCC] -> IO [TestCC] getTestCCs :: [(Profile, Int)] -> [TestCC] -> IO [TestCC]
getTestCCs [] tcs = pure tcs getTestCCs [] tcs = pure tcs
getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat (show db) p <*> getTestCCs envs' tcs getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat cfg opts (show db) p <*> getTestCCs envs' tcs
(<//) :: TestCC -> Int -> Expectation (<//) :: TestCC -> Int -> Expectation
(<//) cc t = timeout t (getTermLine cc) `shouldReturn` Nothing (<//) cc t = timeout t (getTermLine cc) `shouldReturn` Nothing
@ -176,21 +214,36 @@ userName :: TestCC -> IO [Char]
userName (TestCC ChatController {currentUser} _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser userName (TestCC ChatController {currentUser} _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser
testChat2 :: Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO () testChat2 :: Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO ()
testChat2 p1 p2 test = testChatN [p1, p2] test_ 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 = testChatCfgOpts2 testCfg
testChatCfgOpts2 :: ChatConfig -> ChatOpts -> Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO ()
testChatCfgOpts2 cfg opts p1 p2 test = testChatN cfg opts [p1, p2] test_
where where
test_ :: [TestCC] -> IO () test_ :: [TestCC] -> IO ()
test_ [tc1, tc2] = test tc1 tc2 test_ [tc1, tc2] = test tc1 tc2
test_ _ = error "expected 2 chat clients" test_ _ = error "expected 2 chat clients"
testChat3 :: Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO () testChat3 :: Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO ()
testChat3 p1 p2 p3 test = testChatN [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 where
test_ :: [TestCC] -> IO () test_ :: [TestCC] -> IO ()
test_ [tc1, tc2, tc3] = test tc1 tc2 tc3 test_ [tc1, tc2, tc3] = test tc1 tc2 tc3
test_ _ = error "expected 3 chat clients" test_ _ = error "expected 3 chat clients"
testChat4 :: Profile -> Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> IO () 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 testCfg testOpts [p1, p2, p3, p4] test_
where where
test_ :: [TestCC] -> IO () test_ :: [TestCC] -> IO ()
test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4 test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4
@ -216,7 +269,8 @@ serverCfg =
privateKeyFile = "tests/fixtures/tls/server.key", privateKeyFile = "tests/fixtures/tls/server.key",
certificateFile = "tests/fixtures/tls/server.crt", certificateFile = "tests/fixtures/tls/server.crt",
logStatsInterval = Just 86400, logStatsInterval = Just 86400,
logStatsStartTime = 0 logStatsStartTime = 0,
smpServerVRange = supportedSMPServerVRange
} }
withSmpServer :: IO a -> IO a withSmpServer :: IO a -> IO a

View File

@ -18,9 +18,11 @@ import Data.Char (isDigit)
import qualified Data.Text as T import qualified Data.Text as T
import Simplex.Chat.Call import Simplex.Chat.Call
import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Controller (ChatController (..))
import Simplex.Chat.Options (ChatOpts (..))
import Simplex.Chat.Types (ConnStatus (..), ImageData (..), Profile (..), User (..)) import Simplex.Chat.Types (ConnStatus (..), ImageData (..), Profile (..), User (..))
import Simplex.Chat.Util (unlessM) import Simplex.Messaging.Util (unlessM)
import System.Directory (copyFile, doesFileExist) import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
import System.FilePath ((</>))
import Test.Hspec import Test.Hspec
aliceProfile :: Profile aliceProfile :: Profile
@ -38,12 +40,12 @@ danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing}
chatTests :: Spec chatTests :: Spec
chatTests = do chatTests = do
describe "direct messages" $ 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 quoted replies" testDirectMessageQuotedReply
it "direct message update" testDirectMessageUpdate it "direct message update" testDirectMessageUpdate
it "direct message delete" testDirectMessageDelete it "direct message delete" testDirectMessageDelete
describe "chat groups" $ do 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 join group with 4 members" testGroup2
it "create and delete group" testGroupDelete it "create and delete group" testGroupDelete
it "invitee delete group when in status invited" testGroupDeleteWhenInvited it "invitee delete group when in status invited" testGroupDeleteWhenInvited
@ -65,16 +67,16 @@ chatTests = do
it "send and receive file to group" testGroupFileTransfer it "send and receive file to group" testGroupFileTransfer
it "sender cancelled group file transfer before transfer" testGroupFileSndCancelBeforeTransfer it "sender cancelled group file transfer before transfer" testGroupFileSndCancelBeforeTransfer
describe "messages with files" $ do 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 "send and receive image" testSendImage
it "files folder: send and receive image" testFilesFoldersSendImage it "files folder: send and receive image" testFilesFoldersSendImage
it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete
it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete
it "send and receive image with text and quote" testSendImageWithTextAndQuote 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 it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote
describe "user contact link" $ do 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 "auto accept contact requests" testUserContactLinkAutoAccept
it "deduplicate contact requests" testDeduplicateContactRequests it "deduplicate contact requests" testDeduplicateContactRequests
it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange
@ -85,17 +87,64 @@ chatTests = do
describe "async connection handshake" $ do describe "async connection handshake" $ do
it "connect when initiating client goes offline" testAsyncInitiatingOffline it "connect when initiating client goes offline" testAsyncInitiatingOffline
it "connect when accepting client goes offline" testAsyncAcceptingOffline it "connect when accepting client goes offline" testAsyncAcceptingOffline
it "connect, fully asynchronous (when clients are never simultaneously online)" testFullAsync describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do
xdescribe "async sending and receiving files" $ do it "v2" testFullAsync
it "send and receive file, fully asynchronous" testAsyncFileTransfer it "v1" testFullAsyncV1
it "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer 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 describe "webrtc calls api" $ do
it "negotiate call" testNegotiateCall it "negotiate call" testNegotiateCall
describe "maintenance mode" $ do
it "start/stop/export/import chat" testMaintenanceMode
it "export/import chat with files" testMaintenanceModeWithFiles
testAddContact :: IO () versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec
testAddContact = versionTestMatrix2 runTest = do
testChat2 aliceProfile bobProfile $ it "v2" $ testChat2 aliceProfile bobProfile $ runTest
\alice bob -> do 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" alice ##> "/c"
inv <- getInvitation alice inv <- getInvitation alice
bob ##> ("/c " <> inv) bob ##> ("/c " <> inv)
@ -136,7 +185,6 @@ testAddContact =
alice #$> ("/_get chat @2 count=100", chat, []) alice #$> ("/_get chat @2 count=100", chat, [])
bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY") bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY")
bob #$> ("/_get chat @2 count=100", chat, []) bob #$> ("/_get chat @2 count=100", chat, [])
where
chatsEmpty alice bob = do chatsEmpty alice bob = do
alice @@@ [("@bob", "")] alice @@@ [("@bob", "")]
alice #$> ("/_get chat @2 count=100", chat, []) alice #$> ("/_get chat @2 count=100", chat, [])
@ -308,10 +356,10 @@ testDirectMessageDelete =
bob @@@ [("@alice", "do you receive my messages?")] 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 🙂"))]) bob #$> ("/_get chat @2 count=100", chat', [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))])
testGroup :: IO () testGroup :: Spec
testGroup = testGroup = versionTestMatrix3 runTestGroup
testChat3 aliceProfile bobProfile cathProfile $ where
\alice bob cath -> do runTestGroup alice bob cath = do
connectUsers alice bob connectUsers alice bob
connectUsers alice cath connectUsers alice cath
alice ##> "/g team" alice ##> "/g team"
@ -402,7 +450,7 @@ testGroup =
bob #$> ("/_get chat #1 count=100", chat, []) bob #$> ("/_get chat #1 count=100", chat, [])
cath #$> ("/clear #team", id, "#team: all messages are removed locally ONLY") cath #$> ("/clear #team", id, "#team: all messages are removed locally ONLY")
cath #$> ("/_get chat #1 count=100", chat, []) cath #$> ("/_get chat #1 count=100", chat, [])
where getReadChats :: TestCC -> TestCC -> TestCC -> IO ()
getReadChats alice bob cath = do getReadChats alice bob cath = do
alice @@@ [("#team", "hey team"), ("@cath", ""), ("@bob", "")] alice @@@ [("#team", "hey team"), ("@cath", ""), ("@bob", "")]
alice #$> ("/_get chat #1 count=100", chat, [(1, "hello"), (0, "hi there"), (0, "hey team")]) alice #$> ("/_get chat #1 count=100", chat, [(1, "hello"), (0, "hi there"), (0, "hey team")])
@ -1199,10 +1247,10 @@ testGroupFileSndCancelBeforeTransfer =
bob ##> "/fr 1 ./tests/tmp" bob ##> "/fr 1 ./tests/tmp"
bob <## "file cancelled: test.txt" bob <## "file cancelled: test.txt"
testMessageWithFile :: IO () testMessageWithFile :: Spec
testMessageWithFile = testMessageWithFile = versionTestMatrix2 runTestMessageWithFile
testChat2 aliceProfile bobProfile $ where
\alice bob -> do runTestMessageWithFile alice bob = do
connectUsers alice bob connectUsers alice bob
alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}" alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}"
alice <# "@bob hi, sending a file" alice <# "@bob hi, sending a file"
@ -1410,10 +1458,10 @@ testSendImageWithTextAndQuote =
(alice <## "completed sending file 3 (test.jpg) to bob") (alice <## "completed sending file 3 (test.jpg) to bob")
B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src
testGroupSendImage :: IO () testGroupSendImage :: Spec
testGroupSendImage = testGroupSendImage = versionTestMatrix3 runTestGroupSendImage
testChat3 aliceProfile bobProfile cathProfile $ where
\alice bob cath -> do runTestGroupSendImage alice bob cath = do
createGroup3 "team" alice bob cath 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 ##> "/_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" alice <# "/f #team ./tests/fixtures/test.jpg"
@ -1514,9 +1562,8 @@ 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 #$> ("/_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", "")] cath @@@ [("#team", "hey bob"), ("@alice", ""), ("@bob", "")]
testUserContactLink :: IO () testUserContactLink :: Spec
testUserContactLink = testChat3 aliceProfile bobProfile cathProfile $ testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do
\alice bob cath -> do
alice ##> "/ad" alice ##> "/ad"
cLink <- getContactLink alice True cLink <- getContactLink alice True
bob ##> ("/c " <> cLink) bob ##> ("/c " <> cLink)
@ -1803,11 +1850,8 @@ testFullAsync = withTmpFiles $ do
withNewTestChat "bob" bobProfile $ \bob -> do withNewTestChat "bob" bobProfile $ \bob -> do
bob ##> ("/c " <> inv) bob ##> ("/c " <> inv)
bob <## "confirmation sent!" bob <## "confirmation sent!"
withTestChat "alice" $ \_ -> pure () withTestChat "alice" $ \_ -> pure () -- connecting... notification in UI
withTestChat "bob" $ \_ -> pure () withTestChat "bob" $ \_ -> pure () -- connecting... notification in UI
withTestChat "alice" $ \alice ->
alice <## "1 contacts connected (use /cs for the list)"
withTestChat "bob" $ \_ -> pure ()
withTestChat "alice" $ \alice -> do withTestChat "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)" alice <## "1 contacts connected (use /cs for the list)"
alice <## "bob (Bob): contact is connected" alice <## "bob (Bob): contact is connected"
@ -1815,6 +1859,81 @@ testFullAsync = withTmpFiles $ do
bob <## "1 contacts connected (use /cs for the list)" bob <## "1 contacts connected (use /cs for the list)"
bob <## "alice (Alice): contact is connected" 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 :: IO ()
testAsyncFileTransfer = withTmpFiles $ do testAsyncFileTransfer = withTmpFiles $ do
withNewTestChat "alice" aliceProfile $ \alice -> withNewTestChat "alice" aliceProfile $ \alice ->
@ -1831,8 +1950,8 @@ testAsyncFileTransfer = withTmpFiles $ do
bob <## "use /fr 1 [<dir>/ | <path>] to receive it" bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob ##> "/fr 1 ./tests/tmp" bob ##> "/fr 1 ./tests/tmp"
bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" bob <## "saving file 1 from alice to ./tests/tmp/test.jpg"
withTestChatContactConnected' "alice" -- withTestChatContactConnected' "alice" -- TODO not needed in v2
withTestChatContactConnected' "bob" -- withTestChatContactConnected' "bob" -- TODO not needed in v2
withTestChatContactConnected' "alice" withTestChatContactConnected' "alice"
withTestChatContactConnected' "bob" withTestChatContactConnected' "bob"
withTestChatContactConnected "alice" $ \alice -> do withTestChatContactConnected "alice" $ \alice -> do
@ -1845,6 +1964,36 @@ testAsyncFileTransfer = withTmpFiles $ do
dest <- B.readFile "./tests/tmp/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src 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 [<dir>/ | <path>] 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 :: IO ()
testAsyncGroupFileTransfer = withTmpFiles $ do testAsyncGroupFileTransfer = withTmpFiles $ do
withNewTestChat "alice" aliceProfile $ \alice -> withNewTestChat "alice" aliceProfile $ \alice ->
@ -1868,9 +2017,9 @@ testAsyncGroupFileTransfer = withTmpFiles $ do
withTestChatGroup3Connected' "alice" withTestChatGroup3Connected' "alice"
withTestChatGroup3Connected' "bob" withTestChatGroup3Connected' "bob"
withTestChatGroup3Connected' "cath" withTestChatGroup3Connected' "cath"
withTestChatGroup3Connected' "alice" -- withTestChatGroup3Connected' "alice" -- TODO not needed in v2
withTestChatGroup3Connected' "bob" -- withTestChatGroup3Connected' "bob" -- TODO not needed in v2
withTestChatGroup3Connected' "cath" -- withTestChatGroup3Connected' "cath" -- TODO not needed in v2
withTestChatGroup3Connected' "alice" withTestChatGroup3Connected' "alice"
withTestChatGroup3Connected "bob" $ \bob -> do withTestChatGroup3Connected "bob" $ \bob -> do
bob <## "started receiving file 1 (test.jpg) from alice" bob <## "started receiving file 1 (test.jpg) from alice"
@ -1963,6 +2112,81 @@ testNegotiateCall =
alice <## "message updated" alice <## "message updated"
alice #$> ("/_get chat @2 count=100", chat, [(1, "outgoing call: ended (00:00)")]) 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 :: String -> (TestCC -> IO a) -> IO a
withTestChatContactConnected dbPrefix action = withTestChatContactConnected dbPrefix action =
withTestChat dbPrefix $ \cc -> do withTestChat dbPrefix $ \cc -> do
@ -1972,6 +2196,15 @@ withTestChatContactConnected dbPrefix action =
withTestChatContactConnected' :: String -> IO () withTestChatContactConnected' :: String -> IO ()
withTestChatContactConnected' dbPrefix = withTestChatContactConnected dbPrefix $ \_ -> pure () 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 :: String -> (TestCC -> IO a) -> IO a
withTestChatGroup3Connected dbPrefix action = do withTestChatGroup3Connected dbPrefix action = do
withTestChat dbPrefix $ \cc -> do withTestChat dbPrefix $ \cc -> do
@ -1987,16 +2220,21 @@ startFileTransfer alice bob =
startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes" startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes"
startFileTransfer' :: TestCC -> TestCC -> String -> String -> IO () startFileTransfer' :: TestCC -> TestCC -> String -> String -> IO ()
startFileTransfer' alice bob fileName fileSize = do startFileTransfer' cc1 cc2 fileName fileSize = startFileTransferWithDest' cc1 cc2 fileName fileSize $ Just "./tests/tmp"
alice #> ("/f @bob ./tests/fixtures/" <> fileName)
alice <## "use /fc 1 to cancel sending" startFileTransferWithDest' :: TestCC -> TestCC -> String -> String -> Maybe String -> IO ()
bob <# ("alice> sends file " <> fileName <> " (" <> fileSize <> ")") startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do
bob <## "use /fr 1 [<dir>/ | <path>] to receive it" name1 <- userName cc1
bob ##> "/fr 1 ./tests/tmp" name2 <- userName cc2
bob <## ("saving file 1 from alice to ./tests/tmp/" <> fileName) cc1 #> ("/f @" <> name2 <> " ./tests/fixtures/" <> fileName)
cc1 <## "use /fc 1 to cancel sending"
cc2 <# (name1 <> "> sends file " <> fileName <> " (" <> fileSize <> ")")
cc2 <## "use /fr 1 [<dir>/ | <path>] to receive it"
cc2 ##> ("/fr 1" <> maybe "" (" " <>) fileDest_)
cc2 <## ("saving file 1 from " <> name1 <> " to " <> maybe id (</>) fileDest_ fileName)
concurrently_ concurrently_
(bob <## ("started receiving file 1 (" <> fileName <> ") from alice")) (cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1))
(alice <## ("started sending file 1 (" <> fileName <> ") to bob")) (cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2))
checkPartialTransfer :: String -> IO () checkPartialTransfer :: String -> IO ()
checkPartialTransfer fileName = do checkPartialTransfer fileName = do

View File

@ -16,6 +16,7 @@ import Simplex.Messaging.Crypto.Ratchet
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (ProtocolServer (..), smpClientVRange) import Simplex.Messaging.Protocol (ProtocolServer (..), smpClientVRange)
import Simplex.Messaging.Version
import Test.Hspec import Test.Hspec
protocolTests :: Spec protocolTests :: Spec
@ -42,7 +43,7 @@ connReqData :: ConnReqUriData
connReqData = connReqData =
ConnReqUriData ConnReqUriData
{ crScheme = simplexChat, { crScheme = simplexChat,
crAgentVRange = smpAgentVRange, crAgentVRange = mkVersionRange 1 1,
crSmpQueues = [queue] crSmpQueues = [queue]
} }