diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
index 9618bab95a..ad3148d891 100644
--- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
+++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
@@ -7235,7 +7235,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
connected
- kapcsolódva
+ kapcsolódott
rcv group event chat item
diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff
index 62c6f39d80..df5076fb07 100644
--- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff
+++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff
@@ -5371,7 +5371,7 @@ Hata: %@
Square, circle, or anything in between.
- Kare,daire, veya aralarında herhangi birşey.
+ Kare,daire, veya aralarında herhangi bir şey.
No comment provided by engineer.
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index a5a222f13f..f0a11e1f0c 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -115,11 +115,11 @@
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
- 5CE0E8972BE7F144008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8922BE7F144008D6E06 /* libgmp.a */; };
- 5CE0E8982BE7F144008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8932BE7F144008D6E06 /* libgmpxx.a */; };
- 5CE0E8992BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */; };
- 5CE0E89A2BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */; };
- 5CE0E89B2BE7F144008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8962BE7F144008D6E06 /* libffi.a */; };
+ 5CE0E8A12BEE807A008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E89C2BEE807A008D6E06 /* libffi.a */; };
+ 5CE0E8A22BEE807A008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E89D2BEE807A008D6E06 /* libgmpxx.a */; };
+ 5CE0E8A32BEE807A008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E89E2BEE807A008D6E06 /* libgmp.a */; };
+ 5CE0E8A42BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E89F2BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a */; };
+ 5CE0E8A52BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A02BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a */; };
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; };
@@ -420,11 +420,11 @@
5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = ""; };
5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; };
5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; };
- 5CE0E8922BE7F144008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
- 5CE0E8932BE7F144008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
- 5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a"; sourceTree = ""; };
- 5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a"; sourceTree = ""; };
- 5CE0E8962BE7F144008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 5CE0E89C2BEE807A008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
+ 5CE0E89D2BEE807A008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
+ 5CE0E89E2BEE807A008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
+ 5CE0E89F2BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a"; sourceTree = ""; };
+ 5CE0E8A02BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a"; sourceTree = ""; };
5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; };
5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; };
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -529,12 +529,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 5CE0E8992BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a in Frameworks */,
- 5CE0E8982BE7F144008D6E06 /* libgmpxx.a in Frameworks */,
+ 5CE0E8A52BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a in Frameworks */,
+ 5CE0E8A22BEE807A008D6E06 /* libgmpxx.a in Frameworks */,
+ 5CE0E8A32BEE807A008D6E06 /* libgmp.a in Frameworks */,
+ 5CE0E8A42BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
- 5CE0E89A2BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a in Frameworks */,
- 5CE0E89B2BE7F144008D6E06 /* libffi.a in Frameworks */,
- 5CE0E8972BE7F144008D6E06 /* libgmp.a in Frameworks */,
+ 5CE0E8A12BEE807A008D6E06 /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -601,11 +601,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5CE0E8962BE7F144008D6E06 /* libffi.a */,
- 5CE0E8922BE7F144008D6E06 /* libgmp.a */,
- 5CE0E8932BE7F144008D6E06 /* libgmpxx.a */,
- 5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */,
- 5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */,
+ 5CE0E89C2BEE807A008D6E06 /* libffi.a */,
+ 5CE0E89E2BEE807A008D6E06 /* libgmp.a */,
+ 5CE0E89D2BEE807A008D6E06 /* libgmpxx.a */,
+ 5CE0E89F2BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a */,
+ 5CE0E8A02BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a */,
);
path = Libraries;
sourceTree = "";
@@ -1552,7 +1552,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 213;
+ CURRENT_PROJECT_VERSION = 214;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1577,7 +1577,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
- MARKETING_VERSION = 5.7.1;
+ MARKETING_VERSION = 5.7.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1601,7 +1601,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 213;
+ CURRENT_PROJECT_VERSION = 214;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1626,7 +1626,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 5.7.1;
+ MARKETING_VERSION = 5.7.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1687,7 +1687,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 213;
+ CURRENT_PROJECT_VERSION = 214;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -1702,7 +1702,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 5.7.1;
+ MARKETING_VERSION = 5.7.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1724,7 +1724,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 213;
+ CURRENT_PROJECT_VERSION = 214;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -1739,7 +1739,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 5.7.1;
+ MARKETING_VERSION = 5.7.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1761,7 +1761,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 213;
+ CURRENT_PROJECT_VERSION = 214;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1787,7 +1787,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 5.7.1;
+ MARKETING_VERSION = 5.7.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1812,7 +1812,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 213;
+ CURRENT_PROJECT_VERSION = 214;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1838,7 +1838,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 5.7.1;
+ MARKETING_VERSION = 5.7.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings
index 303b48b6be..b590785606 100644
--- a/apps/ios/hu.lproj/Localizable.strings
+++ b/apps/ios/hu.lproj/Localizable.strings
@@ -2443,7 +2443,7 @@
"member %@ changed to %@" = "%1$@ megváltoztatta a nevét erre: %2$@";
/* rcv group event chat item */
-"member connected" = "kapcsolódva";
+"member connected" = "kapcsolódott";
/* No comment provided by engineer. */
"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: \"%@\". A csoport minden tagja értesítést kap róla.";
diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings
index d2f9aa0943..714a1bc739 100644
--- a/apps/ios/tr.lproj/Localizable.strings
+++ b/apps/ios/tr.lproj/Localizable.strings
@@ -3612,7 +3612,7 @@
"Somebody" = "Biri";
/* No comment provided by engineer. */
-"Square, circle, or anything in between." = "Kare,daire, veya aralarında herhangi birşey.";
+"Square, circle, or anything in between." = "Kare,daire, veya aralarında herhangi bir şey.";
/* chat item text */
"standard end-to-end encryption" = "standart uçtan uca şifreleme";
diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt
index ad0d914ea8..385e6c82a4 100644
--- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt
+++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt
@@ -37,7 +37,7 @@ actual fun ClipboardManager.shareText(text: String) {
}
}
-actual fun shareFile(text: String, fileSource: CryptoFile) {
+fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
val uri = if (fileSource.cryptoArgs != null) {
val tmpFile = File(tmpDir, fileSource.filePath)
tmpFile.deleteOnExit()
@@ -55,20 +55,31 @@ actual fun shareFile(text: String, fileSource: CryptoFile) {
}
val ext = fileSource.filePath.substringAfterLast(".")
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
- val sendIntent: Intent = Intent().apply {
- action = Intent.ACTION_SEND
+ val sendIntent: Intent = Intent(if (justOpen) Intent.ACTION_VIEW else Intent.ACTION_SEND).apply {
/*if (text.isNotEmpty()) {
putExtra(Intent.EXTRA_TEXT, text)
}*/
- putExtra(Intent.EXTRA_STREAM, uri.toUri())
- type = mimeType
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ if (justOpen) {
+ flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ setDataAndType(uri.toUri(), mimeType)
+ } else {
+ putExtra(Intent.EXTRA_STREAM, uri.toUri())
+ type = mimeType
+ }
}
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
}
+actual fun shareFile(text: String, fileSource: CryptoFile) {
+ openOrShareFile(text, fileSource, justOpen = false)
+}
+
+actual fun openFile(fileSource: CryptoFile) {
+ openOrShareFile("", fileSource, justOpen = true)
+}
+
actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt
index 72bb3caaac..bd469298b7 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt
@@ -8,3 +8,4 @@ expect fun UriHandler.sendEmail(subject: String, body: CharSequence)
expect fun ClipboardManager.shareText(text: String)
expect fun shareFile(text: String, fileSource: CryptoFile)
+expect fun openFile(fileSource: CryptoFile)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt
index 5ad98aa216..502074d629 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt
@@ -32,8 +32,8 @@ fun ContactPreferencesView(
) {
val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
val ct = contact.value ?: return
- var featuresAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
- var currentFeaturesAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) }
+ var featuresAllowed by rememberSaveable(ct, user, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
+ var currentFeaturesAllowed by rememberSaveable(ct, user, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) }
fun savePrefs(afterSave: () -> Unit = {}) {
withBGApi {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt
index dbed57d5fc..6ad75057f6 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt
@@ -181,7 +181,8 @@ fun CIFileView(
}
Row(
- Modifier.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
+ Modifier.clickable(onClick = { fileAction() }).padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
+ //Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt
index 5aed7742bc..65fb38575d 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt
@@ -2,8 +2,7 @@ package chat.simplex.common.views.chat.item
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Icon
+import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -113,21 +112,49 @@ fun CIImageView(
}
@Composable
- fun ImageView(painter: Painter, onClick: () -> Unit) {
- Image(
- painter,
- contentDescription = stringResource(MR.strings.image_descr),
- // .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
- // if text is short and take all available width if text is long
- modifier = Modifier
+ fun ImageView(painter: Painter, image: String, fileSource: CryptoFile?, onClick: () -> Unit) {
+ // On my Android device Compose fails to display 6000x6000 px WebP image with exception:
+ // IllegalStateException: Recording currently in progress - missing #endRecording() call?
+ // but can display 5000px image. Using even lower value here just to feel safer.
+ // It happens to WebP because it's not compressed while sending since it can be animated.
+ if (painter.intrinsicSize.width <= 4320 && painter.intrinsicSize.height <= 4320) {
+ Image(
+ painter,
+ contentDescription = stringResource(MR.strings.image_descr),
+ // .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
+ // if text is short and take all available width if text is long
+ modifier = Modifier
+ .width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
+ .combinedClickable(
+ onLongClick = { showMenu.value = true },
+ onClick = onClick
+ )
+ .onRightClick { showMenu.value = true },
+ contentScale = ContentScale.FillWidth,
+ )
+ } else {
+ Box(Modifier
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
.combinedClickable(
onLongClick = { showMenu.value = true },
- onClick = onClick
+ onClick = {}
)
.onRightClick { showMenu.value = true },
- contentScale = ContentScale.FillWidth,
- )
+ contentAlignment = Alignment.Center
+ ) {
+ imageView(base64ToBitmap(image), onClick = {
+ if (fileSource != null) {
+ openFile(fileSource)
+ }
+ })
+ Icon(
+ painterResource(MR.images.ic_open_in_new),
+ contentDescription = stringResource(MR.strings.image_descr),
+ modifier = Modifier.size(30.dp),
+ tint = MaterialTheme.colors.primary,
+ )
+ }
+ }
}
fun fileSizeValid(): Boolean {
@@ -172,9 +199,9 @@ fun CIImageView(
}
}
val loaded = res.value
- if (loaded != null) {
+ if (loaded != null && file != null) {
val (imageBitmap, data, _) = loaded
- SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) })
+ SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
} else {
imageView(base64ToBitmap(image), onClick = {
if (file != null) {
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml
index 5031330e4d..8ef94b53fe 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml
@@ -106,13 +106,13 @@
مخاطب از قبل وجود دارد
لینک اتصال نامعتبر
لطفا بررسی کنید که از لینک صحیح استفاده کردید یا از مخاطبتان بخواهید لینک دیگری برایتان بفرستد.
- مطمئن شوید قالب آدرسهای سرور SMP صحیح است، در خطهای جدا نوشته شده و تکرار نشدهاند.
+ مطمئن شوید قالب نشانیهای سرور SMP صحیح است، در خطهای جدا نوشته شده و تکرار نشدهاند.
خطا در بهروزرسانی پیکربندی شبکه
عدم موفقیت در بارگیری گپ
نام نمایشی نامعتبر!
شما یک نمایه گپ با نام نمایشی یکسان دارید، لطفا نام دیگری انتخاب کنید.
خطا در ایجاد نشانی
- مطمئن شوید قالب آدرسهای سرور XFTP صحیح است، در خطهای جدا نوشته شده و تکرار نشدهاند.
+ مطمئن شوید قالب نشانیهای سرور XFTP صحیح است، در خطهای جدا نوشته شده و تکرار نشدهاند.
خطا در پذیرش درخواست مخاطب
فرستنده ممکن است درخواست اتصال را حذف کرده باشد.
خطا در حذف یادداشتهای خصوصی
@@ -204,4 +204,482 @@
خطا در نمایش اعلان، با توسعهدهندگان تماس بگیرید.
قفل SimpleX فعال نیست!
تصدیق دستگاه غیرفعال است. قفل SimpleX خاموش میشود.
+ ارسال شده
+ نماد زمینه
+ لغو پیشنمایش تصویر
+ لغو پیشنمایش پرونده
+ مصرف باتری برنامه / نامحدود را در تنظیمات برنامه انتخاب کنید.]]>
+ برنامه ممکن است بعد از ۱ دقیقه در پسزمینه بسته شود.
+ مقداردهی اولیه پایگاه داده ممکن نیست
+ عبارت عبور الزامی است
+ برای دریافت اعلانها، لطفا، عبارت عبور پایگاه داده را وارد کنید
+ پایگاه داده به درستی کار نمیکند. برای آگاهی بیشتر لمس کنید
+ سرویس SimpleX Chat
+ در حال دریافت پیامها…
+ روشن کردن
+ عدم موفقیت تصدیق
+ گشودن قفل
+ اطلاعات ورودتان را تایید کنید
+ پاسخ
+ تاریخچه
+ پیام دریافتی
+ حذف
+ بسط دادن
+ پیام برای تمام اعضا به عنوان حذف شده علامتگذاری خواهد شد.
+ توقف
+ دریافت پرونده متوقف خواهد شد.
+ خوش آمدید!
+ خطا در رمزگشایی
+ ارسال پیام مستقیم برای اتصال
+ لطفا، تا زمانی که پرونده در حال بارگیری از موبایل متصل است، منتظر باشید.
+ حذف مخاطب
+ مشاهده رمز امنیتی
+ تایید رمز امنیتی
+ ارسال پیام ناپدید شونده
+ لینک دعوت یکبارمصرف
+ ایجاد گروه محرمانه
+ (برای اشتراکگذاری با مخاطبتان)
+ اگر لینک دعوت SimpleX Chat دریافت کردید، میتوانید آن را در مرورگر خود باز کنید:
+ ورود رمز عبور
+ %d ثانیه
+ رمز عبور فعلی
+ کپی
+ پیام به عنوان حذف شده علامتگذاری خواهد شد. گیرندهها قادر خواهند بود این پیام را آشکار کنند.
+ ارسال غیرموفق
+ برای اتصال لمس کنید
+ امکان رمزگشایی ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعهدهندگان تماس بگیرید.
+ تصویر
+ پرونده پیدا نشد
+ لمس دکمه
+ ایجاد گروه: برای ایجاد یک گروه جدید.]]>
+ بدون تماسهای پسزمینه
+ مصرف باتری برنامه / نامحدود را در تنظیمات برنامه انتخاب کنید.]]>
+ تصدیق
+ ویرایش
+ بدون اطلاعات تحویل
+ لغو پرونده
+ در حال اتصال…
+ شما هیچ گپی ندارید
+ بارگذاری گپها…
+ ویدئو وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.
+ بیشترین اندازه پشتیبانی شده فعلی پرونده %1$s است.
+ خطا در ذخیرهسازی پرونده
+ در حال بارگیری پرونده
+ حذف و مخاطب را باخبر کن
+ متصل
+ مذاکره مجدد رمزگذاری؟
+ شما نیاز دارید به مخاطبتان اجازه ارسال پیامهای صوتی دهید تا بتوانید آنها را ارسال کنید.
+ ارسال پیام زنده
+ پیام ناپدید شونده
+ زمان سفارشی
+ به حافظه کپی شد
+ شروع گپ جدید
+ از گالری
+ تماس تصویری
+ برای محافظت از اطلاعاتتان، قفل SimpleX را روشن کنید.
+\nاز شما خواسته خواهد شد قبل از فعال شدن این ویژگی، تصدیق را تکمیل کنید.
+ عدم وجود رمز عبور
+ %d دقیقه
+ فعالسازی قفل SimpleX
+ وارد کردن رمز عبور
+ ذخیره شده
+ فرستاده شده
+ فرستاده شده از
+ گیرندهها نمیتوانند ببینند این پیام از طرف چه کسی است.
+ آشکار کردن
+ پنهان کردن
+ %d پیام حذف شود؟
+ پیام حذف خواهد شد - این عمل قابل برگشت نیست!
+ پیام برای تمام اعضا حذف خواهد شد.
+ حذف برای من
+ فرستادن
+ بارگیری
+ ویرایش شده
+ ارسال غیرمجاز
+ خوانده نشده
+ خوش آمدید، %1$s!
+ این متن در تنظیمات دردسترس است
+ گپ با توسعهدهندگان
+ گپ پالایش شدهای نیست
+ به %1$s متصل شوید؟
+ گپی انتخاب نشده
+ فرستادن پیام…
+ تعداد ویدئوی بیش از اندازه!
+ فقط ۱۰ ویدئو در هر زمان میتوان ارسال کرد
+ شما نمیتوانید پیامی بفرستید!
+ لطفا با مدیر گروه تماس بگیرید.
+ فقط صاحبان گروه میتوانند پروندهها و رسانه را فعال کنند.
+ دریافت تصویر درخواست شده
+ در انتظار تصویر
+ تصویر در گالری ذخیره شد
+ ویدئو
+ ویدئو وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید.
+ پرونده
+ پرونده حجیم!
+ در انتظار پرونده
+ پرونده وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.
+ پیام صوتی (%1$s)
+ اعلانها
+ مخاطب و تمام پیامها حذف خواهند شد - این عمل قابل برگشت نیست!
+ پیامهای صوتی مجازند؟
+ پیامهای صوتی ممنوع هستند!
+ اسکن رمز QR.]]>
+ اشتراکگذاری پرونده…
+ تعداد تصویر بیش از اندازه!
+ برای اسکن لمس کنید
+ انتخاب پرونده
+ برای شروع گپ جدید
+ تحویل
+ پیام عضو حذف شود؟
+ برای همه
+ ارسال پرونده متوقف خواهد شد.
+ دریافت پرونده متوقف شود؟
+ پرونده از سرورها حذف خواهد شد.
+ ارسال پیام مستقیم
+ اشتراکگذاری رسانه…
+ فقط ۱۰ تصویر در هر زمان میتوان ارسال کرد
+ تماس صوتی
+ لطفا آن را به خاطر داشته باشید یا به طور امن ذخیره کنید - راهی برای بازیابی کلمه عبور وجود ندارد!
+ قفل SimpleX روشن است
+ وقتی برنامه را شروع میکنید یا بعد از ۳۰ ثانیه در پسزمینه آن از سر میگیرید، نیاز به تصدیق خواهید داشت.
+ جستجو
+ غیرفعال کردن قفل SimpleX
+ اشتراکگذاری
+ ذخیره
+ ارسال
+ لغو پیام زنده
+ بازگشت
+ لغو
+ موافقت
+ پیام زنده!
+ ارسال پیام زنده - همزمان با تایپ کردن برای گیرندهها بهروز میشود
+ ورود با استفاده از اطلاعات ورودتان
+ تصدیق موجود نیست
+ خطا در تحویل پیام
+ اطلاعات
+ پیام ارسالی
+ به احتمال زیاد این مخاطب اتصال با شما را حذف کرده است.
+ بدون تاریخچه
+ حذف
+ پیام حذف شود؟
+ پرونده لغو شود؟
+ توقف پرونده
+ ارسال پرونده متوقف شود؟
+ لغو
+ در حال اتصال…
+ گپها
+ شما به گروه دعوت شدهاید
+ پیوستن به عنوان %s
+ جستجو یا الصاق لینک SimpleX
+ برای شروع گپ جدید لمس کنید
+ اشتراکگذاری پیام…
+ در انتظار تصویر
+ پیام صوتی…
+ تعیین نام مخاطب…
+ قطع اتصال
+ در حال انتظار
+ نشانی دریافت تغییر کند؟
+ تغییر نشانی لغو خواهد شد. نشانی دریافت پیشین استفاده خواهد شد.
+ مذاکره مجدد
+ فقط صاحبان گروه میتوانند پیامهای صوتی را فعال کنند.
+ تایید
+ بازنشاندن
+ اسکن رمز QR
+ (اسکن یا الصاق از حافظه)
+ (تنها ذخیره شده توسط اعضای گروه)
+ فعال کردن دسترسی دوربین
+ اجازه داده نشد!
+ دوربین
+ سپاس برای نصب SimpleX Chat!
+ گشودن در موبایل را لمس کنید، سپس اتصال را در برنامه لمس کنید.]]>
+ درخواست اتصال پذیرفته شود؟
+ پیامهای صوتی مجاز نیستند
+ لینکهای SimpleX مجاز نیستند
+ پروندهها و رسانه مجاز نیست
+ در انتظار ویدئو
+ دریافت ویدئو درخواست شده
+ ویدئو ارسال شد
+ تصویر وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.
+ پیام صوتی
+ ضبط پیام صوتی
+ ارسال پیام
+ بدون جزئیات
+ اتصال به وسیله لینک / رمز QR
+ دوربین موجود نیست
+ تصویر
+ ایجاد لینک دعوت یکبارمصرف
+ برای پرسش هر سوال و دریافت اطلاعات به توسعهدهندگان SimpleX Chat متصل شوید.]]>
+ افزودن مخاطب: برای ایجاد لینک دعوت جدید، یا اتصال از طریق لینکی که دریافت کردید.]]>
+ اتصال از طریق لینک
+ تصویر وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید!
+ نوع قفل SimpleX
+ تصدیق سامانه
+ تغییر رمز عبور
+ در پاسخ به
+ فوری
+ تصدیق دستگاه فعال نیست. زمانی که تصدیق دستگاه را فعال کنید میتوانید قفل SimpleX را از طریق تنظیمات روشن کنید.
+ اجازه دادن
+ ضمیمه
+ امکان رمزگشایی تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعهدهندگان تماس بگیرید.
+ پروندهها و رسانه ممنوع است!
+ تصویر ارسال شد
+ در انتظار ویدئو
+ مخاطبتان پروندهای ارسال کرد که از بیشترین اندازه پشتیبانی شده (%1$s) بزرگتر است.
+ پرونده وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید.
+ پرونده ذخیره شد
+ رمزگذاری در حال کار است و نیازی به توافق رمزگذاری جدید نیست. ممکن است باعث بروز خطاهای اتصال شود!
+ لطفا از مخاطبتان بخواهید ارسال پیامهای صوتی را فعال کند.
+ ارسال
+ ویدئو
+ بالا، سپس:
+ مخاطب حذف شود؟
+ خطا
+ آدرس دریافت به سرور متفاوتی تغییر خواهد یافت. تغییر نشانی وقتی تکمیل خواهد شد که فرستنده آنلاین شود.
+ تایید شما ممکن نیست؛ لطفا دوباره امتحان کنید.
+ پرونده
+ شما ناظر هستید
+ گپ پاک شود؟
+ تمام پیامها حذف خواهند شد - این عمل قابل برگشت نیست!
+ حذف
+ پذیرش ناشناس
+ برگزیده
+ پذیرفتن
+ رد کردن
+ یادداشتهای خصوصی پاک شود؟
+ پاک کردن
+ پاکسازی
+ حذف
+ علامتگذاری به عنوان خوانده نشده
+ تعیین نام مخاطب
+ مخاطبی که این لینک را با او به اشتراک گذاشتید قادر به اتصال نخواهد بود!
+ اگر نپذیرید، فرستنده باخبر نخواهد شد.
+ تمام پیامها حذف خواهند شد - این عمل قابل برگشت نیست! پیامها فقط برای شما حذف خواهند شد.
+ پاکسازی گپ
+ اتصال را پذیرفتید
+ مخاطب هنوز متصل نشده است!
+ شما مخاطبی را دعوت کردید
+ اتصال در حال انتظار حذف شود؟
+ اتصالی که پذیرفتید لغو خواهد شد!
+ بیصدا
+ لغو بیصدا
+ علامتگذاری به عنوان خوانده شده
+ خلع برگزیده
+ تیم SimpleX
+ گپ جدید
+ روش استفاده
+ راهنمای مارکداون
+ لینک نامعتبر!
+ اشتراکگذاری لینک یک بار مصرف
+ یا رمز QR را اسکن کنید
+ تلاش مجدد
+ عبارت عبور و صدور پایگاه داده
+ افزودن سرورهای از پیش تنظیم شده
+ افزودن به دستگاه دیگر
+ حذف سرور
+ چگونه از سرورهای خود استفاده کنید
+ تصویر پیشنمایش لینک
+ نمایههای گپ شما
+ سرورهای SMP
+ SimpleX Chat را برای ترمینال نصب کنید
+ در GitHub ستاره بزنید
+ همکاری کنید
+ رمز QR
+ یک نمایه تصادفی جدید به اشتراک گذاشته خواهد شد.
+ اتصال از طریق لینک
+ لینک دعوت یکبارمصرف
+ %s تایید شده است
+ ایجاد نمایه گپ
+ کنسول گپ
+ آزمایش سرور
+ وارد کردن دستی سرور
+ سرورهای ICE (یکی در هر خط)
+ خطا در ذخیره کردن سرورهای ICE
+ جایگزین تصویر نمایه
+ دکمه بستن
+ لغو پیشنمایش لینک
+ تنظیمات
+ کمک
+ لوگوی SimpleX
+ ایمیل
+ این لینک، یک لینک اتصال معتبر نیست!
+ درخواست اتصال ارسال شد!
+ وقتی دستگاه میزبان گروه آنلاین شد، به گروه متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!
+ لینکی که دریافت کردید را الصاق کنید تا به مخاطبتان متصل شوید…
+ نمایه شما %1$s به اشتراک گذاشته خواهد شد.
+ برای اتصال، مخاطبتان میتواند رمز QR را اسکن یا از لینک در برنامه استفاده کند.
+ اگر نمیتوانید ملاقات حضوری داشته باشید، رمز QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.
+ میتوانید نشانی خود را به صورت لینک یا رمز QR به اشتراک بگذارید - هر کسی میتواند به شما متصل شود.
+ وقتی اشخاص درخواست اتصال کنند، شما میتوانید آن را بپذیرید یا رد کنید.
+ یا این رمز را نشان دهید
+ میتوانید دوباره لینک دعوت را در جزئیات اتصال مشاهده کنید.
+ نگهداشتن
+ در حال ایجاد لینک…
+ لینکی که دریافت کردید را الصاق کنید
+ متن الصاقی شما یک لینک SimpleX نیست.
+ سرور شما
+ سرور از پیش تنظیم شده
+ نشانی سرور نامعتبر!
+ بهکارگیری برای اتصالهای جدید
+ رمز QR نامعتبر
+ رمز امنیتی نادرست!
+ رمز امنیتی را از برنامه مخاطبتان اسکن کنید.
+ علامتگذاری به عنوان تایید شده
+ %s تایید نشده است
+ برای تایید رمزگذاری سرتاسر، روی دستگاههای خود، کد را با مخاطبتان مقایسه(یا اسکن) کنید.
+ سرورهای XFTP شما
+ چگونه
+ در حال استفاده از سرورهای SimpleX Chat.
+ تنظیم سرورهای ICE
+ میخواهد به شما متصل شود!
+ وقتی دستگاه مخاطبتان آنلاین شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!
+ رمز QR را در تماس تصویری اسکن کنید، یا مخاطبتان میتواند یک لینک دعوت به اشتراک بگذارد.]]>
+ دعوت استعمال نشده نگه داشته شود؟
+ نشانی SimpleX شما
+ مارکداون در پیامها
+ ایدهها و سوالات را ارسال کنید
+ به ما ایمیل بفرستید
+ قفل SimpleX
+ افزودن سرور…
+ آزمایش سرورها
+ ذخیره سرورها
+ عدم موفقیت آزمایش سرور!
+ عدم موفقیت آزمایش چند سرور:
+ اسکن رمز QR سرور
+ سرورهای SMP شما
+ سرورهای XFTP
+ از سرورهای SimpleX Chat استفاده شود؟
+ سرورهای WebRTC ICE ذخیره شده حذف خواهند شد.
+ سرورهای ICE شما
+ اتصال
+ الصاق
+ گشودن در برنامه موبایل کلیک کنید.]]>
+ افزودن مخاطب
+ نشانی SimpleX
+ پاکسازی تایید
+ نشانی سرور از پیش تنظیم شده
+ رمز امنیتی
+ بهکارگیری از سرور
+ سرورها برای اتصالهای جدید نمایه گپ فعلی شما
+ سرورها ذخیره شوند؟
+ به برنامه امتیاز بدهید
+ نشانی سرور شما
+ نشانی سرور را بررسی و دوباره امتحان کنید.
+ لازم است مخاطبتان آنلاین باشد تا اتصال کامل شود.
+\nمیتوانید این اتصال را لغو و مخاطب را حذف کنید (و بعدا با یک لینک جدید امتحان کنید).
+ نشانی SimpleX
+ این رمز QR یک لینک نیست!
+ راهنمای کاربر.]]>
+ این رشته متن، یک لینک اتصال نیست!
+ رمزی که اسکن کردید یک رمز QR لینک SimpleX نیست.
+ اسکن رمز
+ تصویر نمایه
+ بیشتر
+ نمایش رمز QR
+ رمز QR نامعتبر
+ وقتی درخواست اتصال شما پذیرفته شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!
+ رمز QR را در تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.]]>
+ نمایه گپ شما ارسال خواهد شد
+\nبه مخاطبتان
+ اطلاعات بیشتر
+ اگر بعدا نشانی خود را حذف کنید، مخاطبان خود را از دست نخواهید داد.
+ این لینک دعوت یکبارمصرف را به اشتراک بگذارید
+ برای الصاق لینک لمس کنید
+ تنظیمات شما
+ میزبانهای Onion برای اتصال الزامی خواهد بود.
+\nلطفا توجه داشته باشید: شما بدون نشانی onion. قادر نخواهید بود به سرورها متصل شوید.
+ ذخیره
+ تنظیمات میزبانهای onion. به روز شود؟
+ پورت
+ از پروکسی SOCKS استفاده شود؟
+ از اتصال مستقیم اینترنت استفاده شود؟
+ تنظیمات پیشرفته شبکه
+ تنظیمات پروکسی SOCKS
+ هاست
+ الزامی
+ از میزبانهای Onion وقتی موجود باشند استفاده خواهد شد.
+ از میزبانهای Onion وقتی موجود باشند استفاده خواهد شد.
+ ظاهر
+ نسخه برنامه: v%s
+ نمایش گزینههای توسعهدهنده
+ شناسههای پایگاه داده و گزینه انزوای ترابری
+ مخاطبانتان متصل باقی خواهند ماند.
+ اشتراکگذاری لینک
+ دسترسی به سرورها از طریق پروکسی SOCKS روی پورت %d؟ پروکسی باید قبل از فعال کردن این گزینه، راه اندازی شده باشد.
+ برای هر نمایه گپی که در برنامه دارید استفاده خواهد شد.]]>
+ نمایش خطاهای داخلی
+ نمایش تماسهای کند API
+ از میزبانهای Onion استفاده نخواهد شد.
+ خیر
+ استفاده از میزبانهای onion. را روی «خیر» تنظیم کنید اگر پروکسی SOCKS از آنها پشتیبانی نمیکند.]]>
+ سفارشی کردن تم
+ نسخه برنامه
+ رنگهای تم
+ نسخه هسته: v%s
+ simplexmq: v%s (%2s)
+ اعلانها از کار خواهند افتاد تا زمانی که برنامه را دوباره راهاندازی کنید
+ ایجاد نشانی
+ نشانی حذف شود؟
+ شبکه و سرورها
+ استفاده از پروکسی SOCKS
+ پورت %d
+ استفاده از میزبانهای onion.
+ وقتی موجود بود
+ از میزبانهای Onion استفاده نخواهد شد.
+ انزوای ترابری
+ نمایه گپ
+ اتصال
+ ساختار برنامه: %s
+ تنظیمات شبکه
+ میزبانهای Onion برای اتصال الزامی خواهد بود.
+ نمایش:
+ لطفا توجه داشته باشید: واسطههای پیام و پرونده از طریق پروکسی SOCKS متصل میشوند. تماسها و ارسال پیشنمایشهای لینک از اتصال مستقیم استفاده میکنند.]]>
+ گزینههای توسعهدهنده
+ پنهان کردن:
+ بسته شود؟
+ تمام مخاطبانتان متصل باقی خواهند ماند.
+ تمام مخاطبانتان متصل باقی خواهند ماند. بهروزرسانی نمایه به مخاطبانتان ارسال خواهد شد.
+ اگر تایید کنید، سرورهای پیامرسانی خواهند توانست نشانی IP، و فراهمکننده شما را ببینند - و این که به چه سرورهایی متصل میشوید.
+ مطمئن شوید قالب نشانیهای سرور WebRTC ICE صحیح است، در خطهای جدا نوشته شده و تکرار نشدهاند.
+ یک اتصال جدای TCP (و اطلاعات ورود SOCKS) برای هر مخاطب و عضو گروه استفاده خواهد شد.
+\nلطفا توجه داشته باشید: اگر اتصالهای زیادی داشته باشید، مصرف باتری و ترافیک شما میتواند به شکل قابل توجه بالاتر باشد و بعضی اتصالها ممکن است با موفقیت انجام نشوند.
+ حالت انزوای ترابری به روز شود؟
+ ویرایش تصویر
+ ایجاد نشانی SimpleX
+ اشتراکگذاری با مخاطبان
+ بهروزرسانی نمایه به مخاطبانتان ارسال خواهد شد.
+ پذیرفتن خودکار
+ ورود پیام خوشامدگویی…(اختیاری)
+ تنظیمات ذخیره شوند؟
+ ذخیره تنظیمات پذیرفتن خودکار
+ حذف نشانی
+ سلام!
+\nبه وسیله SimpleX Chat به من متصل شوید: %s
+ نشانی ایجاد نشود
+ ادامه
+ نمایه فعلی شما
+ نام کامل:
+ ذخیره و مخاطبان مطلع شوند
+ بیایید در SimpleX Chat گفتگو کنیم
+ تنظیمات ذخیره شوند؟
+ پنهان کردن نمایه
+ کلمه عبور برای نمایش
+ ذخیره کلمه عبور نمایه
+ اشتراکگذاری نشانی متوقف شود؟
+ توقف اشتراکگذاری
+ میتوانید بعدا آن را ایجاد کنید
+ میتوانید آن را از طریق تنظیمات برای مخاطبان SimpleX خود قابل رویت کنید.
+ نام نمایه:
+ حذف تصویر
+ ذخیره و اعضای گروه مطلع شوند
+ ذخیره و مخاطب مطلع شود
+ خروج بدون ذخیره کردن
+ برای آشکار کردن نمایه پنهان خود، یک کلمه عبور کامل در فیلد جستجو در صفحه نمایههای گپتان وارد کنید.
+ دعوت از دوستان
+ تایید کلمه عبور
+ کلمه عبور نمایه پنهان
+ به نمایه خود نشانی اضافه کنید، تا مخاطبانتان بتوانند آن را با اشخاص دیگر به اشتراک بگذارند. بهروزرسانی نمایه به مخاطبانتان ارسال خواهد شد.
+ نشانی با مخاطبان به اشتراک گذاشته شود؟
+ یک نشانی ایجاد کنید تا اشخاص بتوانند به شما متصل شوند.
+ نمایه شما روی دستگاهتان ذخیره شده و فقط با مخاطبانتان به اشتراک گذاشته میشود. سرورهای SimpleX قادر به دیدن نمایه شما نیستند.
+ خطا در ذخیره کردن کلمه عبور کاربر
\ No newline at end of file
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
index 96fe8870ff..e3a1f124aa 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
@@ -180,9 +180,9 @@
Csoport szerepkör megváltoztatása?
Zárolási mód megváltoztatása
Kapcsolódott
- kapcsolódva
+ kapcsolódott
Kapcsolódás
- kapcsolódva
+ kapcsolódott
Csatlakoztatott telefon
kapcsolódva
Szerepkör megváltoztatása
@@ -1151,7 +1151,7 @@
fogadott válasz…
Adatbázismentés visszaállítása?
Üzenetek fogadása…
- %s és %s kapcsolódtak
+ %s és %s kapcsolódott
megfigyelő szerep
Port
Jelkód beállítása
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
index feb6345c9c..83d59b3705 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
@@ -46,7 +46,7 @@
太文字
音声通話
音声とビデオ通話
- ※注意※:喪失したら、パスフレーズの回復・変更ができません。]]>
+ ※注意※:紛失するとパスフレーズの回復・変更ができません。]]>
グループ全員の接続が継続します。
送信相手が消えるメッセージを送るのを許可する。
送信相手が永久メッセージ削除するのを許可する。(24時間)
@@ -179,7 +179,7 @@
チャットが停止してます。
承諾すると、メッセージのサーバがIPアドレス、ISP、接続サーバを特定できます。
チャット設定
- 消す
+ チャットを削除
閉じるボタン
未検証の状態に戻す
画像をギャラリーに保存しました。
@@ -868,7 +868,7 @@
サーバのQRコードを読み込む
テストに失敗したサーバがあります:
SimpleX Chatサーバを使いますか?
- SimpleX Chatを使っています。
+ SimpleX Chatサーバを使用中。
リンクを送る
simplexmq: バージョン%s (%2s)
応答を待機中…
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml
index 5d1195a020..b6f268e6f1 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml
@@ -88,4 +88,71 @@
Camera și microfon
Atenție: arhiva va fi ștearsă.]]>
Anulează previzualizarea fișierului
+ Un profil de conversație gol cu numele furnizat este creat, și aplicația se deschide ca de obicei.
+ Se conectează deja!
+ Aplicația poate primi notificări doar când rulează, niciun serviciu în fundal nu va fi lansat.
+ Se alătură deja grupului!
+ Mereu pornit
+ Un nou profil aleatoriu va fi distribuit.
+ Android Keystore este folosit pentru a stoca în siguranță fraza de acces - permite serviciului de notificare să funcționeze.
+ și %d alte evenimente
+ Răspunde la apel
+ Android Keystore va fi folosit pentru a stoca în siguranță fraza de acces după ce repornești aplicația sau schimbi fraza de acces - va permite primirea notificărilor.
+ APLICAȚIE
+ Creează grup
+ Apeluri audio și video
+ Arhivează și încarcă
+ Bază de date de arhivare
+ Se creează un link de arhivare
+ Creează un link de invitare unic.
+ Acceptă automat imagini
+ Apel audio
+ Apel audio
+ Audio oprit
+ PICTOGRAMĂ APLICAȚIE
+ Cod de acces aplicație
+ Creează grup secret
+ Creează coadă
+ Serviciul în fundal rulează mereu - notificările vor fi afișate imediat ce sunt disponibile.
+ Autentificare
+ Autentificare eșuată
+ Codul de acces curent
+ Autentificare indisponibilă
+ Atașează
+ Înapoi
+ Creează grup secret
+ Se creează link…
+ apel audio (necriptat e2e)
+ Creează fișier
+ Adaugă contact: pentru a crea un nou link de invitare, sau a te conecta printr-un link pe care l-ai primit.]]>
+ Autentificare anulată
+ Cod de acces aplicație este înlocuit cu cod de acces de autodistrugere.
+ Apeluri audio/video
+ "
+\nDisponibil în v5.1"
+ Cod de acces aplicație
+ Migrare date aplicație
+ Eroare critică
+ Aplică
+ Creează profil
+ Creează profil
+ Acceptă automat cererile de contactare
+ personalizat
+ În prezent dimensiunea maximă pentru fișiere este %1$s.
+ Creează adresă SimpleX
+ Acceptare automată
+ Creează-ți profilul
+ Apeluri audio și video
+ Audio pornit
+ Frază de acces curentă…
+ Creează link pentru grup
+ Creează link
+ creator
+ Fundal
+ Creează profil nou în aplicația desktop. 💻
+ Aplicația criptează fișierele locale noi (cu excepția videoclipurilor).
+ autor
+ Arabă, Bulgară, Finlandeză, Ebraică, Thailandeză și Ucraineană - mulțumită utilizatorilor și Weblate.
+ Apelurile audio/video sunt interzise.
+ (prezent)
\ No newline at end of file
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
index 915c643dee..9ae8707c11 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
@@ -1769,5 +1769,5 @@
Kablolu ethernet
Profil resimleri
Profil resimlerini şekillendir
- Kare,daire, veya aralarında herhangi birşey.
+ Kare,daire, veya aralarında herhangi bir şey.
\ No newline at end of file
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml
index dc893c7d17..939071da2b 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml
@@ -144,4 +144,32 @@
Tự động chấp nhận
Quay về
Tự động chấp nhận hình ảnh
+ Bluetooth
+ Nhóm tốt hơn
+ Chặn thành viên nhóm
+ đã chặn %s
+ Tin nhắn tốt hơn
+ Xin lưu ý: việc sử dụng cùng một cơ sở dữ liệu trên hai thiết bị sẽ phá vỡ quá trình giải mã tin nhắn từ các kết nối của bạn, như một biện pháp bảo vệ.]]>
+ Tạo nhóm: để tạo một nhóm mới.]]>
+ Tốt nhất cho pin. Bạn sẽ chỉ nhận được thông báo khi ứng dụng đang chạy (KHÔNG có dịch vụ nền).]]>
+ Xin lưu ý: bạn sẽ KHÔNG thể khôi phục hoặc thay đổi passphrase nếu bạn làm mất nó.]]>
+ Bị chặn bởi quản trị viên
+ Chặn thành viên
+ đã chặn
+ bị chặn bởi quản trị viên
+ đã chặn
+ Tính năng tối ưu hóa pin đang được kích hoạt, tắt dịch vụ nền và các yêu cầu định kỳ về tin nhắn mới. Bạn có thể kích hoạt lại chúng thông qua cài đặt.
+ Có thể tắt thông qua cài đặt - thông báo vẫn sẽ được hiển thị khi ứng dụng đang chạy.]]>
+ Chặn thành viên?
+ Cả bạn và liên hệ của bạn đều có thể xóa tin nhắn đã gửi mà không thể hoàn tác. (24 tiếng)
+ Chặn tất cả
+ Chặn thành viên cho tất cả?
+ Chặn
+ Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn thoại.
+ Cả bạn và liên hệ của bạn đều có thể thực hiện cuộc gọi.
+ in đậm
+ Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn tự xóa.
+ Cả bạn và liên hệ của bạn đều có thể thả cảm xúc tin nhắn.
+ Xin lưu ý: relay tin nhắn và tệp được kết nối thông qua SOCKS proxy. Các cuộc gọi và bản xem trước liên kết sử dụng kết nối trực tiếp.]]>
+ Tốt cho pin. Dịch vụ nền kiểm tra tin nhắn 10 phút một lần. Bạn có thể bỏ lỡ các cuộc gọi hoặc tin nhắn khẩn cấp.]]>
\ No newline at end of file
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt
index 5817275a5f..a10f675085 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt
@@ -9,6 +9,7 @@ import java.io.File
import java.net.URI
import java.net.URLEncoder
import chat.simplex.res.MR
+import java.awt.Desktop
actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
val subjectEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20")
@@ -41,3 +42,30 @@ actual fun shareFile(text: String, fileSource: CryptoFile) {
}.launch(fileSource.filePath)
}
}
+
+actual fun openFile(fileSource: CryptoFile) {
+ try {
+ val filePath = filePathForShare(fileSource) ?: return
+ Desktop.getDesktop().open(File(filePath))
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to open the file: " + e.stackTraceToString())
+ AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
+ }
+}
+
+fun filePathForShare(fileSource: CryptoFile): String? {
+ return if (fileSource.cryptoArgs != null) {
+ val tmpFile = File(tmpDir, fileSource.filePath)
+ tmpFile.deleteOnExit()
+ try {
+ decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return null, tmpFile.absolutePath)
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString())
+ AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
+ return null
+ }
+ tmpFile.absolutePath
+ } else {
+ getAppFilePath(fileSource.filePath)
+ }
+}
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt
index 9b2911350f..cd206c8e4e 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt
@@ -59,20 +59,7 @@ actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = w
}
if (fileSource != null) {
- val filePath: String = if (fileSource.cryptoArgs != null) {
- val tmpFile = File(tmpDir, fileSource.filePath)
- tmpFile.deleteOnExit()
- try {
- decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return@withLongRunningApi, tmpFile.absolutePath)
- } catch (e: Exception) {
- Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString())
- AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
- return@withLongRunningApi
- }
- tmpFile.absolutePath
- } else {
- getAppFilePath(fileSource.filePath)
- }
+ val filePath = filePathForShare(fileSource) ?: return@withLongRunningApi
when {
desktopPlatform.isWindows() -> clipboard.setText(AnnotatedString("\"${File(filePath).absolutePath}\""))
else -> clipboard.setText(AnnotatedString(filePath))
diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties
index 0806f086da..aaebfcc0d5 100644
--- a/apps/multiplatform/gradle.properties
+++ b/apps/multiplatform/gradle.properties
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
-android.version_name=5.7.1
-android.version_code=202
+android.version_name=5.7.2
+android.version_code=204
-desktop.version_name=5.7.1
-desktop.version_code=42
+desktop.version_name=5.7.2
+desktop.version_code=43
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
diff --git a/cabal.project b/cabal.project
index 405634e515..39ac306bb2 100644
--- a/cabal.project
+++ b/cabal.project
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: c842e1a8c9cae03a74ca899744c419812a467433
+ tag: 7dee48d07a60ba2a6652f6b3ee6f8628d3dd80f2
source-repository-package
type: git
diff --git a/package.yaml b/package.yaml
index 7201b664ac..77911a7fe6 100644
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
name: simplex-chat
-version: 5.7.1.0
+version: 5.7.2.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 6db4b64113..a2a3592a49 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."c842e1a8c9cae03a74ca899744c419812a467433" = "01fvmijrh9n6lmjkba68p1bvppxk61rahsw47yqiph9n76qdpj1y";
+ "https://github.com/simplex-chat/simplexmq.git"."7dee48d07a60ba2a6652f6b3ee6f8628d3dd80f2" = "1wr90hpxyr3y8y3k5q9b0qg4g6aggd3ibpa8jpsnsnb0gwqg235g";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index e9e1a4a723..7baa6ee745 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
-version: 5.7.1.0
+version: 5.7.2.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -141,6 +141,7 @@ library
Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
Simplex.Chat.Migrations.M20240324_custom_data
Simplex.Chat.Migrations.M20240402_item_forwarded
+ Simplex.Chat.Migrations.M20240430_ui_theme
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
@@ -176,6 +177,7 @@ library
Simplex.Chat.Types
Simplex.Chat.Types.Preferences
Simplex.Chat.Types.Shared
+ Simplex.Chat.Types.UITheme
Simplex.Chat.Types.Util
Simplex.Chat.Util
Simplex.Chat.View
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 7fed6ba266..1ca0bdc478 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -249,6 +249,7 @@ newChatController
showLiveItems <- newTVarIO False
encryptLocalFiles <- newTVarIO False
tempDirectory <- newTVarIO optTempDirectory
+ assetsDirectory <- newTVarIO Nothing
contactMergeEnabled <- newTVarIO True
pure
ChatController
@@ -285,6 +286,7 @@ newChatController
showLiveItems,
encryptLocalFiles,
tempDirectory,
+ assetsDirectory,
logFilePath = logFile,
contactMergeEnabled
}
@@ -630,6 +632,17 @@ processChatCommand' vr = \case
createDirectoryIfMissing True rf
chatWriteVar remoteHostsFolder $ Just rf
ok_
+ -- has to be called before StartChat
+ APISetAppFilePaths cfg -> do
+ setFolder filesFolder $ appFilesFolder cfg
+ setFolder tempDirectory $ appTempFolder cfg
+ setFolder assetsDirectory $ appAssetsFolder cfg
+ mapM_ (setFolder remoteHostsFolder) $ appRemoteHostsFolder cfg
+ ok_
+ where
+ setFolder sel f = do
+ createDirectoryIfMissing True f
+ chatWriteVar sel $ Just f
APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_
SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_
APIExportArchive cfg -> checkChatStopped $ lift (exportArchive cfg) >> ok_
@@ -1226,6 +1239,25 @@ processChatCommand' vr = \case
conn <- getPendingContactConnection db userId connId
liftIO $ updateContactConnectionAlias db userId conn localAlias
pure $ CRConnectionAliasUpdated user conn'
+ APISetUserUIThemes uId uiThemes -> withUser $ \user@User {userId} -> do
+ user'@User {userId = uId'} <- withStore $ \db -> do
+ user' <- getUser db uId
+ liftIO $ setUserUIThemes db user uiThemes
+ pure user'
+ when (userId == uId') $ chatWriteVar currentUser $ Just (user :: User) {uiThemes}
+ ok user'
+ APISetChatUIThemes (ChatRef cType chatId) uiThemes -> withUser $ \user -> case cType of
+ CTDirect -> do
+ withStore $ \db -> do
+ ct <- getContact db vr user chatId
+ liftIO $ setContactUIThemes db user ct uiThemes
+ ok user
+ CTGroup -> do
+ withStore $ \db -> do
+ g <- getGroupInfo db vr user chatId
+ liftIO $ setGroupUIThemes db user g uiThemes
+ ok user
+ _ -> pure $ chatCmdError (Just user) "not supported"
APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text
APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken
APIRegisterToken token mode -> withUser $ \_ ->
@@ -2007,9 +2039,9 @@ processChatCommand' vr = \case
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
withFileLock "receiveFile" fileId . procCmd $ do
- (user, ft) <- withStore (`getRcvFileTransferById` fileId)
+ (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId)
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
- ft' <- (if encrypt then setFileToEncrypt else pure) ft
+ ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft
receiveFile' user ft' rcvInline_ filePath_
SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do
withFileLock "setFileToReceive" fileId . procCmd $ do
@@ -3578,7 +3610,7 @@ processAgentMessageNoConn = \case
processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM ()
processAgentMsgSndFile _corrId aFileId msg = do
(cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId)
- withEntityLock_ cRef_ $ withFileLock "processAgentMsgSndFile" fileId $
+ withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $
withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case
Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user))
_ -> do
@@ -3705,7 +3737,7 @@ splitFileDescr rfdText = do
processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM ()
processAgentMsgRcvFile _corrId aFileId msg = do
(cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId)
- withEntityLock_ cRef_ $ withFileLock "processAgentMsgRcvFile" fileId $
+ withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $
withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case
Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user))
_ -> do
@@ -7015,9 +7047,13 @@ chatCommandP =
"/_app activate" $> APIActivateChat True,
"/_app suspend " *> (APISuspendChat <$> A.decimal),
"/_resubscribe all" $> ResubscribeAllConnections,
+ -- deprecated, use /set file paths
"/_temp_folder " *> (SetTempFolder <$> filePath),
+ -- /_files_folder deprecated, use /set file paths
("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath),
+ -- deprecated, use /set file paths
"/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath),
+ "/set file paths " *> (APISetAppFilePaths <$> jsonP),
"/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP),
"/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
"/_db export " *> (APIExportArchive <$> jsonP),
@@ -7073,6 +7109,8 @@ chatCommandP =
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP),
+ "/_set theme user " *> (APISetUserUIThemes <$> A.decimal <*> optional (A.space *> jsonP)),
+ "/_set theme " *> (APISetChatUIThemes <$> chatRefP <*> optional (A.space *> jsonP)),
"/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString),
"/_ntf get" $> APIGetNtfToken,
"/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP),
diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs
index 572ce0c67b..6996cc1d87 100644
--- a/src/Simplex/Chat/AppSettings.hs
+++ b/src/Simplex/Chat/AppSettings.hs
@@ -11,6 +11,7 @@ import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ
import Data.Maybe (fromMaybe)
import Data.Text (Text)
+import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Client (NetworkConfig, defaultNetworkConfig)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
import Simplex.Messaging.Util (catchAll_)
@@ -43,7 +44,11 @@ data AppSettings = AppSettings
confirmDBUpgrades :: Maybe Bool,
androidCallOnLockScreen :: Maybe LockScreenCalls,
iosCallKitEnabled :: Maybe Bool,
- iosCallKitCallsInRecents :: Maybe Bool
+ iosCallKitCallsInRecents :: Maybe Bool,
+ uiProfileImageCornerRadius :: Maybe Double,
+ uiColorScheme :: Maybe UIColorScheme,
+ uiDarkColorScheme :: Maybe DarkColorScheme,
+ uiThemes :: Maybe UIThemes
}
deriving (Show)
@@ -69,7 +74,11 @@ defaultAppSettings =
confirmDBUpgrades = Just False,
androidCallOnLockScreen = Just LSCShow,
iosCallKitEnabled = Just True,
- iosCallKitCallsInRecents = Just False
+ iosCallKitCallsInRecents = Just False,
+ uiProfileImageCornerRadius = Just 22.5,
+ uiColorScheme = Just UCSSystem,
+ uiDarkColorScheme = Just DCSSimplex,
+ uiThemes = Nothing
}
defaultParseAppSettings :: AppSettings
@@ -94,13 +103,17 @@ defaultParseAppSettings =
confirmDBUpgrades = Nothing,
androidCallOnLockScreen = Nothing,
iosCallKitEnabled = Nothing,
- iosCallKitCallsInRecents = Nothing
+ iosCallKitCallsInRecents = Nothing,
+ uiProfileImageCornerRadius = Nothing,
+ uiColorScheme = Nothing,
+ uiDarkColorScheme = Nothing,
+ uiThemes = Nothing
}
combineAppSettings :: AppSettings -> AppSettings -> AppSettings
combineAppSettings platformDefaults storedSettings =
AppSettings
- { appPlatform = p appPlatform,
+ { appPlatform = p appPlatform,
networkConfig = p networkConfig,
privacyEncryptLocalFiles = p privacyEncryptLocalFiles,
privacyAcceptImages = p privacyAcceptImages,
@@ -119,7 +132,11 @@ combineAppSettings platformDefaults storedSettings =
confirmDBUpgrades = p confirmDBUpgrades,
iosCallKitEnabled = p iosCallKitEnabled,
iosCallKitCallsInRecents = p iosCallKitCallsInRecents,
- androidCallOnLockScreen = p androidCallOnLockScreen
+ androidCallOnLockScreen = p androidCallOnLockScreen,
+ uiProfileImageCornerRadius = p uiProfileImageCornerRadius,
+ uiColorScheme = p uiColorScheme,
+ uiDarkColorScheme = p uiDarkColorScheme,
+ uiThemes = p uiThemes
}
where
p :: (AppSettings -> Maybe a) -> Maybe a
@@ -157,6 +174,10 @@ instance FromJSON AppSettings where
iosCallKitEnabled <- p "iosCallKitEnabled"
iosCallKitCallsInRecents <- p "iosCallKitCallsInRecents"
androidCallOnLockScreen <- p "androidCallOnLockScreen"
+ uiProfileImageCornerRadius <- p "uiProfileImageCornerRadius"
+ uiColorScheme <- p "uiColorScheme"
+ uiDarkColorScheme <- p "uiDarkColorScheme"
+ uiThemes <- p "uiThemes"
pure
AppSettings
{ appPlatform,
@@ -178,7 +199,11 @@ instance FromJSON AppSettings where
confirmDBUpgrades,
iosCallKitEnabled,
iosCallKitCallsInRecents,
- androidCallOnLockScreen
+ androidCallOnLockScreen,
+ uiProfileImageCornerRadius,
+ uiColorScheme,
+ uiDarkColorScheme,
+ uiThemes
}
where
p key = v .:? key <|> pure Nothing
diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs
index d51f60f5f1..8550c03438 100644
--- a/src/Simplex/Chat/Archive.hs
+++ b/src/Simplex/Chat/Archive.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
@@ -44,14 +45,22 @@ archiveChatDbFile = "simplex_v1_chat.db"
archiveFilesFolder :: String
archiveFilesFolder = "simplex_v1_files"
+archiveAssetsFolder :: String
+archiveAssetsFolder = "simplex_v1_assets"
+
+wallpapersFolder :: String
+wallpapersFolder = "wallpapers"
+
exportArchive :: ArchiveConfig -> CM' ()
exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
withTempDir cfg "simplex-chat." $ \dir -> do
- StorageFiles {chatStore, agentStore, filesPath} <- storageFiles
+ StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles
copyFile (dbFilePath chatStore) $ dir > archiveChatDbFile
copyFile (dbFilePath agentStore) $ dir > archiveAgentDbFile
forM_ filesPath $ \fp ->
copyDirectoryFiles fp $ dir > archiveFilesFolder
+ forM_ assetsPath $ \fp ->
+ copyDirectoryFiles (fp > wallpapersFolder) $ dir > archiveAssetsFolder > wallpapersFolder
let method = if disableCompression == Just True then Z.Store else Z.Deflate
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
@@ -59,24 +68,24 @@ importArchive :: ArchiveConfig -> CM' [ArchiveError]
importArchive cfg@ArchiveConfig {archivePath} =
withTempDir cfg "simplex-chat." $ \dir -> do
Z.withArchive archivePath $ Z.unpackInto dir
- fs@StorageFiles {chatStore, agentStore, filesPath} <- storageFiles
+ fs@StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles
liftIO $ closeSQLiteStore `withStores` fs
backup `withDBs` fs
copyFile (dir > archiveChatDbFile) $ dbFilePath chatStore
copyFile (dir > archiveAgentDbFile) $ dbFilePath agentStore
- copyFiles dir filesPath
- `E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e]
+ errs <- copyFiles (dir > archiveFilesFolder) filesPath
+ errs' <- copyFiles (dir > archiveAssetsFolder > wallpapersFolder) ((> wallpapersFolder) <$> assetsPath)
+ pure $ errs <> errs'
where
backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak"
- copyFiles dir filesPath = do
- let filesDir = dir > archiveFilesFolder
- case filesPath of
- Just fp ->
- ifM
- (doesDirectoryExist filesDir)
- (copyDirectoryFiles filesDir fp)
- (pure [])
- _ -> pure []
+ copyFiles fromDir = \case
+ Just fp ->
+ ifM
+ (doesDirectoryExist fromDir)
+ (copyDirectoryFiles fromDir fp)
+ (pure [])
+ `E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e]
+ _ -> pure []
withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a)
withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of
@@ -85,7 +94,7 @@ withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of
copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError]
copyDirectoryFiles fromDir toDir = do
- createDirectoryIfMissing False toDir
+ createDirectoryIfMissing True toDir
fs <- listDirectory fromDir
foldM copyFileCatchError [] fs
where
@@ -103,6 +112,7 @@ deleteStorage = do
liftIO $ closeSQLiteStore `withStores` fs
remove `withDBs` fs
mapM_ removeDir $ filesPath fs
+ mapM_ removeDir $ assetsPath fs
mapM_ removeDir =<< chatReadVar tempDirectory
where
remove f = whenM (doesFileExist f) $ removeFile f
@@ -111,15 +121,17 @@ deleteStorage = do
data StorageFiles = StorageFiles
{ chatStore :: SQLiteStore,
agentStore :: SQLiteStore,
- filesPath :: Maybe FilePath
+ filesPath :: Maybe FilePath,
+ assetsPath :: Maybe FilePath
}
storageFiles :: CM' StorageFiles
storageFiles = do
- ChatController {chatStore, filesFolder, smpAgent} <- ask
+ ChatController {chatStore, filesFolder, assetsDirectory, smpAgent} <- ask
let agentStore = agentClientStore smpAgent
filesPath <- readTVarIO filesFolder
- pure StorageFiles {chatStore, agentStore, filesPath}
+ assetsPath <- readTVarIO assetsDirectory
+ pure StorageFiles {chatStore, agentStore, filesPath, assetsPath}
sqlCipherExport :: DBEncryptionConfig -> CM ()
sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key', keepKey} =
@@ -177,9 +189,9 @@ testSQL k =
T.unlines $
keySQL k
<> [ "PRAGMA foreign_keys = ON;",
- "PRAGMA secure_delete = ON;",
- "SELECT count(*) FROM sqlite_master;"
- ]
+ "PRAGMA secure_delete = ON;",
+ "SELECT count(*) FROM sqlite_master;"
+ ]
keySQL :: BA.ScrubbedBytes -> [Text]
keySQL k = ["PRAGMA key = " <> keyString k <> ";" | not (BA.null k)]
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 013b774e05..418f4df853 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -63,6 +63,7 @@ import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserCont
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
+import Simplex.Chat.Types.UITheme
import Simplex.Chat.Util (liftIOEither)
import Simplex.FileTransfer.Description (FileDescriptionURI)
import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo)
@@ -229,6 +230,7 @@ data ChatController = ChatController
showLiveItems :: TVar Bool,
encryptLocalFiles :: TVar Bool,
tempDirectory :: TVar (Maybe FilePath),
+ assetsDirectory :: TVar (Maybe FilePath),
logFilePath :: Maybe FilePath,
contactMergeEnabled :: TVar Bool
}
@@ -265,6 +267,7 @@ data ChatCommand
| SetTempFolder FilePath
| SetFilesFolder FilePath
| SetRemoteHostsFolder FilePath
+ | APISetAppFilePaths AppFilePathsConfig
| APISetEncryptLocalFiles Bool
| SetContactMergeEnabled Bool
| APIExportArchive ArchiveConfig
@@ -311,6 +314,8 @@ data ChatCommand
| APISetContactPrefs ContactId Preferences
| APISetContactAlias ContactId LocalAlias
| APISetConnectionAlias Int64 LocalAlias
+ | APISetUserUIThemes UserId (Maybe UIThemeEntityOverrides)
+ | APISetChatUIThemes ChatRef (Maybe UIThemeEntityOverrides)
| APIParseMarkdown Text
| APIGetNtfToken
| APIRegisterToken DeviceToken NotificationsMode
@@ -926,6 +931,14 @@ instance StrEncoding DBEncryptionKey where
instance FromJSON DBEncryptionKey where
parseJSON = strParseJSON "DBEncryptionKey"
+data AppFilePathsConfig = AppFilePathsConfig
+ { appFilesFolder :: FilePath,
+ appTempFolder :: FilePath,
+ appAssetsFolder :: FilePath,
+ appRemoteHostsFolder :: Maybe FilePath
+ }
+ deriving (Show)
+
data ContactSubStatus = ContactSubStatus
{ contact :: Contact,
contactError :: Maybe ChatError
@@ -1397,6 +1410,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError)
+$(JQ.deriveJSON defaultJSON ''AppFilePathsConfig)
+
$(JQ.deriveJSON defaultJSON ''ContactSubStatus)
$(JQ.deriveJSON defaultJSON ''MemberSubStatus)
diff --git a/src/Simplex/Chat/Migrations/M20240430_ui_theme.hs b/src/Simplex/Chat/Migrations/M20240430_ui_theme.hs
new file mode 100644
index 0000000000..1f4b9805cf
--- /dev/null
+++ b/src/Simplex/Chat/Migrations/M20240430_ui_theme.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Simplex.Chat.Migrations.M20240430_ui_theme where
+
+import Database.SQLite.Simple (Query)
+import Database.SQLite.Simple.QQ (sql)
+
+m20240430_ui_theme :: Query
+m20240430_ui_theme =
+ [sql|
+ALTER TABLE users ADD COLUMN ui_themes TEXT;
+ALTER TABLE contacts ADD COLUMN ui_themes TEXT;
+ALTER TABLE groups ADD COLUMN ui_themes TEXT;
+|]
+
+down_m20240430_ui_theme :: Query
+down_m20240430_ui_theme =
+ [sql|
+ALTER TABLE users DROP COLUMN ui_themes;
+ALTER TABLE contacts DROP COLUMN ui_themes;
+ALTER TABLE groups DROP COLUMN ui_themes;
+|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index f2d8e59ca7..f2f94d019c 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -34,7 +34,8 @@ CREATE TABLE users(
show_ntfs INTEGER NOT NULL DEFAULT 1,
send_rcpts_contacts INTEGER NOT NULL DEFAULT 0,
send_rcpts_small_groups INTEGER NOT NULL DEFAULT 0,
- user_member_profile_updated_at TEXT, -- 1 for active user
+ user_member_profile_updated_at TEXT,
+ ui_themes TEXT, -- 1 for active user
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE RESTRICT
@@ -74,6 +75,7 @@ CREATE TABLE contacts(
contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0,
contact_status TEXT NOT NULL DEFAULT 'active',
custom_data BLOB,
+ ui_themes TEXT,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -122,7 +124,8 @@ CREATE TABLE groups(
send_rcpts INTEGER,
via_group_link_uri_hash BLOB,
user_member_profile_sent_at TEXT,
- custom_data BLOB, -- received
+ custom_data BLOB,
+ ui_themes TEXT, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index d42fd1d6f6..bae9d00bfd 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -22,8 +22,6 @@ import Control.Monad
import Control.Monad.Except
import Data.Int (Int64)
import Data.Maybe (catMaybes, fromMaybe)
-import Data.Text (Text)
-import Data.Time.Clock (UTCTime (..))
import Database.SQLite.Simple (Only (..), (:.) (..))
import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Protocol
@@ -32,7 +30,6 @@ import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
-import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (ConnId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -100,20 +97,20 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
db
[sql|
SELECT
- c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
- p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.custom_data
+ c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
+ p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.custom_data
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|]
(userId, contactId)
- toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData)] -> Either StoreError Contact
- toContact' contactId conn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)] =
+ toContact' :: Int64 -> Connection -> [ContactRow'] -> Either StoreError Contact
+ toContact' contactId conn [(profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)] =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
activeConn = Just conn
- in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData}
+ in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData}
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ groupMemberId c = ExceptT $ do
@@ -125,7 +122,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
- g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
+ g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs
index 650940b520..deb0f9fc4c 100644
--- a/src/Simplex/Chat/Store/Direct.hs
+++ b/src/Simplex/Chat/Store/Direct.hs
@@ -69,6 +69,7 @@ module Simplex.Chat.Store.Direct
setConnConnReqInv,
resetContactConnInitiated,
setContactCustomData,
+ setContactUIThemes,
)
where
@@ -87,6 +88,7 @@ import Simplex.Chat.Messages
import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
+import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -176,7 +178,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash =
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
- cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
+ cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -222,7 +224,26 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p
let profile = toLocalProfile profileId p localAlias
userPreferences = emptyChatPrefs
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
- pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
+ pure $
+ Contact
+ { contactId,
+ localDisplayName,
+ profile,
+ activeConn = Just conn,
+ viaGroup = Nothing,
+ contactUsed,
+ contactStatus = CSActive,
+ chatSettings = defaultChatSettings,
+ userPreferences,
+ mergedPreferences,
+ createdAt = currentTs,
+ updatedAt = currentTs,
+ chatTs = Just currentTs,
+ contactGroupMemberId = Nothing,
+ contactGrpInvSent = False,
+ customData = Nothing,
+ uiThemes = Nothing
+ }
deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO ()
deleteContactConnectionsAndFiles db userId Contact {contactId} = do
@@ -579,7 +600,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
- cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
+ cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -725,7 +746,26 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
contactId <- insertedRowId db
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
- pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
+ pure $
+ Contact
+ { contactId,
+ localDisplayName,
+ profile = toLocalProfile profileId profile "",
+ activeConn = Just conn,
+ viaGroup = Nothing,
+ contactUsed,
+ contactStatus = CSActive,
+ chatSettings = defaultChatSettings,
+ userPreferences,
+ mergedPreferences,
+ createdAt,
+ updatedAt = createdAt,
+ chatTs = Just createdAt,
+ contactGroupMemberId = Nothing,
+ contactGrpInvSent = False,
+ uiThemes = Nothing,
+ customData = Nothing
+ }
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
getContactIdByName db User {userId} cName =
@@ -744,7 +784,7 @@ getContact_ db vr user@User {userId} contactId deleted =
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
- cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
+ cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -889,3 +929,8 @@ setContactCustomData :: DB.Connection -> User -> Contact -> Maybe CustomData ->
setContactCustomData db User {userId} Contact {contactId} customData = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE contacts SET custom_data = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (customData, updatedAt, userId, contactId)
+
+setContactUIThemes :: DB.Connection -> User -> Contact -> Maybe UIThemeEntityOverrides -> IO ()
+setContactUIThemes db User {userId} Contact {contactId} uiThemes = do
+ updatedAt <- getCurrentTime
+ DB.execute db "UPDATE contacts SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (uiThemes, updatedAt, userId, contactId)
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index 23796f95e7..d8655c818b 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -3,8 +3,8 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
@@ -117,6 +117,7 @@ module Simplex.Chat.Store.Groups
updateUnknownMemberAnnounced,
updateUserMemberProfileSentAt,
setGroupCustomData,
+ setGroupUIThemes,
)
where
@@ -141,6 +142,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
+import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -151,19 +153,19 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
import Simplex.Messaging.Version
import UnliftIO.STM
-type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe CustomData) :. GroupMemberRow
+type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow
type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences))
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo
-toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData) :. userMemberRow) =
+toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
- in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData}
+ in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData}
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) =
@@ -274,7 +276,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
- g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
+ g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -348,6 +350,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
updatedAt = currentTs,
chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs,
+ uiThemes = Nothing,
customData = Nothing
}
@@ -414,6 +417,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
updatedAt = currentTs,
chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs,
+ uiThemes = Nothing,
customData = Nothing
},
groupMemberId
@@ -633,7 +637,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
- g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
+ g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences
FROM groups g
@@ -1298,7 +1302,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
- g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
+ g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1394,7 +1398,7 @@ getGroupInfo db vr User {userId, userContactId} groupId =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
- g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
+ g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1956,7 +1960,7 @@ createMemberContact
authErrCounter = 0
}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
- pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, customData = Nothing}
+ pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing}
getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do
@@ -1993,7 +1997,7 @@ createMemberContactInvited
contactId <- createContactUpdateMember currentTs userPreferences
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
- mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
+ mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing}
m' = m {memberContactId = Just contactId}
pure (mCt', m')
where
@@ -2198,3 +2202,8 @@ setGroupCustomData :: DB.Connection -> User -> GroupInfo -> Maybe CustomData ->
setGroupCustomData db User {userId} GroupInfo {groupId} customData = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE groups SET custom_data = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (customData, updatedAt, userId, groupId)
+
+setGroupUIThemes :: DB.Connection -> User -> GroupInfo -> Maybe UIThemeEntityOverrides -> IO ()
+setGroupUIThemes db User {userId} GroupInfo {groupId} uiThemes = do
+ updatedAt <- getCurrentTime
+ DB.execute db "UPDATE groups SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (uiThemes, updatedAt, userId, groupId)
diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs
index 7a3fb75da3..2b44778272 100644
--- a/src/Simplex/Chat/Store/Migrations.hs
+++ b/src/Simplex/Chat/Store/Migrations.hs
@@ -105,6 +105,7 @@ import Simplex.Chat.Migrations.M20240228_pq
import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
import Simplex.Chat.Migrations.M20240324_custom_data
import Simplex.Chat.Migrations.M20240402_item_forwarded
+import Simplex.Chat.Migrations.M20240430_ui_theme
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -209,7 +210,8 @@ schemaMigrations =
("20240228_pq", m20240228_pq, Just down_m20240228_pq),
("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id),
("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data),
- ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded)
+ ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded),
+ ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme)
]
-- | The list of migrations in ascending order by date
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index c92bf3391c..9b83e7299f 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -57,6 +57,7 @@ module Simplex.Chat.Store.Profiles
deleteCommand,
updateCommandStatus,
getCommandDataByCorrId,
+ setUserUIThemes,
)
where
@@ -82,6 +83,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
+import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -123,7 +125,7 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image,
(profileId, displayName, userId, True, currentTs, currentTs, currentTs)
contactId <- insertedRowId db
DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId)
- pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing, Nothing)
+ pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing, Nothing, Nothing)
getUsersInfo :: DB.Connection -> IO [UserInfo]
getUsersInfo db = getUsers db >>= mapM getUserInfo
@@ -274,8 +276,8 @@ updateUserProfile db user p'
where
updateUserMemberProfileUpdatedAt_ currentTs
| userMemberProfileChanged = do
- DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId)
- pure $ Just currentTs
+ DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId)
+ pure $ Just currentTs
| otherwise = pure userMemberProfileUpdatedAt
userMemberProfileChanged = newName /= displayName || newFullName /= fullName || newImage /= image
User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, image, localAlias}, userMemberProfileUpdatedAt} = user
@@ -619,3 +621,8 @@ getCommandDataByCorrId db User {userId} corrId =
where
toCommandData :: (CommandId, Maybe Int64, CommandFunction, CommandStatus) -> CommandData
toCommandData (cmdId, cmdConnId, cmdFunction, cmdStatus) = CommandData {cmdId, cmdConnId, cmdFunction, cmdStatus}
+
+setUserUIThemes :: DB.Connection -> User -> Maybe UIThemeEntityOverrides -> IO ()
+setUserUIThemes db User {userId} uiThemes = do
+ updatedAt <- getCurrentTime
+ DB.execute db "UPDATE users SET ui_themes = ?, updated_at = ? WHERE user_id = ?" (uiThemes, updatedAt, userId)
diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs
index fe86b0c2df..1f16ebfef0 100644
--- a/src/Simplex/Chat/Store/Shared.hs
+++ b/src/Simplex/Chat/Store/Shared.hs
@@ -3,8 +3,8 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
@@ -33,6 +33,7 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Remote.Types
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
+import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -380,16 +381,18 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|]
[":user_id" := userId, ":profile_id" := profileId]
-type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData)
+type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe UIThemeEntityOverrides, Maybe CustomData)
+
+type ContactRow = Only ContactId :. ContactRow'
toContact :: VersionRangeChat -> User -> ContactRow :. MaybeConnectionRow -> Contact
-toContact vr user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)) :. connRow) =
+toContact vr user ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
activeConn = toMaybeConnection vr connRow
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
incognito = maybe False connIncognito activeConn
mergedPreferences = contactUserPreferences user userPreferences preferences incognito
- in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData}
+ in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData}
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile
getProfileById db userId profileId =
@@ -418,15 +421,15 @@ userQuery :: Query
userQuery =
[sql|
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences,
- u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at
+ u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
|]
-toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (Bool, Bool, Bool, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime) -> User
-toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, contactLink, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt)) =
- User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt}
+toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (Bool, Bool, Bool, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User
+toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, contactLink, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) =
+ User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt, uiThemes}
where
profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences = userPreferences, localAlias = ""}
fullPreferences = mergePreferences Nothing userPreferences
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index f7174a635b..0d07d5e3cb 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -45,6 +45,7 @@ import Database.SQLite.Simple.Ok
import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
+import Simplex.Chat.Types.UITheme
import Simplex.Chat.Types.Util
import Simplex.FileTransfer.Description (FileDigest)
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId)
@@ -117,7 +118,8 @@ data User = User
showNtfs :: Bool,
sendRcptsContacts :: Bool,
sendRcptsSmallGroups :: Bool,
- userMemberProfileUpdatedAt :: Maybe UTCTime
+ userMemberProfileUpdatedAt :: Maybe UTCTime,
+ uiThemes :: Maybe UIThemeEntityOverrides
}
deriving (Show)
@@ -175,6 +177,7 @@ data Contact = Contact
chatTs :: Maybe UTCTime,
contactGroupMemberId :: Maybe GroupMemberId,
contactGrpInvSent :: Bool,
+ uiThemes :: Maybe UIThemeEntityOverrides,
customData :: Maybe CustomData
}
deriving (Eq, Show)
@@ -372,6 +375,7 @@ data GroupInfo = GroupInfo
updatedAt :: UTCTime,
chatTs :: Maybe UTCTime,
userMemberProfileSentAt :: Maybe UTCTime,
+ uiThemes :: Maybe UIThemeEntityOverrides,
customData :: Maybe CustomData
}
deriving (Eq, Show)
diff --git a/src/Simplex/Chat/Types/UITheme.hs b/src/Simplex/Chat/Types/UITheme.hs
new file mode 100644
index 0000000000..9f9c106d1f
--- /dev/null
+++ b/src/Simplex/Chat/Types/UITheme.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Simplex.Chat.Types.UITheme where
+
+import Data.Aeson (FromJSON (..), ToJSON (..))
+import qualified Data.Aeson as J
+import qualified Data.Aeson.TH as JQ
+import qualified Data.Attoparsec.ByteString.Char8 as A
+import Data.Char (toLower)
+import Data.Maybe (fromMaybe)
+import Database.SQLite.Simple.FromField (FromField (..))
+import Database.SQLite.Simple.ToField (ToField (..))
+import Simplex.Chat.Types.Util
+import Simplex.Messaging.Encoding.String
+import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_)
+import Simplex.Messaging.Util ((<$?>))
+
+data UIThemes = UIThemes
+ { light :: Maybe UITheme,
+ dark :: Maybe UITheme,
+ simplex :: Maybe UITheme
+ }
+ deriving (Eq, Show)
+
+data UITheme = UITheme
+ { base :: ThemeColorScheme,
+ wallpaper :: Maybe ChatWallpaper,
+ colors :: UIColors
+ }
+ deriving (Eq, Show)
+
+data UIColorMode = UCMLight | UCMDark
+ deriving (Eq, Show)
+
+data UIThemeEntityOverrides = UIThemeEntityOverrides
+ { light :: Maybe UIThemeEntityOverride,
+ dark :: Maybe UIThemeEntityOverride
+ }
+ deriving (Eq, Show)
+
+data UIThemeEntityOverride = UIThemeEntityOverride
+ { mode :: UIColorMode,
+ wallpaper :: Maybe ChatWallpaper,
+ colors :: UIColors
+ }
+ deriving (Eq, Show)
+
+data ThemeColorScheme = TCSLight | TCSDark | TCSSimplex
+ deriving (Eq, Show)
+
+data UIColorScheme
+ = UCSSystem
+ | UCSLight
+ | UCSDark
+ | UCSSimplex
+ deriving (Show)
+
+data DarkColorScheme = DCSDark | DCSSimplex
+ deriving (Show)
+
+instance StrEncoding ThemeColorScheme where
+ strEncode = \case
+ TCSLight -> "LIGHT"
+ TCSDark -> "DARK"
+ TCSSimplex -> "SIMPLEX"
+ strDecode = \case
+ "LIGHT" -> Right TCSLight
+ "DARK" -> Right TCSDark
+ "SIMPLEX" -> Right TCSSimplex
+ _ -> Left "bad ColorScheme"
+ strP = strDecode <$?> A.takeTill (== ' ')
+
+instance FromJSON ThemeColorScheme where
+ parseJSON = strParseJSON "ThemeColorScheme"
+
+instance ToJSON ThemeColorScheme where
+ toJSON = strToJSON
+ toEncoding = strToJEncoding
+
+data ChatWallpaper = ChatWallpaper
+ { preset :: Maybe ChatWallpaperPreset,
+ imageFile :: Maybe FilePath,
+ background :: Maybe UIColor,
+ tint :: Maybe UIColor,
+ scaleType :: Maybe ChatWallpaperScale,
+ scale :: Maybe Double
+ }
+ deriving (Eq, Show)
+
+data ChatWallpaperScale = CWSFill | CWSFit | CWSRepeat
+ deriving (Eq, Show)
+
+data UIColors = UIColors
+ { accent :: Maybe UIColor,
+ accentVariant :: Maybe UIColor,
+ secondary :: Maybe UIColor,
+ secondaryVariant :: Maybe UIColor,
+ background :: Maybe UIColor,
+ menus :: Maybe UIColor,
+ title :: Maybe UIColor,
+ sentMessage :: Maybe UIColor,
+ receivedMessage :: Maybe UIColor
+ }
+ deriving (Eq, Show)
+
+defaultUIColors :: UIColors
+defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
+
+data ChatWallpaperPreset
+ = CWPKids
+ | CWPCats
+ | CWPPets
+ | CWPFlowers
+ | CWPHearts
+ | CWPSocial
+ | CWPTravel
+ | CWPInternet
+ | CWPSpace
+ | CWPSchool
+ deriving (Eq, Show)
+
+newtype UIColor = UIColor String
+ deriving (Eq, Show)
+
+instance FromJSON UIColor where
+ parseJSON v = toColor =<< J.parseJSON v
+ where
+ toColor s@('#' : cs)
+ | length cs == 8 && all hexDigit cs = pure $ UIColor s
+ toColor _ = fail "bad UIColor"
+ hexDigit c = (c >= '0' && c <= '9') || (let c' = toLower c in c' >= 'a' && c' <= 'f')
+
+instance ToJSON UIColor where
+ toJSON (UIColor t) = J.toJSON t
+ toEncoding (UIColor t) = J.toEncoding t
+
+$(JQ.deriveJSON (enumJSON $ dropPrefix "DCS") ''DarkColorScheme)
+
+$(JQ.deriveJSON (enumJSON $ dropPrefix "UCM") ''UIColorMode)
+
+$(JQ.deriveJSON (enumJSON $ dropPrefix "UCS") ''UIColorScheme)
+
+$(JQ.deriveJSON (enumJSON $ dropPrefix "CWS") ''ChatWallpaperScale)
+
+$(JQ.deriveJSON (enumJSON $ dropPrefix "CWP") ''ChatWallpaperPreset)
+
+$(JQ.deriveJSON defaultJSON ''ChatWallpaper)
+
+$(JQ.deriveJSON defaultJSON ''UIColors)
+
+$(JQ.deriveJSON defaultJSON ''UIThemeEntityOverride)
+
+$(JQ.deriveJSON defaultJSON ''UIThemeEntityOverrides)
+
+$(JQ.deriveJSON defaultJSON ''UITheme)
+
+$(JQ.deriveJSON defaultJSON ''UIThemes)
+
+instance ToField UIThemeEntityOverrides where
+ toField = toField . encodeJSON
+
+instance FromField UIThemeEntityOverrides where
+ fromField = fromTextField_ $ Just . fromMaybe (UIThemeEntityOverrides Nothing Nothing) . decodeJSON
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 0ef54e3259..ca0af67344 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -49,6 +49,7 @@ import Simplex.Chat.Styled
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
+import Simplex.Chat.Types.UITheme
import qualified Simplex.FileTransfer.Transport as XFTPTransport
import Simplex.Messaging.Agent.Client (ActivePendingSubs (..), ProtocolTestFailure (..), ProtocolTestStep (..), SubInfo (..), SubscriptionsInfo (..))
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
@@ -82,7 +83,7 @@ serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . response
responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString]
responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case
- CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile
+ CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes
CRUsersList users -> viewUsersList users
CRChatStarted -> ["chat started"]
CRChatRunning -> ["chat is running"]
@@ -1204,7 +1205,7 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
]
viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
-viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, customData} stats incognitoProfile =
+viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile =
["contact ID: " <> sShow contactId]
<> maybe [] viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink
@@ -1216,15 +1217,20 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
<> [viewConnectionVerified (contactSecurityCode ct)]
<> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn]
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
+ <> viewUITheme uiThemes
<> viewCustomData customData
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
-viewGroupInfo GroupInfo {groupId, customData} s =
+viewGroupInfo GroupInfo {groupId, uiThemes, customData} s =
[ "group ID: " <> sShow groupId,
"current members: " <> sShow (currentMembers s)
]
+ <> viewUITheme uiThemes
<> viewCustomData customData
+viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString]
+viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> plain (LB.toStrict $ J.encode uiThemes)])
+
viewCustomData :: Maybe CustomData -> [StyledString]
viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)])
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index 294540a62c..a8afa05af3 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}
@@ -15,6 +16,8 @@ import qualified Data.Text as T
import Simplex.Chat.Store.Shared (createContact)
import Simplex.Chat.Types (ConnStatus (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
+import Simplex.Chat.Types.UITheme
+import Simplex.Chat.Types.Util (encodeJSON)
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import System.Directory (copyFile, createDirectoryIfMissing)
import Test.Hspec hiding (it)
@@ -73,6 +76,7 @@ chatProfileTests = do
it "direct messages" testGroupPrefsDirectForRole
it "files & media" testGroupPrefsFilesForRole
it "SimpleX links" testGroupPrefsSimplexLinksForRole
+ it "set user, contact and group UI theme" testSetUITheme
testUpdateProfile :: HasCallStack => FilePath -> IO ()
testUpdateProfile =
@@ -1935,8 +1939,8 @@ testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danP
dan <## "#team: you joined the group"
dan
<### [ "#team: member alice (Alice) is connected",
- "#team: member bob (Bob) is connected"
- ],
+ "#team: member bob (Bob) is connected"
+ ],
do
alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
alice <## "#team: new member dan is connected",
@@ -1947,7 +1951,7 @@ testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danP
-- dan cannot send direct messages to alice (owner)
dan ##> "@alice hello alice"
dan <## "bad chat command: direct messages not allowed"
- (alice )
+ (alice )
-- but alice can
alice `send` "@dan hello dan"
alice <## "member #team dan does not have direct connection, creating"
@@ -2029,3 +2033,54 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil
cc <## "alice updated group #team:"
cc <## "updated group preferences:"
cc <## "SimpleX links: on for owners"
+
+testSetUITheme :: HasCallStack => FilePath -> IO ()
+testSetUITheme =
+ testChat2 aliceProfile bobProfile $ \alice bob -> do
+ connectUsers alice bob
+ alice ##> "/g team"
+ alice <## "group #team is created"
+ alice <## "to add members use /a team or /create link #team"
+ alice #$> ("/_set theme user 1 " <> theme UCMDark, id, "ok")
+ alice #$> ("/_set theme @2 " <> theme UCMDark, id, "ok")
+ alice #$> ("/_set theme #1 " <> theme UCMDark, id, "ok")
+ alice ##> "/u"
+ userInfo alice "alice (Alice)"
+ alice <## ("UI themes: " <> theme UCMDark)
+ alice ##> "/create user alice2"
+ userInfo alice "alice2"
+ alice ##> "/u alice"
+ userInfo alice "alice (Alice)"
+ alice <## ("UI themes: " <> theme UCMDark)
+ alice ##> "/i @bob"
+ contactInfo alice
+ alice <## ("UI themes: " <> theme UCMDark)
+ alice ##> "/i #team"
+ groupInfo alice
+ alice <## ("UI themes: " <> theme UCMDark)
+ alice #$> ("/_set theme user 1", id, "ok")
+ alice #$> ("/_set theme @2", id, "ok")
+ alice #$> ("/_set theme #1", id, "ok")
+ alice ##> "/u"
+ userInfo alice "alice (Alice)"
+ alice ##> "/i @bob"
+ contactInfo alice
+ alice ##> "/i #team"
+ groupInfo alice
+ where
+ theme cm = T.unpack $ encodeJSON UIThemeEntityOverrides {light = Nothing, dark = Just $ UIThemeEntityOverride cm Nothing defaultUIColors}
+ userInfo a name = do
+ a <## ("user profile: " <> name)
+ a <## "use /p to change it"
+ a <## "(the updated profile will be sent to all your contacts)"
+ contactInfo a = do
+ a <## "contact ID: 2"
+ a <## "receiving messages via: localhost"
+ a <## "sending messages via: localhost"
+ a <## "you've shared main profile with this contact"
+ a <## "connection not verified, use /code command to see security code"
+ a <## "quantum resistant end-to-end encryption"
+ a <## "peer chat protocol version range: (Version 1, Version 8)"
+ groupInfo a = do
+ a <## "group ID: 1"
+ a <## "current members: 1"
diff --git a/website/langs/cs.json b/website/langs/cs.json
index 6e2416e4a2..b8777eea38 100644
--- a/website/langs/cs.json
+++ b/website/langs/cs.json
@@ -252,5 +252,6 @@
"jobs": "Připojit k týmu",
"hero-overlay-card-3-p-2": "Trail of Bits přezkoumala kryptografii a síťové komponenty SimpleX platformy v listopadu 2022.",
"hero-overlay-card-3-p-3": "Přečtěte si více v ohlášení.",
- "docs-dropdown-9": "Ke stažení"
+ "docs-dropdown-9": "Ke stažení",
+ "docs-dropdown-10": "Transparentnost"
}
diff --git a/website/langs/fr.json b/website/langs/fr.json
index fc898ecd4b..7ead7882db 100644
--- a/website/langs/fr.json
+++ b/website/langs/fr.json
@@ -24,7 +24,7 @@
"copyright-label": "© 2020-2023 SimpleX | Projet Open-Source",
"simplex-chat-protocol": "Protocole SimpleX Chat",
"terminal-cli": "Terminal CLI",
- "terms-and-privacy-policy": "Conditions et politique de confidentialité",
+ "terms-and-privacy-policy": "Politique de confidentialité",
"hero-header": "La vie privée redéfinie",
"hero-subheader": "La première messagerie
sans identifiant d'utilisateur",
"hero-p-1": "Les autres applications ont des IDs d'utilisateur : Signal, Matrix, Session, Briar, Jami, Cwtch, etc.
SimpleX n'en a pas, pas même des nombres aléatoires.
Ce qui améliore radicalement votre vie privée.",
@@ -254,5 +254,7 @@
"docs-dropdown-9": "Téléchargements",
"please-enable-javascript": "Veuillez activer JavaScript pour voir le code QR.",
"please-use-link-in-mobile-app": "Veuillez utiliser le lien dans l'application mobile",
- "docs-dropdown-10": "Transparence"
+ "docs-dropdown-10": "Transparence",
+ "docs-dropdown-12": "Sécurité",
+ "docs-dropdown-11": "FAQ"
}
diff --git a/website/langs/ru.json b/website/langs/ru.json
index cf56f1dfd4..4a9100c76d 100644
--- a/website/langs/ru.json
+++ b/website/langs/ru.json
@@ -235,7 +235,7 @@
"privacy-matters-overlay-card-1-p-4": "Платформа SimpleX защищает конфиденциальность ваших контактов лучше, чем любая другая альтернатива, полностью предотвращая доступ к вашему социальному графику каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.",
"hero-overlay-card-1-p-6": "Подробнее читайте в техническом документе SimpleX.",
"simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями P2P имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.",
- "terms-and-privacy-policy": "Условия & Политика Конфиденциальности",
+ "terms-and-privacy-policy": "Политика Конфиденциальности",
"simplex-network-overlay-card-1-li-1": "Сети P2P полагаются на тот или иной вариант DHT для маршрутизации сообщений. Проекты DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети - O(log N).",
"privacy-matters-section-label": "Убедитесь, что ваш мессенджер не может получить доступ к вашим данным!",
"simplex-unique-overlay-card-3-p-1": "SimpleX Chat хранит все пользовательские данные на клиентских устройствах в портативном формате зашифрованной базы данных которую можно перенести на другое устройство.",
@@ -253,5 +253,6 @@
"simplex-private-5-title": "Многоуровневое
Заполнения содержимого",
"please-use-link-in-mobile-app": "Пожалуйста, воспользуйтесь ссылкой в мобильном приложении",
"please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код.",
- "docs-dropdown-10": "Прозрачность"
+ "docs-dropdown-10": "Прозрачность",
+ "docs-dropdown-12": "Безопасность"
}