Compare commits

..

38 Commits

Author SHA1 Message Date
JRoberts
5578183777 android: version 4.0 (59) 2022-09-24 20:02:02 +04:00
JRoberts
520800ded0 android: disable new chat buttons and notifications settings when chat is stopped (#1116) 2022-09-24 19:59:35 +04:00
JRoberts
47a6a81854 android: version 4.0 (58) 2022-09-24 18:57:47 +04:00
JRoberts
2a4b7b83a4 android: fix 'choose attachment' layout for German language (#1115) 2022-09-24 18:39:02 +04:00
Stanislav Dmitrenko
792c442aa3 Fixes #1105 (#1108) 2022-09-24 15:29:42 +01:00
JRoberts
54b39e8d00 android: ui fixes (#1114)
* new chat buttons alignment

* translations
2022-09-24 15:24:54 +01:00
Stanislav Dmitrenko
39f82e9e1a android: single call to initialize chat, fix (#1109)
* JNI experiments

* Next try

* Next try

* Final JNI code for the new library method

* remove unused functions

* android: refactor, fix; ios: change entropy bounds

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-09-24 13:45:10 +01:00
Evgeny Poberezkin
ab7fed1628 ios: use single call to initialize chat controller (#1110)
* ios: use single call to initialize chat controller

* update logs

* comments
2022-09-24 09:28:22 +01:00
Evgeny Poberezkin
c94691c89e android: fix string 2022-09-23 23:01:02 +01:00
Evgeny Poberezkin
3c95b76e5a android: add German translations (#1079)
* android: add file for German translations

* translations

* complete translation

* translations (up to line 630)

* update german strings file

* remove unused strings

* translations for Android completed

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: M Sarmad Qadeer <MSarmadQadeer@gmail.com>
2022-09-23 22:57:00 +01:00
Evgeny Poberezkin
a977a0dd17 core: single function to initialize the chat controller only if encryption key is correct (#1107) 2022-09-23 22:22:56 +04:00
JRoberts
e1a7b02e59 android: version 4.0-beta.3 (57) 2022-09-23 18:40:28 +04:00
JRoberts
3537d3871c android: version 4.0-beta.3 (56) 2022-09-23 18:01:26 +04:00
JRoberts
093e7b4c78 ios: version 4.0 (75) 2022-09-23 16:52:47 +04:00
Evgeny Poberezkin
6e9cf2ba91 ios: fix background refresh crash, memoize migrateChatDatabase (#1103)
* ios: fix background refresh crash, memoize migrateChatDatabase

* store returned value
2022-09-23 12:51:40 +01:00
Evgeny Poberezkin
7c06961ff8 android: update/remove strings (#1104) 2022-09-23 12:51:27 +01:00
Stanislav Dmitrenko
2b53406ccf android: floating button for creating a new chat (#1102)
* Floating button for creating a new chat
- also fixed two-line preview in a chat list

* Button color in light theme

* update button color, remove elevation

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-09-23 00:03:06 +01:00
Stanislav Dmitrenko
06c79cc2bc android: Icons (#1100)
* Icons

* Icons in app's info

* Icon foreground

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-09-22 23:28:17 +01:00
Evgeny Poberezkin
5e870ec30f ios: fix chat list layout (#1094)
* ios: fix chat list layout

* set chat row height depending on dynamic type size

* update chat list layout sizes for ios15/16
2022-09-22 22:27:20 +01:00
Stanislav Dmitrenko
6a6d246dc5 android: UI fixes (#1099)
* UI fixes

* eol
2022-09-22 21:50:12 +01:00
Stanislav Dmitrenko
07191bfb61 android: UX for making connections (#1098)
* android: UX for making connections

* Tabs background color was specified

* Different icon and description

* update texts, fix layout on ios small screens

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-09-22 21:27:46 +01:00
JRoberts
b62895ca76 android: version 4.0-beta.2 (55) 2022-09-22 22:08:15 +04:00
Evgeny Poberezkin
bd2a748169 ios: allow export of unencrypted database (#1097) 2022-09-22 13:10:25 +01:00
Evgeny Poberezkin
06c7b9a995 update simplemq (sql function) 2022-09-22 12:29:50 +01:00
JRoberts
42b6bf96ff ios: version 4.0 (74) 2022-09-22 14:20:41 +04:00
JRoberts
aa6fb2fc12 core: ignore file cancel errors when deleting chat/chat item (#1093) 2022-09-22 13:06:50 +04:00
Evgeny Poberezkin
8bfeab7071 ios: update simplex library 2022-09-22 10:04:26 +01:00
Stanislav Dmitrenko
97662b040e android: Replace "make connection" screen with two buttons (#1091) 2022-09-22 09:35:44 +01:00
Stanislav Dmitrenko
a9ba16b07a android: Allow configuring WebRTC ICE servers (#1090)
* Allow configuring WebRTC ICE servers

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-09-22 09:16:25 +01:00
JRoberts
36d93f5b0e android: alerts on connection errors (#1087) 2022-09-22 11:55:11 +04:00
Evgeny Poberezkin
ba25850b73 ios: update icons and logos (#1089) 2022-09-22 08:37:45 +01:00
Evgeny Poberezkin
9b75553ddc ios: UX for making connections (#1088)
* ios: UX for making connections

* combine paste and scan into one view

* translations
2022-09-22 08:36:39 +01:00
Evgeny Poberezkin
b390630f4b ios: fix occasionally broken QR code when creating invitation link (#1086) 2022-09-21 16:28:01 +01:00
Evgeny Poberezkin
df329d305b ios: replace "make connection" screen with two buttons (#1084)
* ios: replace "make connection" screen with two buttons

* add "no chats", update translations
2022-09-21 15:11:52 +01:00
JRoberts
59b4ce2474 mobile: decrease mark read delay (#1085) 2022-09-21 17:39:29 +04:00
JRoberts
9442656bbe ios: alerts on connection errors (#1080)
* ios: alerts on connection errors

* fixes

* refactor

* refactor

* refactor

* translations

* delete contact with groups error

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-09-21 17:18:48 +04:00
Evgeny Poberezkin
0494cce77d ios: remove destructive role from swipe actions to avoid pre-emptive row deletion before confirmations (#1083) 2022-09-21 11:52:28 +01:00
Evgeny Poberezkin
909a8aaf9c webrtc: change server ports to 443, ios: allow configuring WebRTC ICE servers (#1077)
* webrtc: change server ports to 443

* pass stun/turn servers from user default

* ios: configure WebRTC ICE servers

* translations

* update servers

* translations
2022-09-21 10:19:13 +01:00
162 changed files with 3379 additions and 1295 deletions

View File

@@ -13,7 +13,9 @@
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
<arrangement>
<rules>

View File

@@ -1,6 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 29
targetSdk 32
versionCode 54
versionName "4.0-beta.1"
versionCode 59
versionName "4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {

View File

@@ -24,8 +24,8 @@ var TransformOperation;
let activeCall;
const processCommand = (function () {
const defaultIceServers = [
{ urls: ["stun:stun.simplex.im:5349"] },
{ urls: ["turn:turn.simplex.im:5349"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
{ urls: ["stun:stun.simplex.im:443"] },
{ urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
];
function getCallConfig(encodedInsertableStreams, iceServers, relay) {
return {
@@ -495,6 +495,7 @@ function callCryptoFunction() {
const initialPlainTextRequired = {
key: 10,
delta: 3,
empty: 1,
};
const IV_LENGTH = 12;
function encryptFrame(key) {
@@ -505,7 +506,9 @@ function callCryptoFunction() {
const initial = data.subarray(0, n);
const plaintext = data.subarray(n, data.byteLength);
try {
const ciphertext = new Uint8Array(plaintext.length ? await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv.buffer }, key, plaintext) : 0);
const ciphertext = plaintext.length
? new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv.buffer }, key, plaintext))
: new Uint8Array(0);
frame.data = concatN(initial, ciphertext, iv).buffer;
controller.enqueue(frame);
}
@@ -523,7 +526,9 @@ function callCryptoFunction() {
const ciphertext = data.subarray(n, data.byteLength - IV_LENGTH);
const iv = data.subarray(data.byteLength - IV_LENGTH, data.byteLength);
try {
const plaintext = new Uint8Array(ciphertext.length ? await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext) : 0);
const plaintext = ciphertext.length
? new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext))
: new Uint8Array(0);
frame.data = concatN(initial, plaintext).buffer;
controller.enqueue(frame);
}

View File

@@ -22,42 +22,33 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass
}
// from simplex-chat
typedef void* chat_ctrl;
typedef long* chat_ctrl;
extern char *chat_migrate_db(const char *path, const char *key);
extern chat_ctrl chat_init_key(const char *path, const char *key);
extern chat_ctrl chat_init(const char *path); // deprecated
extern char *chat_migrate_init(const char *path, const char *key, chat_ctrl *ctrl);
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
extern char *chat_parse_markdown(const char *str);
JNIEXPORT jstring JNICALL
Java_chat_simplex_app_SimplexAppKt_chatMigrateDB(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_migrate_db(_dbPath, _dbKey));
jlong _ctrl = (jlong) 0;
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, &_ctrl));
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
return res;
}
JNIEXPORT jlong JNICALL
Java_chat_simplex_app_SimplexAppKt_chatInitKey(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
jlong ctrl = (jlong)chat_init_key(_dbPath, _dbKey);
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
return ctrl;
}
JNIEXPORT jlong JNICALL
Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring dbPath) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
jlong ctrl = (jlong)chat_init(_dbPath);
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
return ctrl;
// Creating array of Object's (boxed values can be passed, eg. Long instead of long)
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
// Java's String
(*env)->SetObjectArrayElement(env, ret, 0, res);
// Java's Long
(*env)->SetObjectArrayElement(env, ret, 1,
(*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Long"),
(*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Long"), "<init>", "(J)V"),
_ctrl));
return ret;
}
JNIEXPORT jstring JNICALL

View File

@@ -10,6 +10,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationsMode
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*
@@ -26,9 +27,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int
// SimpleX API
typealias ChatCtrl = Long
external fun chatMigrateDB(dbPath: String, dbKey: String): String
external fun chatInitKey(dbPath: String, dbKey: String): ChatCtrl
external fun chatInit(dbPath: String): ChatCtrl
external fun chatMigrateInit(dbPath: String, dbKey: String): Array<Any>
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
external fun chatRecvMsg(ctrl: ChatCtrl): String
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
@@ -38,20 +37,24 @@ class SimplexApp: Application(), LifecycleEventObserver {
lateinit var chatController: ChatController
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.getDatabaseKey() ?: ""
val res = DatabaseUtils.migrateChatDatabase(dbKey)
val ctrl = if (res.second is DBMigrationResult.OK) {
chatInitKey(getFilesDirectory(applicationContext), dbKey)
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() ?: ""
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
val res: DBMigrationResult = kotlin.runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
val ctrl = if (res is DBMigrationResult.OK) {
migrated[1] as Long
} else null
if (::chatController.isInitialized) {
chatController.ctrl = ctrl
} else {
chatController = ChatController(ctrl, ntfManager, applicationContext, appPreferences)
}
chatModel.chatDbEncrypted.value = res.first
chatModel.chatDbStatus.value = res.second
if (res.second != DBMigrationResult.OK) {
Log.d(TAG, "Unable to migrate successfully: ${res.second}")
chatModel.chatDbEncrypted.value = dbKey != ""
chatModel.chatDbStatus.value = res
if (res != DBMigrationResult.OK) {
Log.d(TAG, "Unable to migrate successfully: $res")
} else if (startChat) {
// If we migrated successfully means previous re-encryption process on database level finished successfully too
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)

View File

@@ -30,6 +30,7 @@ class ChatModel(val controller: ChatController) {
val chatDbChanged = mutableStateOf<Boolean>(false)
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val chatDbDeleted = mutableStateOf(false)
val chats = mutableStateListOf<Chat>()
// current chat
@@ -37,7 +38,6 @@ class ChatModel(val controller: ChatController) {
val chatItems = mutableStateListOf<ChatItem>()
val groupMembers = mutableStateListOf<GroupMember>()
var connReqInvitation: String? = null
val terminalItems = mutableStateListOf<TerminalItem>()
val userAddress = mutableStateOf<String?>(null)
val userSMPServers = mutableStateOf<(List<String>)?>(null)
@@ -1442,6 +1442,9 @@ enum class FormatColor(val color: String) {
@Serializable
class SndFileTransfer() {}
@Serializable
class RcvFileTransfer() {}
@Serializable
class FileTransferMeta() {}

View File

@@ -25,6 +25,7 @@ import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.ConnectViaLinkTab
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationPreviewMode
import chat.simplex.app.views.usersettings.NotificationsMode
@@ -87,6 +88,7 @@ class AppPreferences(val context: Context) {
)
val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
@@ -105,6 +107,7 @@ class AppPreferences(val context: Context) {
val networkTCPKeepIntvl = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_INTVL, KeepAliveOpts.defaults.keepIntvl)
val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt)
val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false)
val connectViaLinkTab = mkStrPreference(SHARED_PREFS_CONNECT_VIA_LINK_TAB, ConnectViaLinkTab.SCAN.name)
val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true)
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
@@ -174,6 +177,7 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN = "CallsOnLockScreen"
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
private const val SHARED_PREFS_WEBRTC_ICE_SERVERS = "WebrtcICEServers"
private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
@@ -192,6 +196,7 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl"
private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt"
private const val SHARED_PREFS_INCOGNITO = "Incognito"
private const val SHARED_PREFS_CONNECT_VIA_LINK_TAB = "ConnectViaLinkTab"
private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase"
private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase"
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
@@ -385,9 +390,15 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
suspend fun apiSendMessage(type: ChatType, id: Long, file: String? = null, quotedItemId: Long? = null, mc: MsgContent): AChatItem? {
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc)
val r = sendCmd(cmd)
if (r is CR.NewChatItem ) return r.chatItem
Log.e(TAG, "apiSendMessage bad response: ${r.responseType} ${r.details}")
return null
return when (r) {
is CR.NewChatItem -> r.chatItem
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiSendMessage", generalGetString(R.string.error_sending_message), r)
}
null
}
}
}
suspend fun apiUpdateChatItem(type: ChatType, id: Long, itemId: Long, mc: MsgContent): AChatItem? {
@@ -475,9 +486,15 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
suspend fun apiAddContact(): String? {
val r = sendCmd(CC.AddContact())
if (r is CR.Invitation) return r.connReqInvitation
Log.e(TAG, "apiAddContact bad response: ${r.responseType} ${r.details}")
return null
return when (r) {
is CR.Invitation -> r.connReqInvitation
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiAddContact", generalGetString(R.string.connection_error), r)
}
null
}
}
}
suspend fun apiConnect(connReq: String): Boolean {
@@ -509,7 +526,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return false
}
else -> {
apiErrorAlert("apiConnect", "Connection error", r)
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiConnect", generalGetString(R.string.connection_error), r)
}
return false
}
}
@@ -526,7 +545,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
if (e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.ContactGroups) {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.cannot_delete_contact),
String.format(generalGetString(R.string.contact_cannot_be_deleted_as_they_are_in_groups), e.errorType.contact.displayName, e.errorType.groupNames)
String.format(generalGetString(R.string.contact_cannot_be_deleted_as_they_are_in_groups), e.errorType.contact.displayName, e.errorType.groupNames.joinToString(", "))
)
}
}
@@ -581,9 +600,15 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
suspend fun apiCreateUserAddress(): String? {
val r = sendCmd(CC.CreateMyAddress())
if (r is CR.UserContactLinkCreated) return r.connReqContact
Log.e(TAG, "apiCreateUserAddress bad response: ${r.responseType} ${r.details}")
return null
return when (r) {
is CR.UserContactLinkCreated -> r.connReqContact
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateUserAddress", generalGetString(R.string.error_creating_address), r)
}
null
}
}
}
suspend fun apiDeleteUserAddress(): Boolean {
@@ -606,9 +631,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? {
val r = sendCmd(CC.ApiAcceptContact(contactReqId))
if (r is CR.AcceptingContactRequest) return r.contact
Log.e(TAG, "apiAcceptContactRequest bad response: ${r.responseType} ${r.details}")
return null
return when {
r is CR.AcceptingContactRequest -> r.contact
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.SMP
&& r.chatError.agentError.smpErr is SMPErrorType.AUTH -> {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.connection_error_auth),
generalGetString(R.string.sender_may_have_deleted_the_connection_request)
)
null
}
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiAcceptContactRequest", generalGetString(R.string.error_accepting_contact_request), r)
}
null
}
}
}
suspend fun apiRejectContactRequest(contactReqId: Long): Boolean {
@@ -666,9 +706,22 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
suspend fun apiReceiveFile(fileId: Long): AChatItem? {
val r = sendCmd(CC.ReceiveFile(fileId))
if (r is CR.RcvFileAccepted) return r.chatItem
Log.e(TAG, "apiReceiveFile bad response: ${r.responseType} ${r.details}")
return null
return when (r) {
is CR.RcvFileAccepted -> r.chatItem
is CR.RcvFileAcceptedSndCancelled -> {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.cannot_receive_file),
generalGetString(R.string.sender_cancelled_file_transfer)
)
null
}
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiReceiveFile", generalGetString(R.string.error_receiving_file), r)
}
null
}
}
}
suspend fun apiNewGroup(p: GroupProfile): GroupInfo? {
@@ -680,9 +733,15 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
suspend fun apiAddMember(groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? {
val r = sendCmd(CC.ApiAddMember(groupId, contactId, memberRole))
if (r is CR.SentGroupInvitation) return r.member
Log.e(TAG, "apiAddMember bad response: ${r.responseType} ${r.details}")
return null
return when (r) {
is CR.SentGroupInvitation -> r.member
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiAddMember", generalGetString(R.string.error_adding_members), r)
}
null
}
}
}
suspend fun apiJoinGroup(groupId: Long) {
@@ -699,11 +758,11 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
} else if (e is ChatError.ChatErrorStore && e.storeError is StoreError.GroupNotFound) {
deleteGroup()
AlertManager.shared.showAlertMsg(generalGetString(R.string.alert_title_no_group), generalGetString(R.string.alert_message_no_group))
} else {
AlertManager.shared.showAlertMsg(generalGetString(R.string.alert_title_join_group_error), "$e")
} else if (!(networkErrorAlert(r))) {
apiErrorAlert("apiJoinGroup", generalGetString(R.string.error_joining_group), r)
}
}
else -> Log.e(TAG, "apiJoinGroup bad response: ${r.responseType} ${r.details}")
else -> apiErrorAlert("apiJoinGroup", generalGetString(R.string.error_joining_group), r)
}
}
@@ -746,6 +805,30 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
private fun networkErrorAlert(r: CR): Boolean {
return when {
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.BROKER
&& r.chatError.agentError.brokerErr is BrokerErrorType.TIMEOUT -> {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.connection_timeout),
generalGetString(R.string.network_error_desc)
)
true
}
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.BROKER
&& r.chatError.agentError.brokerErr is BrokerErrorType.NETWORK -> {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.connection_error),
generalGetString(R.string.network_error_desc)
)
true
}
else -> false
}
}
fun apiErrorAlert(method: String, title: String, r: CR) {
val errMsg = "${r.responseType}: ${r.details}"
Log.e(TAG, "$method bad response: $errMsg")
@@ -882,7 +965,16 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
withCall(r, r.contact) { call ->
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey)
val useRelay = chatModel.controller.appPrefs.webrtcPolicyRelay.get()
chatModel.callCommand.value = WCallCommand.Offer(offer = r.offer.rtcSession, iceCandidates = r.offer.rtcIceCandidates, media = r.callType.media, aesKey = r.sharedKey, relay = useRelay)
val iceServers = getIceServers()
Log.d(TAG, ".callOffer iceServers $iceServers")
chatModel.callCommand.value = WCallCommand.Offer(
offer = r.offer.rtcSession,
iceCandidates = r.offer.rtcIceCandidates,
media = r.callType.media,
aesKey = r.sharedKey,
iceServers = iceServers,
relay = useRelay
)
}
}
is CR.CallAnswer -> {
@@ -1592,6 +1684,7 @@ sealed class CR {
@Serializable @SerialName("groupUpdated") class GroupUpdated(val toGroup: GroupInfo): CR()
// receiving file events
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileStart") class RcvFileStart(val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val chatItem: AChatItem): CR()
// sending file events
@@ -1676,6 +1769,7 @@ sealed class CR {
is ConnectedToGroupMember -> "connectedToGroupMember"
is GroupRemoved -> "groupRemoved"
is GroupUpdated -> "groupUpdated"
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
is RcvFileAccepted -> "rcvFileAccepted"
is RcvFileStart -> "rcvFileStart"
is RcvFileComplete -> "rcvFileComplete"
@@ -1761,6 +1855,7 @@ sealed class CR {
is ConnectedToGroupMember -> "groupInfo: $groupInfo\nmember: $member"
is GroupRemoved -> json.encodeToString(groupInfo)
is GroupUpdated -> json.encodeToString(toGroup)
is RcvFileAcceptedSndCancelled -> noDetails()
is RcvFileAccepted -> json.encodeToString(chatItem)
is RcvFileStart -> json.encodeToString(chatItem)
is RcvFileComplete -> json.encodeToString(chatItem)

View File

@@ -51,7 +51,14 @@ class CallManager(val chatModel: ChatModel) {
)
showCallView.value = true
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
callCommand.value = WCallCommand.Start (media = invitation.callType.media, aesKey = invitation.sharedKey, relay = useRelay)
val iceServers = getIceServers()
Log.d(TAG, "answerIncomingCall iceServers: $iceServers")
callCommand.value = WCallCommand.Start(
media = invitation.callType.media,
aesKey = invitation.sharedKey,
iceServers = iceServers,
relay = useRelay
)
callInvitations.remove(invitation.contact.id)
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
activeCallInvitation.value = null

View File

@@ -3,11 +3,13 @@ package chat.simplex.app.views.call
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.Contact
import chat.simplex.app.views.helpers.generalGetString
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URI
data class Call(
val contact: Contact,
@@ -115,7 +117,7 @@ sealed class WCallResponse {
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
@Serializable class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type
@Serializable
@@ -153,4 +155,48 @@ class ConnectionState(
val iceConnectionState: String,
val iceGatheringState: String,
val signalingState: String
)
)
// the servers are expected in this format:
// stun:stun.simplex.im:443
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443
fun parseRTCIceServer(str: String): RTCIceServer? {
var s = replaceScheme(str, "stun:")
s = replaceScheme(s, "turn:")
val u = runCatching { URI(s) }.getOrNull()
if (u != null) {
val scheme = u.scheme
val host = u.host
val port = u.port
if (u.path == "" && (scheme == "stun" || scheme == "turn")) {
val userInfo = u.userInfo?.split(":")
return RTCIceServer(
urls = listOf("$scheme:$host:$port"),
username = userInfo?.getOrNull(0),
credential = userInfo?.getOrNull(1)
)
}
}
return null
}
private fun replaceScheme(s: String, scheme: String): String = if (s.startsWith(scheme)) s.replace(scheme, "$scheme//") else s
fun parseRTCIceServers(servers: List<String>): List<RTCIceServer>? {
val iceServers: ArrayList<RTCIceServer> = ArrayList()
for (s in servers) {
val server = parseRTCIceServer(s)
if (server != null) {
iceServers.add(server)
} else {
return null
}
}
return if (iceServers.isEmpty()) null else iceServers
}
fun getIceServers(): List<RTCIceServer>? {
val value = SimplexApp.context.chatController.appPrefs.webrtcIceServers.get() ?: return null
val servers: List<String> = value.split("\n")
return parseRTCIceServers(servers)
}

View File

@@ -569,7 +569,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
if (cItem.isRcvNew) {
LaunchedEffect(cItem.id) {
scope.launch {
delay(750)
delay(600)
markRead(CC.ItemRange(cItem.id, cItem.id), null)
}
}

View File

@@ -43,10 +43,12 @@ fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () ->
selectedRole = selectedRole,
inviteMembers = {
withApi {
selectedContacts.forEach {
val member = chatModel.controller.apiAddMember(groupInfo.groupId, it, selectedRole.value)
for (contactId in selectedContacts) {
val member = chatModel.controller.apiAddMember(groupInfo.groupId, contactId, selectedRole.value)
if (member != null) {
chatModel.upsertGroupMember(groupInfo, member)
} else {
break
}
}
close.invoke()

View File

@@ -151,7 +151,7 @@ fun GroupChatInfoLayout(
}
SectionDivider()
}
SectionItemView(height = 50.dp) {
SectionItemView(minHeight = 50.dp) {
MemberRow(groupInfo.membership, user = true)
}
if (members.isNotEmpty()) {
@@ -243,7 +243,7 @@ fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, addMembers: ()
fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
Column {
members.forEachIndexed { index, member ->
SectionItemView(height = 50.dp) {
SectionItemView(minHeight = 50.dp) {
MemberRow(member, showMemberInfo)
}
if (index < members.lastIndex) {

View File

@@ -462,7 +462,7 @@ fun ChatListNavLinkLayout(
showMenu: MutableState<Boolean>,
stopped: Boolean
) {
var modifier = Modifier.fillMaxWidth().height(88.dp)
var modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp)
if (!stopped) modifier = modifier.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
Surface(modifier) {
Row(

View File

@@ -14,7 +14,10 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
@@ -22,11 +25,12 @@ import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.Indigo
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.NewChatSheet
import chat.simplex.app.views.onboarding.MakeConnection
import chat.simplex.app.views.usersettings.SettingsView
import chat.simplex.app.views.usersettings.simplexTeamUri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -80,6 +84,26 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
drawerContent = { SettingsView(chatModel, setPerformLA) },
sheetPeekHeight = 0.dp,
sheetContent = { NewChatSheet(chatModel, scaffoldCtrl) },
floatingActionButton = {
FloatingActionButton(
onClick = {
if (!stopped) {
if (!scaffoldCtrl.expanded.value) scaffoldCtrl.expand() else scaffoldCtrl.collapse()
}
},
Modifier.padding(bottom = 90.dp),
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp,
hoveredElevation = 0.dp,
focusedElevation = 0.dp,
),
backgroundColor = if (!stopped) MaterialTheme.colors.primary else HighOrLowlight,
contentColor = Color.White
) {
Icon(Icons.Default.Edit, stringResource(R.string.add_contact_or_create_group))
}
},
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp),
) {
Box {
@@ -91,7 +115,9 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
if (chatModel.chats.isNotEmpty()) {
ChatList(chatModel, search = searchInList)
} else {
MakeConnection(chatModel)
if (!stopped) {
OnboardingButtons(scaffoldCtrl)
}
}
}
if (scaffoldCtrl.expanded.value) {
@@ -106,6 +132,50 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
}
}
@Composable
private fun OnboardingButtons(scaffoldCtrl: ScaffoldController) {
Box {
Column(Modifier.fillMaxSize().padding(6.dp), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
val uriHandler = LocalUriHandler.current
ConnectButton(generalGetString(R.string.chat_with_developers)) {
uriHandler.openUri(simplexTeamUri)
}
Spacer(Modifier.height(10.dp))
ConnectButton(generalGetString(R.string.tap_to_start_new_chat)) {
scaffoldCtrl.toggleSheet()
}
val color = MaterialTheme.colors.primary
Canvas(modifier = Modifier.width(46.dp).height(10.dp), onDraw = {
val trianglePath = Path().apply {
moveTo(0.dp.toPx(), 0f)
lineTo(16.dp.toPx(), 0.dp.toPx())
lineTo(8.dp.toPx(), 10.dp.toPx())
lineTo(0.dp.toPx(), 0.dp.toPx())
}
drawPath(
color = color,
path = trianglePath
)
})
Spacer(Modifier.height(80.dp))
}
Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = HighOrLowlight)
}
}
@Composable
private fun ConnectButton(text: String, onClick: () -> Unit) {
Box(
Modifier
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colors.primary)
.clickable { onClick() }
.padding(vertical = 10.dp, horizontal = 20.dp),
) {
Text(text, color = Color.White)
}
}
@Composable
fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
var showSearch by rememberSaveable { mutableStateOf(false) }
@@ -121,20 +191,14 @@ fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stop
}
}
}
if (!stopped) {
if (stopped) {
barButtons.add {
IconButton(onClick = { scaffoldCtrl.toggleSheet() }) {
Icon(
Icons.Outlined.AddCircle,
stringResource(R.string.add_contact),
tint = MaterialTheme.colors.primary,
IconButton(onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.chat_is_stopped_indication),
generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)
)
}
}
} else {
barButtons.add {
IconButton(onClick = { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_is_stopped_indication),
generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)) }) {
}) {
Icon(
Icons.Filled.Report,
generalGetString(R.string.chat_is_stopped_indication),

View File

@@ -62,6 +62,7 @@ fun DatabaseView(
importArchiveAlert(m, context, uri, progressIndicator)
}
}
val chatDbDeleted = remember { m.chatDbDeleted }
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(getAppFilesDirectory(context))) }
LaunchedEffect(m.chatRunning) {
runChat.value = m.chatRunning.value ?: true
@@ -79,6 +80,7 @@ fun DatabaseView(
chatArchiveName,
chatArchiveTime,
chatLastStart,
chatDbDeleted.value,
appFilesCountAndSize,
startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) },
stopChatAlert = { stopChatAlert(m, runChat, context) },
@@ -115,6 +117,7 @@ fun DatabaseLayout(
chatArchiveName: MutableState<String?>,
chatArchiveTime: MutableState<Instant?>,
chatLastStart: MutableState<Instant?>,
chatDbDeleted: Boolean,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
startChat: () -> Unit,
stopChatAlert: () -> Unit,
@@ -137,7 +140,7 @@ fun DatabaseLayout(
)
SectionView(stringResource(R.string.run_chat_section)) {
RunChatSetting(runChat, stopped, startChat, stopChatAlert)
RunChatSetting(runChat, stopped, chatDbDeleted, startChat, stopChatAlert)
}
SectionSpacer()
@@ -230,6 +233,7 @@ fun DatabaseLayout(
fun RunChatSetting(
runChat: Boolean,
stopped: Boolean,
chatDbDeleted: Boolean,
startChat: () -> Unit,
stopChatAlert: () -> Unit
) {
@@ -248,6 +252,7 @@ fun RunChatSetting(
)
Spacer(Modifier.fillMaxWidth().weight(1f))
Switch(
enabled= !chatDbDeleted,
checked = runChat,
onCheckedChange = { runChatSwitch ->
if (runChatSwitch) {
@@ -516,6 +521,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
withApi {
try {
m.controller.apiDeleteStorage()
m.chatDbDeleted.value = true
DatabaseUtils.removeDatabaseKey()
m.controller.appPrefs.storeDBPassphrase.set(true)
operationEnded(m, progressIndicator) {
@@ -569,6 +575,7 @@ fun PreviewDatabaseLayout() {
chatArchiveName = remember { mutableStateOf("dummy_archive") },
chatArchiveTime = remember { mutableStateOf(Clock.System.now()) },
chatLastStart = remember { mutableStateOf(Clock.System.now()) },
chatDbDeleted = false,
appFilesCountAndSize = remember { mutableStateOf(0 to 0L) },
startChat = {},
stopChatAlert = {},

View File

@@ -3,7 +3,6 @@ package chat.simplex.app.views.helpers
import android.util.Log
import chat.simplex.app.*
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.model.json
import chat.simplex.app.views.usersettings.Cryptor
import kotlinx.serialization.*
import java.io.File
@@ -41,31 +40,27 @@ object DatabaseUtils {
appPreferences.initializationVectorDBPassphrase.set(null)
}
fun migrateChatDatabase(useKey: String? = null): Pair<Boolean, DBMigrationResult> {
Log.d(TAG, "migrateChatDatabase ${appPreferences.storeDBPassphrase.get()}")
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
fun useDatabaseKey(): String {
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
var dbKey = ""
val useKeychain = appPreferences.storeDBPassphrase.get()
if (useKey != null) {
dbKey = useKey
} else if (useKeychain) {
if (useKeychain) {
if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) {
dbKey = randomDatabasePassword()
setDatabaseKey(dbKey)
appPreferences.initialRandomDBPassphrase.set(true)
} else {
dbKey = getDatabaseKey() ?: ""
}
}
Log.d(TAG, "migrateChatDatabase DB path: $dbAbsolutePathPrefix")
val migrated = chatMigrateDB(dbAbsolutePathPrefix, dbKey)
val res: DBMigrationResult = kotlin.runCatching {
json.decodeFromString<DBMigrationResult>(migrated)
}.getOrElse { DBMigrationResult.Unknown(migrated) }
val encrypted = dbKey != ""
return encrypted to res
return dbKey
}
private fun randomDatabasePassword(): String = ByteArray(32).apply { SecureRandom().nextBytes(this) }.toBase64String()
private fun randomDatabasePassword(): String {
val s = ByteArray(32)
SecureRandom().nextBytes(s)
return s.toBase64String().replace("\n", "")
}
}
@Serializable

View File

@@ -0,0 +1,95 @@
package chat.simplex.app.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExpandLess
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun <T> ExposedDropDownSettingRow(
title: String,
values: List<Pair<T, String>>,
selection: State<T>,
label: String? = null,
icon: ImageVector? = null,
iconTint: Color = HighOrLowlight,
enabled: State<Boolean> = mutableStateOf(true),
onSelected: (T) -> Unit
) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
var expanded by remember { mutableStateOf(false) }
if (icon != null) {
Icon(
icon,
"",
Modifier.padding(end = 8.dp),
tint = iconTint
)
}
Text(title, color = if (enabled.value) Color.Unspecified else HighOrLowlight)
Spacer(Modifier.fillMaxWidth().weight(1f))
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded && enabled.value
}
) {
Row(
Modifier.padding(start = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
Text(
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = HighOrLowlight
)
Spacer(Modifier.size(12.dp))
Icon(
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
generalGetString(R.string.icon_descr_more_button),
tint = HighOrLowlight
)
}
ExposedDropdownMenu(
modifier = Modifier.widthIn(min = 200.dp),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
values.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
onSelected(selectionOption.first)
expanded = false
}
) {
Text(
selectionOption.second + (if (label != null) " $label" else ""),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
}
}

View File

@@ -58,11 +58,11 @@ fun <T> SectionViewSelectable(
}
@Composable
fun SectionItemView(click: (() -> Unit)? = null, height: Dp = 46.dp, disabled: Boolean = false, content: (@Composable RowScope.() -> Unit)) {
fun SectionItemView(click: (() -> Unit)? = null, minHeight: Dp = 46.dp, disabled: Boolean = false, content: (@Composable RowScope.() -> Unit)) {
val modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth()
.height(height)
.sizeIn(minHeight = minHeight)
Row(
if (click == null || disabled) modifier else modifier.clickable(onClick = click),
verticalAlignment = Alignment.CenterVertically
@@ -74,7 +74,7 @@ fun SectionItemView(click: (() -> Unit)? = null, height: Dp = 46.dp, disabled: B
@Composable
fun SectionItemViewSpaceBetween(
click: (() -> Unit)? = null,
height: Dp = 46.dp,
minHeight: Dp = 46.dp,
padding: PaddingValues = PaddingValues(horizontal = 8.dp),
disabled: Boolean = false,
content: (@Composable () -> Unit)
@@ -82,7 +82,7 @@ fun SectionItemViewSpaceBetween(
val modifier = Modifier
.padding(padding)
.fillMaxWidth()
.height(height)
.sizeIn(minHeight = minHeight)
Row(
if (click == null || disabled) modifier else modifier.clickable(onClick = click),
horizontalArrangement = Arrangement.SpaceBetween,

View File

@@ -2,6 +2,8 @@ package chat.simplex.app.views.newchat
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.TheaterComedy
@@ -12,7 +14,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -20,20 +21,16 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.app.views.helpers.shareText
import chat.simplex.app.views.helpers.*
@Composable
fun AddContactView(chatModel: ChatModel) {
val connReq = chatModel.connReqInvitation
if (connReq != null) {
val cxt = LocalContext.current
AddContactLayout(
chatModelIncognito = chatModel.incognito.value,
connReq = connReq,
share = { shareText(cxt, connReq) }
)
}
fun AddContactView(chatModel: ChatModel, connReqInvitation: String) {
val cxt = LocalContext.current
AddContactLayout(
chatModelIncognito = chatModel.incognito.value,
connReq = connReqInvitation,
share = { shareText(cxt, connReqInvitation) }
)
}
@Composable
@@ -41,21 +38,18 @@ fun AddContactLayout(chatModelIncognito: Boolean, connReq: String, share: () ->
BoxWithConstraints {
val screenHeight = maxHeight
Column(
Modifier.padding(bottom = 16.dp),
Modifier.verticalScroll(rememberScrollState()).padding(bottom = 16.dp),
verticalArrangement = Arrangement.SpaceBetween,
) {
Text(
stringResource(R.string.add_contact),
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
modifier = Modifier
.padding(vertical = 5.dp)
.padding(horizontal = 8.dp)
Modifier.padding(bottom = 16.dp),
style = MaterialTheme.typography.h1,
)
Text(
stringResource(R.string.show_QR_code_for_your_contact_to_scan_from_the_app__multiline),
modifier = Modifier.padding(horizontal = 8.dp)
)
Row(Modifier.padding(horizontal = 8.dp)) {
Row {
InfoAboutIncognito(
chatModelIncognito,
true,
@@ -63,18 +57,27 @@ fun AddContactLayout(chatModelIncognito: Boolean, connReq: String, share: () ->
generalGetString(R.string.your_profile_will_be_sent)
)
}
QRCode(
connReq, Modifier
.weight(1f, fill = false)
.aspectRatio(1f)
.padding(vertical = 3.dp)
)
if (connReq.isNotEmpty()) {
QRCode(
connReq, Modifier
.aspectRatio(1f)
.padding(vertical = 3.dp)
)
} else {
CircularProgressIndicator(
Modifier
.size(36.dp)
.padding(4.dp)
.align(Alignment.CenterHorizontally),
color = HighOrLowlight,
strokeWidth = 3.dp
)
}
Text(
stringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel),
annotatedStringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel),
lineHeight = 22.sp,
modifier = Modifier
.padding(horizontal = 8.dp)
.padding(bottom = if (screenHeight > 600.dp) 16.dp else 8.dp)
.padding(bottom = if (screenHeight > 600.dp) 8.dp else 0.dp)
)
Row(
Modifier.fillMaxWidth(),

View File

@@ -0,0 +1,73 @@
package chat.simplex.app.views.newchat
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.usersettings.UserAddressView
enum class ConnectViaLinkTab {
SCAN, PASTE
}
@Composable
fun ConnectViaLinkView(m: ChatModel) {
val selection = remember {
mutableStateOf(
runCatching { ConnectViaLinkTab.valueOf(m.controller.appPrefs.connectViaLinkTab.get()!!) }.getOrDefault(ConnectViaLinkTab.SCAN)
)
}
val tabTitles = ConnectViaLinkTab.values().map {
when (it) {
ConnectViaLinkTab.SCAN -> stringResource(R.string.scan_QR_code)
ConnectViaLinkTab.PASTE -> stringResource(R.string.paste_the_link_you_received)
}
}
Column(
Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(Modifier.weight(1f)) {
when (selection.value) {
ConnectViaLinkTab.SCAN -> {
ScanToConnectView(m)
}
ConnectViaLinkTab.PASTE -> {
PasteToConnectView(m)
}
}
}
TabRow(
selectedTabIndex = selection.value.ordinal,
backgroundColor = MaterialTheme.colors.background,
contentColor = MaterialTheme.colors.primary,
) {
tabTitles.forEachIndexed { index, it ->
Tab(
selected = selection.value.ordinal == index,
onClick = {
selection.value = ConnectViaLinkTab.values()[index]
m.controller.appPrefs.connectViaLinkTab.set(selection.value .name)
},
text = { Text(it, fontSize = 13.sp) },
icon = {
Icon(
if (ConnectViaLinkTab.SCAN.ordinal == index) Icons.Outlined.QrCode else Icons.Outlined.Article,
it
)
},
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = HighOrLowlight,
)
}
}
}
}

View File

@@ -0,0 +1,89 @@
package chat.simplex.app.views.newchat
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.usersettings.UserAddressView
enum class CreateLinkTab {
ONE_TIME, LONG_TERM
}
@Composable
fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
val selection = remember { mutableStateOf(initialSelection) }
val connReqInvitation = remember { mutableStateOf("") }
val creatingConnReq = remember { mutableStateOf(false) }
LaunchedEffect(selection.value) {
if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() && !creatingConnReq.value) {
createInvitation(m, creatingConnReq, connReqInvitation)
}
}
val tabTitles = CreateLinkTab.values().map {
when {
it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() -> stringResource(R.string.create_one_time_link)
it == CreateLinkTab.ONE_TIME -> stringResource(R.string.one_time_link)
it == CreateLinkTab.LONG_TERM -> stringResource(R.string.your_contact_address)
else -> ""
}
}
Column(
Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(Modifier.weight(1f)) {
when (selection.value) {
CreateLinkTab.ONE_TIME -> {
AddContactView(m, connReqInvitation.value)
}
CreateLinkTab.LONG_TERM -> {
UserAddressView(m)
}
}
}
TabRow(
selectedTabIndex = selection.value.ordinal,
backgroundColor = MaterialTheme.colors.background,
contentColor = MaterialTheme.colors.primary,
) {
tabTitles.forEachIndexed { index, it ->
Tab(
selected = selection.value.ordinal == index,
onClick = {
selection.value = CreateLinkTab.values()[index]
},
text = { Text(it, fontSize = 13.sp) },
icon = {
Icon(
if (CreateLinkTab.ONE_TIME.ordinal == index) Icons.Outlined.RepeatOne else Icons.Outlined.AllInclusive,
it
)
},
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = HighOrLowlight,
)
}
}
}
}
private fun createInvitation(m: ChatModel, creatingConnReq: MutableState<Boolean>, connReqInvitation: MutableState<String>) {
creatingConnReq.value = true
withApi {
val connReq = m.controller.apiAddContact()
if (connReq != null) {
connReqInvitation.value = connReq
} else {
creatingConnReq.value = false
}
}
}

View File

@@ -1,6 +1,6 @@
package chat.simplex.app.views.newchat
import android.Manifest
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -22,32 +22,18 @@ import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chatlist.ScaffoldController
import chat.simplex.app.views.helpers.ModalManager
import chat.simplex.app.views.helpers.withApi
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
if (newChatCtrl.expanded.value) BackHandler { newChatCtrl.collapse() }
NewChatSheetLayout(
addContact = {
withApi {
// show spinner
chatModel.connReqInvitation = chatModel.controller.apiAddContact()
// hide spinner
if (chatModel.connReqInvitation != null) {
newChatCtrl.collapse()
ModalManager.shared.showModal { AddContactView(chatModel) }
}
}
},
scanCode = {
newChatCtrl.collapse()
ModalManager.shared.showCustomModal { close -> ScanToConnectView(chatModel, close) }
cameraPermissionState.launchPermissionRequest()
ModalManager.shared.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) }
},
pasteLink = {
connectViaLink = {
newChatCtrl.collapse()
ModalManager.shared.showCustomModal { close -> PasteToConnectView(chatModel, close) }
ModalManager.shared.showModal { ConnectViaLinkView(chatModel) }
},
createGroup = {
newChatCtrl.collapse()
@@ -59,8 +45,7 @@ fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController) {
@Composable
fun NewChatSheetLayout(
addContact: () -> Unit,
scanCode: () -> Unit,
pasteLink: () -> Unit,
connectViaLink: () -> Unit,
createGroup: () -> Unit
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -73,7 +58,7 @@ fun NewChatSheetLayout(
Divider(Modifier.padding(horizontal = 8.dp))
Box(boxModifier) {
ActionRowButton(
stringResource(R.string.create_one_time_link),
stringResource(R.string.share_one_time_link),
stringResource(R.string.to_share_with_your_contact),
Icons.Outlined.AddLink,
click = addContact
@@ -82,19 +67,10 @@ fun NewChatSheetLayout(
Divider(Modifier.padding(horizontal = 8.dp))
Box(boxModifier) {
ActionRowButton(
stringResource(R.string.paste_received_link),
stringResource(R.string.paste_received_link_from_clipboard),
Icons.Outlined.Article,
click = pasteLink
)
}
Divider(Modifier.padding(horizontal = 8.dp))
Box(boxModifier) {
ActionRowButton(
stringResource(R.string.scan_QR_code),
stringResource(R.string.in_person_or_in_video_call__bracketed),
stringResource(R.string.connect_via_link_or_qr),
stringResource(R.string.connect_via_link_or_qr_from_clipboard_or_in_person),
Icons.Outlined.QrCode,
click = scanCode
click = connectViaLink
)
}
Divider(Modifier.padding(horizontal = 8.dp))
@@ -125,7 +101,7 @@ fun ActionRowButton(
Column {
Text(
text,
textAlign = TextAlign.Center,
textAlign = TextAlign.Left,
fontWeight = FontWeight.Bold,
color = tint
)
@@ -133,7 +109,7 @@ fun ActionRowButton(
if (comment != null) {
Text(
comment,
textAlign = TextAlign.Center,
textAlign = TextAlign.Left,
style = MaterialTheme.typography.body2
)
}
@@ -189,8 +165,7 @@ fun PreviewNewChatSheet() {
SimpleXTheme {
NewChatSheetLayout(
addContact = {},
scanCode = {},
pasteLink = {},
connectViaLink = {},
createGroup = {}
)
}

View File

@@ -3,18 +3,17 @@ package chat.simplex.app.views.newchat
import android.content.ClipboardManager
import android.content.res.Configuration
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.getSystemService
@@ -25,16 +24,15 @@ import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.*
@Composable
fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
val connectionLink = remember { mutableStateOf("")}
fun PasteToConnectView(chatModel: ChatModel) {
val connectionLink = remember { mutableStateOf("") }
val context = LocalContext.current
val clipboard = getSystemService(context, ClipboardManager::class.java)
BackHandler(onBack = close)
PasteToConnectLayout(
chatModel.incognito.value,
connectionLink = connectionLink,
pasteFromClipboard = {
connectionLink.value = clipboard?.primaryClip?.getItemAt(0)?.coerceToText(context) as String
connectionLink.value = clipboard?.primaryClip?.getItemAt(0)?.coerceToText(context) as? String ?: return@PasteToConnectLayout
},
connectViaLink = { connReqUri ->
try {
@@ -48,9 +46,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
text = generalGetString(R.string.this_string_is_not_a_connection_link)
)
}
close()
},
close = close
)
}
@@ -60,52 +56,49 @@ fun PasteToConnectLayout(
connectionLink: MutableState<String>,
pasteFromClipboard: () -> Unit,
connectViaLink: (String) -> Unit,
close: () -> Unit
) {
ModalView(close) {
Column(
Column(
Modifier.verticalScroll(rememberScrollState()).padding(bottom = 16.dp),
verticalArrangement = Arrangement.SpaceBetween,
) {
Text(
stringResource(R.string.connect_via_link),
Modifier.padding(bottom = 16.dp),
verticalArrangement = Arrangement.SpaceBetween,
) {
Text(
stringResource(R.string.connect_via_link),
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
modifier = Modifier.padding(vertical = 5.dp)
)
Text(stringResource(R.string.paste_connection_link_below_to_connect))
style = MaterialTheme.typography.h1,
)
Text(stringResource(R.string.paste_connection_link_below_to_connect))
InfoAboutIncognito(
chatModelIncognito,
true,
generalGetString(R.string.incognito_random_profile_from_contact_description),
generalGetString(R.string.profile_will_be_sent_to_contact_sending_link)
)
InfoAboutIncognito(
chatModelIncognito,
true,
generalGetString(R.string.incognito_random_profile_from_contact_description),
generalGetString(R.string.profile_will_be_sent_to_contact_sending_link)
)
Box(Modifier.padding(top = 16.dp, bottom = 6.dp)) {
TextEditor(Modifier.height(180.dp), text = connectionLink)
}
Row(
Modifier.fillMaxWidth().padding(bottom = 6.dp),
horizontalArrangement = Arrangement.Start,
) {
if (connectionLink.value == "") {
SimpleButton(text = "Paste", icon = Icons.Outlined.ContentPaste) {
pasteFromClipboard()
}
} else {
SimpleButton(text = "Clear", icon = Icons.Outlined.Clear) {
connectionLink.value = ""
}
}
Spacer(Modifier.weight(1f).fillMaxWidth())
SimpleButton(text = "Connect", icon = Icons.Outlined.Link) {
connectViaLink(connectionLink.value)
}
}
Text(annotatedStringResource(R.string.you_can_also_connect_by_clicking_the_link))
Box(Modifier.padding(top = 16.dp, bottom = 6.dp)) {
TextEditor(Modifier.height(180.dp), text = connectionLink)
}
Row(
Modifier.fillMaxWidth().padding(bottom = 6.dp),
horizontalArrangement = Arrangement.Start,
) {
if (connectionLink.value == "") {
SimpleButton(text = stringResource(R.string.paste_button), icon = Icons.Outlined.ContentPaste) {
pasteFromClipboard()
}
} else {
SimpleButton(text = stringResource(R.string.clear_verb), icon = Icons.Outlined.Clear) {
connectionLink.value = ""
}
}
Spacer(Modifier.weight(1f).fillMaxWidth())
SimpleButton(text = stringResource(R.string.connect_button), icon = Icons.Outlined.Link) {
connectViaLink(connectionLink.value)
}
}
Text(annotatedStringResource(R.string.you_can_also_connect_by_clicking_the_link))
}
}
@@ -130,7 +123,6 @@ fun PreviewPasteToConnectTextbox() {
e.printStackTrace()
}
},
close = {}
)
}
}

View File

@@ -1,14 +1,15 @@
package chat.simplex.app.views.newchat
import android.Manifest
import android.content.res.Configuration
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -16,10 +17,14 @@ import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
BackHandler(onBack = close)
fun ScanToConnectView(chatModel: ChatModel) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ConnectContactLayout(
chatModelIncognito = chatModel.incognito.value,
qrCodeScanner = {
@@ -35,10 +40,8 @@ fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
text = generalGetString(R.string.this_QR_code_is_not_a_link)
)
}
close()
}
},
close = close
)
}
@@ -67,33 +70,31 @@ suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) {
}
@Composable
fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable () -> Unit, close: () -> Unit) {
ModalView(close) {
Column(
fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable () -> Unit) {
Column(
Modifier.verticalScroll(rememberScrollState()).padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
generalGetString(R.string.scan_QR_code),
Modifier.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
generalGetString(R.string.scan_QR_code),
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
modifier = Modifier.padding(vertical = 5.dp)
)
InfoAboutIncognito(
chatModelIncognito,
true,
generalGetString(R.string.incognito_random_profile_description),
generalGetString(R.string.your_profile_will_be_sent)
)
Box(
Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1F)
) { qrCodeScanner() }
Text(
annotatedStringResource(R.string.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link),
lineHeight = 22.sp
)
}
style = MaterialTheme.typography.h1,
)
InfoAboutIncognito(
chatModelIncognito,
true,
generalGetString(R.string.incognito_random_profile_description),
generalGetString(R.string.your_profile_will_be_sent)
)
Box(
Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1F)
) { qrCodeScanner() }
Text(
annotatedStringResource(R.string.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link),
lineHeight = 22.sp
)
}
}
@@ -109,7 +110,6 @@ fun PreviewConnectContactLayout() {
ConnectContactLayout(
chatModelIncognito = false,
qrCodeScanner = { Surface {} },
close = {},
)
}
}

View File

@@ -1,172 +0,0 @@
package chat.simplex.app.views.onboarding
import android.Manifest
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.User
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.usersettings.simplexTeamUri
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun MakeConnection(chatModel: ChatModel) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
MakeConnectionLayout(
chatModel.currentUser.value,
createLink = {
withApi {
// show spinner
chatModel.connReqInvitation = chatModel.controller.apiAddContact()
// hide spinner
if (chatModel.connReqInvitation != null) {
ModalManager.shared.showModal { AddContactView(chatModel) }
}
}
},
pasteLink = {
ModalManager.shared.showCustomModal { close -> PasteToConnectView(chatModel, close) }
},
scanCode = {
ModalManager.shared.showCustomModal { close -> ScanToConnectView(chatModel, close) }
cameraPermissionState.launchPermissionRequest()
},
about = {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
}
)
}
@Composable
fun MakeConnectionLayout(
user: User?,
createLink: () -> Unit,
pasteLink: () -> Unit,
scanCode: () -> Unit,
about: () -> Unit
) {
Surface {
Column(
Modifier
.fillMaxSize()
.background(color = MaterialTheme.colors.background)
.padding(20.dp)
) {
Text(
if (user == null) stringResource(R.string.make_private_connection)
else String.format(stringResource(R.string.personal_welcome), user.profile.displayName),
style = MaterialTheme.typography.h1,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
annotatedStringResource(R.string.to_make_your_first_private_connection_choose),
modifier = Modifier.padding(bottom = 16.dp)
)
ActionRow(
Icons.Outlined.AddLink,
R.string.create_1_time_link_qr_code,
R.string.it_s_secure_to_share__only_one_contact_can_use_it,
createLink
)
ActionRow(
Icons.Outlined.Article,
R.string.paste_the_link_you_received,
R.string.or_open_the_link_in_the_browser_and_tap_open_in_mobile,
pasteLink
)
ActionRow(
Icons.Outlined.QrCode,
R.string.scan_contact_s_qr_code,
R.string.in_person_or_via_a_video_call__the_most_secure_way_to_connect,
scanCode
)
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.or))
}
val uriHandler = LocalUriHandler.current
ActionRow(
Icons.Outlined.Tag,
R.string.connect_with_the_developers,
R.string.to_ask_any_questions_and_to_receive_simplex_chat_updates,
{ uriHandler.openUri(simplexTeamUri) }
)
Spacer(Modifier.fillMaxHeight().weight(1f))
SimpleButton(
text = stringResource(R.string.about_simplex),
icon = Icons.Outlined.ArrowBackIosNew,
click = about
)
}
}
}
@Composable
private fun ActionRow(icon: ImageVector, @StringRes titleId: Int, @StringRes textId: Int, action: () -> Unit) {
Row(
Modifier
.clickable { action() }
.padding(bottom = 16.dp)
) {
Icon(icon, stringResource(titleId), tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(end = 10.dp).size(40.dp))
Column {
Text(stringResource(titleId), color = MaterialTheme.colors.primary)
Text(annotatedStringResource(textId))
}
}
// Button(action: action, label: {
// HStack(alignment: .top, spacing: 20) {
// Image(systemName: icon)
// .resizable()
// .scaledToFit()
// .frame(width: 30, height: 30)
// .padding(.leading, 4)
// .padding(.top, 6)
// VStack(alignment: .leading) {
// Group {
// Text(title).font(.headline)
// Text(text).foregroundColor(.primary)
// }
// .multilineTextAlignment(.leading)
// }
// }
// })
// .padding(.bottom)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
@Composable
fun PreviewMakeConnection() {
SimpleXTheme {
MakeConnectionLayout(
user = User.sampleData,
createLink = {},
pasteLink = {},
scanCode = {},
about = {}
)
}
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -44,9 +45,9 @@ fun SimpleXInfoLayout(
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 16.dp))
InfoRow("🎭", R.string.privacy_redefined, R.string.first_platform_without_user_ids)
InfoRow("📭", R.string.immune_to_spam_and_abuse, R.string.people_can_connect_only_via_links_you_share)
InfoRow("🤝", R.string.decentralized, R.string.opensource_protocol_and_code_anybody_can_run_servers)
InfoRow(painterResource(R.drawable.privacy), R.string.privacy_redefined, R.string.first_platform_without_user_ids)
InfoRow(painterResource(R.drawable.shield), R.string.immune_to_spam_and_abuse, R.string.people_can_connect_only_via_links_you_share)
InfoRow(painterResource(R.drawable.decentralized), R.string.decentralized, R.string.opensource_protocol_and_code_anybody_can_run_servers)
Spacer(
Modifier
@@ -85,11 +86,11 @@ fun SimpleXLogo() {
}
@Composable
private fun InfoRow(emoji: String, @StringRes titleId: Int, @StringRes textId: Int) {
private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: Int) {
Row(Modifier.padding(bottom = 20.dp), verticalAlignment = Alignment.Top) {
Text(emoji, fontSize = 36.sp, modifier = Modifier
Image(icon, contentDescription = null, modifier = Modifier
.width(60.dp)
.padding(end = 16.dp))
.padding(top = 8.dp, end = 16.dp))
Column(horizontalAlignment = Alignment.Start) {
Text(stringResource(titleId), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h3, lineHeight = 24.sp)
Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.caption)

View File

@@ -6,8 +6,6 @@ import SectionView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -17,12 +15,17 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.ExposedDropDownSettingRow
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun CallSettingsView(m: ChatModel) {
fun CallSettingsView(m: ChatModel,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
) {
CallSettingsLayout(
webrtcPolicyRelay = m.controller.appPrefs.webrtcPolicyRelay,
callOnLockScreen = m.controller.appPrefs.callOnLockScreen
callOnLockScreen = m.controller.appPrefs.callOnLockScreen,
editIceServers = showModal { RTCServersView(m) }
)
}
@@ -30,6 +33,7 @@ fun CallSettingsView(m: ChatModel) {
fun CallSettingsLayout(
webrtcPolicyRelay: Preference<Boolean>,
callOnLockScreen: Preference<CallOnLockScreen>,
editIceServers: () -> Unit,
) {
Column(
Modifier.fillMaxWidth(),
@@ -48,18 +52,33 @@ fun CallSettingsLayout(
}
SectionDivider()
Column(Modifier.padding(start = 10.dp, top = 12.dp)) {
Text(stringResource(R.string.call_on_lock_screen))
Row {
SharedPreferenceRadioButton(stringResource(R.string.no_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.DISABLE)
Spacer(Modifier.fillMaxWidth().weight(1f))
SharedPreferenceRadioButton(stringResource(R.string.show_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.SHOW)
Spacer(Modifier.fillMaxWidth().weight(1f))
SharedPreferenceRadioButton(stringResource(R.string.accept_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.ACCEPT)
}
val enabled = remember { mutableStateOf(true) }
SectionItemView { LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it }) }
SectionDivider()
SectionItemView(editIceServers) { Text(stringResource(R.string.webrtc_ice_servers)) }
}
}
}
@Composable
private fun LockscreenOpts(lockscreenOpts: State<CallOnLockScreen>, enabled: State<Boolean>, onSelected: (CallOnLockScreen) -> Unit) {
val values = remember {
CallOnLockScreen.values().map {
when (it) {
CallOnLockScreen.DISABLE -> it to generalGetString(R.string.no_call_on_lock_screen)
CallOnLockScreen.SHOW -> it to generalGetString(R.string.show_call_on_lock_screen)
CallOnLockScreen.ACCEPT -> it to generalGetString(R.string.accept_call_on_lock_screen)
}
}
}
ExposedDropDownSettingRow(
generalGetString(R.string.call_on_lock_screen),
values,
lockscreenOpts,
icon = null,
enabled = enabled,
onSelected = onSelected
)
}
@Composable

View File

@@ -115,7 +115,7 @@ fun NetworkAndServersView(
Modifier.padding(start = 16.dp, bottom = 24.dp),
style = MaterialTheme.typography.h1
)
SectionView {
SectionView(generalGetString(R.string.settings_section_title_messages)) {
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showModal { SMPServersView(it) })
SectionDivider()
SectionItemView {
@@ -128,6 +128,10 @@ fun NetworkAndServersView(
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
}
}
Spacer(Modifier.height(8.dp))
SectionView(generalGetString(R.string.settings_section_title_calls)) {
SettingsActionItem(Icons.Outlined.ElectricalServices, stringResource(R.string.webrtc_ice_servers), showModal { RTCServersView(it) })
}
}
}
@@ -142,6 +146,7 @@ fun UseSocksProxySwitch(
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {

View File

@@ -0,0 +1,210 @@
package chat.simplex.app.views.usersettings
import SectionItemViewSpaceBetween
import androidx.compose.runtime.Composable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.call.parseRTCIceServers
import chat.simplex.app.views.helpers.*
@Composable
fun RTCServersView(
chatModel: ChatModel
) {
var userRTCServers by remember {
mutableStateOf(chatModel.controller.appPrefs.webrtcIceServers.get()?.split("\n") ?: listOf())
}
var isUserRTCServers by remember { mutableStateOf(userRTCServers.isNotEmpty()) }
var editRTCServers by remember { mutableStateOf(!isUserRTCServers) }
val userRTCServersStr = remember { mutableStateOf(if (isUserRTCServers) userRTCServers.joinToString(separator = "\n") else "") }
fun saveUserRTCServers() {
val srvs = userRTCServersStr.value.split("\n")
if (srvs.isNotEmpty() && srvs.toSet().size == srvs.size && parseRTCIceServers(srvs) != null) {
userRTCServers = srvs
chatModel.controller.appPrefs.webrtcIceServers.set(srvs.joinToString(separator = "\n"))
editRTCServers = false
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.error_saving_ICE_servers),
generalGetString(R.string.ensure_ICE_server_address_are_correct_format_and_unique)
)
}
}
fun resetRTCServers() {
isUserRTCServers = false
userRTCServers = listOf()
chatModel.controller.appPrefs.webrtcIceServers.set(null)
}
RTCServersLayout(
isUserRTCServers = isUserRTCServers,
editRTCServers = editRTCServers,
userRTCServersStr = userRTCServersStr,
isUserRTCServersOnOff = { switch ->
if (switch) {
isUserRTCServers = true
} else if (userRTCServers.isNotEmpty()) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.use_simplex_chat_servers__question),
text = generalGetString(R.string.saved_ICE_servers_will_be_removed),
confirmText = generalGetString(R.string.confirm_verb),
onConfirm = {
resetRTCServers()
isUserRTCServers = false
userRTCServersStr.value = ""
}
)
} else {
isUserRTCServers = false
userRTCServersStr.value = ""
}
},
cancelEdit = {
isUserRTCServers = userRTCServers.isNotEmpty()
editRTCServers = !isUserRTCServers
userRTCServersStr.value = if (isUserRTCServers) userRTCServers.joinToString(separator = "\n") else ""
},
saveRTCServers = ::saveUserRTCServers,
editOn = { editRTCServers = true },
)
}
@Composable
fun RTCServersLayout(
isUserRTCServers: Boolean,
editRTCServers: Boolean,
userRTCServersStr: MutableState<String>,
isUserRTCServersOnOff: (Boolean) -> Unit,
cancelEdit: () -> Unit,
saveRTCServers: () -> Unit,
editOn: () -> Unit,
) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
stringResource(R.string.your_ICE_servers),
Modifier.padding(bottom = 24.dp),
style = MaterialTheme.typography.h1
)
SectionItemViewSpaceBetween(padding = PaddingValues()) {
Text(stringResource(R.string.configure_ICE_servers), Modifier.padding(end = 24.dp))
Switch(
checked = isUserRTCServers,
onCheckedChange = isUserRTCServersOnOff,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
)
}
if (!isUserRTCServers) {
Text(stringResource(R.string.using_simplex_chat_servers), lineHeight = 22.sp)
} else {
Text(stringResource(R.string.enter_one_ICE_server_per_line))
if (editRTCServers) {
TextEditor(Modifier.height(160.dp), text = userRTCServersStr)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(horizontalAlignment = Alignment.Start) {
Row {
Text(
stringResource(R.string.cancel_verb),
color = MaterialTheme.colors.primary,
modifier = Modifier
.clickable(onClick = cancelEdit)
)
Spacer(Modifier.padding(horizontal = 8.dp))
Text(
stringResource(R.string.save_servers_button),
color = MaterialTheme.colors.primary,
modifier = Modifier.clickable(onClick = {
saveRTCServers()
})
)
}
}
Column(horizontalAlignment = Alignment.End) {
howToButton()
}
}
} else {
Surface(
modifier = Modifier
.height(160.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
) {
SelectionContainer(
Modifier.verticalScroll(rememberScrollState())
) {
Text(
userRTCServersStr.value,
Modifier
.padding(vertical = 5.dp, horizontal = 7.dp),
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 14.sp),
)
}
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(horizontalAlignment = Alignment.Start) {
Text(
stringResource(R.string.edit_verb),
color = MaterialTheme.colors.primary,
modifier = Modifier
.clickable(onClick = editOn)
)
}
Column(horizontalAlignment = Alignment.End) {
howToButton()
}
}
}
}
}
}
@Composable
private fun howToButton() {
val uriHandler = LocalUriHandler.current
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/WEBRTC.md#configure-mobile-apps") }
) {
Text(stringResource(R.string.how_to), color = MaterialTheme.colors.primary)
Icon(
Icons.Outlined.OpenInNew, stringResource(R.string.how_to), tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 5.dp)
)
}
}

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.usersettings
import SectionItemViewSpaceBetween
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -58,7 +59,7 @@ fun SMPServersView(chatModel: ChatModel) {
if (userSMPServers.isNotEmpty()) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.use_simplex_chat_servers__question),
text = generalGetString(R.string.saved_SMP_servers_will_br_removed),
text = generalGetString(R.string.saved_SMP_servers_will_be_removed),
confirmText = generalGetString(R.string.confirm_verb),
onConfirm = {
saveSMPServers(listOf())
@@ -107,9 +108,7 @@ fun SMPServersLayout(
Modifier.padding(bottom = 24.dp),
style = MaterialTheme.typography.h1
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
SectionItemViewSpaceBetween(padding = PaddingValues()) {
Text(stringResource(R.string.configure_SMP_servers), Modifier.padding(end = 24.dp))
Switch(
checked = isUserSMPServers,
@@ -198,7 +197,7 @@ fun SMPServersLayout(
}
@Composable
fun howToButton() {
private fun howToButton() {
val uriHandler = LocalUriHandler.current
Row(
verticalAlignment = Alignment.CenterVertically,

View File

@@ -31,6 +31,8 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.TerminalView
import chat.simplex.app.views.database.DatabaseView
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.CreateLinkTab
import chat.simplex.app.views.newchat.CreateLinkView
import chat.simplex.app.views.onboarding.SimpleXInfo
@Composable
@@ -113,16 +115,16 @@ fun SettingsLayout(
SectionDivider()
SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { onClickIncognitoInfo(showModal) }
SectionDivider()
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { UserAddressView(it) }, disabled = stopped)
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { CreateLinkView(it, CreateLinkTab.LONG_TERM) }, disabled = stopped)
SectionDivider()
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
}
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_settings)) {
SettingsActionItem(Icons.Outlined.Bolt, stringResource(R.string.notifications), showSettingsModal { NotificationsSettingsView(it, showCustomModal) })
SettingsActionItem(Icons.Outlined.Bolt, stringResource(R.string.notifications), showSettingsModal { NotificationsSettingsView(it, showCustomModal) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it) }, disabled = stopped)
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
SectionDivider()

View File

@@ -61,7 +61,7 @@ fun UserAddressLayout(
verticalArrangement = Arrangement.Top
) {
Text(
stringResource(R.string.your_chat_address),
stringResource(R.string.your_contact_address),
Modifier.padding(bottom = 16.dp),
style = MaterialTheme.typography.h1,
)
@@ -77,12 +77,17 @@ fun UserAddressLayout(
) {
if (userAddress == null) {
Text(
stringResource(R.string.if_you_delete_address_you_wont_lose_contacts),
stringResource(R.string.if_you_later_delete_address_you_wont_lose_contacts),
Modifier.padding(bottom = 12.dp),
lineHeight = 22.sp
)
SimpleButton(stringResource(R.string.create_address), icon = Icons.Outlined.QrCode, click = createAddress)
} else {
Text(
stringResource(R.string.if_you_delete_address_you_wont_lose_contacts),
Modifier.padding(bottom = 12.dp),
lineHeight = 22.sp
)
QRCode(userAddress, Modifier.weight(1f, fill = false).aspectRatio(1f))
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,808 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="thousand_abbreviation">k</string>
<!-- Connect via Link - MainActivity.kt -->
<string name="connect_via_contact_link">Über den Kontakt-Link verbinden?</string>
<string name="connect_via_invitation_link">Über den Einladungs-Link verbinden?</string>
<string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet von dem Sie diesen Link erhalten haben.</string>
<string name="connect_via_link_verb">Verbinden</string>
<!-- Server info - ChatModel.kt -->
<string name="server_connected">Verbunden</string>
<string name="server_error">Fehler</string>
<string name="server_connecting">verbinde</string>
<string name="connected_to_server_to_receive_messages_from_contact">Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
<!-- Item Content - ChatModel.kt -->
<string name="deleted_description">Gelöscht</string>
<string name="sending_files_not_yet_supported">Das Senden von Dateien wird noch nicht unterstützt</string>
<string name="receiving_files_not_yet_supported">Der Empfang von Dateien wird noch nicht unterstützt</string>
<string name="sender_you_pronoun">Sie</string>
<string name="unknown_message_format">Unbekanntes Nachrichtenformat</string>
<string name="invalid_message_format">Unzulässiges Nachrichtenformat</string>
<!-- PendingContactConnection - ChatModel.kt -->
<string name="connection_local_display_name">Verbindung <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="display_name_connection_established">Verbindung hergestellt</string>
<string name="display_name_invited_to_connect">Für eine Verbindung eingeladen</string>
<string name="display_name_connecting">verbinde …</string>
<string name="description_you_shared_one_time_link">Sie haben einen Einmal-Link geteilt</string>
<string name="description_you_shared_one_time_link_incognito">Sie haben Inkognito einen Einmal-Link geteilt</string>
<string name="description_via_contact_address_link">über einen Kontaktadressen Link</string>
<string name="description_via_contact_address_link_incognito">Inkognito über einen Kontaktadressen Link</string>
<string name="description_via_one_time_link">über einen Einmal-Link</string>
<string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string>
<!-- SimpleXAPI.kt -->
<string name="error_saving_smp_servers">Fehler beim Speichern des SMP-Server</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.</string>
<string name="error_setting_network_config">Fehler bei der Aktualisierung der Netzwerk-Konfiguration.</string>
<!-- API Error Responses - SimpleXAPI.kt -->
<string name="connection_timeout">Verbindungszeitüberschreitung</string>
<string name="connection_error">Verbindungsfehler</string>
<string name="network_error_desc">Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.</string>
<string name="error_sending_message">Fehler beim Senden der Nachricht</string>
<string name="error_adding_members">Fehler beim Hinzufügen von Mitgliedern</string>
<string name="error_joining_group">Fehler beim Beitritt zur Gruppe</string>
<string name="cannot_receive_file">Datei kann nicht empfangen werden</string>
<string name="sender_cancelled_file_transfer">Der Absender hat die Dateiübertragung abgebrochen.</string>
<string name="error_receiving_file">Fehler beim Empfangen der Datei</string>
<string name="error_creating_address">Fehler beim Erstellen der Adresse</string>
<string name="contact_already_exists">Kontakt ist bereits vorhanden</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits über diesen Link mit <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> verbunden.</string>
<string name="invalid_connection_link">Ungültiger Verbindungslink</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string>
<string name="connection_error_auth">Verbindungsfehler (AUTH)</string>
<string name="connection_error_auth_desc">Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln - Bitte melden Sie es uns.\nBitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben.</string>
<string name="error_accepting_contact_request">Fehler beim Akzeptieren der Kontaktanfrage</string>
<string name="sender_may_have_deleted_the_connection_request">Der Absender hat möglicherweise die Verbindungsanfrage gelöscht.</string>
<string name="cannot_delete_contact">Der Kontakt kann nicht gelöscht werden!</string>
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Der Kontakt mit <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> kann nicht gelöscht werden, da er Mitglied einer oder mehrerer dieser Gruppen ist <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
<string name="error_deleting_contact">Fehler beim Löschen des Kontakts</string>
<string name="error_deleting_group">Fehler beim Löschen der Gruppe</string>
<string name="error_deleting_contact_request">Fehler beim Löschen der Kontakt-Anfrage</string>
<string name="error_deleting_pending_contact_connection">Fehler beim Löschen der anstehenden Kontaktaufnahme</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Sofortige Benachrichtigungen</string>
<string name="service_notifications">Sofortige Benachrichtigungen!</string>
<string name="service_notifications_disabled">Sofortige Benachrichtigungen sind deaktiviert!</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Um Ihre Privatsphäre zu schützen kann statt der Push-Benachrichtigung der <b><xliff:g id="appName">SimpleX</xliff:g> Hintergrunddienst genutzt werden</b> dieser benötigt ein paar Prozent Akkuleistung am Tag.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Diese können über die Einstellungen deaktiviert werden</b> Solange die App abläuft werden Benachrichtigungen weiterhin angezeigt.</string>
<string name="turn_off_battery_optimization">Um diese Funktion zu nutzen, ist es nötig, die Einstellung <b>Akkuoptimierung</b> für <xliff:g id="appName">SimpleX</xliff:g> im nächsten Dialog zu <b>deaktivieren</b>. Ansonsten werden die Benachrichtigungen deaktiviert.</string>
<string name="turning_off_service_and_periodic">Die Akkuoptimierung ist aktiv, der Hintergrunddienst und die regelmäßige Nachfrage nach neuen Nachrichten ist abgeschaltet. Sie können diese Funktion in den Einstellungen wieder aktivieren.</string>
<string name="periodic_notifications">Regelmäßige Benachrichtigungen</string>
<string name="periodic_notifications_disabled">Regelmäßige Benachrichtigungen sind deaktiviert!</string>
<string name="periodic_notifications_desc">Die App holt regelmäßig neue Nachrichten ab — dies benötigt ein paar Prozent Akkuleistung am Tag. Die App nutzt keine Push-Benachrichtigungen — es werden keine Daten von Ihrem Gerät an Server gesendet.</string>
<string name="enter_passphrase_notification_title">Passwort wird benötigt</string>
<string name="enter_passphrase_notification_desc">Geben Sie bitte das Datenbank-Passwort ein, um Benachrichtigungen zu erhalten.</string>
<string name="database_initialization_error_title">Die Datenbank kann nicht initialisiert werden</string>
<string name="database_initialization_error_desc">Die Datenbank arbeitet nicht richtig. Tippen Sie für weitere Informationen.</string>
<!-- SimpleX Chat foreground Service -->
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> Hintergrunddienst</string>
<string name="simplex_service_notification_text">Empfange Nachrichten …</string>
<string name="hide_notification">Verberge</string>
<!-- Notification channels -->
<string name="ntf_channel_messages">SimpleX Chat Nachrichten</string>
<string name="ntf_channel_calls">SimpleX Chat Anrufe</string>
<string name="ntf_channel_calls_lockscreen">SimpleX Chat Anrufe (Sperrbildschirm)</string>
<!-- Notifications -->
<string name="settings_notifications_mode_title">Benachrichtigungsdienst</string>
<string name="settings_notification_preview_mode_title">Vorschau anzeigen</string>
<string name="settings_notification_preview_title">Benachrichtigungsvorschau</string>
<string name="notifications_mode_off">Wird ausgeführt, wenn die App geöffnet ist</string>
<string name="notifications_mode_periodic">Startet regelmäßig</string>
<string name="notifications_mode_service">Immer aktiv</string>
<string name="notifications_mode_off_desc">Die App kann Benachrichtigungen nur empfangen, wenn sie ausgeführt wird, es wird kein Hintergrunddienst gestartet.</string>
<string name="notifications_mode_periodic_desc">Überprüft alle 10 Minuten auf neue Nachrichten für bis zu einer Minute.</string>
<string name="notifications_mode_service_desc">Der Hintergrunddienst läuft immer Benachrichtigungen werden angezeigt, sobald Nachrichten verfügbar sind.</string>
<string name="notification_preview_mode_message">Nachrichtentext</string>
<string name="notification_preview_mode_contact">Kontaktname</string>
<string name="notification_preview_mode_hidden">Verborgen</string>
<string name="notification_preview_mode_message_desc">Kontakt und Nachricht anzeigen</string>
<string name="notification_preview_mode_contact_desc">Nur Kontakt anzeigen</string>
<string name="notification_display_mode_hidden_desc">Kontakt und Nachricht verbergen</string>
<string name="notification_preview_somebody">Kontakt verbergen:</string>
<string name="notification_preview_new_message">neue Nachricht</string>
<!-- local authentication notice - SimpleXAPI.kt -->
<string name="la_notice_title_simplex_lock">SimpleX Sperre</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">Um Ihre Informationen zu schützen, schalten Sie die SimpleX Sperre ein.\nSie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird.</string>
<string name="la_notice_turn_on">Aktivieren</string>
<!-- LocalAuthentication.kt -->
<string name="auth_simplex_lock_turned_on">SimpleX Sperre aktiviert</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen.</string>
<string name="auth_unlock">Entsperren</string>
<string name="auth_log_in_using_credential">Melden Sie sich mit Ihren Zugangsdaten an</string>
<string name="auth_enable_simplex_lock">SimpleX Sperre aktivieren</string>
<string name="auth_disable_simplex_lock">SimpleX Sperre deaktivieren</string>
<string name="auth_confirm_credential">Bestätigen Sie Ihre Zugangsdaten</string>
<string name="auth_error">Authentifizierungsfehler</string>
<string name="auth_error_w_desc">Authentifizierungsfehler: <xliff:g id="desc">%1$s</xliff:g></string>
<string name="auth_failed">Authentifizierung fehlgeschlagen</string>
<string name="auth_unavailable">Authentifizierung nicht verfügbar</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Geräteauthentifizierung ist nicht aktiviert. Sie können die SimpleX Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben.</string>
<string name="auth_device_authentication_is_disabled_turning_off">Geräteauthentifizierung ist deaktiviert. SimpleX Sperre ist abgeschaltet.</string>
<string name="auth_retry">Wiederholen</string>
<string name="auth_stop_chat">Chat beenden</string>
<string name="auth_open_chat_console">Chat-Konsole öffnen</string>
<!-- Chat Actions - ChatItemView.kt (and general) -->
<string name="reply_verb">Antwort</string>
<string name="share_verb">Teilen</string>
<string name="copy_verb">Kopieren</string>
<string name="save_verb">Speichern</string>
<string name="edit_verb">Bearbeiten</string>
<string name="delete_verb">Löschen</string>
<string name="delete_message__question">Nachricht löschen?</string>
<string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht - dies kann nicht rückgängig gemacht werden!</string>
<string name="for_me_only">Nur für mich</string>
<string name="for_everybody">Für alle</string>
<!-- CIMetaView.kt -->
<string name="icon_descr_edited">bearbeitet</string>
<string name="icon_descr_sent_msg_status_sent">gesendet</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">nicht autorisiertes Senden</string>
<string name="icon_descr_sent_msg_status_send_failed">Senden fehlgeschlagen</string>
<string name="icon_descr_received_msg_status_unread">ungelesen</string>
<!-- ChatListView.kt -->
<string name="personal_welcome">Willkommen <xliff:g>%1$s</xliff:g>!</string>
<string name="welcome">Willkommen!</string>
<string name="this_text_is_available_in_settings">Dieser Text ist in den Einstellungen verfügbar.</string>
<string name="your_chats">Ihre Chats</string>
<string name="contact_connection_pending">verbinde …</string>
<string name="group_preview_you_are_invited">Sie sind zu der Gruppe eingeladen</string>
<string name="group_preview_join_as">beitreten als %s</string>
<string name="group_connection_pending">verbinde …</string>
<string name="tap_to_start_new_chat">Tippen Sie um einen neuen Chat zu starten</string>
<string name="chat_with_developers">Chatten Sie mit den Entwicklern</string>
<string name="you_have_no_chats">Sie haben keine Chats</string>
<!-- ComposeView.kt, helpers -->
<string name="attach">Anhängen</string>
<string name="icon_descr_context">Kontextsymbol</string>
<string name="icon_descr_cancel_image_preview">Bildervorschau abbrechen</string>
<string name="icon_descr_cancel_file_preview">Dateivorschau abbrechen</string>
<!-- Images - CIImageView.kt -->
<string name="image_descr">Bild</string>
<string name="icon_descr_waiting_for_image">Warten auf ein Bild</string>
<string name="icon_descr_asked_to_receive">Um Empfang des Bildes gebeten</string>
<string name="icon_descr_image_snd_complete">Bild gesendet</string>
<string name="waiting_for_image">Warten auf ein Bild</string>
<string name="image_will_be_received_when_contact_is_online">Das Bild wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</string>
<string name="image_saved">Bild wurde im Fotoalbum gespeichert</string>
<!-- Files - CIFileView.kt -->
<string name="icon_descr_file">Datei</string>
<string name="large_file">Große Datei!</string>
<string name="contact_sent_large_file">Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
<string name="maximum_supported_file_size">Die derzeit maximal unterstützte Dateigröße beträgt <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="waiting_for_file">Warte auf Datei</string>
<string name="file_will_be_received_when_contact_is_online">Die Datei wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</string>
<string name="file_saved">Datei gespeichert</string>
<string name="file_not_found">Datei nicht gefunden</string>
<string name="error_saving_file">Fehler beim Speichern der Datei</string>
<!-- Chat Info Settings - ChatInfoView.kt -->
<string name="notifications">Benachrichtigungen</string>
<!-- Chat Info Actions - ChatInfoView.kt -->
<string name="delete_contact_question">Kontakt löschen?</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Der Kontakt und alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden!</string>
<string name="button_delete_contact">Kontakt löschen</string>
<string name="text_field_set_contact_placeholder">Kontaktname festlegen…</string>
<string name="icon_descr_server_status_connected">Verbunden</string>
<string name="icon_descr_server_status_disconnected">Getrennt</string>
<string name="icon_descr_server_status_error">Fehler</string>
<string name="icon_descr_server_status_pending">Ausstehend</string>
<!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Nachricht senden</string>
<!-- General Actions / Responses -->
<string name="back">Zurück</string>
<string name="cancel_verb">Abbrechen</string>
<string name="confirm_verb">Bestätigen</string>
<string name="ok">OK</string>
<string name="no_details">keine Details</string>
<string name="add_contact">Kontakt hinzufügen</string>
<string name="copied">In die Zwischenablage kopiert</string>
<!-- NewChatSheet -->
<string name="add_contact_or_create_group">Neuen Chat starten</string>
<string name="share_one_time_link">Erstellen Sie einen einmaligen Einladungslink</string>
<string name="connect_via_link_or_qr">Per Link / QR-Code verbinden</string>
<string name="scan_QR_code">QR-Code scannen</string>
<string name="create_group">Geheime Gruppe erstellen</string>
<string name="to_share_with_your_contact">(zum Teilen mit Ihrem Kontakt)</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(Scannen oder Einfügen aus der Zwischenablage)</string>
<string name="only_stored_on_members_devices">(wird nur von Gruppenmitgliedern gespeichert)</string>
<!-- GetImageView -->
<string name="toast_permission_denied">Berechtigung verweigert!</string>
<string name="use_camera_button">Kamera\nverwenden</string>
<string name="from_gallery_button">Aus dem\nFotoalbum</string>
<string name="choose_file">Datei\nauswählen</string>
<!-- help - ChatHelpView.kt -->
<string name="thank_you_for_installing_simplex">Danke, dass Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> installiert haben!</string>
<string name="you_can_connect_to_simplex_chat_founder">Sie können sich <font color="#0088ff">mit <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.</string>
<string name="to_start_a_new_chat_help_header">Um einen neuen Chat zu starten</string>
<string name="chat_help_tap_button">Schaltfläche antippen</string>
<string name="above_then_preposition_continuation">über, dann:</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>Neuen Kontakt hinzufügen</b>: Um Ihren Einmal-QR-Code für Ihren Kontakt zu erstellen.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR-Code scannen</b>: Um sich mit Ihrem Kontakt zu verbinden, der Ihnen seinen QR-Code zeigt.</string>
<string name="to_connect_via_link_title">Über Link verbinden</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Wenn Sie einen <xliff:g id="appName">SimpleX Chat</xliff:g> Einladungslink erhalten haben, können Sie ihn in Ihrem Browser öffnen:</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 Desktop: Angezeigten QR-Code aus der App scannen, über <b>QR-Code scannen</b>.</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 Handy: Tippen Sie auf <b>In mobiler App öffnen</b> und dann auf <b>Verbinden</b> in der App.</string>
<!-- Contact Request Alert Dialogue - ChatListNavLinkView.kt -->
<string name="accept_connection_request__question">Die Verbindungsanfrage akzeptieren?</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Wenn Sie ablehnen, wird der Absender NICHT benachrichtigt.</string>
<string name="accept_contact_button">Akzeptieren</string>
<string name="accept_contact_incognito_button">Inkognito akzeptieren</string>
<string name="reject_contact_button">Ablehnen</string>
<!-- Clear Chat - ChatListNavLinkView.kt -->
<string name="clear_chat_question">Chatinhalte löschen?</string>
<string name="clear_chat_warning">Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</string>
<string name="clear_verb">Löschen</string>
<string name="clear_chat_button">Chatinhalte löschen</string>
<string name="mark_read">Als gelesen markieren</string>
<!-- Actions - ChatListNavLinkView.kt -->
<string name="mute_chat">Stummschalten</string>
<string name="unmute_chat">Stummschaltung aufheben</string>
<!-- Pending contact connection alert dialogues -->
<string name="you_invited_your_contact">Sie haben Ihren Kontakt eingeladen</string>
<string name="you_accepted_connection">Sie haben die Verbindung akzeptiert</string>
<string name="delete_pending_connection__question">Ausstehende Verbindung löschen?</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Der Kontakt, mit dem Sie diesen Link geteilt haben, kann sich NICHT verbinden!</string>
<string name="connection_you_accepted_will_be_cancelled">Die von Ihnen akzeptierte Verbindung wird abgebrochen!</string>
<!-- Contact Pending Alert Dialogue - ChatListNavLinkView.kt -->
<string name="alert_title_contact_connection_pending">Ihr Kontakt ist noch nicht verbunden!</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ihr Kontakt muss online sein, damit die Verbindung hergestellt werden kann.\nSie können diese Verbindung abbrechen und den Kontakt entfernen (und es später nochmals mit einem neuen Link versuchen).</string>
<!-- Contact Request Information - ContactRequestView.kt -->
<string name="contact_wants_to_connect_with_you">möchte sich mit Ihnen verbinden!</string>
<!-- Image Placeholder - ChatInfoImage.kt -->
<string name="icon_descr_profile_image_placeholder">Platzhalter für Profilbild</string>
<string name="image_descr_profile_image">Profilbild</string>
<!-- Content Descriptions -->
<string name="icon_descr_close_button">Schließen Schaltfläche</string>
<string name="image_descr_link_preview">Vorschaubild verlinken</string>
<string name="icon_descr_cancel_link_preview">Link Vorschau abbrechen</string>
<string name="icon_descr_settings">Einstellungen</string>
<string name="image_descr_qr_code">QR Code</string>
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> Adresse</string>
<string name="icon_descr_help">Hilfe</string>
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> Team</string>
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> Logo</string>
<string name="icon_descr_email">E-Mail</string>
<string name="icon_descr_more_button">Mehr</string>
<!-- Add Contact - AddContactView.kt -->
<string name="invalid_QR_code">Ungültiger QR-Code</string>
<string name="this_QR_code_is_not_a_link">Dieser QR-Code beschreibt keinen Link!</string>
<string name="invalid_contact_link">Ungültiger Link!</string>
<string name="this_link_is_not_a_valid_connection_link">Dieser Link ist kein gültiger Verbindungslink!</string>
<string name="connection_request_sent">Verbindungsanfrage gesendet!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, wenn das Gerät Ihres Kontakts online ist, bitte warten oder schauen Sie später nochmal nach!</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Zeigen Sie Ihrem Kontakt den QR-Code aus der App zum Scannen.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs anzeigen</b> oder einen Einladungslink über einen anderen Kanal mit Ihrem Kontakt teilen.</string >
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird\nan Ihren Kontakt gesendet.</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.</string>
<string name="share_invitation_link">Einladungslink teilen</string>
<string name="paste_connection_link_below_to_connect">Fügen Sie den erhaltenen Link in das Feld unten ein, um sich mit Ihrem Kontakt zu verbinden.</string>
<string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet.</string>
<!-- PasteToConnect.kt -->
<string name="connect_via_link">Über einen Link verbinden</string>
<string name="connect_button">Verbinden</string>
<string name="paste_button">Einfügen</string>
<string name="this_string_is_not_a_connection_link">Diese Zeichenfolge entspricht keinem gültigen Verbindungslink!</string>
<string name="you_can_also_connect_by_clicking_the_link">Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche <b>In mobiler App öffnen</b>.</string>
<!-- CreateLinkView.kt -->
<string name="create_one_time_link">Link / QR-Code erstellen</string>
<string name="one_time_link">Einmaliger Einladungs-Link</string>
<string name="your_contact_address">Ihre Kontaktadresse</string>
<!-- settings - SettingsView.kt -->
<string name="your_settings">Ihre Einstellungen</string>
<string name="your_simplex_contact_address">Ihre <xliff:g id="appName">SimpleX</xliff:g> Kontaktadresse</string>
<string name="database_passphrase_and_export">Datenbank-Passwort &amp; -Export</string>
<string name="about_simplex_chat">Über <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="how_to_use_simplex_chat">Wie man SimpleX nutzt</string>
<string name="markdown_help">Markdown Hilfe</string>
<string name="markdown_in_messages">Markdown in Nachrichten</string>
<string name="chat_with_the_founder">Verbinden Sie sich mit den Entwicklern</string>
<string name="send_us_an_email">Senden Sie uns eine E-Mail</string>
<string name="chat_lock">SimpleX Sperre</string>
<string name="chat_console">Chat Konsole</string>
<string name="smp_servers">SMP-Server</string>
<string name="install_simplex_chat_for_terminal">Installieren Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> als Terminalanwendung</string>
<string name="use_simplex_chat_servers__question">Verwenden Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Server?</string>
<string name="saved_SMP_servers_will_be_removed">Gespeicherte SMP-Server werden entfernt.</string>
<string name="your_SMP_servers">Ihre SMP-Server</string>
<string name="configure_SMP_servers">SMP-Server konfigurieren</string>
<string name="using_simplex_chat_servers">Verwendung von <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Servern.</string>
<string name="enter_one_SMP_server_per_line">SMP-Server (einer pro Zeile)</string>
<string name="how_to">Anleitung</string>
<string name="saved_ICE_servers_will_be_removed">Gespeicherte WebRTC ICE-Server werden entfernt.</string>
<string name="your_ICE_servers">Ihre ICE-Server</string>
<string name="configure_ICE_servers">Konfigurieren Sie ICE-Server</string>
<string name="enter_one_ICE_server_per_line">ICE-Server (einer pro Zeile)</string>
<string name="error_saving_ICE_servers">Fehler beim Speichern der ICE-Server</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.</string>
<string name="save_servers_button">Speichern</string>
<string name="network_and_servers">Netzwerk &amp; Server</string>
<string name="network_settings">Erweiterte Netzwerkeinstellungen</string>
<string name="network_settings_title">Netzwerkeinstellungen</string>
<string name="network_socks_toggle">SOCKS-Proxy verwenden (Port 9050)</string>
<string name="network_enable_socks">SOCKS-Proxy verwenden?</string>
<string name="network_enable_socks_info">Zugriff auf die Server über SOCKS-Proxy auf Port 9050? Der Proxy muss gestartet werden, bevor diese Option aktiviert wird.</string>
<string name="network_disable_socks">Direkte Internetverbindung verwenden?</string>
<string name="network_disable_socks_info">Wenn Sie dies bestätigen, können die Messaging-Server Ihre IP-Adresse und Ihren Provider sehen und mit welchen Servern Sie sich verbinden.</string>
<string name="update_onion_hosts_settings_question">Einstellung für .onion-Hosts aktualisieren?</string>
<string name="network_use_onion_hosts">Verwende .onion-Hosts</string>
<string name="network_use_onion_hosts_prefer">Wenn verfügbar</string>
<string name="network_use_onion_hosts_no">Nein</string>
<string name="network_use_onion_hosts_required">Erforderlich</string>
<string name="network_use_onion_hosts_prefer_desc">Onion-Hosts werden verwendet, wenn sie verfügbar sind.</string>
<string name="network_use_onion_hosts_no_desc">Onion-Hosts werden nicht verwendet.</string>
<string name="network_use_onion_hosts_required_desc">Für die Verbindung werden Onion-Hosts benötigt.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion-Hosts werden verwendet, wenn sie verfügbar sind.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion-Hosts werden nicht verwendet.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Für die Verbindung werden Onion-Hosts benötigt.</string>
<string name="appearance_settings">Design</string>
<!-- Address Items - UserAddressView.kt -->
<string name="create_address">Adresse erstellen</string>
<string name="delete_address__question">Adresse löschen?</string>
<string name="all_your_contacts_will_remain_connected">Alle Ihre Kontakte bleiben verbunden.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Sie können Ihre Adresse als Link oder als QR-Code teilen Jeder kann sich mit Ihnen verbinden.</string>
<string name="if_you_delete_address_you_wont_lose_contacts">Wenn Sie diese Adresse löschen, werden Sie Ihre Kontakte nicht verlieren.</string>
<string name="if_you_later_delete_address_you_wont_lose_contacts">Wenn Sie diese Adresse später löschen, werden Sie Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren.</string>
<string name="share_link">Link teilen</string>
<string name="delete_address">Adresse löschen</string>
<!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Angezeigter Name:</string>
<string name="full_name__field">"Vollständiger Name:</string>
<string name="your_chat_profile">Ihr Chat-Profil</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\n\n<xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
<string name="edit_image">Bild bearbeiten</string>
<string name="delete_image">Bild löschen</string>
<string name="save_and_notify_contacts">Speichern (und Kontakte benachrichtigen)</string>
<!-- Welcome Prompts - WelcomeView.kt -->
<string name="you_control_your_chat">Sie haben volle Kontrolle über Ihren Chat!</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Die Messaging- und Anwendungsplattform zum Schutz Ihrer Privatsphäre und Sicherheit.</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">Wir speichern auf den Servern keinen Ihrer Kontakte oder keine Ihrer Nachrichten (sofern einmal zugestellt).</string>
<string name="create_profile">Profil erstellen</string>
<string name="your_profile_is_stored_on_your_device">Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.</string>
<string name="profile_is_only_shared_with_your_contacts">Das Profil wird nur mit Ihren Kontakten geteilt.</string>
<string name="display_name_cannot_contain_whitespace">Der angezeigte Name darf keine Leerzeichen enthalten.</string>
<string name="display_name">Angezeigter Name</string>
<string name="full_name_optional__prompt">Vollständiger Name (optional)</string>
<string name="create_profile_button">Erstellen</string>
<string name="about_simplex">Über SimpleX</string>
<!-- markdown demo - MarkdownHelpView.kt -->
<string name="how_to_use_markdown">Wie Sie Markdowns anwenden</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Sie können Markdowns verwenden, um Nachrichten zu formatieren:</string>
<string name="bold">fett</string>
<string name="italic">kursiv</string>
<string name="strikethrough">durchstreichen</string>
<string name="a_plus_b">a + b</string>
<string name="colored">farbig</string>
<string name="secret">geheim</string>
<!-- CICallStatus - in chat items -->
<string name="callstatus_calling">Anruf…</string>
<string name="callstatus_missed">Anruf verpasst</string>
<string name="callstatus_rejected">Anruf abgelehnt</string>
<string name="callstatus_accepted">Anruf angenommen</string>
<string name="callstatus_connecting">Anruf wird verbunden…</string>
<string name="callstatus_in_progress">Anruf läuft</string>
<string name="callstatus_ended">Anruf beendet <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstatus_error">Fehler bei Anruf</string>
<!-- CallState -->
<string name="callstate_starting">Verbindung wird gestartet…</string>
<string name="callstate_waiting_for_answer">Warten auf Antwort…</string>
<string name="callstate_waiting_for_confirmation">Warten auf Bestätigung…</string>
<string name="callstate_received_answer">Antwort erhalten…</string>
<string name="callstate_received_confirmation">Bestätigung erhalten…</string>
<string name="callstate_connecting">Verbindung wird hergestellt…</string>
<string name="callstate_connected">verbunden</string>
<string name="callstate_ended">beendet</string>
<!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">Die nächste Generation von privatem Messaging</string>
<string name="privacy_redefined">Datenschutz neu definiert</string>
<string name="first_platform_without_user_ids">Die erste Plattform ohne Benutzerkennungen Privat per Design</string>
<string name="immune_to_spam_and_abuse">Immun gegen Spam und Missbrauch</string>
<string name="people_can_connect_only_via_links_you_share">Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte teilen.</string>
<string name="decentralized">Dezentral</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-Source-Protokoll und -Code Jeder kann seine eigenen Server nutzen.</string>
<string name="create_your_profile">Erstellen Sie Ihr Profil</string>
<string name="make_private_connection">Stellen Sie eine private Verbindung her</string>
<string name="how_it_works">Wie es funktioniert</string>
<!-- How SimpleX Works -->
<string name="how_simplex_works">Wie <xliff:g id="appName">SimpleX</xliff:g> funktioniert</string>
<string name="many_people_asked_how_can_it_deliver">Viele Menschen haben gefragt: <i>Wenn <xliff:g id="appName">SimpleX</xliff:g> keine Benutzerkennungen hat, wie kann es dann Nachrichten zustellen?</i></string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Zum Schutz Ihrer Privatsphäre verwendet <xliff:g id="appName">SimpleX</xliff:g> an Stelle von Benutzer-IDs, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind.</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">Sie legen fest, über welche Server Sie Ihre Nachrichten <b>empfangen</b> und an Ihre Kontakte <b>senden</b>.</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Nur die Endgeräte speichern Benutzerprofile, Kontakte, Gruppen und Nachrichten, die über eine <b>2-Schichten Ende-zu-Ende-Verschlüsselung</b> gesendet werden.</string>
<string name="read_more_in_github">Erfahren Sie mehr dazu in unserem GitHub-Repository.</string>
<string name="read_more_in_github_with_link">Erfahren Sie mehr dazu in unserem <font color="#0088ff">GitHub-Repository</font>.</string>
<!-- MakeConnection -->
<string name="paste_the_link_you_received">Fügen Sie den erhaltenen Link ein</string>
<!-- Call -->
<string name="incoming_video_call">Eingehender Videoanruf</string>
<string name="incoming_audio_call">Eingehender Audioanruf</string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> möchte sich mit Ihnen verbinden per</string>
<string name="video_call_no_encryption">Videoanruf (nicht E2E verschlüsselt)</string>
<string name="encrypted_video_call">E2E-verschlüsseltem Videoanruf</string>
<string name="audio_call_no_encryption">Audioanruf (nicht E2E verschlüsselt)</string>
<string name="encrypted_audio_call">E2E-verschlüsseltem Audioanruf</string>
<string name="accept">Akzeptieren</string>
<string name="reject">Ablehnen</string>
<string name="ignore">Ignorieren</string>
<string name="call_already_ended">Anruf ist bereits beendet!</string>
<string name="icon_descr_video_call">Videoanruf</string>
<string name="icon_descr_audio_call">Audioanruf</string>
<!-- Call settings -->
<string name="settings_audio_video_calls">Audio- &amp; Videoanrufe</string>
<string name="your_calls">Ihre Anrufe</string>
<string name="connect_calls_via_relay">Über ein Relais verbinden</string>
<string name="call_on_lock_screen">Anrufe auf Sperrbildschirm:</string>
<string name="accept_call_on_lock_screen">Akzeptieren</string>
<string name="show_call_on_lock_screen">Anzeigen</string>
<string name="no_call_on_lock_screen">Deaktivieren</string>
<string name="your_ice_servers">Ihre ICE-Server</string>
<string name="webrtc_ice_servers">WebRTC ICE-Server</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Öffnen Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g>, um den Anruf anzunehmen.</string>
<string name="allow_accepting_calls_from_lock_screen">Aktivieren Sie Anrufe vom Sperrbildschirm über die Einstellungen.</string>
<string name="open_verb">Öffnen</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">E2E-verschlüsselt</string>
<string name="status_no_e2e_encryption">Keine E2E-Verschlüsselung</string>
<string name="status_contact_has_e2e_encryption">Kontakt hat E2E-Verschlüsselung</string>
<string name="status_contact_has_no_e2e_encryption">Kontakt hat keine E2E-Verschlüsselung</string>
<string name="call_connection_peer_to_peer">Peer-to-Peer</string>
<string name="call_connection_via_relay">über Relais</string>
<string name="icon_descr_hang_up">Auflegen</string>
<string name="icon_descr_video_off">Video aus</string>
<string name="icon_descr_video_on">Video an</string>
<string name="icon_descr_audio_off">Audio aus</string>
<string name="icon_descr_audio_on">Audio an</string>
<string name="icon_descr_speaker_off">Lautsprecher aus</string>
<string name="icon_descr_speaker_on">Lautsprecher an</string>
<string name="icon_descr_flip_camera">Kamera umdrehen</string>
<!-- Call items -->
<string name="icon_descr_call_pending_sent">Anstehender Anruf</string>
<string name="icon_descr_call_missed">Verpasster Anruf</string>
<string name="icon_descr_call_rejected">Abgelehnter Anruf</string>
<string name="icon_descr_call_connecting">Anruf wird verbunden</string>
<string name="icon_descr_call_progress">Laufendes Gespräch</string>
<string name="icon_descr_call_ended">Anruf beendet</string>
<string name="answer_call">Anruf annehmen</string>
<!-- Message integrity -->
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> übersprungene Nachricht(en)</string>
<string name="integrity_msg_bad_hash">Ungültiger Nachrichten-Hash</string>
<string name="integrity_msg_bad_id">Falsche Nachrichten-ID</string>
<string name="integrity_msg_duplicate">Doppelte Nachricht</string>
<string name="alert_title_skipped_messages">Übersprungene Nachrichten</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Dies kann unter folgenden Umständen passieren:\n1. Die Nachrichten verfallen auf dem Server, wenn sie 30 Tage lang nicht empfangen wurden.\n2. Der Server, den Sie zum Empfangen der Nachrichten von diesem Kontakt verwenden, wurde aktualisiert und neu gestartet.\n3. Die Verbindung ist kompromittiert.\nBitte nehmen Sie über die Einstellungen Verbindung mit den Entwicklern auf, um Updates zu den Servern zu erhalten.\nWir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu verhindern.</string>
<!-- Privacy settings -->
<string name="privacy_and_security">Datenschutz &amp; Sicherheit</string>
<string name="your_privacy">Ihre Privatsphäre</string>
<string name="auto_accept_images">Bilder automatisch akzeptieren</string>
<string name="send_link_previews">Link-Vorschau senden</string>
<!-- Settings sections -->
<string name="settings_section_title_you">DU</string>
<string name="settings_section_title_settings">EINSTELLUNGEN</string>
<string name="settings_section_title_help">HILFE</string>
<string name="settings_section_title_develop">ENTWICKELN</string>
<string name="settings_section_title_device">GERÄT</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="settings_developer_tools">Entwicklertools</string>
<string name="settings_experimental_features">Experimentelle Funktionen</string>
<string name="settings_section_title_socks">SOCKS-PROXY</string>
<string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">DESIGN</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_incognito">Inkognito Modus</string>
<!-- DatabaseView.kt -->
<string name="your_chat_database">Ihre Chat-Datenbank</string>
<string name="run_chat_section">CHAT STARTEN</string>
<string name="chat_is_running">Chat läuft</string>
<string name="chat_is_stopped">Chat ist beendet</string>
<string name="chat_database_section">CHAT-DATENBANK</string>
<string name="database_passphrase">Datenbank-Passwort</string>
<string name="export_database">Datenbank exportieren</string>
<string name="import_database">Datenbank importieren</string>
<string name="new_database_archive">Neues Datenbankarchiv</string>
<string name="old_database_archive">Altes Datenbankarchiv</string>
<string name="delete_database">Datenbank löschen</string>
<string name="error_starting_chat">Fehler beim Starten des Chats</string>
<string name="stop_chat_question">Chat beenden?</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Sie können keine Nachrichten empfangen oder senden, solange der Chat angehalten ist.</string>
<string name="stop_chat_confirmation">Beenden</string>
<string name="set_password_to_export">Passwort zum Exportieren festlegen</string>
<string name="set_password_to_export_desc">Die Datenbank ist mit einem zufälligen Passwort verschlüsselt. Bitte ändern Sie es vor dem Exportieren.</string>
<string name="error_stopping_chat">Fehler beim Beenden des Chats</string>
<string name="error_exporting_chat_database">Fehler beim Exportieren der Chat-Datenbank</string>
<string name="import_database_question">Chat-Datenbank importieren?</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.\nDiese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
<string name="import_database_confirmation">Importieren</string>
<string name="error_deleting_database">Fehler beim Löschen der Chat-Datenbank</string>
<string name="error_importing_database">Fehler beim Importieren der Chat-Datenbank</string>
<string name="chat_database_imported">Chat-Datenbank importiert</string>
<string name="restart_the_app_to_use_imported_chat_database">Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden.</string>
<string name="delete_chat_profile_question">Chat-Profil löschen?</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
<string name="chat_database_deleted">Chat-Datenbank gelöscht</string>
<string name="restart_the_app_to_create_a_new_chat_profile">Starten Sie die App neu, um ein neues Chat-Profil zu erstellen.</string>
<string name="you_must_use_the_most_recent_version_of_database">Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte.</string>
<string name="stop_chat_to_enable_database_actions">Chat beenden, um Datenbankaktionen zu erlauben.</string>
<string name="files_section">DATEIEN</string>
<string name="delete_files_and_media">Dateien \&amp; Medien löschen</string>
<string name="delete_files_and_media_question">Dateien und Medien löschen?</string>
<string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden - alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string>
<string name="no_received_app_files">Keine empfangenen oder gesendeten Dateien</string>
<string name="total_files_count_and_size">%d Datei(en) mit einem Gesamtspeicherverbrauch von %s</string>
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Passwort im Keystore sichern</string>
<string name="database_encrypted">Datenbank verschlüsselt!</string>
<string name="error_encrypting_database">Fehler beim Verschlüsseln der Datenbank</string>
<string name="remove_passphrase_from_keychain">Passwort aus dem Keystore entfernen?</string>
<string name="notifications_will_be_hidden">Benachrichtigungen werden nur solange zugestellt, bis die App angehalten wird!</string>
<string name="remove_passphrase">Entfernen</string>
<string name="encrypt_database">Verschlüsseln</string>
<string name="update_database">Aktualisieren</string>
<string name="current_passphrase">Aktuelles Passwort…</string>
<string name="new_passphrase">Neues Passwort…</string>
<string name="confirm_new_passphrase">Neues Passwort bestätigen…</string>
<string name="update_database_passphrase">Datenbank-Passwort aktualisieren</string>
<string name="enter_correct_current_passphrase">Bitte geben Sie das korrekte, aktuelle Passwort ein.</string>
<string name="database_is_not_encrypted">Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen.</string>
<string name="keychain_is_storing_securely">Der Android Keystore wird verwendet, um das Passwort sicher zu speichern. Dies ermöglicht die ordentliche Funktion des Benachrichtigungsdienstes.</string>
<string name="encrypted_with_random_passphrase">Die Datenbank wird mit einem zufälligen Passwort verschlüsselt, Sie können es ändern.</string>
<string name="impossible_to_recover_passphrase"><b>Bitte beachten Sie</b>: Sie können das Passwort NICHT wiederherstellen oder ändern, wenn Sie es vergessen haben oder verlieren.</string>
<string name="keychain_allows_to_receive_ntfs">Der Android Keystore wird verwendet, um das Passwort sicher zu speichern, nachdem Sie die App neu gestartet oder das Passwort geändert haben dies ermöglicht den Empfang von Benachrichtigungen.</string>
<string name="you_have_to_enter_passphrase_every_time">Sie müssen das Passwort jedes Mal eingeben, wenn die App startet. Es wird nicht auf dem Gerät gespeichert.</string>
<string name="encrypt_database_question">Datenbank verschlüsseln?</string>
<string name="change_database_passphrase_question">Datenbank-Passwort ändern?</string>
<string name="database_will_be_encrypted">Datenbank wird verschlüsselt.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">Die Datenbank wird verschlüsselt, und das Passwort im Keystore gespeichert.</string>
<string name="database_encryption_will_be_updated">Das Passwort für die Datenbankverschlüsselung wird aktualisiert und im Keystore gespeichert.</string>
<string name="database_passphrase_will_be_updated">Das Passwort für die Datenbankverschlüsselung wird aktualisiert.</string>
<string name="store_passphrase_securely">Bitte bewahren Sie das Passwort sicher auf, Sie können es NICHT mehr ändern, wenn Sie es vergessen haben oder verlieren.</string>
<string name="store_passphrase_securely_without_recover">Bitte bewahren Sie das Passwort sicher auf, Sie können NICHT mehr auf den Chat zugreifen, wenn Sie es vergessen haben oder verlieren.</string>
<!-- DatabaseErrorView.kt -->
<string name="wrong_passphrase">Falsches Datenbank-Passwort</string>
<string name="encrypted_database">Verschlüsselte Datenbank</string>
<string name="database_error">Datenbankfehler</string>
<string name="keychain_error">Keystore Fehler</string>
<string name="passphrase_is_different">Das Datenbank-Passwort unterscheidet sich von dem im Keystore gespeicherten.</string>
<string name="file_with_path">Datei: %s</string>
<string name="database_passphrase_is_required">Das Datenbank-Passwort ist erforderlich, um den Chat zu öffnen.</string>
<string name="error_with_info">Fehler: %s</string>
<string name="cannot_access_keychain">Die App kann nicht auf den Keystore zugreifen, um das Datenbank-Passwort zu speichern.</string>
<string name="unknown_database_error_with_info">Unbekannter Datenbankfehler: %s</string>
<string name="wrong_passphrase_title">Falsches Passwort!</string>
<string name="enter_correct_passphrase">Geben Sie das korrekte Passwort ein.</string>
<string name="unknown_error">Unbekannter Fehler</string>
<string name="enter_passphrase">Passwort eingeben…</string>
<string name="save_passphrase_and_open_chat">Passwort speichern und Chat öffnen</string>
<string name="open_chat">Chat öffnen</string>
<string name="database_backup_can_be_restored">Der Versuch, das Passwort der Datenbank zu ändern, konnte nicht abgeschlossen werden.</string>
<string name="restore_database">Datenbanksicherung wiederherstellen</string>
<string name="restore_database_alert_title">Datenbanksicherung wiederherstellen?</string>
<string name="restore_database_alert_desc">Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="restore_database_alert_confirm">Wiederherstellen</string>
<string name="database_restore_error">Fehler bei der Wiederherstellung der Datenbank</string>
<!-- ChatModel.chatRunning interactions -->
<string name="chat_is_stopped_indication">Chat wurde beendet</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten.</string>
<!-- ChatArchiveView.kt -->
<string name="chat_archive_header">Datenbank Archiv</string>
<string name="chat_archive_section">CHAT ARCHIV</string>
<string name="save_archive">Archiv speichern</string>
<string name="delete_archive">Archiv löschen</string>
<string name="archive_created_on_ts">Erstellt am <xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="delete_chat_archive_question">Chat Archiv löschen?</string>
<!-- Groups -->
<string name="group_invitation_item_description">Einladung zur Gruppe <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="join_group_question">Der Gruppe beitreten?</string>
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Sie sind zu einer Gruppe eingeladen worden. Treten Sie bei, um sich mit Gruppenmitgliedern zu verbinden.</string>
<string name="join_group_button">Beitreten</string>
<string name="join_group_incognito_button">Inkognito beitreten</string>
<string name="joining_group">Gruppe beitreten</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sie sind dieser Gruppe beigetreten. Sie werden mit dem einladenden Gruppenmitglied verbunden.</string>
<string name="leave_group_button">Verlassen</string>
<string name="leave_group_question">Gruppe verlassen?</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.</string>
<string name="icon_descr_add_members">Mitglieder einladen</string>
<string name="icon_descr_group_inactive">Gruppe inaktiv</string>
<string name="alert_title_group_invitation_expired">Einladung abgelaufen!</string>
<string name="alert_message_group_invitation_expired">Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt.</string>
<string name="alert_title_no_group">Die Gruppe wurde nicht gefunden!</string>
<string name="alert_message_no_group">Diese Gruppe existiert nicht mehr.</string>
<string name="alert_title_cant_invite_contacts">Kontakte können nicht eingeladen werden!</string>
<string name="alert_title_cant_invite_contacts_descr">Sie verwenden ein Inkognito-Profil für diese Gruppe. Um zu verhindern, dass Sie Ihr Hauptprofil teilen, ist in diesem Fall das Einladen von Kontakten nicht erlaubt.</string>
<!-- CIGroupInvitationView.kt -->
<string name="you_sent_group_invitation">Sie haben eine Gruppeneinladung gesendet</string>
<string name="you_are_invited_to_group">Sie sind zur Gruppe eingeladen</string>
<string name="group_invitation_tap_to_join">Zum Beitreten tippen</string>
<string name="group_invitation_tap_to_join_incognito">Tippen, um Inkognito beizutreten</string>
<string name="you_joined_this_group">Sie sind dieser Gruppe beigetreten</string>
<string name="you_rejected_group_invitation">Sie haben die Gruppeneinladung abgelehnt</string>
<string name="group_invitation_expired">Die Gruppeneinladung ist abgelaufen</string>
<!-- Group event chat items -->
<string name="rcv_group_event_member_added">Sie haben <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> eingeladen.</string>
<string name="rcv_group_event_member_connected">verbunden</string>
<string name="rcv_group_event_member_left">verlassen</string>
<string name="rcv_group_event_member_deleted">entfernte <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g>.</string>
<string name="rcv_group_event_user_deleted">hat Sie entfernt</string>
<string name="rcv_group_event_group_deleted">Gruppe gelöscht</string>
<string name="rcv_group_event_updated_group_profile">aktualisiertes Gruppenprofil</string>
<string name="snd_group_event_member_deleted">Sie haben <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> entfernt.</string>
<string name="snd_group_event_user_left">Sie haben verlassen</string>
<string name="snd_group_event_group_profile_updated">Gruppenprofil aktualisiert</string>
<!-- GroupMemberRole -->
<string name="group_member_role_member">Mitglied</string>
<string name="group_member_role_admin">Admin</string>
<string name="group_member_role_owner">Eigentümer</string>
<!-- GroupMemberStatus -->
<string name="group_member_status_removed">entfernt</string>
<string name="group_member_status_left">verlassen</string>
<string name="group_member_status_group_deleted">Gruppe gelöscht</string>
<string name="group_member_status_invited">eingeladen</string>
<string name="group_member_status_introduced">Verbindung (erstellt)</string>
<string name="group_member_status_intro_invitation">Verbindung (eingeladen)</string>
<string name="group_member_status_accepted">Verbindung (akzeptiert)</string>
<string name="group_member_status_announced">Verbindung (angekündigt)</string>
<string name="group_member_status_connected">verbunden</string>
<string name="group_member_status_complete">vollständig</string>
<string name="group_member_status_creator">Ersteller</string>
<string name="group_member_status_connecting">verbinden</string>
<!-- AddGroupMembersView.kt -->
<string name="no_contacts_to_add">Keine Kontakte zum Hinzufügen</string>
<string name="new_member_role">Neue Mitgliedsrolle</string>
<string name="icon_descr_expand_role">Rollenauswahl erweitern</string>
<string name="invite_to_group_button">In Gruppe einladen</string>
<string name="icon_descr_contact_checked">Kontakt geprüft</string>
<string name="clear_contacts_selection_button">Löschen</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> Kontakt(e) ausgewählt</string>
<string name="no_contacts_selected">Keine Kontakte ausgewählt</string>
<string name="invite_prohibited">Kontakt kann nicht eingeladen werden!</string>
<string name="invite_prohibited_description">Sie versuchen, einen Kontakt, mit dem Sie ein Inkognito-Profil geteilt haben, in die Gruppe einzuladen, in der Sie Ihr Hauptprofil verwenden.</string>
<!-- GroupChatInfoView.kt -->
<string name="button_add_members">Mitglieder einladen</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MITGLIEDER</string>
<string name="group_info_member_you">Sie: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="button_delete_group">Gruppe löschen</string>
<string name="delete_group_question">Gruppe löschen?</string>
<string name="delete_group_for_all_members_cannot_undo_warning">Die Gruppe wird für alle Mitglieder gelöscht - dies kann nicht rückgängig gemacht werden!</string>
<string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!</string>
<string name="button_leave_group">Gruppe verlassen</string>
<string name="button_edit_group_profile">Gruppenprofil bearbeiten</string>
<!-- For Console chat info section -->
<string name="section_title_for_console">FÜR KONSOLE</string>
<string name="info_row_local_name">Lokaler Name</string>
<string name="info_row_database_id">Datenbank-ID</string>
<!-- GroupMemberInfoView.kt -->
<string name="button_remove_member">Mitglied entfernen</string>
<string name="button_send_direct_message">Direktnachricht senden</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden!</string>
<string name="remove_member_confirmation">Entfernen</string>
<string name="member_info_section_title_member">MITGLIED</string>
<string name="info_row_group">Gruppe</string>
<string name="info_row_connection">Verbindung</string>
<string name="conn_level_desc_direct">direkt</string>
<string name="conn_level_desc_indirect">indirekt (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<!-- ConnectionStats -->
<string name="conn_stats_section_title_servers">SERVER</string>
<string name="receiving_via">Empfangen über</string>
<string name="sending_via">Senden über</string>
<string name="network_status">Netzwerkstatus</string>
<!-- AddGroupView.kt -->
<string name="create_secret_group_title">Geheime Gruppe erstellen</string>
<string name="group_is_decentralized">Die Gruppe ist vollständig dezentralisiert sie ist nur für Mitglieder sichtbar.</string>
<string name="group_display_name_field">Anzeigename der Gruppe:</string>
<string name="group_full_name_field">Vollständiger Gruppenname:</string>
<string name="group_unsupported_incognito_main_profile_sent">Der Inkognito-Modus wird hier nicht unterstützt - Ihr Hauptprofil wird an die Gruppenmitglieder gesendet</string>
<string name="group_main_profile_sent">Ihr Chat-Profil wird an Gruppenmitglieder gesendet</string>
<!-- GroupProfileView.kt -->
<string name="group_profile_is_stored_on_members_devices">Das Gruppenprofil wird auf den Geräten der Mitglieder gespeichert, nicht auf den Servern.</string>
<string name="save_group_profile">Gruppenprofil speichern</string>
<string name="error_saving_group_profile">Fehler beim Speichern des Gruppenprofils</string>
<!-- AdvancedNetworkSettings.kt -->
<string name="network_options_reset_to_defaults">Auf Standardwerte zurücksetzen</string>
<string name="network_option_seconds_label">sek</string>
<string name="network_option_tcp_connection_timeout">Timeout der TCP-Verbindung</string>
<string name="network_option_protocol_timeout">Protokollzeitüberschreitung</string>
<string name="network_option_ping_interval">PING-Intervall</string>
<string name="network_option_enable_tcp_keep_alive">TCP-Keep-alive aktivieren</string>
<string name="network_options_revert">Zurückkehren</string>
<string name="network_options_save">Speichern</string>
<string name="update_network_settings_question">Netzwerkeinstellungen aktualisieren?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden.</string>
<string name="update_network_settings_confirmation">Aktualisieren</string>
<!-- Incognito mode -->
<string name="incognito">Inkognito</string>
<string name="incognito_random_profile">Ihr Zufallsprofil</string>
<string name="incognito_random_profile_description">Ein zufälliges Profil wird an Ihren Kontakt gesendet.</string>
<string name="incognito_random_profile_from_contact_description">Ein zufälliges Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben.</string>
<string name="incognito_info_protects">Der Inkognito-Modus schützt die Privatsphäre Ihres Hauptprofilnamens und -bildes für jeden neuen Kontakt wird ein neues Zufallsprofil erstellt.</string>
<string name="incognito_info_allows">Es ermöglicht viele anonyme Verbindungen ohne gemeinsame Daten zwischen diesen in einem einzigen Chat-Profil zu haben.</string>
<string name="incognito_info_share">Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil für die Gruppen verwendet, zu denen sie Sie eingeladen haben.</string>
<string name="incognito_info_find">Um das für eine Inkognito-Verbindung verwendete Profil zu finden, tippen Sie oben im Chat auf den Kontakt- oder Gruppennamen.</string>
<!-- Default themes -->
<string name="theme_system">System</string>
<string name="theme_light">Hell</string>
<string name="theme_dark">Dunkel</string>
<!-- Appearance.kt -->
<string name="theme">Design</string>
<string name="save_color">Farbe speichern</string>
<string name="reset_color">Farben zurücksetzen</string>
<string name="color_primary">Akzent</string>
</resources>

View File

@@ -43,12 +43,24 @@
<string name="error_setting_network_config">Ошибка при сохранении настроек сети</string>
<!-- API Error Responses - SimpleXAPI.kt -->
<string name="connection_timeout">Превышено время соединения</string>
<string name="connection_error">Ошибка соединения</string>
<string name="network_error_desc">Пожалуйста, проверьте ваше соединение с сетью и попробуйте еще раз.</string>
<string name="error_sending_message">Ошибка при отправке сообщения</string>
<string name="error_adding_members">Ошибка при добавлении членов группы</string>
<string name="error_joining_group">Ошибка при вступлении в группу</string>
<string name="cannot_receive_file">Невозможно получить файл</string>
<string name="sender_cancelled_file_transfer">Отправитель отменил передачу файла.</string>
<string name="error_receiving_file">Ошибка при получении файла</string>
<string name="error_creating_address">Ошибка при создании адреса</string>
<string name="contact_already_exists">Существующий контакт</string>
<string name="you_are_already_connected_to_vName_via_this_link">Вы уже соединены с <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> через эту ссылку.</string>
<string name="invalid_connection_link">Ошибка в ссылке контакта</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Пожалуйста, проверьте, что вы использовали правильную ссылку, или попросите ваш контакт отправить вам новую.</string>
<string name="connection_error_auth">Ошибка соединения (AUTH)</string>
<string name="connection_error_auth_desc">Возможно, ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите ваш контакт создать еще одну ссылку и проверьте ваше соединение с сетью.</string>
<string name="error_accepting_contact_request">Ошибка при принятии запроса на соединение</string>
<string name="sender_may_have_deleted_the_connection_request">Отправитель мог удалить запрос на соединение.</string>
<string name="cannot_delete_contact">Невозможно удалить контакт!</string>
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Контакт <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> не может быть удален, так как является членом групп(ы) <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
<string name="error_deleting_contact">Ошибка удаления контакта</string>
@@ -152,6 +164,9 @@
<string name="group_preview_you_are_invited">вы приглашены в группу</string>
<string name="group_preview_join_as">вступить как %s</string>
<string name="group_connection_pending">соединяется…</string>
<string name="tap_to_start_new_chat">Нажмите, чтобы начать чат</string>
<string name="chat_with_developers">Соединиться с разработчиками</string>
<string name="you_have_no_chats">У вас нет чатов</string>
<!-- ComposeView.kt, helpers -->
<string name="attach">Прикрепить</string>
@@ -201,18 +216,17 @@
<string name="confirm_verb">Подтвердить</string>
<string name="ok">OK</string>
<string name="no_details">нет описания</string>
<string name="add_contact">Добавить контакт</string>
<string name="add_contact">Одноразовая ссылка</string>
<string name="copied">Скопировано в буфер обмена</string>
<!-- NewChatSheet -->
<string name="add_contact_or_create_group">Начать новый разговор</string>
<string name="create_one_time_link">Создать одноразовую ссылку</string>
<string name="paste_received_link">Вставить полученную ссылку</string>
<string name="scan_QR_code">Сканировать QR код</string>
<string name="share_one_time_link">Создать ссылку-приглашение</string>
<string name="connect_via_link_or_qr">Соединиться через ссылку / QR код</string>
<string name="scan_QR_code">Сканировать\nQR код</string>
<string name="create_group">Создать секретную группу</string>
<string name="to_share_with_your_contact">(чтобы отправить вашему контакту)</string>
<string name="in_person_or_in_video_call__bracketed">(при встрече или через видеозвонок)</string>
<string name="paste_received_link_from_clipboard">(вставить ссылку из буфера обмена)</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(сканировать или вставить из буфера)</string>
<string name="only_stored_on_members_devices">(хранится только у членов группы)</string>
<!-- GetImageView -->
@@ -291,14 +305,22 @@
<string name="connection_request_sent">Запрос на соединение послан!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Соединение будет установлено когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Соединение будет установлено когда ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже!</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Покажите QR код вашему контакту, чтобы сосканировать его из приложения.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Если вы не можете встретиться лично, вы можете <b>показать QR код во время видеозвонка</b> или отправить ссылку через любой другой канал связи.</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Ваш контакт может сосканировать QR код в приложении.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Если вы не можете встретиться лично, вы можете <b>показать QR код во время видеозвонка</b> или поделиться ссылкой.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Ваш профиль будет отправлен\nвашему контакту</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Если вы не можете встретиться лично, вы можете <b>сосканировать QR код во время видеозвонка</b>, или ваш контакт может отправить вам ссылку.</string>
<string name="share_invitation_link">Поделиться ссылкой</string>
<string name="paste_connection_link_below_to_connect">Чтобы соединиться, вставьте в это поле ссылку, полученную от вашего контакта.</string>
<string name="your_profile_will_be_sent">Ваш профиль будет отправлен вашему контакту</string>
<!-- PasteToConnect.kt -->
<string name="connect_button">Соединиться</string>
<string name="paste_button">Вставить</string>
<!-- CreateLinkView.kt -->
<string name="create_one_time_link">Создать одноразовую ссылку</string>
<string name="one_time_link">Одноразовая ссылка</string>
<string name="your_contact_address">Ваш SimpleX адрес</string>
<!-- settings - SettingsView.kt -->
<string name="your_settings">Настройки</string>
@@ -315,12 +337,18 @@
<string name="smp_servers">SMP серверы</string>
<string name="install_simplex_chat_for_terminal"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> для терминала</string>
<string name="use_simplex_chat_servers__question">Использовать серверы предосталенные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>?</string>
<string name="saved_SMP_servers_will_br_removed">Сохраненные SMP серверы будут удалены.</string>
<string name="saved_SMP_servers_will_be_removed">Сохраненные SMP серверы будут удалены.</string>
<string name="your_SMP_servers">Ваши SMP серверы</string>
<string name="configure_SMP_servers">Настройка SMP серверов</string>
<string name="using_simplex_chat_servers">Используются серверы предоставленные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
<string name="enter_one_SMP_server_per_line">Введите SMP серверы, каждый сервер в отдельной строке:</string>
<string name="how_to">Инфо</string>
<string name="saved_ICE_servers_will_be_removed">Сохраненные WebRTC ICE серверы будут удалены.</string>
<string name="your_ICE_servers">Ваши ICE серверы</string>
<string name="configure_ICE_servers">Настройка ICE серверов</string>
<string name="enter_one_ICE_server_per_line">ICE серверы (один на строке)</string>
<string name="error_saving_ICE_servers">Ошибка при сохранении ICE серверов</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется.</string>
<string name="save_servers_button">Сохранить</string>
<string name="network_and_servers">Сеть и серверы</string>
<string name="network_settings">Настройки сети</string>
@@ -347,9 +375,9 @@
<string name="create_address">Создать адрес</string>
<string name="delete_address__question">Удалить адрес?</string>
<string name="all_your_contacts_will_remain_connected">Все контакты, которые соединились через этот адрес, сохранятся.</string>
<string name="your_chat_address">Ваш <xliff:g id="appName">SimpleX</xliff:g> адрес</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать адрес как ссылку или как QR код - через него можно с вами соединиться.</string>
<string name="if_you_delete_address_you_wont_lose_contacts">Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
<string name="if_you_later_delete_address_you_wont_lose_contacts">Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
<string name="if_you_delete_address_you_wont_lose_contacts">Вы можете удалить адрес, сохранив контакты, которые через него соединились.</string>
<string name="share_link">Поделиться\nссылкой</string>
<string name="delete_address">Удалить\nадрес</string>
@@ -386,7 +414,7 @@
<string name="secret">секрет</string>
<string name="connect_via_link">Соединиться через ссылку</string>
<string name="this_string_is_not_a_connection_link">Эта строка не является ссылкой-приглашением!</string>
<string name="you_can_also_connect_by_clicking_the_link">Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку <b>Open in mobile app</b>.</string>
<string name="you_can_also_connect_by_clicking_the_link">Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку <b>Открыть в приложении</b>.</string>
<!-- CICallStatus -->
<string name="callstatus_calling">входящий звонок…</string>
@@ -430,16 +458,7 @@
<string name="read_more_in_github_with_link">Узнайте больше из нашего <font color="#0088ff">GitHub репозитория</font>.</string>
<!-- MakeConnection -->
<string name="to_make_your_first_private_connection_choose">Чтобы добавить ваш первый контакт, выберите <b>одно из</b>:</string>
<string name="create_1_time_link_qr_code">Создать ссылку / QR код</string>
<string name="it_s_secure_to_share__only_one_contact_can_use_it">Ей безопасно поделиться - только один контакт может использовать её.</string>
<string name="paste_the_link_you_received">Вставьте полученную ссылку</string>
<string name="or_open_the_link_in_the_browser_and_tap_open_in_mobile">Или откройте ссылку в браузере и нажмите <b>Open in mobile</b>.</string>
<string name="scan_contact_s_qr_code">Сосканировать QR код контакта</string>
<string name="in_person_or_via_a_video_call__the_most_secure_way_to_connect">При встрече или в видеозвонке самый безопасный способ установить соединение</string>
<string name="or">или</string>
<string name="connect_with_the_developers">Соединиться с разработчиками</string>
<string name="to_ask_any_questions_and_to_receive_simplex_chat_updates">Чтобы задать вопросы и получать уведомления о <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
<string name="paste_the_link_you_received">Вставить полученную ссылку</string>
<!-- Call -->
<string name="incoming_video_call">Входящий видеозвонок</string>
@@ -464,6 +483,8 @@
<string name="accept_call_on_lock_screen">Принимать</string>
<string name="show_call_on_lock_screen">Показывать</string>
<string name="no_call_on_lock_screen">Выключить</string>
<string name="your_ice_servers">Ваши ICE серверы</string>
<string name="webrtc_ice_servers">WebRTC ICE серверы</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Откройте <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\nчтобы принять звонок</string>
@@ -521,6 +542,8 @@
<string name="settings_section_title_socks">SOCKS ПРОКСИ</string>
<string name="settings_section_title_icon">ИКОНКА</string>
<string name="settings_section_title_themes">ТЕМЫ</string>
<string name="settings_section_title_messages">СООБЩЕНИЯ</string>
<string name="settings_section_title_calls">ЗВОНКИ</string>
<string name="settings_section_title_incognito">Режим Инкогнито</string>
<!-- DatabaseView.kt -->
@@ -645,7 +668,6 @@
<string name="alert_message_group_invitation_expired">Приглашение в группу больше не действительно, оно было удалено отправителем.</string>
<string name="alert_title_no_group">Группа не найдена!</string>
<string name="alert_message_no_group">Эта группа больше не существует.</string>
<string name="alert_title_join_group_error">Ошибка приглашения</string>
<string name="alert_title_cant_invite_contacts">Нельзя пригласить контакты!</string>
<string name="alert_title_cant_invite_contacts_descr">Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено</string>

View File

@@ -43,12 +43,24 @@
<string name="error_setting_network_config">Error updating network configuration</string>
<!-- API Error Responses - SimpleXAPI.kt -->
<string name="connection_timeout">Connection timeout</string>
<string name="connection_error">Connection error</string>
<string name="network_error_desc">Please check your network connection and try again.</string>
<string name="error_sending_message">Error sending message</string>
<string name="error_adding_members">Error adding member(s)</string>
<string name="error_joining_group">Error joining group</string>
<string name="cannot_receive_file">Cannot receive file</string>
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>Error receiving file
<string name="error_receiving_file">Error receiving file</string>
<string name="error_creating_address">Error creating address</string>
<string name="contact_already_exists">Contact already exists</string>
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> via this link.</string>
<string name="invalid_connection_link">Invalid connection link</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
<string name="connection_error_auth">Connection error (AUTH)</string>
<string name="connection_error_auth_desc">Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.</string>
<string name="error_accepting_contact_request">Error accepting contact request</string>
<string name="sender_may_have_deleted_the_connection_request">Sender may have deleted the connection request.</string>
<string name="cannot_delete_contact">Can\'t delete contact!</string>
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Contact <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> cannot be deleted, they are a member of the group(s) <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
<string name="error_deleting_contact">Error deleting contact</string>
@@ -152,6 +164,9 @@
<string name="group_preview_you_are_invited">you are invited to group</string>
<string name="group_preview_join_as">join as %s</string>
<string name="group_connection_pending">connecting…</string>
<string name="tap_to_start_new_chat">Tap to start a new chat</string>
<string name="chat_with_developers">Chat with the developers</string>
<string name="you_have_no_chats">You have no chats</string>
<!-- ComposeView.kt, helpers -->
<string name="attach">Attach</string>
@@ -201,18 +216,17 @@
<string name="confirm_verb">Confirm</string>
<string name="ok">OK</string>
<string name="no_details">no details</string>
<string name="add_contact">Add contact</string>
<string name="add_contact">One-time invitation link</string>
<string name="copied">Copied to clipboard</string>
<!-- NewChatSheet -->
<string name="add_contact_or_create_group">Start new chat</string>
<string name="create_one_time_link">Create link / QR code</string>
<string name="paste_received_link">Connect via received link</string>
<string name="share_one_time_link">Create one-time invitation link</string>
<string name="connect_via_link_or_qr">Connect via link / QR code</string>
<string name="scan_QR_code">Scan QR code</string>
<string name="create_group">Create secret group</string>
<string name="to_share_with_your_contact">(to share with your contact)</string>
<string name="in_person_or_in_video_call__bracketed">(in person or in video call)</string>
<string name="paste_received_link_from_clipboard">(paste link from clipboard)</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scan or paste from clipboard)</string>
<string name="only_stored_on_members_devices">(only stored by group members)</string>
<!-- GetImageView -->
@@ -291,8 +305,8 @@
<string name="connection_request_sent">Connection request sent!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">You will be connected when your connection request is accepted, please wait or check later!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">You will be connected when your contact\'s device is online, please wait or check later!</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Show QR code for your contact to scan from the app.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">If you can\'t meet in person, you can <b>show QR code in the video call</b>, or you can share the invitation link via any other channel.</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Your contact can scan it from the app.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">If you can\'t meet in person, <b>show QR code in the video call</b>, or share the link.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Your chat profile will be sent\nto your contact</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">If you cannot meet in person, you can <b>scan QR code in the video call</b>, or your contact can share an invitation link.</string>
<string name="share_invitation_link">Share invitation link</string>
@@ -301,9 +315,16 @@
<!-- PasteToConnect.kt -->
<string name="connect_via_link">Connect via link</string>
<string name="connect_button">Connect</string>
<string name="paste_button">Paste</string>
<string name="this_string_is_not_a_connection_link">This string is not a connection link!</string>
<string name="you_can_also_connect_by_clicking_the_link">You can also connect by clicking the link. If it opens in the browser, click <b>Open in mobile app</b> button.</string>
<!-- CreateLinkView.kt -->
<string name="create_one_time_link">Create one-time invitation link</string>
<string name="one_time_link">One-time invitation link</string>
<string name="your_contact_address">Your contact address</string>
<!-- settings - SettingsView.kt -->
<string name="your_settings">Your settings</string>
<string name="your_simplex_contact_address">Your <xliff:g id="appName">SimpleX</xliff:g> contact address</string>
@@ -319,12 +340,18 @@
<string name="smp_servers">SMP servers</string>
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
<string name="use_simplex_chat_servers__question">Use <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers?</string>
<string name="saved_SMP_servers_will_br_removed">Saved SMP servers will be removed.</string>
<string name="saved_SMP_servers_will_be_removed">Saved SMP servers will be removed.</string>
<string name="your_SMP_servers">Your SMP servers</string>
<string name="configure_SMP_servers">Configure SMP servers</string>
<string name="using_simplex_chat_servers">Using <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers.</string>
<string name="enter_one_SMP_server_per_line">Enter one SMP server per line:</string>
<string name="how_to">How to</string>
<string name="saved_ICE_servers_will_be_removed">Saved WebRTC ICE servers will be removed.</string>
<string name="your_ICE_servers">Your ICE servers</string>
<string name="configure_ICE_servers">Configure ICE servers</string>
<string name="enter_one_ICE_server_per_line">ICE servers (one per line)</string>
<string name="error_saving_ICE_servers">Error saving ICE servers</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</string>
<string name="save_servers_button">Save</string>
<string name="network_and_servers">Network &amp; servers</string>
<string name="network_settings">Advanced network settings</string>
@@ -351,9 +378,9 @@
<string name="create_address">Create address</string>
<string name="delete_address__question">Delete address?</string>
<string name="all_your_contacts_will_remain_connected">All your contacts will remain connected.</string>
<string name="your_chat_address">Your chat address</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">You can share your address as a link or as a QR code - anybody will be able to connect to you.</string>
<string name="if_you_delete_address_you_wont_lose_contacts">If you later delete it - you won\'t lose your contacts.</string>
<string name="if_you_later_delete_address_you_wont_lose_contacts">If you later delete it - you won\'t lose your contacts made via the address.</string>
<string name="if_you_delete_address_you_wont_lose_contacts">If you delete it - you won\'t lose your contacts made via this address.</string>
<string name="share_link">Share link</string>
<string name="delete_address">Delete address</string>
@@ -431,16 +458,7 @@
<string name="read_more_in_github_with_link">Read more in our <font color="#0088ff">GitHub repository</font>.</string>
<!-- MakeConnection -->
<string name="to_make_your_first_private_connection_choose">To make your first private connection, choose <b>one of the following</b>:</string>
<string name="create_1_time_link_qr_code">Create 1-time link / QR code</string>
<string name="it_s_secure_to_share__only_one_contact_can_use_it">It\'s secure to share - only one contact can use it.</string>
<string name="paste_the_link_you_received">Paste the link you received</string>
<string name="or_open_the_link_in_the_browser_and_tap_open_in_mobile">Or open the link in the browser and tap <b>Open in mobile</b>.</string>
<string name="scan_contact_s_qr_code">Scan contact\'s QR code</string>
<string name="in_person_or_via_a_video_call__the_most_secure_way_to_connect">In person or via a video call the most secure way to connect.</string>
<string name="or">or</string>
<string name="connect_with_the_developers">Connect with the developers</string>
<string name="to_ask_any_questions_and_to_receive_simplex_chat_updates">To ask any questions and to receive <xliff:g id="appNameFull">SimpleX Chat</xliff:g> updates.</string>
<string name="paste_the_link_you_received">Paste received link</string>
<!-- Call -->
<string name="incoming_video_call">Incoming video call</string>
@@ -465,6 +483,8 @@
<string name="accept_call_on_lock_screen">Accept</string>
<string name="show_call_on_lock_screen">Show</string>
<string name="no_call_on_lock_screen">Disable</string>
<string name="your_ice_servers">Your ICE servers</string>
<string name="webrtc_ice_servers">WebRTC ICE servers</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g> to accept call</string>
@@ -522,6 +542,8 @@
<string name="settings_section_title_socks">SOCKS PROXY</string>
<string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_incognito">Incognito mode</string>
<!-- DatabaseView.kt -->
@@ -646,7 +668,6 @@
<string name="alert_message_group_invitation_expired">Group invitation is no longer valid, it was removed by sender.</string>
<string name="alert_title_no_group">Group not found!</string>
<string name="alert_message_no_group">This group no longer exists.</string>
<string name="alert_title_join_group_error">Error joining group</string>
<string name="alert_title_cant_invite_contacts">Can\'t invite contacts!</string>
<string name="alert_title_cant_invite_contacts_descr">You\'re using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</string>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 788 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "decentralized.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "decentralized@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "decentralized@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "60.png",
"filename" : "icon-light.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "120.png",
"filename" : "icon-light@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "180.png",
"filename" : "icon-light@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show More