Merge remote-tracking branch 'origin/master' into ab/diff-subs

This commit is contained in:
Alexander Bondarenko
2024-05-10 21:01:33 +03:00
42 changed files with 1213 additions and 170 deletions
@@ -7235,7 +7235,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.</target>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>kapcsolódva</target>
<target>kapcsolódott</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="message received" xml:space="preserve">
@@ -5371,7 +5371,7 @@ Hata: %@</target>
</trans-unit>
<trans-unit id="Square, circle, or anything in between." xml:space="preserve">
<source>Square, circle, or anything in between.</source>
<target>Kare,daire, veya aralarında herhangi birşey.</target>
<target>Kare,daire, veya aralarında herhangi bir şey.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
+32 -32
View File
@@ -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 = "<group>"; };
5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
5CE0E8922BE7F144008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CE0E8932BE7F144008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
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 = "<group>"; };
5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a"; sourceTree = "<group>"; };
5CE0E8962BE7F144008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CE0E89C2BEE807A008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CE0E89D2BEE807A008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CE0E89E2BEE807A008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
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 = "<group>"; };
5CE0E8A02BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a"; sourceTree = "<group>"; };
5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
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 = "<group>";
@@ -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;
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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";
@@ -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)
@@ -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)
@@ -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 {
@@ -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)
) {
@@ -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) {
@@ -106,13 +106,13 @@
<string name="contact_already_exists">مخاطب از قبل وجود دارد</string>
<string name="invalid_connection_link">لینک اتصال نامعتبر</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">لطفا بررسی کنید که از لینک صحیح استفاده کردید یا از مخاطبتان بخواهید لینک دیگری برایتان بفرستد.</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">مطمئن شوید قالب آدرس‌های سرور SMP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور SMP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string>
<string name="error_setting_network_config">خطا در به‌روزرسانی پیکربندی شبکه</string>
<string name="failed_to_parse_chat_title">عدم موفقیت در بارگیری گپ</string>
<string name="failed_to_create_user_invalid_title">نام نمایشی نامعتبر!</string>
<string name="failed_to_create_user_duplicate_desc">شما یک نمایه گپ با نام نمایشی یکسان دارید، لطفا نام دیگری انتخاب کنید.</string>
<string name="error_creating_address">خطا در ایجاد نشانی</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">مطمئن شوید قالب آدرس‌های سرور XFTP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور XFTP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string>
<string name="error_accepting_contact_request">خطا در پذیرش درخواست مخاطب</string>
<string name="sender_may_have_deleted_the_connection_request">فرستنده ممکن است درخواست اتصال را حذف کرده باشد.</string>
<string name="error_deleting_note_folder">خطا در حذف یادداشت‌های خصوصی</string>
@@ -204,4 +204,482 @@
<string name="error_showing_desktop_notification">خطا در نمایش اعلان، با توسعه‌دهندگان تماس بگیرید.</string>
<string name="lock_not_enabled">قفل SimpleX فعال نیست!</string>
<string name="auth_device_authentication_is_disabled_turning_off">تصدیق دستگاه غیرفعال است. قفل SimpleX خاموش می‌شود.</string>
<string name="icon_descr_sent_msg_status_sent">ارسال شده</string>
<string name="icon_descr_context">نماد زمینه</string>
<string name="icon_descr_cancel_image_preview">لغو پیش‌نمایش تصویر</string>
<string name="icon_descr_cancel_file_preview">لغو پیش‌نمایش پرونده</string>
<string name="system_restricted_background_warn"><![CDATA[برای فعال کردن اعلان‌ها، لطفا <b>مصرف باتری برنامه</b> / <b>نامحدود</b> را در تنظیمات برنامه انتخاب کنید.]]></string>
<string name="system_restricted_background_in_call_desc">برنامه ممکن است بعد از ۱ دقیقه در پس‌زمینه بسته شود.</string>
<string name="database_initialization_error_title">مقداردهی اولیه پایگاه داده ممکن نیست</string>
<string name="enter_passphrase_notification_title">عبارت عبور الزامی است</string>
<string name="enter_passphrase_notification_desc">برای دریافت اعلان‌ها، لطفا، عبارت عبور پایگاه داده را وارد کنید</string>
<string name="database_initialization_error_desc">پایگاه داده به درستی کار نمی‌کند. برای آگاهی بیشتر لمس کنید</string>
<string name="simplex_service_notification_title">سرویس SimpleX Chat</string>
<string name="simplex_service_notification_text">در حال دریافت پیام‌ها…</string>
<string name="la_notice_turn_on">روشن کردن</string>
<string name="la_auth_failed">عدم موفقیت تصدیق</string>
<string name="auth_unlock">گشودن قفل</string>
<string name="auth_confirm_credential">اطلاعات ورودتان را تایید کنید</string>
<string name="reply_verb">پاسخ</string>
<string name="edit_history">تاریخچه</string>
<string name="received_message">پیام دریافتی</string>
<string name="moderate_verb">حذف</string>
<string name="expand_verb">بسط دادن</string>
<string name="moderate_message_will_be_marked_warning">پیام برای تمام اعضا به عنوان حذف شده علامت‌گذاری خواهد شد.</string>
<string name="stop_file__confirm">توقف</string>
<string name="stop_rcv_file__message">دریافت پرونده متوقف خواهد شد.</string>
<string name="welcome">خوش آمدید!</string>
<string name="image_decoding_exception_title">خطا در رمزگشایی</string>
<string name="compose_send_direct_message_to_connect">ارسال پیام مستقیم برای اتصال</string>
<string name="loading_remote_file_desc">لطفا، تا زمانی که پرونده در حال بارگیری از موبایل متصل است، منتظر باشید.</string>
<string name="button_delete_contact">حذف مخاطب</string>
<string name="view_security_code">مشاهده رمز امنیتی</string>
<string name="verify_security_code">تایید رمز امنیتی</string>
<string name="send_disappearing_message">ارسال پیام ناپدید شونده</string>
<string name="add_contact">لینک دعوت یک‌بارمصرف</string>
<string name="create_group">ایجاد گروه محرمانه</string>
<string name="to_share_with_your_contact">(برای اشتراک‌گذاری با مخاطبتان)</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">اگر لینک دعوت SimpleX Chat دریافت کردید، می‌توانید آن را در مرورگر خود باز کنید:</string>
<string name="la_lock_mode_passcode">ورود رمز عبور</string>
<string name="la_seconds">%d ثانیه</string>
<string name="la_current_app_passcode">رمز عبور فعلی</string>
<string name="copy_verb">کپی</string>
<string name="delete_message_mark_deleted_warning">پیام به عنوان حذف شده علامت‌گذاری خواهد شد. گیرنده‌ها قادر خواهند بود این پیام را آشکار کنند.</string>
<string name="icon_descr_sent_msg_status_send_failed">ارسال غیرموفق</string>
<string name="contact_tap_to_connect">برای اتصال لمس کنید</string>
<string name="video_decoding_exception_desc">امکان رمزگشایی ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید.</string>
<string name="image_descr">تصویر</string>
<string name="file_not_found">پرونده پیدا نشد</string>
<string name="chat_help_tap_button">لمس دکمه</string>
<string name="create_group_button_to_create_new_group"><![CDATA[<b>ایجاد گروه</b>: برای ایجاد یک گروه جدید.]]></string>
<string name="system_restricted_background_in_call_title">بدون تماس‌های پس‌زمینه</string>
<string name="system_restricted_background_in_call_warn"><![CDATA[برای برقراری تماس‌ها در پس‌زمینه، لطفا <b>مصرف باتری برنامه</b> / <b>نامحدود</b> را در تنظیمات برنامه انتخاب کنید.]]></string>
<string name="la_authenticate">تصدیق</string>
<string name="edit_verb">ویرایش</string>
<string name="no_info_on_delivery">بدون اطلاعات تحویل</string>
<string name="revoke_file__action">لغو پرونده</string>
<string name="group_connection_pending">در حال اتصال…</string>
<string name="you_have_no_chats">شما هیچ گپی ندارید</string>
<string name="loading_chats">بارگذاری گپ‌ها…</string>
<string name="video_will_be_received_when_contact_completes_uploading">ویدئو وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string>
<string name="maximum_supported_file_size">بیشترین اندازه پشتیبانی شده فعلی پرونده %1$s است.</string>
<string name="error_saving_file">خطا در ذخیره‌سازی پرونده</string>
<string name="loading_remote_file_title">در حال بارگیری پرونده</string>
<string name="delete_and_notify_contact">حذف و مخاطب را باخبر کن</string>
<string name="icon_descr_server_status_connected">متصل</string>
<string name="sync_connection_force_question">مذاکره مجدد رمزگذاری؟</string>
<string name="you_need_to_allow_to_send_voice">شما نیاز دارید به مخاطبتان اجازه ارسال پیام‌های صوتی دهید تا بتوانید آن‌ها را ارسال کنید.</string>
<string name="send_live_message">ارسال پیام زنده</string>
<string name="disappearing_message">پیام ناپدید شونده</string>
<string name="send_disappearing_message_custom_time">زمان سفارشی</string>
<string name="copied">به حافظه کپی شد</string>
<string name="add_contact_or_create_group">شروع گپ جدید</string>
<string name="from_gallery_button">از گالری</string>
<string name="call_service_notification_video_call">تماس تصویری</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">برای محافظت از اطلاعاتتان، قفل SimpleX را روشن کنید.
\nاز شما خواسته خواهد شد قبل از فعال شدن این ویژگی، تصدیق را تکمیل کنید.</string>
<string name="la_no_app_password">عدم وجود رمز عبور</string>
<string name="la_minutes">%d دقیقه</string>
<string name="auth_enable_simplex_lock">فعال‌سازی قفل SimpleX</string>
<string name="la_enter_app_passcode">وارد کردن رمز عبور</string>
<string name="saved_chat_item_info_tab">ذخیره شده</string>
<string name="forwarded_chat_item_info_tab">فرستاده شده</string>
<string name="forwarded_from_chat_item_info_title">فرستاده شده از</string>
<string name="recipients_can_not_see_who_message_from">گیرنده‌ها نمی‌توانند ببینند این پیام از طرف چه کسی است.</string>
<string name="reveal_verb">آشکار کردن</string>
<string name="hide_verb">پنهان کردن</string>
<string name="delete_messages__question">%d پیام حذف شود؟</string>
<string name="delete_message_cannot_be_undone_warning">پیام حذف خواهد شد - این عمل قابل برگشت نیست!</string>
<string name="moderate_message_will_be_deleted_warning">پیام برای تمام اعضا حذف خواهد شد.</string>
<string name="for_me_only">حذف برای من</string>
<string name="forward_chat_item">فرستادن</string>
<string name="download_file">بارگیری</string>
<string name="icon_descr_edited">ویرایش شده</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">ارسال غیرمجاز</string>
<string name="icon_descr_received_msg_status_unread">خوانده نشده</string>
<string name="personal_welcome">خوش آمدید، %1$s!</string>
<string name="this_text_is_available_in_settings">این متن در تنظیمات دردسترس است</string>
<string name="chat_with_developers">گپ با توسعه‌دهندگان</string>
<string name="no_filtered_chats">گپ پالایش شده‌ای نیست</string>
<string name="connect_with_contact_name_question">به %1$s متصل شوید؟</string>
<string name="no_selected_chat">گپی انتخاب نشده</string>
<string name="forward_message">فرستادن پیام…</string>
<string name="videos_limit_title">تعداد ویدئوی بیش از اندازه!</string>
<string name="videos_limit_desc">فقط ۱۰ ویدئو در هر زمان می‌توان ارسال کرد</string>
<string name="observer_cant_send_message_title">شما نمی‌توانید پیامی بفرستید!</string>
<string name="observer_cant_send_message_desc">لطفا با مدیر گروه تماس بگیرید.</string>
<string name="only_owners_can_enable_files_and_media">فقط صاحبان گروه می‌توانند پرونده‌ها و رسانه را فعال کنند.</string>
<string name="icon_descr_asked_to_receive">دریافت تصویر درخواست شده</string>
<string name="waiting_for_image">در انتظار تصویر</string>
<string name="image_saved">تصویر در گالری ذخیره شد</string>
<string name="video_descr">ویدئو</string>
<string name="video_will_be_received_when_contact_is_online">ویدئو وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید.</string>
<string name="icon_descr_file">پرونده</string>
<string name="large_file">پرونده حجیم!</string>
<string name="waiting_for_file">در انتظار پرونده</string>
<string name="file_will_be_received_when_contact_completes_uploading">پرونده وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string>
<string name="voice_message_with_duration">پیام صوتی (%1$s)</string>
<string name="notifications">اعلان‌ها</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">مخاطب و تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست!</string>
<string name="allow_voice_messages_question">پیام‌های صوتی مجازند؟</string>
<string name="voice_messages_prohibited">پیام‌های صوتی ممنوع هستند!</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 دسکتاپ: از برنامه رمز QR نمایش داده شده را اسکن کنید، از طریق <b>اسکن رمز QR</b>.]]></string>
<string name="share_file">اشتراک‌گذاری پرونده…</string>
<string name="images_limit_title">تعداد تصویر بیش از اندازه!</string>
<string name="tap_to_scan">برای اسکن لمس کنید</string>
<string name="choose_file_title">انتخاب پرونده</string>
<string name="to_start_a_new_chat_help_header">برای شروع گپ جدید</string>
<string name="delivery">تحویل</string>
<string name="delete_member_message__question">پیام عضو حذف شود؟</string>
<string name="for_everybody">برای همه</string>
<string name="stop_snd_file__message">ارسال پرونده متوقف خواهد شد.</string>
<string name="stop_rcv_file__title">دریافت پرونده متوقف شود؟</string>
<string name="revoke_file__message">پرونده از سرورها حذف خواهد شد.</string>
<string name="member_contact_send_direct_message">ارسال پیام مستقیم</string>
<string name="share_image">اشتراک‌گذاری رسانه…</string>
<string name="images_limit_desc">فقط ۱۰ تصویر در هر زمان می‌توان ارسال کرد</string>
<string name="call_service_notification_audio_call">تماس صوتی</string>
<string name="la_please_remember_to_store_password">لطفا آن را به خاطر داشته باشید یا به طور امن ذخیره کنید - راهی برای بازیابی کلمه عبور وجود ندارد!</string>
<string name="auth_simplex_lock_turned_on">قفل SimpleX روشن است</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">وقتی برنامه را شروع می‌کنید یا بعد از ۳۰ ثانیه در پس‌زمینه آن از سر می‌گیرید، نیاز به تصدیق خواهید داشت.</string>
<string name="search_verb">جستجو</string>
<string name="auth_disable_simplex_lock">غیرفعال کردن قفل SimpleX</string>
<string name="share_verb">اشتراک‌گذاری</string>
<string name="save_verb">ذخیره</string>
<string name="send_disappearing_message_send">ارسال</string>
<string name="icon_descr_cancel_live_message">لغو پیام زنده</string>
<string name="back">بازگشت</string>
<string name="cancel_verb">لغو</string>
<string name="ok">موافقت</string>
<string name="live_message">پیام زنده!</string>
<string name="send_live_message_desc">ارسال پیام زنده - همزمان با تایپ کردن برای گیرنده‌ها به‌روز می‌شود</string>
<string name="auth_log_in_using_credential">ورود با استفاده از اطلاعات ورودتان</string>
<string name="auth_unavailable">تصدیق موجود نیست</string>
<string name="message_delivery_error_title">خطا در تحویل پیام</string>
<string name="info_menu">اطلاعات</string>
<string name="sent_message">پیام ارسالی</string>
<string name="message_delivery_error_desc">به احتمال زیاد این مخاطب اتصال با شما را حذف کرده است.</string>
<string name="no_history">بدون تاریخچه</string>
<string name="delete_verb">حذف</string>
<string name="delete_message__question">پیام حذف شود؟</string>
<string name="revoke_file__title">پرونده لغو شود؟</string>
<string name="stop_file__action">توقف پرونده</string>
<string name="stop_snd_file__title">ارسال پرونده متوقف شود؟</string>
<string name="revoke_file__confirm">لغو</string>
<string name="contact_connection_pending">در حال اتصال…</string>
<string name="your_chats">گپ‌ها</string>
<string name="group_preview_you_are_invited">شما به گروه دعوت شده‌اید</string>
<string name="group_preview_join_as">پیوستن به عنوان %s</string>
<string name="search_or_paste_simplex_link">جستجو یا الصاق لینک SimpleX</string>
<string name="tap_to_start_new_chat">برای شروع گپ جدید لمس کنید</string>
<string name="share_message">اشتراک‌گذاری پیام…</string>
<string name="icon_descr_waiting_for_image">در انتظار تصویر</string>
<string name="voice_message_send_text">پیام صوتی…</string>
<string name="text_field_set_contact_placeholder">تعیین نام مخاطب…</string>
<string name="icon_descr_server_status_disconnected">قطع اتصال</string>
<string name="icon_descr_server_status_pending">در حال انتظار</string>
<string name="switch_receiving_address_question">نشانی‌ دریافت تغییر کند؟</string>
<string name="abort_switch_receiving_address_desc">تغییر نشانی‌ لغو خواهد شد. نشانی‌ دریافت پیشین استفاده خواهد شد.</string>
<string name="sync_connection_force_confirm">مذاکره مجدد</string>
<string name="only_group_owners_can_enable_voice">فقط صاحبان گروه می‌توانند پیام‌های صوتی را فعال کنند.</string>
<string name="confirm_verb">تایید</string>
<string name="reset_verb">بازنشاندن</string>
<string name="scan_QR_code">اسکن رمز QR</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(اسکن یا الصاق از حافظه)</string>
<string name="only_stored_on_members_devices">(تنها ذخیره شده توسط اعضای گروه)</string>
<string name="enable_camera_access">فعال کردن دسترسی دوربین</string>
<string name="toast_permission_denied">اجازه داده نشد!</string>
<string name="use_camera_button">دوربین</string>
<string name="thank_you_for_installing_simplex">سپاس برای نصب SimpleX Chat!</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 موبایل: <b>گشودن در موبایل</b> را لمس کنید، سپس <b>اتصال</b> را در برنامه لمس کنید.]]></string>
<string name="accept_connection_request__question">درخواست اتصال پذیرفته شود؟</string>
<string name="voice_messages_not_allowed">پیام‌های صوتی مجاز نیستند</string>
<string name="simplex_links_not_allowed">لینک‌های SimpleX مجاز نیستند</string>
<string name="files_and_media_not_allowed">پرونده‌ها و رسانه مجاز نیست</string>
<string name="icon_descr_waiting_for_video">در انتظار ویدئو</string>
<string name="icon_descr_video_asked_to_receive">دریافت ویدئو درخواست شده</string>
<string name="icon_descr_video_snd_complete">ویدئو ارسال شد</string>
<string name="image_will_be_received_when_contact_completes_uploading">تصویر وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string>
<string name="voice_message">پیام صوتی</string>
<string name="icon_descr_record_voice_message">ضبط پیام صوتی</string>
<string name="icon_descr_send_message">ارسال پیام</string>
<string name="no_details">بدون جزئیات</string>
<string name="connect_via_link_or_qr">اتصال به وسیله لینک / رمز QR</string>
<string name="camera_not_available">دوربین موجود نیست</string>
<string name="gallery_image_button">تصویر</string>
<string name="share_one_time_link">ایجاد لینک دعوت یک‌بارمصرف</string>
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[می‌توانید <font color="#0088ff">برای پرسش هر سوال و دریافت اطلاعات به توسعه‌دهندگان SimpleX Chat متصل شوید</font>.]]></string>
<string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>افزودن مخاطب</b>: برای ایجاد لینک دعوت جدید، یا اتصال از طریق لینکی که دریافت کردید.]]></string>
<string name="to_connect_via_link_title">اتصال از طریق لینک</string>
<string name="image_will_be_received_when_contact_is_online">تصویر وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید!</string>
<string name="la_lock_mode">نوع قفل SimpleX</string>
<string name="la_lock_mode_system">تصدیق سامانه</string>
<string name="la_change_app_passcode">تغییر رمز عبور</string>
<string name="in_reply_to">در پاسخ به</string>
<string name="la_immediately">فوری</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">تصدیق دستگاه فعال نیست. زمانی که تصدیق دستگاه را فعال کنید می‌توانید قفل SimpleX را از طریق تنظیمات روشن کنید.</string>
<string name="allow_verb">اجازه دادن</string>
<string name="attach">ضمیمه</string>
<string name="image_decoding_exception_desc">امکان رمزگشایی تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید.</string>
<string name="files_and_media_prohibited">پرونده‌ها و رسانه ممنوع است!</string>
<string name="icon_descr_image_snd_complete">تصویر ارسال شد</string>
<string name="waiting_for_video">در انتظار ویدئو</string>
<string name="contact_sent_large_file">مخاطبتان پرونده‌ای ارسال کرد که از بیشترین اندازه پشتیبانی شده (%1$s) بزرگتر است.</string>
<string name="file_will_be_received_when_contact_is_online">پرونده وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید.</string>
<string name="file_saved">پرونده ذخیره شد</string>
<string name="sync_connection_force_desc">رمزگذاری در حال کار است و نیازی به توافق رمزگذاری جدید نیست. ممکن است باعث بروز خطاهای اتصال شود!</string>
<string name="ask_your_contact_to_enable_voice">لطفا از مخاطبتان بخواهید ارسال پیام‌های صوتی را فعال کند.</string>
<string name="send_verb">ارسال</string>
<string name="gallery_video_button">ویدئو</string>
<string name="above_then_preposition_continuation">بالا، سپس:</string>
<string name="delete_contact_question">مخاطب حذف شود؟</string>
<string name="icon_descr_server_status_error">خطا</string>
<string name="switch_receiving_address_desc">آدرس دریافت به سرور متفاوتی تغییر خواهد یافت. تغییر نشانی وقتی تکمیل خواهد شد که فرستنده آنلاین شود.</string>
<string name="la_could_not_be_verified">تایید شما ممکن نیست؛ لطفا دوباره امتحان کنید.</string>
<string name="choose_file">پرونده</string>
<string name="you_are_observer">شما ناظر هستید</string>
<string name="clear_chat_question">گپ پاک شود؟</string>
<string name="clear_note_folder_warning">تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست!</string>
<string name="delete_contact_menu_action">حذف</string>
<string name="accept_contact_incognito_button">پذیرش ناشناس</string>
<string name="favorite_chat">برگزیده</string>
<string name="accept_contact_button">پذیرفتن</string>
<string name="reject_contact_button">رد کردن</string>
<string name="clear_note_folder_question">یادداشت‌های خصوصی پاک شود؟</string>
<string name="clear_verb">پاک کردن</string>
<string name="clear_chat_menu_action">پاک‌سازی</string>
<string name="delete_group_menu_action">حذف</string>
<string name="mark_unread">علامت‌گذاری به عنوان خوانده نشده</string>
<string name="set_contact_name">تعیین نام مخاطب</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">مخاطبی که این لینک را با او به اشتراک گذاشتید قادر به اتصال نخواهد بود!</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">اگر نپذیرید، فرستنده باخبر نخواهد شد.</string>
<string name="clear_chat_warning">تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! پیام‌ها فقط برای شما حذف خواهند شد.</string>
<string name="clear_chat_button">پاک‌سازی گپ</string>
<string name="you_accepted_connection">اتصال را پذیرفتید</string>
<string name="alert_title_contact_connection_pending">مخاطب هنوز متصل نشده است!</string>
<string name="you_invited_a_contact">شما مخاطبی را دعوت کردید</string>
<string name="delete_pending_connection__question">اتصال در حال انتظار حذف شود؟</string>
<string name="connection_you_accepted_will_be_cancelled">اتصالی که پذیرفتید لغو خواهد شد!</string>
<string name="mute_chat">بی‌صدا</string>
<string name="unmute_chat">لغو بی‌صدا</string>
<string name="mark_read">علامت‌گذاری به عنوان خوانده شده</string>
<string name="unfavorite_chat">خلع برگزیده</string>
<string name="icon_descr_simplex_team">تیم SimpleX</string>
<string name="new_chat">گپ جدید</string>
<string name="how_to_use_simplex_chat">روش استفاده</string>
<string name="markdown_help">راهنمای مارکداون</string>
<string name="invalid_contact_link">لینک نامعتبر!</string>
<string name="share_invitation_link">اشتراک‌گذاری لینک یک بار مصرف</string>
<string name="or_scan_qr_code">یا رمز QR را اسکن کنید</string>
<string name="retry_verb">تلاش مجدد</string>
<string name="database_passphrase_and_export">عبارت عبور و صدور پایگاه داده</string>
<string name="smp_servers_preset_add">افزودن سرورهای از پیش تنظیم شده</string>
<string name="smp_servers_add_to_another_device">افزودن به دستگاه دیگر</string>
<string name="smp_servers_delete_server">حذف سرور</string>
<string name="how_to_use_your_servers">چگونه از سرورهای خود استفاده کنید</string>
<string name="image_descr_link_preview">تصویر پیش‌نمایش لینک</string>
<string name="your_chat_profiles">نمایه‌های گپ شما</string>
<string name="smp_servers">سرورهای SMP</string>
<string name="install_simplex_chat_for_terminal">SimpleX Chat را برای ترمینال نصب کنید</string>
<string name="star_on_github">در GitHub ستاره بزنید</string>
<string name="contribute">همکاری کنید</string>
<string name="image_descr_qr_code">رمز QR</string>
<string name="connect__a_new_random_profile_will_be_shared">یک نمایه تصادفی جدید به اشتراک گذاشته خواهد شد.</string>
<string name="connect_via_link">اتصال از طریق لینک</string>
<string name="one_time_link">لینک دعوت یک‌بارمصرف</string>
<string name="is_verified">%s تایید شده است</string>
<string name="create_chat_profile">ایجاد نمایه گپ</string>
<string name="chat_console">کنسول گپ</string>
<string name="smp_servers_test_server">آزمایش سرور</string>
<string name="smp_servers_enter_manually">وارد کردن دستی سرور</string>
<string name="enter_one_ICE_server_per_line">سرورهای ICE (یکی در هر خط)</string>
<string name="error_saving_ICE_servers">خطا در ذخیره کردن سرورهای ICE</string>
<string name="icon_descr_profile_image_placeholder">جایگزین تصویر نمایه</string>
<string name="icon_descr_close_button">دکمه بستن</string>
<string name="icon_descr_cancel_link_preview">لغو پیش‌نمایش لینک</string>
<string name="icon_descr_settings">تنظیمات</string>
<string name="icon_descr_help">کمک</string>
<string name="image_descr_simplex_logo">لوگوی SimpleX</string>
<string name="icon_descr_email">ایمیل</string>
<string name="this_link_is_not_a_valid_connection_link">این لینک، یک لینک اتصال معتبر نیست!</string>
<string name="connection_request_sent">درخواست اتصال ارسال شد!</string>
<string name="you_will_be_connected_when_group_host_device_is_online">وقتی دستگاه میزبان گروه آنلاین شد، به گروه متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">لینکی که دریافت کردید را الصاق کنید تا به مخاطبتان متصل شوید…</string>
<string name="connect__your_profile_will_be_shared">نمایه شما %1$s به اشتراک گذاشته خواهد شد.</string>
<string name="scan_qr_to_connect_to_contact">برای اتصال، مخاطبتان می‌تواند رمز QR را اسکن یا از لینک در برنامه استفاده کند.</string>
<string name="if_you_cant_meet_in_person">اگر نمی‌توانید ملاقات حضوری داشته باشید، رمز QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.</string>
<string name="you_can_share_your_address">می‌توانید نشانی‌ خود را به صورت لینک یا رمز QR به اشتراک بگذارید - هر کسی می‌تواند به شما متصل شود.</string>
<string name="you_can_accept_or_reject_connection">وقتی اشخاص درخواست اتصال کنند، شما می‌توانید آن را بپذیرید یا رد کنید.</string>
<string name="or_show_this_qr_code">یا این رمز را نشان دهید</string>
<string name="you_can_view_invitation_link_again">می‌توانید دوباره لینک دعوت را در جزئیات اتصال مشاهده کنید.</string>
<string name="keep_invitation_link">نگه‌داشتن</string>
<string name="creating_link">در حال ایجاد لینک…</string>
<string name="paste_the_link_you_received">لینکی که دریافت کردید را الصاق کنید</string>
<string name="the_text_you_pasted_is_not_a_link">متن الصاقی شما یک لینک SimpleX نیست.</string>
<string name="smp_servers_your_server">سرور شما</string>
<string name="smp_servers_preset_server">سرور از پیش تنظیم شده</string>
<string name="smp_servers_invalid_address">نشانی‌ سرور نامعتبر!</string>
<string name="smp_servers_use_server_for_new_conn">به‌کارگیری برای اتصال‌های جدید</string>
<string name="invalid_qr_code">رمز QR نامعتبر</string>
<string name="incorrect_code">رمز امنیتی نادرست!</string>
<string name="scan_code_from_contacts_app">رمز امنیتی را از برنامه مخاطبتان اسکن کنید.</string>
<string name="mark_code_verified">علامت‌گذاری به عنوان تایید شده</string>
<string name="is_not_verified">%s تایید نشده است</string>
<string name="to_verify_compare">برای تایید رمزگذاری سرتاسر، روی دستگاه‌های خود، کد را با مخاطبتان مقایسه(یا اسکن) کنید.</string>
<string name="your_XFTP_servers">سرورهای XFTP شما</string>
<string name="how_to">چگونه</string>
<string name="using_simplex_chat_servers">در حال استفاده از سرورهای SimpleX Chat.</string>
<string name="configure_ICE_servers">تنظیم سرورهای ICE</string>
<string name="contact_wants_to_connect_with_you">می‌خواهد به شما متصل شود!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">وقتی دستگاه مخاطبتان آنلاین شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[اگر نمی‌توانید ملاقات حضوری داشته باشید، <b>رمز QR را در تماس تصویری اسکن کنید</b>، یا مخاطبتان می‌تواند یک لینک دعوت به اشتراک بگذارد.]]></string>
<string name="keep_unused_invitation_question">دعوت استعمال نشده نگه داشته شود؟</string>
<string name="your_simplex_contact_address">نشانی‌ SimpleX شما</string>
<string name="markdown_in_messages">مارکداون در پیام‌ها</string>
<string name="chat_with_the_founder">ایده‌ها و سوالات را ارسال کنید</string>
<string name="send_us_an_email">به ما ایمیل بفرستید</string>
<string name="chat_lock">قفل SimpleX</string>
<string name="smp_servers_add">افزودن سرور…</string>
<string name="smp_servers_test_servers">آزمایش سرورها</string>
<string name="smp_servers_save">ذخیره سرورها</string>
<string name="smp_servers_test_failed">عدم موفقیت آزمایش سرور!</string>
<string name="smp_servers_test_some_failed">عدم موفقیت آزمایش چند سرور:</string>
<string name="smp_servers_scan_qr">اسکن رمز QR سرور</string>
<string name="your_SMP_servers">سرورهای SMP شما</string>
<string name="xftp_servers">سرورهای XFTP</string>
<string name="use_simplex_chat_servers__question">از سرورهای SimpleX Chat استفاده شود؟</string>
<string name="saved_ICE_servers_will_be_removed">سرورهای WebRTC ICE ذخیره شده حذف خواهند شد.</string>
<string name="your_ICE_servers">سرورهای ICE شما</string>
<string name="connect_button">اتصال</string>
<string name="paste_button">الصاق</string>
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[شما همچنین می‌توانید با کلیک روی لینک متصل شوید. اگر در مرورگر باز شد، روی دکمه <b>گشودن در برنامه موبایل</b> کلیک کنید.]]></string>
<string name="add_contact_tab">افزودن مخاطب</string>
<string name="simplex_address">نشانی‌ SimpleX</string>
<string name="clear_verification">پاک‌سازی تایید</string>
<string name="smp_servers_preset_address">نشانی‌ سرور از پیش تنظیم شده</string>
<string name="security_code">رمز امنیتی</string>
<string name="smp_servers_use_server">به‌کارگیری از سرور</string>
<string name="smp_servers_per_user">سرورها برای اتصال‌های جدید نمایه گپ فعلی شما</string>
<string name="smp_save_servers_question">سرورها ذخیره شوند؟</string>
<string name="rate_the_app">به برنامه امتیاز بدهید</string>
<string name="smp_servers_your_server_address">نشانی‌ سرور شما</string>
<string name="smp_servers_check_address">نشانی‌ سرور را بررسی و دوباره امتحان کنید.</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">لازم است مخاطبتان آنلاین باشد تا اتصال کامل شود.
\nمی‌توانید این اتصال را لغو و مخاطب را حذف کنید (و بعدا با یک لینک جدید امتحان کنید).</string>
<string name="icon_descr_address">نشانی‌ SimpleX</string>
<string name="this_QR_code_is_not_a_link">این رمز QR یک لینک نیست!</string>
<string name="read_more_in_user_guide_with_link"><![CDATA[مطالعه بیشتر در <font color="#0088ff">راهنمای کاربر</font>.]]></string>
<string name="this_string_is_not_a_connection_link">این رشته متن، یک لینک اتصال نیست!</string>
<string name="code_you_scanned_is_not_simplex_link_qr_code">رمزی که اسکن کردید یک رمز QR لینک SimpleX نیست.</string>
<string name="scan_code">اسکن رمز</string>
<string name="image_descr_profile_image">تصویر نمایه</string>
<string name="icon_descr_more_button">بیشتر</string>
<string name="show_QR_code">نمایش رمز QR</string>
<string name="invalid_QR_code">رمز QR نامعتبر</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">وقتی درخواست اتصال شما پذیرفته شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[اگر نمی‌توانید ملاقات حضوری داشته باشید، <b>رمز QR را در تماس تصویری نمایش دهید</b>، یا لینک را به اشتراک بگذارید.]]></string>
<string name="your_chat_profile_will_be_sent_to_your_contact">نمایه گپ شما ارسال خواهد شد
\nبه مخاطبتان</string>
<string name="learn_more">اطلاعات بیشتر</string>
<string name="you_wont_lose_your_contacts_if_delete_address">اگر بعدا نشانی‌ خود را حذف کنید، مخاطبان خود را از دست نخواهید داد.</string>
<string name="share_this_1_time_link">این لینک دعوت یک‌بارمصرف را به اشتراک بگذارید</string>
<string name="tap_to_paste_link">برای الصاق لینک لمس کنید</string>
<string name="your_settings">تنظیمات شما</string>
<string name="network_use_onion_hosts_required_desc">میزبان‌های Onion برای اتصال الزامی خواهد بود.
\nلطفا توجه داشته باشید: شما بدون نشانی‌ onion. قادر نخواهید بود به سرورها متصل شوید.</string>
<string name="save_servers_button">ذخیره</string>
<string name="update_onion_hosts_settings_question">تنظیمات میزبان‌های onion. به روز شود؟</string>
<string name="port_verb">پورت</string>
<string name="network_enable_socks">از پروکسی SOCKS استفاده شود؟</string>
<string name="network_disable_socks">از اتصال مستقیم اینترنت استفاده شود؟</string>
<string name="network_settings">تنظیمات پیشرفته شبکه</string>
<string name="network_socks_proxy_settings">تنظیمات پروکسی SOCKS</string>
<string name="host_verb">هاست</string>
<string name="network_use_onion_hosts_required">الزامی</string>
<string name="network_use_onion_hosts_prefer_desc">از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد.</string>
<string name="appearance_settings">ظاهر</string>
<string name="app_version_name">نسخه برنامه: v%s</string>
<string name="show_developer_options">نمایش گزینه‌های توسعه‌دهنده</string>
<string name="developer_options">شناسه‌های پایگاه داده و گزینه انزوای ترابری</string>
<string name="your_contacts_will_remain_connected">مخاطبانتان متصل باقی خواهند ماند.</string>
<string name="share_link">اشتراک‌گذاری لینک</string>
<string name="network_enable_socks_info">دسترسی به سرورها از طریق پروکسی SOCKS روی پورت %d؟ پروکسی باید قبل از فعال کردن این گزینه، راه اندازی شده باشد.</string>
<string name="network_session_mode_user_description"><![CDATA[یک اتصال جدای TCP (و اطلاعات ورود SOCKS) <b>برای هر نمایه گپی که در برنامه دارید</b> استفاده خواهد شد.]]></string>
<string name="show_internal_errors">نمایش خطاهای داخلی</string>
<string name="show_slow_api_calls">نمایش تماس‌های کند API</string>
<string name="network_use_onion_hosts_no_desc_in_alert">از میزبان‌های Onion استفاده نخواهد شد.</string>
<string name="network_use_onion_hosts_no">خیر</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[<i>استفاده از میزبان‌های onion.</i> را روی «خیر» تنظیم کنید اگر پروکسی SOCKS از آنها پشتیبانی نمی‌کند.]]></string>
<string name="customize_theme_title">سفارشی کردن تم</string>
<string name="app_version_title">نسخه برنامه</string>
<string name="theme_colors_section_title">رنگ‌های تم</string>
<string name="core_version">نسخه هسته: v%s</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="shutdown_alert_desc">اعلان‌ها از کار خواهند افتاد تا زمانی که برنامه را دوباره راه‌اندازی کنید</string>
<string name="create_address">ایجاد نشانی</string>
<string name="delete_address__question">نشانی حذف شود؟</string>
<string name="network_and_servers">شبکه و سرورها</string>
<string name="network_socks_toggle_use_socks_proxy">استفاده از پروکسی SOCKS</string>
<string name="network_proxy_port">پورت %d</string>
<string name="network_use_onion_hosts">استفاده از میزبان‌های onion.</string>
<string name="network_use_onion_hosts_prefer">وقتی موجود بود</string>
<string name="network_use_onion_hosts_no_desc">از میزبان‌های Onion استفاده نخواهد شد.</string>
<string name="network_session_mode_transport_isolation">انزوای ترابری</string>
<string name="network_session_mode_user">نمایه گپ</string>
<string name="network_session_mode_entity">اتصال</string>
<string name="app_version_code">ساختار برنامه: %s</string>
<string name="network_settings_title">تنظیمات شبکه</string>
<string name="network_use_onion_hosts_required_desc_in_alert">میزبان‌های Onion برای اتصال الزامی خواهد بود.</string>
<string name="show_dev_options">نمایش:</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>لطفا توجه داشته باشید</b>: واسطه‌های پیام و پرونده از طریق پروکسی SOCKS متصل می‌شوند. تماس‌ها و ارسال پیش‌نمایش‌های لینک از اتصال مستقیم استفاده می‌کنند.]]></string>
<string name="developer_options_section">گزینه‌های توسعه‌دهنده</string>
<string name="hide_dev_options">پنهان کردن:</string>
<string name="shutdown_alert_question">بسته شود؟</string>
<string name="all_your_contacts_will_remain_connected">تمام مخاطبانتان متصل باقی خواهند ماند.</string>
<string name="all_your_contacts_will_remain_connected_update_sent">تمام مخاطبانتان متصل باقی خواهند ماند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string>
<string name="network_disable_socks_info">اگر تایید کنید، سرورهای پیام‌رسانی خواهند توانست نشانی‌ IP، و فراهم‌کننده شما را ببینند - و این که به چه سرورهایی متصل می‌شوید.</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور WebRTC ICE صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string>
<string name="network_session_mode_entity_description">یک اتصال جدای TCP (و اطلاعات ورود SOCKS) <b>برای هر مخاطب و عضو گروه</b> استفاده خواهد شد.
\n<b>لطفا توجه داشته باشید</b>: اگر اتصال‌های زیادی داشته باشید، مصرف باتری و ترافیک شما می‌تواند به شکل قابل توجه بالاتر باشد و بعضی اتصال‌ها ممکن است با موفقیت انجام نشوند.</string>
<string name="update_network_session_mode_question">حالت انزوای ترابری به روز شود؟</string>
<string name="edit_image">ویرایش تصویر</string>
<string name="create_simplex_address">ایجاد نشانی‌ SimpleX</string>
<string name="share_with_contacts">اشتراک‌گذاری با مخاطبان</string>
<string name="profile_update_will_be_sent_to_contacts">به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string>
<string name="auto_accept_contact">پذیرفتن خودکار</string>
<string name="enter_welcome_message_optional">ورود پیام خوشامدگویی…(اختیاری)</string>
<string name="save_settings_question">تنظیمات ذخیره شوند؟</string>
<string name="save_auto_accept_settings">ذخیره تنظیمات پذیرفتن خودکار</string>
<string name="delete_address">حذف نشانی</string>
<string name="email_invite_body">سلام!
\nبه وسیله SimpleX Chat به من متصل شوید: %s</string>
<string name="dont_create_address">نشانی ایجاد نشود</string>
<string name="continue_to_next_step">ادامه</string>
<string name="your_current_profile">نمایه فعلی شما</string>
<string name="full_name__field">نام کامل:</string>
<string name="save_and_notify_contacts">ذخیره و مخاطبان مطلع شوند</string>
<string name="email_invite_subject">بیایید در SimpleX Chat گفتگو کنیم</string>
<string name="save_preferences_question">تنظیمات ذخیره شوند؟</string>
<string name="hide_profile">پنهان کردن نمایه</string>
<string name="password_to_show">کلمه عبور برای نمایش</string>
<string name="save_profile_password">ذخیره کلمه عبور نمایه</string>
<string name="stop_sharing_address">اشتراک‌گذاری نشانی متوقف شود؟</string>
<string name="stop_sharing">توقف اشتراک‌گذاری</string>
<string name="you_can_create_it_later">می‌توانید بعدا آن را ایجاد کنید</string>
<string name="you_can_make_address_visible_via_settings">می‌توانید آن را از طریق تنظیمات برای مخاطبان SimpleX خود قابل رویت کنید.</string>
<string name="display_name__field">نام نمایه:</string>
<string name="delete_image">حذف تصویر</string>
<string name="save_and_notify_group_members">ذخیره و اعضای گروه مطلع شوند</string>
<string name="save_and_notify_contact">ذخیره و مخاطب مطلع شود</string>
<string name="exit_without_saving">خروج بدون ذخیره کردن</string>
<string name="to_reveal_profile_enter_password">برای آشکار کردن نمایه پنهان خود، یک کلمه عبور کامل در فیلد جستجو در صفحه نمایه‌های گپتان وارد کنید.</string>
<string name="invite_friends">دعوت از دوستان</string>
<string name="confirm_password">تایید کلمه عبور</string>
<string name="hidden_profile_password">کلمه عبور نمایه پنهان</string>
<string name="add_address_to_your_profile">به نمایه خود نشانی اضافه کنید، تا مخاطبانتان بتوانند آن را با اشخاص دیگر به اشتراک بگذارند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string>
<string name="share_address_with_contacts_question">نشانی با مخاطبان به اشتراک گذاشته شود؟</string>
<string name="create_address_and_let_people_connect">یک نشانی ایجاد کنید تا اشخاص بتوانند به شما متصل شوند.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">نمایه شما روی دستگاهتان ذخیره شده و فقط با مخاطبانتان به اشتراک گذاشته می‌شود. سرورهای SimpleX قادر به دیدن نمایه شما نیستند.</string>
<string name="error_saving_user_password">خطا در ذخیره کردن کلمه عبور کاربر</string>
</resources>
@@ -180,9 +180,9 @@
<string name="change_member_role_question">Csoport szerepkör megváltoztatása?</string>
<string name="change_lock_mode">Zárolási mód megváltoztatása</string>
<string name="notification_contact_connected">Kapcsolódott</string>
<string name="rcv_group_event_member_connected">kapcsolódva</string>
<string name="rcv_group_event_member_connected">kapcsolódott</string>
<string name="connect_via_link_verb">Kapcsolódás</string>
<string name="group_member_status_connected">kapcsolódva</string>
<string name="group_member_status_connected">kapcsolódott</string>
<string name="connected_mobile">Csatlakoztatott telefon</string>
<string name="server_connected">kapcsolódva</string>
<string name="change_role">Szerepkör megváltoztatása</string>
@@ -1151,7 +1151,7 @@
<string name="callstate_received_answer">fogadott válasz…</string>
<string name="restore_database_alert_title">Adatbázismentés visszaállítása?</string>
<string name="simplex_service_notification_text">Üzenetek fogadása…</string>
<string name="rcv_group_event_2_members_connected">%s és %s kapcsolódtak</string>
<string name="rcv_group_event_2_members_connected">%s és %s kapcsolódott</string>
<string name="you_are_observer">megfigyelő szerep</string>
<string name="port_verb">Port</string>
<string name="set_passcode">Jelkód beállítása</string>
@@ -46,7 +46,7 @@
<string name="bold_text">太文字</string>
<string name="icon_descr_audio_call">音声通話</string>
<string name="settings_audio_video_calls">音声とビデオ通話</string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>※注意※</b>喪失したら、パスフレーズの回復・変更ができません。]]></string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>※注意※</b>紛失するとパスフレーズの回復・変更ができません。]]></string>
<string name="all_group_members_will_remain_connected">グループ全員の接続が継続します。</string>
<string name="allow_your_contacts_to_send_disappearing_messages">送信相手が消えるメッセージを送るのを許可する。</string>
<string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。(24時間)</string>
@@ -179,7 +179,7 @@
<string name="chat_is_stopped_indication">チャットが停止してます。</string>
<string name="network_disable_socks_info">承諾すると、メッセージのサーバがIPアドレス、ISP、接続サーバを特定できます。</string>
<string name="chat_preferences">チャット設定</string>
<string name="clear_chat_button">消す</string>
<string name="clear_chat_button">チャットを削除</string>
<string name="icon_descr_close_button">閉じるボタン</string>
<string name="clear_verification">未検証の状態に戻す</string>
<string name="image_saved">画像をギャラリーに保存しました。</string>
@@ -868,7 +868,7 @@
<string name="smp_servers_scan_qr">サーバのQRコードを読み込む</string>
<string name="smp_servers_test_some_failed">テストに失敗したサーバがあります:</string>
<string name="use_simplex_chat_servers__question">SimpleX Chatサーバを使いますか?</string>
<string name="using_simplex_chat_servers">SimpleX Chatを使っています</string>
<string name="using_simplex_chat_servers">SimpleX Chatサーバを使用中</string>
<string name="share_link">リンクを送る</string>
<string name="core_simplexmq_version">simplexmq: バージョン%s (%2s)</string>
<string name="callstate_waiting_for_answer">応答を待機中…</string>
@@ -88,4 +88,71 @@
<string name="permissions_camera_and_record_audio">Camera și microfon</string>
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Atenție</b>: arhiva va fi ștearsă.]]></string>
<string name="icon_descr_cancel_file_preview">Anulează previzualizarea fișierului</string>
<string name="empty_chat_profile_is_created">Un profil de conversație gol cu numele furnizat este creat, și aplicația se deschide ca de obicei.</string>
<string name="connect_plan_already_connecting">Se conectează deja!</string>
<string name="notifications_mode_off_desc">Aplicația poate primi notificări doar când rulează, niciun serviciu în fundal nu va fi lansat.</string>
<string name="connect_plan_already_joining_the_group">Se alătură deja grupului!</string>
<string name="notifications_mode_service">Mereu pornit</string>
<string name="connect__a_new_random_profile_will_be_shared">Un nou profil aleatoriu va fi distribuit.</string>
<string name="keychain_is_storing_securely">Android Keystore este folosit pentru a stoca în siguranță fraza de acces - permite serviciului de notificare să funcționeze.</string>
<string name="rcv_group_and_other_events">și %d alte evenimente</string>
<string name="answer_call">Răspunde la apel</string>
<string name="keychain_allows_to_receive_ntfs">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.</string>
<string name="settings_section_title_app">APLICAȚIE</string>
<string name="create_group_button">Creează grup</string>
<string name="v4_6_audio_video_calls">Apeluri audio și video</string>
<string name="migrate_from_device_archive_and_upload">Arhivează și încarcă</string>
<string name="migrate_from_device_archiving_database">Bază de date de arhivare</string>
<string name="migrate_from_device_creating_archive_link">Se creează un link de arhivare</string>
<string name="share_one_time_link">Creează un link de invitare unic.</string>
<string name="auto_accept_images">Acceptă automat imagini</string>
<string name="call_service_notification_audio_call">Apel audio</string>
<string name="icon_descr_audio_call">Apel audio</string>
<string name="icon_descr_audio_off">Audio oprit</string>
<string name="settings_section_title_icon">PICTOGRAMĂ APLICAȚIE</string>
<string name="la_app_passcode">Cod de acces aplicație</string>
<string name="create_secret_group_title">Creează grup secret</string>
<string name="smp_server_test_create_queue">Creează coadă</string>
<string name="notifications_mode_service_desc">Serviciul în fundal rulează mereu - notificările vor fi afișate imediat ce sunt disponibile.</string>
<string name="la_authenticate">Autentificare</string>
<string name="la_auth_failed">Autentificare eșuată</string>
<string name="la_current_app_passcode">Codul de acces curent</string>
<string name="auth_unavailable">Autentificare indisponibilă</string>
<string name="attach">Atașează</string>
<string name="back">Înapoi</string>
<string name="create_group">Creează grup secret</string>
<string name="creating_link">Se creează link…</string>
<string name="audio_call_no_encryption">apel audio (necriptat e2e)</string>
<string name="smp_server_test_create_file">Creează fișier</string>
<string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Adaugă contact</b>: pentru a crea un nou link de invitare, sau a te conecta printr-un link pe care l-ai primit.]]></string>
<string name="authentication_cancelled">Autentificare anulată</string>
<string name="app_passcode_replaced_with_self_destruct">Cod de acces aplicație este înlocuit cu cod de acces de autodistrugere.</string>
<string name="audio_video_calls">Apeluri audio/video</string>
<string name="available_in_v51">"
\nDisponibil în v5.1"</string>
<string name="v5_0_app_passcode">Cod de acces aplicație</string>
<string name="v5_6_app_data_migration">Migrare date aplicație</string>
<string name="agent_critical_error_title">Eroare critică</string>
<string name="migrate_to_device_apply_onion">Aplică</string>
<string name="create_profile">Creează profil</string>
<string name="create_another_profile_button">Creează profil</string>
<string name="v4_2_auto_accept_contact_requests">Acceptă automat cererile de contactare</string>
<string name="custom_time_picker_custom">personalizat</string>
<string name="maximum_supported_file_size">În prezent dimensiunea maximă pentru fișiere este %1$s.</string>
<string name="create_simplex_address">Creează adresă SimpleX</string>
<string name="auto_accept_contact">Acceptare automată</string>
<string name="create_your_profile">Creează-ți profilul</string>
<string name="settings_audio_video_calls">Apeluri audio și video</string>
<string name="icon_descr_audio_on">Audio pornit</string>
<string name="current_passphrase">Frază de acces curentă…</string>
<string name="create_group_link">Creează link pentru grup</string>
<string name="button_create_group_link">Creează link</string>
<string name="group_member_status_creator">creator</string>
<string name="color_background">Fundal</string>
<string name="v5_3_new_desktop_app_descr">Creează profil nou în aplicația desktop. 💻</string>
<string name="v5_3_encrypt_local_files_descr">Aplicația criptează fișierele locale noi (cu excepția videoclipurilor).</string>
<string name="group_member_role_author">autor</string>
<string name="v5_3_new_interface_languages_descr">Arabă, Bulgară, Finlandeză, Ebraică, Thailandeză și Ucraineană - mulțumită utilizatorilor și Weblate.</string>
<string name="calls_prohibited_with_this_contact">Apelurile audio/video sunt interzise.</string>
<string name="item_info_current">(prezent)</string>
</resources>
@@ -1769,5 +1769,5 @@
<string name="network_type_ethernet">Kablolu ethernet</string>
<string name="settings_section_title_profile_images">Profil resimleri</string>
<string name="v5_7_shape_profile_images">Profil resimlerini şekillendir</string>
<string name="v5_7_shape_profile_images_descr">Kare,daire, veya aralarında herhangi birşey.</string>
<string name="v5_7_shape_profile_images_descr">Kare,daire, veya aralarında herhangi bir şey.</string>
</resources>
@@ -144,4 +144,32 @@
<string name="auto_accept_contact">Tự động chấp nhận</string>
<string name="back">Quay về</string>
<string name="auto_accept_images">Tự động chấp nhận hình ảnh</string>
<string name="audio_device_bluetooth">Bluetooth</string>
<string name="v5_4_better_groups">Nhóm tốt hơn</string>
<string name="v5_4_block_group_members">Chặn thành viên nhóm</string>
<string name="rcv_group_event_member_blocked">đã chặn %s</string>
<string name="v5_1_better_messages">Tin nhắn tốt hơn</string>
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Xin lưu ý</b>: 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ệ.]]></string>
<string name="create_group_button_to_create_new_group"><![CDATA[<b>Tạo nhóm</b>: để tạo một nhóm mới.]]></string>
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Tốt nhất cho pin</b>. 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).]]></string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Xin lưu ý</b>: bạn sẽ KHÔNG thể khôi phục hoặc thay đổi passphrase nếu bạn làm mất nó.]]></string>
<string name="member_blocked_by_admin">Bị chặn bởi quản trị viên</string>
<string name="block_member_button">Chặn thành viên</string>
<string name="member_info_member_blocked">đã chặn</string>
<string name="blocked_by_admin_item_description">bị chặn bởi quản trị viên</string>
<string name="blocked_item_description">đã chặn</string>
<string name="turning_off_service_and_periodic">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.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Có thể tắt thông qua cài đặt</b> - thông báo vẫn sẽ được hiển thị khi ứng dụng đang chạy.]]></string>
<string name="block_member_question">Chặn thành viên?</string>
<string name="both_you_and_your_contacts_can_delete">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)</string>
<string name="block_for_all">Chặn tất cả</string>
<string name="block_for_all_question">Chặn thành viên cho tất cả?</string>
<string name="block_member_confirmation">Chặn</string>
<string name="both_you_and_your_contact_can_send_voice">Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn thoại.</string>
<string name="both_you_and_your_contact_can_make_calls">Cả bạn và liên hệ của bạn đều có thể thực hiện cuộc gọi.</string>
<string name="bold_text">in đậm</string>
<string name="both_you_and_your_contact_can_send_disappearing">Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn tự xóa.</string>
<string name="both_you_and_your_contact_can_add_message_reactions">Cả bạn và liên hệ của bạn đều có thể thả cảm xúc tin nhắn.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Xin lưu ý</b>: 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.]]></string>
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Tốt cho pin</b>. 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.]]></string>
</resources>
@@ -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)
}
}
@@ -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))
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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";
+3 -1
View File
@@ -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
+42 -4
View File
@@ -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),
+31 -6
View File
@@ -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
+32 -20
View File
@@ -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)]
+15
View File
@@ -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)
@@ -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;
|]
+5 -2
View File
@@ -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
+6 -9
View File
@@ -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,
+50 -5
View File
@@ -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)
+19 -10
View File
@@ -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)
+3 -1
View File
@@ -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
+10 -3
View File
@@ -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)
+11 -8
View File
@@ -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
+5 -1
View File
@@ -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)
+166
View File
@@ -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
+9 -3
View File
@@ -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)])
+58 -3
View File
@@ -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 <name> 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 <display name> 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"
+2 -1
View File
@@ -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 <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">ohlášení</a>.",
"docs-dropdown-9": "Ke stažení"
"docs-dropdown-9": "Ke stažení",
"docs-dropdown-10": "Transparentnost"
}
+4 -2
View File
@@ -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<br>sans identifiant d'utilisateur",
"hero-p-1": "Les autres applications ont des IDs d'utilisateur : Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> SimpleX n'en a pas, <strong>pas même des nombres aléatoires</strong>.<br > 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"
}
+3 -2
View File
@@ -235,7 +235,7 @@
"privacy-matters-overlay-card-1-p-4": "Платформа SimpleX защищает конфиденциальность ваших контактов лучше, чем любая другая альтернатива, полностью предотвращая доступ к вашему социальному графику каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.",
"hero-overlay-card-1-p-6": "Подробнее читайте в <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>техническом документе SimpleX</a>.",
"simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями <a href='https://ru.wikipedia.org/wiki/Peer-to-peer'>P2P</a> имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.",
"terms-and-privacy-policy": "Условия & Политика Конфиденциальности",
"terms-and-privacy-policy": "Политика Конфиденциальности",
"simplex-network-overlay-card-1-li-1": "Сети P2P полагаются на тот или иной вариант <a href='https://ru.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> для маршрутизации сообщений. Проекты DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети - <em>O(log N)</em>.",
"privacy-matters-section-label": "Убедитесь, что ваш мессенджер не может получить доступ к вашим данным!",
"simplex-unique-overlay-card-3-p-1": "SimpleX Chat хранит все пользовательские данные на клиентских устройствах в <strong>портативном формате зашифрованной базы данных</strong> которую можно перенести на другое устройство.",
@@ -253,5 +253,6 @@
"simplex-private-5-title": "Многоуровневое<br>Заполнения содержимого",
"please-use-link-in-mobile-app": "Пожалуйста, воспользуйтесь ссылкой в мобильном приложении",
"please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код.",
"docs-dropdown-10": "Прозрачность"
"docs-dropdown-10": "Прозрачность",
"docs-dropdown-12": "Безопасность"
}