diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5ca8fef810..d2c53bbe35 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone project
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Build changelog
id: build_changelog
- uses: mikepenz/release-changelog-builder-action@v1
+ uses: mikepenz/release-changelog-builder-action@v4
with:
configuration: .github/changelog_conf.json
failOnError: true
@@ -71,16 +71,16 @@ jobs:
disk-root: "C:"
- name: Clone project
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Setup Haskell
- uses: haskell/actions/setup@v1
+ uses: haskell/actions/setup@v2
with:
ghc-version: "8.10.7"
cabal-version: "latest"
- name: Cache dependencies
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: |
${{ matrix.cache_path }}
diff --git a/README.md b/README.md
index 47c825f8da..31ad125c26 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ You must:
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
-You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-3](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fp-j-D_PrY2UMDchFHEUtbSES0nmzCnvD%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA3gBfMjB_GDEmKQwjNdqGbnX91yfuZ7nRJgQijsx5Khc%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%2262MvNZ_Ec2mmlS8V0QNtLQ%3D%3D%22%7D)
+You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-4](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Fw2GlucRXtRVgYnbt_9ZP-kmt76DekxxS%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0tJhTyMGUxznwmjb7aT24P1I1Wry_iURTuhOFlMb1Eo%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22WoPxjFqGEDlVazECOSi2dg%3D%3D%22%7D)
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle
index 52e58dd81b..4f8b6cbe30 100644
--- a/apps/android/app/build.gradle
+++ b/apps/android/app/build.gradle
@@ -13,8 +13,8 @@ android {
targetSdk 32
// !!!
// skip version code after release to F-Droid, as it uses two version codes
- versionCode 125
- versionName "5.1.2"
+ versionCode 127
+ versionName "5.1.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -170,6 +170,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
+ implementation "com.jakewharton:process-phoenix:2.1.2"
}
// Don't do anything if no compression is needed
diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt
index 0e5af1b6be..7a2aecaae6 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt
@@ -10,6 +10,7 @@ import chat.simplex.app.ui.theme.DefaultTheme
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationsMode
+import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.*
@@ -101,6 +102,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun onCreate() {
super.onCreate()
+ if (ProcessPhoenix.isPhoenixProcess(this)) {
+ return;
+ }
context = this
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
withBGApi {
diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt
index 497dc2ac86..42bfbe7298 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt
@@ -166,7 +166,6 @@ class SimplexService: Service() {
it.setPackage(packageName)
};
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
- applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
index 1875dc9cac..2966a65709 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
@@ -6,7 +6,7 @@ import SectionItemView
import SectionItemViewWithIcon
import SectionView
import TextIconSpaced
-import android.content.Context
+import android.content.*
import android.content.res.Configuration
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -25,16 +25,16 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import androidx.fragment.app.FragmentActivity
+import androidx.work.WorkManager
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.database.DatabaseView
import chat.simplex.app.views.helpers.*
-import chat.simplex.app.views.newchat.CreateLinkTab
-import chat.simplex.app.views.newchat.CreateLinkView
import chat.simplex.app.views.onboarding.SimpleXInfo
import chat.simplex.app.views.onboarding.WhatsNewView
+import com.jakewharton.processphoenix.ProcessPhoenix
@Composable
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit) {
@@ -181,7 +181,9 @@ fun SettingsLayout(
}
SectionDividerSpaced()
- SectionView(stringResource(R.string.settings_section_title_develop)) {
+ SectionView(stringResource(R.string.settings_section_title_app)) {
+ SettingsActionItem(painterResource(R.drawable.ic_restart_alt), stringResource(R.string.settings_restart_app), ::restartApp, extraPadding = true)
+ SettingsActionItem(painterResource(R.drawable.ic_power_settings_new), stringResource(R.string.settings_shutdown), { shutdownAppAlert(::shutdownApp) }, extraPadding = true)
SettingsActionItem(painterResource(R.drawable.ic_code), stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true)
AppVersionItem(showVersion)
}
@@ -490,6 +492,26 @@ private fun runAuth(title: String, desc: String, context: Context, onFinish: (su
)
}
+private fun restartApp() {
+ ProcessPhoenix.triggerRebirth(SimplexApp.context)
+ shutdownApp()
+}
+
+private fun shutdownApp() {
+ WorkManager.getInstance(SimplexApp.context).cancelAllWork()
+ SimplexService.safeStopService(SimplexApp.context)
+ Runtime.getRuntime().exit(0)
+}
+
+private fun shutdownAppAlert(onConfirm: () -> Unit) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(R.string.shutdown_alert_question),
+ text = generalGetString(R.string.shutdown_alert_desc),
+ destructive = true,
+ onConfirm = onConfirm
+ )
+}
+
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
diff --git a/apps/android/app/src/main/res/drawable/ic_power_settings_new.xml b/apps/android/app/src/main/res/drawable/ic_power_settings_new.xml
new file mode 100644
index 0000000000..a73a866f2e
--- /dev/null
+++ b/apps/android/app/src/main/res/drawable/ic_power_settings_new.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/apps/android/app/src/main/res/drawable/ic_restart_alt.xml b/apps/android/app/src/main/res/drawable/ic_restart_alt.xml
new file mode 100644
index 0000000000..92043a534d
--- /dev/null
+++ b/apps/android/app/src/main/res/drawable/ic_restart_alt.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/apps/android/app/src/main/res/values-cs/strings.xml b/apps/android/app/src/main/res/values-cs/strings.xml
index dc17bd2d53..a8d430c6a1 100644
--- a/apps/android/app/src/main/res/values-cs/strings.xml
+++ b/apps/android/app/src/main/res/values-cs/strings.xml
@@ -723,7 +723,6 @@
\n3. Spojení je kompromitováno.
VY
PODPOŘIT SIMPLEX CHAT
- VÝVOJ
Nástroje pro vývojáře
inkognito mód
Chat databáze
diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml
index bbbe6f3a7a..939bc087af 100644
--- a/apps/android/app/src/main/res/values-de/strings.xml
+++ b/apps/android/app/src/main/res/values-de/strings.xml
@@ -564,7 +564,6 @@
EINSTELLUNGEN
HILFE
UNTERSTÜTZUNG VON SIMPLEX CHAT
- ENTWICKLUNG
GERÄT
CHATS
Entwicklertools
diff --git a/apps/android/app/src/main/res/values-es/strings.xml b/apps/android/app/src/main/res/values-es/strings.xml
index 1a6f3d8818..862465fe98 100644
--- a/apps/android/app/src/main/res/values-es/strings.xml
+++ b/apps/android/app/src/main/res/values-es/strings.xml
@@ -114,7 +114,6 @@
Llamada con cifrado de extremo a extremo
cifrado de extremo a extremo
mensaje duplicado
- DESARROLLO
Herramientas desarrollo
Eliminar los archivos de todos los perfiles
Eliminar mensaje
diff --git a/apps/android/app/src/main/res/values-fr/strings.xml b/apps/android/app/src/main/res/values-fr/strings.xml
index 1f2bff2a09..c1a6268a8f 100644
--- a/apps/android/app/src/main/res/values-fr/strings.xml
+++ b/apps/android/app/src/main/res/values-fr/strings.xml
@@ -535,7 +535,6 @@
Appel déjà terminé !
Appels audio et vidéo
chiffré de bout en bout
- DEVELOPPER
Fonctionnalités expérimentales
SOCKS PROXY
THEMES
diff --git a/apps/android/app/src/main/res/values-hi/strings.xml b/apps/android/app/src/main/res/values-hi/strings.xml
index ac5cfc339d..8b619b3bfa 100644
--- a/apps/android/app/src/main/res/values-hi/strings.xml
+++ b/apps/android/app/src/main/res/values-hi/strings.xml
@@ -271,7 +271,6 @@
%s के लिए पता बदल रहा है…
दोबारा मत दिखाओ
आप और आपका संपर्क दोनों ध्वनि संदेश भेज सकते हैं।
- विकास करना
आवाज़ बंद करना
अपने संपर्कों को गायब होने वाले संदेश भेजने की अनुमति दें।
ऑडियो और वीडियो कॉल
diff --git a/apps/android/app/src/main/res/values-it/strings.xml b/apps/android/app/src/main/res/values-it/strings.xml
index 4b172ce681..3dd6c9d846 100644
--- a/apps/android/app/src/main/res/values-it/strings.xml
+++ b/apps/android/app/src/main/res/values-it/strings.xml
@@ -395,7 +395,6 @@
Password del database
Eliminare il profilo di chat\?
Elimina database
- SVILUPPA
Strumenti di sviluppo
DISPOSITIVO
Errore nell\'eliminazione del database della chat
diff --git a/apps/android/app/src/main/res/values-iw/strings.xml b/apps/android/app/src/main/res/values-iw/strings.xml
index 05b2f182d0..942687a1de 100644
--- a/apps/android/app/src/main/res/values-iw/strings.xml
+++ b/apps/android/app/src/main/res/values-iw/strings.xml
@@ -328,7 +328,6 @@
מחק קבוצה
למחוק קבוצה\?
💻 שולחן עבודה: סירקו את קוד ה־QR המוצג באפליקציה, באמצעות סריקת קוד QR.
- פיתוח
מכשיר
נעילת המכשיר מושבתת. מכבה נעילת SimpleX.
נעילת המכשיר אינה מופעלת. ניתן להפעיל את נעילת SimpleX, לאחר שתפעילו את נעילת המכשיר.
diff --git a/apps/android/app/src/main/res/values-ja/strings.xml b/apps/android/app/src/main/res/values-ja/strings.xml
index 04b86d8868..883cf2b99b 100644
--- a/apps/android/app/src/main/res/values-ja/strings.xml
+++ b/apps/android/app/src/main/res/values-ja/strings.xml
@@ -349,7 +349,6 @@
カメラ切り替え
不在着信
拒否した通話
- 開発
重複メッセージ
ヘルプ
アプリ画面を守る
diff --git a/apps/android/app/src/main/res/values-ko/strings.xml b/apps/android/app/src/main/res/values-ko/strings.xml
index ed05450d1e..544ea2a015 100644
--- a/apps/android/app/src/main/res/values-ko/strings.xml
+++ b/apps/android/app/src/main/res/values-ko/strings.xml
@@ -232,7 +232,6 @@
주소를 삭제할까요\?
이미지 삭제
탈중앙화
- 개발
개발자 도구
기기
데이터베이스 비밀번호
diff --git a/apps/android/app/src/main/res/values-nl/strings.xml b/apps/android/app/src/main/res/values-nl/strings.xml
index 9a420ec046..f40123fa0d 100644
--- a/apps/android/app/src/main/res/values-nl/strings.xml
+++ b/apps/android/app/src/main/res/values-nl/strings.xml
@@ -242,7 +242,6 @@
Verwijderen voor iedereen
Link verwijderen
direct
- ONTWIKKELEN
APPARAAT
Verwijder alle bestanden
Berichten verwijderen na
diff --git a/apps/android/app/src/main/res/values-pl/strings.xml b/apps/android/app/src/main/res/values-pl/strings.xml
index e3d4f5f894..794cc6f7d8 100644
--- a/apps/android/app/src/main/res/values-pl/strings.xml
+++ b/apps/android/app/src/main/res/values-pl/strings.xml
@@ -515,7 +515,6 @@
CZATY
Hasło do bazy danych
Usuń bazę danych
- DEWELOPERSKIE
Narzędzia deweloperskie
URZĄDZENIE
Błąd uruchamiania czatu
diff --git a/apps/android/app/src/main/res/values-pt-rBR/strings.xml b/apps/android/app/src/main/res/values-pt-rBR/strings.xml
index 8d3f536fe7..ba715afaaf 100644
--- a/apps/android/app/src/main/res/values-pt-rBR/strings.xml
+++ b/apps/android/app/src/main/res/values-pt-rBR/strings.xml
@@ -745,7 +745,6 @@
chamada perdida
Você pode usar markdown para formatar mensagens:
chamada rejeitada
- DESENVOLVER
saiu
Versão do banco de dados incompatível
Remover membro
diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml
index dd17625aab..3cfb5ac057 100644
--- a/apps/android/app/src/main/res/values-ru/strings.xml
+++ b/apps/android/app/src/main/res/values-ru/strings.xml
@@ -568,7 +568,6 @@
НАСТРОЙКИ
ПОМОЩЬ
ПОДДЕРЖАТЬ SIMPLEX CHAT
- ДЛЯ РАЗРАБОТЧИКОВ
УСТРОЙСТВО
ЧАТЫ
Инструменты разработчика
diff --git a/apps/android/app/src/main/res/values-zh-rCN/strings.xml b/apps/android/app/src/main/res/values-zh-rCN/strings.xml
index 686f79d04d..f6e6ef2406 100644
--- a/apps/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/apps/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -378,7 +378,6 @@
加密数据库?
数据库使用随机密码进行加密,您可以更改它。
打开聊天需要数据库密码。
- 开发
停止聊天错误
导入聊天数据库?
删除聊天数据库错误
diff --git a/apps/android/app/src/main/res/values-zh-rTW/strings.xml b/apps/android/app/src/main/res/values-zh-rTW/strings.xml
index 01e7c2067d..23414ebec2 100644
--- a/apps/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/apps/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -681,7 +681,6 @@
重覆的訊息
保護應用程式螢幕
傳送可以預覽的連結
- 開發
裝置
幫助
設定
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index 2017bae844..13108eb090 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -602,6 +602,8 @@
Hide:
Show developer options
Database IDs and Transport isolation option.
+ Shutdown?
+ Notifications will stop working until you re-launch the app
Create address
@@ -851,9 +853,11 @@
SETTINGS
HELP
SUPPORT SIMPLEX CHAT
- DEVELOP
+ APP
DEVICE
CHATS
+ Restart
+ Shutdown
Developer tools
Experimental features
SOCKS PROXY
diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift
index 7e167681ef..5d464e1774 100644
--- a/apps/ios/Shared/Views/Chat/ChatView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatView.swift
@@ -537,8 +537,20 @@ struct ChatView: View {
private func menu(live: Bool) -> [UIMenuElement] {
var menu: [UIMenuElement] = []
if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed {
+ let rs = allReactions()
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction,
- let rm = reactionUIMenu() {
+ rs.count > 0 {
+ var rm: UIMenu
+ if #available(iOS 16, *) {
+ var children: [UIMenuElement] = Array(rs.prefix(topReactionsCount(rs)))
+ if let sm = reactionUIMenu(rs) {
+ children.append(sm)
+ }
+ rm = UIMenu(title: "", options: .displayInline, children: children)
+ rm.preferredElementSize = .small
+ } else {
+ rm = reactionUIMenuPreiOS16(rs)
+ }
menu.append(rm)
}
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
@@ -602,20 +614,36 @@ struct ChatView: View {
}
}
- private func reactionUIMenu() -> UIMenu? {
- let rs = MsgReaction.values.compactMap { r in
+ private func reactionUIMenuPreiOS16(_ rs: [UIAction]) -> UIMenu {
+ UIMenu(
+ title: NSLocalizedString("React...", comment: "chat item menu"),
+ image: UIImage(systemName: "face.smiling"),
+ children: rs
+ )
+ }
+
+ @available(iOS 16.0, *)
+ private func reactionUIMenu(_ rs: [UIAction]) -> UIMenu? {
+ var children = rs
+ children.removeFirst(min(rs.count, topReactionsCount(rs)))
+ if children.count == 0 { return nil }
+ return UIMenu(
+ title: "",
+ image: UIImage(systemName: "ellipsis"),
+ children: children
+ )
+ }
+
+ private func allReactions() -> [UIAction] {
+ MsgReaction.values.compactMap { r in
ci.reactions.contains(where: { $0.userReacted && $0.reaction == r })
? nil
: UIAction(title: r.text) { _ in setReaction(add: true, reaction: r) }
}
- if rs.count > 0 {
- return UIMenu(
- title: NSLocalizedString("React...", comment: "chat item menu"),
- image: UIImage(systemName: "face.smiling"),
- children: rs
- )
- }
- return nil
+ }
+
+ private func topReactionsCount(_ rs: [UIAction]) -> Int {
+ rs.count > 4 ? 3 : 4
}
private func setReaction(add: Bool, reaction: MsgReaction) {
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 61c9fa1932..cba3bed4ec 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -83,11 +83,6 @@
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */; };
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; };
5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; };
- 5CA4874A2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */; };
- 5CA4874B2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */; };
- 5CA4874C2A228AF400409F23 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487472A228AF300409F23 /* libgmp.a */; };
- 5CA4874D2A228AF400409F23 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487482A228AF300409F23 /* libffi.a */; };
- 5CA4874E2A228AF400409F23 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487492A228AF400409F23 /* libgmpxx.a */; };
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */; };
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; };
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; };
@@ -152,6 +147,11 @@
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
+ 643EE9682A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9632A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a */; };
+ 643EE9692A372E8700678085 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9642A372E8700678085 /* libffi.a */; };
+ 643EE96A2A372E8700678085 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9652A372E8700678085 /* libgmpxx.a */; };
+ 643EE96B2A372E8700678085 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9662A372E8700678085 /* libgmp.a */; };
+ 643EE96C2A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9672A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a */; };
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
@@ -344,11 +344,6 @@
5CA059D7279559F40002BEB4 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
5CA059DB279559F40002BEB4 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; };
5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOSLaunchTests.swift; sourceTree = ""; };
- 5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a"; sourceTree = ""; };
- 5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a"; sourceTree = ""; };
- 5CA487472A228AF300409F23 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
- 5CA487482A228AF300409F23 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
- 5CA487492A228AF400409F23 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
5CA7DFC229302AF000F7FDDE /* AppSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSheet.swift; sourceTree = ""; };
5CA85D0A297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; };
5CA85D0B297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; };
@@ -427,6 +422,11 @@
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = ""; };
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; };
+ 643EE9632A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a"; sourceTree = ""; };
+ 643EE9642A372E8700678085 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 643EE9652A372E8700678085 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
+ 643EE9662A372E8700678085 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
+ 643EE9672A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a"; sourceTree = ""; };
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; };
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; };
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; };
@@ -498,12 +498,12 @@
buildActionMask = 2147483647;
files = (
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
- 5CA4874D2A228AF400409F23 /* libffi.a in Frameworks */,
- 5CA4874C2A228AF400409F23 /* libgmp.a in Frameworks */,
- 5CA4874B2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a in Frameworks */,
- 5CA4874A2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a in Frameworks */,
+ 643EE96A2A372E8700678085 /* libgmpxx.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
- 5CA4874E2A228AF400409F23 /* libgmpxx.a in Frameworks */,
+ 643EE9682A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a in Frameworks */,
+ 643EE96B2A372E8700678085 /* libgmp.a in Frameworks */,
+ 643EE9692A372E8700678085 /* libffi.a in Frameworks */,
+ 643EE96C2A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -564,11 +564,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5CA487482A228AF300409F23 /* libffi.a */,
- 5CA487472A228AF300409F23 /* libgmp.a */,
- 5CA487492A228AF400409F23 /* libgmpxx.a */,
- 5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */,
- 5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */,
+ 643EE9642A372E8700678085 /* libffi.a */,
+ 643EE9662A372E8700678085 /* libgmp.a */,
+ 643EE9652A372E8700678085 /* libgmpxx.a */,
+ 643EE9672A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a */,
+ 643EE9632A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a */,
);
path = Libraries;
sourceTree = "";
@@ -1470,7 +1470,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 149;
+ CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1491,7 +1491,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 5.1.2;
+ MARKETING_VERSION = 5.1.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1512,7 +1512,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 149;
+ CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1533,7 +1533,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 5.1.2;
+ MARKETING_VERSION = 5.1.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1592,7 +1592,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 149;
+ CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1605,7 +1605,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 5.1.2;
+ MARKETING_VERSION = 5.1.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1624,7 +1624,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 149;
+ CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1637,7 +1637,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 5.1.2;
+ MARKETING_VERSION = 5.1.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/cabal.project b/cabal.project
index 2da1398f5c..9714126479 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 89caf5572980b776bd750caa3c918ae4488612d8
+ tag: 2efe1496d2622a56656a6a00a2cc19005e754468
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 130b98d4f8..312d11c14e 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."89caf5572980b776bd750caa3c918ae4488612d8" = "0glils00403s4xxl84xa22wylfsrcpjj5j3v7srqhrmikm1jds4p";
+ "https://github.com/simplex-chat/simplexmq.git"."2efe1496d2622a56656a6a00a2cc19005e754468" = "0syy10zn3ci1nbs00r0l5vhvqkwlqcgmmd5c196pch0xvrk5qbfi";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index b983c19266..5fe76b9293 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -112,6 +112,7 @@ defaultChatConfig =
fileChunkSize = 15780, -- do not change
xftpDescrPartSize = 14000,
inlineFiles = defaultInlineFilesConfig,
+ autoAcceptFileSize = 0,
xftpFileConfig = Just defaultXFTPFileConfig,
tempDir = Nothing,
showReactions = False,
@@ -158,9 +159,9 @@ createChatDatabase filePrefix key confirmMigrations = runExceptT $ do
pure ChatDatabase {chatStore, agentStore}
newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController
-newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize}, optFilesFolder, showReactions, allowInstantFiles} sendToast = do
- let inlineFiles' = if allowInstantFiles then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
- config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles'}
+newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize}, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize} sendToast = do
+ let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
+ config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize}
sendNotification = fromMaybe (const $ pure ()) sendToast
firstTime = dbNew chatStore
activeTo <- newTVarIO ActiveNone
@@ -1109,6 +1110,17 @@ processChatCommand = \case
case memberConnId m of
Just connId -> withAgent (\a -> switchConnectionAsync a "" connId) >> ok user
_ -> throwChatError CEGroupMemberNotActive
+ APIAbortSwitchContact contactId -> withUser $ \user -> do
+ ct <- withStore $ \db -> getContact db user contactId
+ connectionStats <- withAgent $ \a -> abortConnectionSwitch a $ contactConnId ct
+ pure $ CRContactSwitchAborted user ct connectionStats
+ APIAbortSwitchGroupMember gId gMemberId -> withUser $ \user -> do
+ (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
+ case memberConnId m of
+ Just connId -> do
+ connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId
+ pure $ CRGroupMemberSwitchAborted user g m connectionStats
+ _ -> throwChatError CEGroupMemberNotActive
APIGetContactCode contactId -> withUser $ \user -> do
ct@Contact {activeConn = conn@Connection {connId}} <- withStore $ \db -> getContact db user contactId
code <- getConnectionCode (contactConnId ct)
@@ -1163,6 +1175,8 @@ processChatCommand = \case
GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo
SwitchContact cName -> withContactName cName APISwitchContact
SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember
+ AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact
+ AbortSwitchGroupMember gName mName -> withMemberName gName mName APIAbortSwitchGroupMember
GetContactCode cName -> withContactName cName APIGetContactCode
GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode
VerifyContact cName code -> withContactName cName (`APIVerifyContact` code)
@@ -2809,7 +2823,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> pure ()
SWITCH qd phase cStats -> do
toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats)
- when (phase /= SPConfirmed) $ case qd of
+ when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing
QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
OK ->
@@ -2988,7 +3002,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
checkSndInlineFTComplete conn msgId
SWITCH qd phase cStats -> do
toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
- when (phase /= SPConfirmed) $ case qd of
+ when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing
QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
OK ->
@@ -3332,8 +3346,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
let ExtMsgContent _ _ itemTTL live_ = mcExtMsgContent mc
timed_ = rcvContactCITimed ct itemTTL
live = fromMaybe False live_
- ciFile_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct
- ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) ciFile_ timed_ live
+ file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct
+ ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
+ autoAcceptFile file_
whenContactNtfs user ct $ do
showMsgToast (c <> "> ") content formattedText
setActive $ ActiveC c
@@ -3344,6 +3359,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions})
pure ci
+ autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> m ()
+ autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do
+ ChatConfig {autoAcceptFileSize = sz} <- asks config
+ when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView
+
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
@@ -3380,7 +3400,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
cancelGroupMessageFile _gInfo _m _sharedMsgId _msgMeta = do
pure ()
- processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv))
+ processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> m (Maybe (RcvFileTransfer, CIFile 'MDRcv))
processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do
ChatConfig {fileChunkSize} <- asks config
inline <- receiveInlineMode fInv (Just mc) fileChunkSize
@@ -3392,7 +3412,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
withStore' $ \db -> startRcvInlineFT db user ft fPath inline
pure (Just fPath, CIFSRcvAccepted)
_ -> pure (Nothing, CIFSRcvInvitation)
- pure CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol}
+ pure (ft, CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol})
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
@@ -3503,8 +3523,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
let ExtMsgContent _ _ itemTTL live_ = mcExtMsgContent mc
timed_ = rcvGroupCITimed gInfo itemTTL
live = fromMaybe False live_
- ciFile_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m
- ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) ciFile_ timed_ live
+ file_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m
+ ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live
+ autoAcceptFile file_
let g = groupName' gInfo
whenGroupNtfs user gInfo $ do
showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText
@@ -4850,8 +4871,12 @@ chatCommandP =
("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName),
"/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
"/_switch @" *> (APISwitchContact <$> A.decimal),
+ "/_abort switch #" *> (APIAbortSwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
+ "/_abort switch @" *> (APIAbortSwitchContact <$> A.decimal),
"/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName),
"/switch " *> char_ '@' *> (SwitchContact <$> displayName),
+ "/abort switch #" *> (AbortSwitchGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName),
+ "/abort switch " *> char_ '@' *> (AbortSwitchContact <$> displayName),
"/_get code @" *> (APIGetContactCode <$> A.decimal),
"/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal),
"/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> textP)),
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 951ddb7735..3b4d499b30 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -103,6 +103,7 @@ data ChatConfig = ChatConfig
fileChunkSize :: Integer,
xftpDescrPartSize :: Int,
inlineFiles :: InlineFilesConfig,
+ autoAcceptFileSize :: Integer,
xftpFileConfig :: Maybe XFTPFileConfig, -- Nothing - XFTP is disabled
tempDir :: Maybe FilePath,
showReactions :: Bool,
@@ -283,6 +284,8 @@ data ChatCommand
| APIGroupMemberInfo GroupId GroupMemberId
| APISwitchContact ContactId
| APISwitchGroupMember GroupId GroupMemberId
+ | APIAbortSwitchContact ContactId
+ | APIAbortSwitchGroupMember GroupId GroupMemberId
| APIGetContactCode ContactId
| APIGetGroupMemberCode GroupId GroupMemberId
| APIVerifyContact ContactId (Maybe Text)
@@ -294,6 +297,8 @@ data ChatCommand
| GroupMemberInfo GroupName ContactName
| SwitchContact ContactName
| SwitchGroupMember GroupName ContactName
+ | AbortSwitchContact ContactName
+ | AbortSwitchGroupMember GroupName ContactName
| GetContactCode ContactName
| GetGroupMemberCode GroupName ContactName
| VerifyContact ContactName (Maybe Text)
@@ -402,6 +407,8 @@ data ChatResponse
| CRNetworkConfig {networkConfig :: NetworkConfig}
| CRContactInfo {user :: User, contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile}
| CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
+ | CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
+ | CRGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
| CRContactSwitch {user :: User, contact :: Contact, switchProgress :: SwitchProgress}
| CRGroupMemberSwitch {user :: User, groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress}
| CRContactCode {user :: User, contact :: Contact, connectionCode :: Text}
diff --git a/src/Simplex/Chat/Messages/ChatItemContent.hs b/src/Simplex/Chat/Messages/ChatItemContent.hs
index f69bd59b55..e83aea8182 100644
--- a/src/Simplex/Chat/Messages/ChatItemContent.hs
+++ b/src/Simplex/Chat/Messages/ChatItemContent.hs
@@ -383,14 +383,18 @@ sndGroupEventToText = \case
rcvConnEventToText :: RcvConnEvent -> Text
rcvConnEventToText = \case
RCESwitchQueue phase -> case phase of
+ SPStarted -> "started changing address for you..."
+ SPConfirmed -> "confirmed changing address for you..."
+ SPSecured -> "secured new address for you..."
SPCompleted -> "changed address for you"
- _ -> decodeLatin1 (strEncode phase) <> " changing address for you..."
sndConnEventToText :: SndConnEvent -> Text
sndConnEventToText = \case
SCESwitchQueue phase m -> case phase of
+ SPStarted -> "started changing address" <> forMember m <> "..."
+ SPConfirmed -> "confirmed changing address" <> forMember m <> "..."
+ SPSecured -> "secured new address" <> forMember m <> "..."
SPCompleted -> "you changed address" <> forMember m
- _ -> decodeLatin1 (strEncode phase) <> " changing address" <> forMember m <> "..."
where
forMember member_ =
maybe "" (\GroupMemberRef {profile = Profile {displayName}} -> " for " <> displayName) member_
diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs
index 10badd0e2c..eab0cf4d67 100644
--- a/src/Simplex/Chat/Mobile.hs
+++ b/src/Simplex/Chat/Mobile.hs
@@ -130,6 +130,7 @@ mobileChatOpts dbFilePrefix dbKey =
optFilesFolder = Nothing,
showReactions = False,
allowInstantFiles = True,
+ autoAcceptFileSize = 0,
muteNotifications = True,
maintenance = True
}
diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs
index e16beb9d4f..0b39b8dd4f 100644
--- a/src/Simplex/Chat/Options.hs
+++ b/src/Simplex/Chat/Options.hs
@@ -22,6 +22,7 @@ import qualified Data.ByteString.Char8 as B
import Numeric.Natural (Natural)
import Options.Applicative
import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString)
+import Simplex.FileTransfer.Description (mb)
import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
@@ -37,6 +38,7 @@ data ChatOpts = ChatOpts
optFilesFolder :: Maybe FilePath,
showReactions :: Bool,
allowInstantFiles :: Bool,
+ autoAcceptFileSize :: Integer,
muteNotifications :: Bool,
maintenance :: Bool
}
@@ -236,6 +238,15 @@ chatOptsP appDir defaultDbFileName = do
<> short 'f'
<> help "Send and receive instant files without acceptance"
)
+ autoAcceptFileSize <-
+ flag' (mb 1) (short 'a' <> help "Automatically accept files up to 1MB")
+ <|> option
+ auto
+ ( long "auto-accept-files"
+ <> metavar "FILE_SIZE"
+ <> help "Automatically accept files up to specified size"
+ <> value 0
+ )
muteNotifications <-
switch
( long "mute"
@@ -256,6 +267,7 @@ chatOptsP appDir defaultDbFileName = do
optFilesFolder,
showReactions,
allowInstantFiles,
+ autoAcceptFileSize,
muteNotifications,
maintenance
}
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index f6cec6e3b9..0b78c46256 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -47,6 +47,7 @@ import qualified Simplex.FileTransfer.Protocol as XFTP
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..))
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
import Simplex.Messaging.Agent.Protocol
+import Simplex.Messaging.Agent.Store (canAbortRcvSwitch)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
@@ -80,6 +81,8 @@ responseToView user_ ChatConfig {logLevel, showReactions, testView} liveItems ts
CRNetworkConfig cfg -> viewNetworkConfig cfg
CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile
CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats
+ CRContactSwitchAborted {} -> ["switch aborted"]
+ CRGroupMemberSwitchAborted {} -> ["switch aborted"]
CRContactSwitch u ct progress -> ttyUser u $ viewContactSwitch ct progress
CRGroupMemberSwitch u g m progress -> ttyUser u $ viewGroupMemberSwitch g m progress
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
@@ -918,24 +921,46 @@ viewConnectionVerified (Just _) = "connection verified" -- TODO show verificatio
viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code"
viewConnectionStats :: ConnectionStats -> [StyledString]
-viewConnectionStats ConnectionStats {rcvServers, sndServers} =
- ["receiving messages via: " <> viewServerHosts rcvServers | not $ null rcvServers]
- <> ["sending messages via: " <> viewServerHosts sndServers | not $ null sndServers]
+viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} =
+ ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo]
+ <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo]
viewServers :: ProtocolTypeI p => (a -> ProtoServerWithAuth p) -> NonEmpty a -> [StyledString]
viewServers f = map (plain . B.unpack . strEncode . f) . L.toList
-viewServerHosts :: [SMPServer] -> StyledString
-viewServerHosts = plain . intercalate ", " . map showSMPServer
+viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString
+viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo
+ where
+ showQueueInfo RcvQueueInfo {rcvServer, rcvSwitchStatus, canAbortSwitch} =
+ let switchCanBeAborted = if canAbortSwitch then ", can be aborted" else ""
+ in showSMPServer rcvServer
+ <> maybe "" (\s -> " (" <> showSwitchStatus s <> switchCanBeAborted <> ")") rcvSwitchStatus
+ showSwitchStatus = \case
+ RSSwitchStarted -> "switch started"
+ RSSendingQADD -> "switch started"
+ RSSendingQUSE -> "switch confirmed"
+ RSReceivedMessage -> "switch secured"
+
+viewSndQueuesInfo :: [SndQueueInfo] -> StyledString
+viewSndQueuesInfo = plain . intercalate ", " . map showQueueInfo
+ where
+ showQueueInfo SndQueueInfo {sndServer, sndSwitchStatus} =
+ showSMPServer sndServer
+ <> maybe "" (\s -> " (" <> showSwitchStatus s <> ")") sndSwitchStatus
+ showSwitchStatus = \case
+ SSSendingQKEY -> "switch started"
+ SSSendingQTEST -> "switch secured"
viewContactSwitch :: Contact -> SwitchProgress -> [StyledString]
viewContactSwitch _ (SwitchProgress _ SPConfirmed _) = []
+viewContactSwitch _ (SwitchProgress _ SPSecured _) = []
viewContactSwitch ct (SwitchProgress qd phase _) = case qd of
QDRcv -> [ttyContact' ct <> ": you " <> viewSwitchPhase phase]
QDSnd -> [ttyContact' ct <> " " <> viewSwitchPhase phase <> " for you"]
viewGroupMemberSwitch :: GroupInfo -> GroupMember -> SwitchProgress -> [StyledString]
viewGroupMemberSwitch _ _ (SwitchProgress _ SPConfirmed _) = []
+viewGroupMemberSwitch _ _ (SwitchProgress _ SPSecured _) = []
viewGroupMemberSwitch g m (SwitchProgress qd phase _) = case qd of
QDRcv -> [ttyGroup' g <> ": you " <> viewSwitchPhase phase <> " for " <> ttyMember m]
QDSnd -> [ttyGroup' g <> ": " <> ttyMember m <> " " <> viewSwitchPhase phase <> " for you"]
@@ -952,8 +977,11 @@ viewSecurityCode name cmd code testView
| otherwise = [name <> " security code:", plain code, "pass this code to your contact and use " <> highlight cmd <> " to verify"]
viewSwitchPhase :: SwitchPhase -> StyledString
-viewSwitchPhase SPCompleted = "changed address"
-viewSwitchPhase phase = plain (strEncode phase) <> " changing address"
+viewSwitchPhase = \case
+ SPStarted -> "started changing address"
+ SPConfirmed -> "confirmed changing address"
+ SPSecured -> "secured new address"
+ SPCompleted -> "changed address"
viewUserProfileUpdated :: Profile -> Profile -> [StyledString]
viewUserProfileUpdated Profile {displayName = n, fullName, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', image = image', contactLink = contactLink', preferences = prefs'} =
@@ -1478,6 +1506,7 @@ viewChatError logLevel = \case
DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e]
e -> ["chat database error: " <> sShow e]
ChatErrorAgent err entity_ -> case err of
+ CMD PROHIBITED -> [withConnEntity <> "error: command is prohibited"]
SMP SMP.AUTH ->
[ withConnEntity
<> "error: connection authorization failed - this could happen if connection was deleted,\
diff --git a/stack.yaml b/stack.yaml
index fdb23bc1e2..228a1c698f 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 89caf5572980b776bd750caa3c918ae4488612d8
+ commit: 2efe1496d2622a56656a6a00a2cc19005e754468
- github: kazu-yamamoto/http2
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
# - ../direct-sqlcipher
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index b813569089..6cc5cc1d0d 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -74,6 +74,7 @@ testOpts =
optFilesFolder = Nothing,
showReactions = True,
allowInstantFiles = True,
+ autoAcceptFileSize = 0,
muteNotifications = True,
maintenance = False
}
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index 3d03f54b31..9590b5e13e 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -73,9 +73,11 @@ chatDirectTests = do
it "user profile privacy: hide profiles and notificaitons" testUserPrivacy
describe "chat item expiration" $ do
it "set chat item TTL" testSetChatItemTTL
- describe "queue rotation" $ do
+ describe "connection switch" $ do
it "switch contact to a different queue" testSwitchContact
+ it "stop switching contact to a different queue" testAbortSwitchContact
it "switch group member to a different queue" testSwitchGroupMember
+ it "stop switching group member to a different queue" testAbortSwitchGroupMember
describe "connection verification code" $ do
it "verificationCode function converts ByteString to series of digits" $ \_ ->
verificationCode (C.sha256Hash "abcd") `shouldBe` "61889 38426 63934 09576 96390 79389 84124 85253 63658 69469 70853 37788 95900 68296 20156 25"
@@ -1845,6 +1847,33 @@ testSwitchContact =
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "changed address for you")])
alice <##> bob
+testAbortSwitchContact :: HasCallStack => FilePath -> IO ()
+testAbortSwitchContact tmp = do
+ withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ connectUsers alice bob
+ alice #$> ("/switch bob", id, "ok")
+ alice <## "bob: you started changing address"
+ -- repeat switch is prohibited
+ alice ##> "/switch bob"
+ alice <## "error: command is prohibited"
+ -- stop switch
+ alice #$> ("/abort switch bob", id, "switch aborted")
+ -- repeat switch stop is prohibited
+ alice ##> "/abort switch bob"
+ alice <## "error: command is prohibited"
+ withTestChatContactConnected tmp "bob" $ \bob -> do
+ bob <## "alice started changing address for you"
+ -- alice changes address again
+ alice #$> ("/switch bob", id, "ok")
+ alice <## "bob: you started changing address"
+ bob <## "alice started changing address for you"
+ bob <## "alice changed address for you"
+ alice <## "bob: you changed address"
+ alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "started changing address..."), (1, "started changing address..."), (1, "you changed address")])
+ bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")])
+ alice <##> bob
+
testSwitchGroupMember :: HasCallStack => FilePath -> IO ()
testSwitchGroupMember =
testChat2 aliceProfile bobProfile $
@@ -1862,6 +1891,37 @@ testSwitchGroupMember =
bob #> "#team hi"
alice <# "#team bob> hi"
+testAbortSwitchGroupMember :: HasCallStack => FilePath -> IO ()
+testAbortSwitchGroupMember tmp = do
+ withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ createGroup2 "team" alice bob
+ alice #$> ("/switch #team bob", id, "ok")
+ alice <## "#team: you started changing address for bob"
+ -- repeat switch is prohibited
+ alice ##> "/switch #team bob"
+ alice <## "error: command is prohibited"
+ -- stop switch
+ alice #$> ("/abort switch #team bob", id, "switch aborted")
+ -- repeat switch stop is prohibited
+ alice ##> "/abort switch #team bob"
+ alice <## "error: command is prohibited"
+ withTestChatContactConnected tmp "bob" $ \bob -> do
+ bob <## "#team: connected to server(s)"
+ bob <## "#team: alice started changing address for you"
+ -- alice changes address again
+ alice #$> ("/switch #team bob", id, "ok")
+ alice <## "#team: you started changing address for bob"
+ bob <## "#team: alice started changing address for you"
+ bob <## "#team: alice changed address for you"
+ alice <## "#team: you changed address for bob"
+ alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "started changing address for bob..."), (1, "started changing address for bob..."), (1, "you changed address for bob")])
+ bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")])
+ alice #> "#team hey"
+ bob <# "#team alice> hey"
+ bob #> "#team hi"
+ alice <# "#team bob> hi"
+
testMarkContactVerified :: HasCallStack => FilePath -> IO ()
testMarkContactVerified =
testChat2 aliceProfile bobProfile $ \alice bob -> do
diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs
index 9ecf95a729..83c99b0c17 100644
--- a/tests/ChatTests/Files.hs
+++ b/tests/ChatTests/Files.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}
@@ -68,6 +69,7 @@ chatFileTests = do
xit' "receive file marked to receive on chat start" testXFTPMarkToReceive
it "error receiving file" testXFTPRcvError
it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat
+ it "should accept file automatically with CLI option" testAutoAcceptFile
runTestFileTransfer :: HasCallStack => TestCC -> TestCC -> IO ()
runTestFileTransfer alice bob = do
@@ -1386,6 +1388,32 @@ testXFTPCancelRcvRepeat =
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
+testAutoAcceptFile :: HasCallStack => FilePath -> IO ()
+testAutoAcceptFile =
+ testChatCfgOpts2 cfg opts aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do
+ connectUsers alice bob
+ bob ##> "/_files_folder ./tests/tmp/bob_files"
+ bob <## "ok"
+ alice #> "/f @bob ./tests/fixtures/test.jpg"
+ alice <## "use /fc 1 to cancel sending"
+ alice <## "completed uploading file 1 (test.jpg) for bob"
+ bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
+ bob <## "use /fr 1 [/ | ] to receive it"
+ bob <## "saving file 1 from alice to test.jpg"
+ bob <## "started receiving file 1 (test.jpg) from alice"
+ bob <## "completed receiving file 1 (test.jpg) from alice"
+ (bob )
+ alice #> "/f @bob ./tests/fixtures/test_1MB.pdf"
+ alice <## "use /fc 2 to cancel sending"
+ alice <## "completed uploading file 2 (test_1MB.pdf) for bob"
+ bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)"
+ bob <## "use /fr 2 [/ | ] to receive it"
+ -- no auto accept for large files
+ (bob )
+ where
+ cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
+ opts = (testOpts :: ChatOpts) {autoAcceptFileSize = 200000}
+
xftpCLI :: [String] -> IO [String]
xftpCLI params = lines <$> capture_ (withArgs params xftpClientCLI)