Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2024-12-25 23:19:38 +00:00
165 changed files with 13953 additions and 9596 deletions
@@ -27,6 +27,14 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- Allows to query app name and icon that can open specific file type -->
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="*/*" />
</intent>
</queries>
<application
android:name="SimplexApp"
android:allowBackup="false"
@@ -115,7 +123,6 @@
android:launchMode="singleInstance"
android:supportsPictureInPicture="true"
android:autoRemoveFromRecents="true"
android:screenOrientation="portrait"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
<provider
@@ -32,8 +32,10 @@ object MessagesFetcherWorker {
SimplexApp.context.getWorkManagerInstance().enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
}
fun cancelAll() {
Log.d(TAG, "Worker: canceled all tasks")
fun cancelAll(withLog: Boolean = true) {
if (withLog) {
Log.d(TAG, "Worker: canceled all tasks")
}
SimplexApp.context.getWorkManagerInstance().cancelUniqueWork(UNIQUE_WORK_TAG)
}
}
@@ -33,6 +33,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.map
import java.io.*
import java.util.*
import java.util.concurrent.TimeUnit
@@ -151,6 +152,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
* */
fun schedulePeriodicServiceRestartWorker() = CoroutineScope(Dispatchers.Default).launch {
if (!allowToStartServiceAfterAppExit()) {
getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
return@launch
}
val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get()
@@ -172,6 +174,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch {
if (!allowToStartPeriodically()) {
MessagesFetcherWorker.cancelAll(withLog = false)
return@launch
}
MessagesFetcherWorker.scheduleWork()
@@ -227,7 +230,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
SimplexService.safeStopService()
}
}
if (mode != NotificationsMode.SERVICE) {
getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
}
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
@@ -244,6 +249,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
override fun androidChatStopped() {
getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
SimplexService.safeStopService()
MessagesFetcherWorker.cancelAll()
}
@@ -360,6 +366,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
return true
}
override fun androidCreateActiveCallState(): Closeable = ActiveCallState()
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
}
}
@@ -139,6 +139,7 @@ class SimplexService: Service() {
if (chatDbStatus != DBMigrationResult.OK) {
Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus")
showPassphraseNotification(chatDbStatus)
androidAppContext.getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
safeStopService()
return@withLongRunningApi
}
@@ -469,53 +470,65 @@ class SimplexService: Service() {
)
}
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert {
val ignoreOptimization = {
AlertManager.shared.hideAlert()
askAboutIgnoringBatteryOptimization()
private var showingIgnoreNotification = false
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) {
// that's workaround for situation when the app receives onPause/onResume events multiple times
// (for example, after showing system alert for enabling notifications) which triggers showing that alert multiple times
if (showingIgnoreNotification) {
return
}
val disableNotifications = {
AlertManager.shared.hideAlert()
disableNotifications(mode, showOffAlert)
}
AlertDialog(
onDismissRequest = disableNotifications,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
Text(
if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc),
Modifier.padding(bottom = 8.dp)
)
Text(annotatedStringResource(MR.strings.turn_off_battery_optimization))
if (platform.androidIsXiaomiDevice() && (mode == NotificationsMode.PERIODIC || mode == NotificationsMode.SERVICE)) {
Text(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization),
Modifier.padding(top = 8.dp)
showingIgnoreNotification = true
AlertManager.shared.showAlert {
val ignoreOptimization = {
AlertManager.shared.hideAlert()
showingIgnoreNotification = false
askAboutIgnoringBatteryOptimization()
}
val disableNotifications = {
AlertManager.shared.hideAlert()
showingIgnoreNotification = false
disableNotifications(mode, showOffAlert)
}
AlertDialog(
onDismissRequest = disableNotifications,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
fontWeight = FontWeight.Bold
)
}
}
},
dismissButton = {
TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) }
},
confirmButton = {
TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) }
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
},
text = {
Column {
Text(
if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc),
Modifier.padding(bottom = 8.dp)
)
Text(annotatedStringResource(MR.strings.turn_off_battery_optimization))
if (platform.androidIsXiaomiDevice() && (mode == NotificationsMode.PERIODIC || mode == NotificationsMode.SERVICE)) {
Text(
annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization),
Modifier.padding(top = 8.dp)
)
}
}
},
dismissButton = {
TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) }
},
confirmButton = {
TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) }
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
}
private fun showBGServiceNoticeSystemRestricted(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert {
@@ -681,6 +694,7 @@ class SimplexService: Service() {
}
ChatController.appPrefs.notificationsMode.set(NotificationsMode.OFF)
StartReceiver.toggleReceiver(false)
androidAppContext.getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
MessagesFetcherWorker.cancelAll()
safeStopService()
}
@@ -87,6 +87,9 @@ kotlin {
implementation("io.coil-kt:coil-compose:2.6.0")
implementation("io.coil-kt:coil-gif:2.6.0")
// Emojis
implementation("androidx.emoji2:emoji2-emojipicker:1.4.0")
implementation("com.jakewharton:process-phoenix:3.0.0")
val cameraXVersion = "1.3.4"
@@ -1,10 +1,11 @@
package chat.simplex.common.platform
import android.util.Log
import chat.simplex.common.model.ChatController.appPrefs
actual object Log {
actual fun d(tag: String, text: String) = Log.d(tag, text).run{}
actual fun e(tag: String, text: String) = Log.e(tag, text).run{}
actual fun i(tag: String, text: String) = Log.i(tag, text).run{}
actual fun w(tag: String, text: String) = Log.w(tag, text).run{}
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) Log.d(tag, text) }
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) Log.e(tag, text) }
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) Log.i(tag, text) }
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) Log.w(tag, text) }
}
@@ -3,19 +3,30 @@ package chat.simplex.common.platform
import android.Manifest
import android.content.*
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler
import androidx.core.graphics.drawable.toBitmap
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import java.io.BufferedOutputStream
import java.io.File
import chat.simplex.res.MR
import java.net.URI
import kotlin.math.min
data class OpenDefaultApp(
val name: String,
val icon: ImageBitmap,
val isSystemChooser: Boolean
)
actual fun ClipboardManager.shareText(text: String) {
var text = text
for (i in 10 downTo 1) {
@@ -37,7 +48,7 @@ actual fun ClipboardManager.shareText(text: String) {
}
}
fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean, useChooser: Boolean = true) {
val uri = if (fileSource.cryptoArgs != null) {
val tmpFile = File(tmpDir, fileSource.filePath)
tmpFile.deleteOnExit()
@@ -67,9 +78,35 @@ fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
type = mimeType
}
}
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
if (useChooser) {
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
} else {
sendIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(sendIntent)
}
}
fun queryDefaultAppForExtension(ext: String, encryptedFileUri: URI): OpenDefaultApp? {
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null
val openIntent = Intent(Intent.ACTION_VIEW)
openIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
openIntent.setDataAndType(encryptedFileUri.toUri(), mimeType)
val pm = androidAppContext.packageManager
//// This method returns the list of apps but no priority, nor default flag
// val resInfoList: List<ResolveInfo> = if (Build.VERSION.SDK_INT >= 33) {
// pm.queryIntentActivities(openIntent, PackageManager.ResolveInfoFlags.of((PackageManager.MATCH_DEFAULT_ONLY).toLong()))
// } else {
// pm.queryIntentActivities(openIntent, PackageManager.MATCH_DEFAULT_ONLY)
// }.sortedBy { it.priority }
// val first = resInfoList.firstOrNull { it.isDefault } ?: resInfoList.firstOrNull() ?: return null
val act = pm.resolveActivity(openIntent, PackageManager.MATCH_DEFAULT_ONLY) ?: return null
// Log.d(TAG, "Default launch action ${act} ${act.loadLabel(pm)} ${act.activityInfo?.name}")
val label = act.loadLabel(pm).toString()
val icon = act.loadIcon(pm).toBitmap().asImageBitmap()
val chooser = act.activityInfo?.name?.endsWith("ResolverActivity") == true
return OpenDefaultApp(label, icon, chooser)
}
actual fun shareFile(text: String, fileSource: CryptoFile) {
@@ -71,8 +71,12 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
}
override fun stop() {
am.unregisterAudioDeviceCallback(audioCallback)
am.removeOnCommunicationDeviceChangedListener(listener)
try {
am.unregisterAudioDeviceCallback(audioCallback)
am.removeOnCommunicationDeviceChangedListener(listener)
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
}
}
override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) {
@@ -6,12 +6,12 @@ import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.media.*
import android.os.Build
import android.os.PowerManager
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
import android.os.PowerManager.WakeLock
import android.view.View
import android.view.ViewGroup
import android.webkit.*
@@ -23,7 +23,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -47,7 +46,6 @@ import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import com.google.accompanist.permissions.*
import dev.icerock.moko.resources.StringResource
@@ -58,6 +56,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.datetime.Clock
import kotlinx.serialization.encodeToString
import java.io.Closeable
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
@SuppressLint("StaticFieldLeak")
@@ -72,49 +71,62 @@ fun activeCallDestroyWebView() = withApi {
Log.d(TAG, "CallView: webview was destroyed")
}
@SuppressLint("SourceLockedOrientationActivity")
@Composable
actual fun ActiveCallView() {
val call = remember { chatModel.activeCall }.value
val scope = rememberCoroutineScope()
val proximityLock = remember {
class ActiveCallState: Closeable {
val proximityLock: WakeLock? = screenOffWakeLock()
var wasConnected = false
val callAudioDeviceManager = CallAudioDeviceManagerInterface.new()
private var closed = false
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
callAudioDeviceManager.start()
}
}
override fun close() {
if (closed) return
closed = true
CallSoundsPlayer.stop()
if (wasConnected) {
CallSoundsPlayer.vibrate()
}
callAudioDeviceManager.stop()
dropAudioManagerOverrides()
if (proximityLock?.isHeld == true) {
proximityLock.release()
}
}
private fun screenOffWakeLock(): WakeLock? {
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
return if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
} else {
null
}
}
val wasConnected = rememberSaveable { mutableStateOf(false) }
}
@SuppressLint("SourceLockedOrientationActivity")
@Composable
actual fun ActiveCallView() {
val call = remember { chatModel.activeCall }.value
val callState = call?.androidCallState as ActiveCallState?
val scope = rememberCoroutineScope()
LaunchedEffect(call) {
if (call?.callState == CallState.Connected && !wasConnected.value) {
if (call?.callState == CallState.Connected && callState != null && !callState.wasConnected) {
CallSoundsPlayer.vibrate(2)
wasConnected.value = true
callState.wasConnected = true
}
}
val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() }
DisposableEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
callAudioDeviceManager.start()
}
onDispose {
CallSoundsPlayer.stop()
if (wasConnected.value) {
CallSoundsPlayer.vibrate()
}
callAudioDeviceManager.stop()
dropAudioManagerOverrides()
if (proximityLock?.isHeld == true) {
proximityLock.release()
}
}
}
LaunchedEffect(chatModel.activeCallViewIsCollapsed.value) {
LaunchedEffect(callState, chatModel.activeCallViewIsCollapsed.value) {
callState ?: return@LaunchedEffect
if (chatModel.activeCallViewIsCollapsed.value) {
if (proximityLock?.isHeld == true) proximityLock.release()
if (callState.proximityLock?.isHeld == true) callState.proximityLock.release()
} else {
delay(1000)
if (proximityLock?.isHeld == false) proximityLock.acquire()
if (callState.proximityLock?.isHeld == false) callState.proximityLock.acquire()
}
}
Box(Modifier.fillMaxSize()) {
@@ -122,6 +134,7 @@ actual fun ActiveCallView() {
Log.d(TAG, "received from WebRTCView: $apiMsg")
val call = chatModel.activeCall.value
if (call != null) {
val callState = call.androidCallState as ActiveCallState
Log.d(TAG, "has active call $call")
val callRh = call.remoteHostId
when (val r = apiMsg.resp) {
@@ -131,9 +144,9 @@ actual fun ActiveCallView() {
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Starting is delayed to make Android <= 11 working good with Bluetooth
callAudioDeviceManager.start()
callState.callAudioDeviceManager.start()
} else {
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
CallSoundsPlayer.startConnectingCallSound(scope)
activeCallWaitDeliveryReceipt(scope)
@@ -143,9 +156,9 @@ actual fun ActiveCallView() {
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Starting is delayed to make Android <= 11 working good with Bluetooth
callAudioDeviceManager.start()
callState.callAudioDeviceManager.start()
} else {
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
}
is WCallResponse.Answer -> withBGApi {
@@ -228,14 +241,14 @@ actual fun ActiveCallView() {
!chatModel.activeCallViewIsCollapsed.value -> true
else -> false
}
if (call != null && showOverlay) {
ActiveCallOverlay(call, chatModel, callAudioDeviceManager)
if (call != null && showOverlay && callState != null) {
ActiveCallOverlay(call, chatModel, callState.callAudioDeviceManager)
}
}
KeyChangeEffect(call?.localMediaSources?.hasVideo) {
if (call != null && call.hasVideo && callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
KeyChangeEffect(callState, call?.localMediaSources?.hasVideo) {
if (call != null && call.hasVideo && callState != null && callState.callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
// enabling speaker on user action (peer action ignored) and not disabling it again
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
}
val context = LocalContext.current
@@ -243,16 +256,12 @@ actual fun ActiveCallView() {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
val prevVolumeControlStream = activity.volumeControlStream
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
// Lock orientation to portrait in order to have good experience with calls
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
chatModel.activeCallViewIsVisible.value = true
// After the first call, End command gets added to the list which prevents making another calls
chatModel.callCommand.removeAll { it is WCallCommand.End }
keepScreenOn(true)
onDispose {
activity.volumeControlStream = prevVolumeControlStream
// Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
chatModel.activeCallViewIsVisible.value = false
chatModel.callCommand.clear()
keepScreenOn(false)
@@ -264,8 +273,8 @@ actual fun ActiveCallView() {
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) {
ActiveCallOverlayLayout(
call = call,
devices = remember { callAudioDeviceManager.devices }.value,
currentDevice = remember { callAudioDeviceManager.currentDevice },
devices = remember(callAudioDeviceManager) { callAudioDeviceManager.devices }.value,
currentDevice = remember(callAudioDeviceManager) { callAudioDeviceManager.currentDevice },
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
@@ -832,7 +841,8 @@ fun PreviewActiveCallOverlayVideo() {
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
)
),
androidCallState = {}
),
devices = emptyList(),
currentDevice = remember { mutableStateOf(null) },
@@ -841,7 +851,7 @@ fun PreviewActiveCallOverlayVideo() {
selectDevice = {},
toggleVideo = {},
toggleSound = {},
flipCamera = {}
flipCamera = {},
)
}
}
@@ -862,7 +872,8 @@ fun PreviewActiveCallOverlayAudio() {
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
)
),
androidCallState = {}
),
devices = emptyList(),
currentDevice = remember { mutableStateOf(null) },
@@ -0,0 +1,57 @@
package chat.simplex.common.views.chat.item
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import chat.simplex.common.model.CryptoFile
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.DefaultDropdownMenu
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@Composable
actual fun SaveOrOpenFileMenu(
showMenu: MutableState<Boolean>,
encrypted: Boolean,
ext: String?,
encryptedUri: URI,
fileSource: CryptoFile,
saveFile: () -> Unit
) {
val defaultApp = remember(encryptedUri.toString()) { if (ext != null) queryDefaultAppForExtension(ext, encryptedUri) else null }
DefaultDropdownMenu(showMenu) {
if (defaultApp != null) {
if (!defaultApp.isSystemChooser) {
ItemAction(
stringResource(MR.strings.open_with_app).format(defaultApp.name),
defaultApp.icon,
textColor = MaterialTheme.colors.primary,
onClick = {
openOrShareFile("", fileSource, justOpen = true, useChooser = false)
showMenu.value = false
}
)
} else {
ItemAction(
stringResource(MR.strings.open_with_app).format(""),
painterResource(MR.images.ic_open_in_new),
color = MaterialTheme.colors.primary,
onClick = {
openOrShareFile("", fileSource, justOpen = true, useChooser = false)
showMenu.value = false
}
)
}
}
ItemAction(
stringResource(MR.strings.save_verb),
painterResource(if (encrypted) MR.images.ic_lock_open_right else MR.images.ic_download),
color = MaterialTheme.colors.primary,
onClick = {
saveFile()
showMenu.value = false
}
)
}
}
@@ -0,0 +1,81 @@
package chat.simplex.common.views.chatlist
import SectionItemView
import android.view.ViewGroup
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.emoji2.emojipicker.EmojiPickerView
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable
actual fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>) {
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
Box(Modifier
.clip(shape = CircleShape)
.clickable {
ModalManager.start.showModalCloseable { close ->
EmojiPicker(close = {
close()
emoji.value = it
})
}
}
.padding(4.dp)
) {
val emojiValue = emoji.value
if (emojiValue != null) {
Text(emojiValue)
} else {
Icon(
painter = painterResource(MR.images.ic_add_reaction),
contentDescription = null,
tint = MaterialTheme.colors.secondary
)
}
}
Spacer(Modifier.width(8.dp))
TagListNameTextField(name, showError = showError)
}
}
@Composable
private fun EmojiPicker(close: (String?) -> Unit) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val topPaddingToContent = topPaddingToContent(false)
Column (
modifier = Modifier.fillMaxSize().navigationBarsPadding().padding(
start = DEFAULT_PADDING_HALF,
end = DEFAULT_PADDING_HALF,
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
),
) {
AndroidView(
factory = { context ->
EmojiPickerView(context).apply {
emojiGridColumns = 10
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
setOnEmojiPickedListener { pickedEmoji ->
close(pickedEmoji.emoji)
}
}
}
)
}
}
@@ -4,19 +4,31 @@ import android.Manifest
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import chat.simplex.common.platform.ntfManager
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import com.google.accompanist.permissions.*
@Composable
actual fun SetNotificationsModeAdditions() {
if (Build.VERSION.SDK_INT >= 33) {
val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
LaunchedEffect(notificationsPermissionState.status == PermissionStatus.Granted) {
if (notificationsPermissionState.status == PermissionStatus.Granted) {
ntfManager.androidCreateNtfChannelsMaybeShowAlert()
val canAsk = appPrefs.canAskToEnableNotifications.get()
if (notificationsPermissionState.status is PermissionStatus.Denied) {
if (notificationsPermissionState.status.shouldShowRationale || !canAsk) {
if (canAsk) {
appPrefs.canAskToEnableNotifications.set(false)
}
Log.w(TAG, "Notifications are disabled and nobody will ask to enable them")
} else {
notificationsPermissionState.launchPermissionRequest()
}
} else {
notificationsPermissionState.launchPermissionRequest()
if (!canAsk) {
// the user allowed notifications in system alert or manually in settings, allow to ask him next time if needed
appPrefs.canAskToEnableNotifications.set(true)
}
ntfManager.androidCreateNtfChannelsMaybeShowAlert()
}
}
} else {
@@ -13,6 +13,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrationToDeviceState
import chat.simplex.common.views.migration.MigrationToState
@@ -81,6 +82,12 @@ object ChatModel {
val groupMembers = mutableStateListOf<GroupMember>()
val groupMembersIndexes = mutableStateMapOf<Long, Int>()
// Chat Tags
val userTags = mutableStateOf(emptyList<ChatTag>())
val activeChatTagFilter = mutableStateOf<ActiveFilter?>(null)
val presetTags = mutableStateMapOf<PresetTagKind, Int>()
val unreadTags = mutableStateMapOf<Long, Int>()
// false: default placement, true: floating window.
// Used for deciding to add terminal items on main thread or not. Floating means appPrefs.terminalAlwaysVisible
var terminalsVisible = setOf<Boolean>()
@@ -196,6 +203,116 @@ object ChatModel {
}
}
fun updateChatTags(rhId: Long?) {
val newPresetTags = mutableMapOf<PresetTagKind, Int>()
val newUnreadTags = mutableMapOf<Long, Int>()
for (chat in chats.value.filter { it.remoteHostId == rhId }) {
for (tag in PresetTagKind.entries) {
if (presetTagMatchesChat(tag, chat.chatInfo)) {
newPresetTags[tag] = (newPresetTags[tag] ?: 0) + 1
}
}
if (chat.unreadTag) {
val chatTags: List<Long> = when (val cInfo = chat.chatInfo) {
is ChatInfo.Direct -> cInfo.contact.chatTags
is ChatInfo.Group -> cInfo.groupInfo.chatTags
else -> emptyList()
}
chatTags.forEach { tag ->
newUnreadTags[tag] = (newUnreadTags[tag] ?: 0) + 1
}
}
}
if (activeChatTagFilter.value is ActiveFilter.PresetTag &&
(newPresetTags[(activeChatTagFilter.value as ActiveFilter.PresetTag).tag] ?: 0) == 0) {
activeChatTagFilter.value = null
}
presetTags.clear()
presetTags.putAll(newPresetTags)
unreadTags.clear()
unreadTags.putAll(newUnreadTags)
}
fun updateChatFavorite(favorite: Boolean, wasFavorite: Boolean) {
val count = presetTags[PresetTagKind.FAVORITES]
if (favorite && !wasFavorite) {
presetTags[PresetTagKind.FAVORITES] = (count ?: 0) + 1
} else if (!favorite && wasFavorite && count != null) {
presetTags[PresetTagKind.FAVORITES] = maxOf(0, count - 1)
if (activeChatTagFilter.value == ActiveFilter.PresetTag(PresetTagKind.FAVORITES) && (presetTags[PresetTagKind.FAVORITES] ?: 0) == 0) {
activeChatTagFilter.value = null
}
}
}
fun addPresetChatTags(chatInfo: ChatInfo) {
for (tag in PresetTagKind.entries) {
if (presetTagMatchesChat(tag, chatInfo)) {
presetTags[tag] = (presetTags[tag] ?: 0) + 1
}
}
}
fun removePresetChatTags(chatInfo: ChatInfo) {
for (tag in PresetTagKind.entries) {
if (presetTagMatchesChat(tag, chatInfo)) {
val count = presetTags[tag]
if (count != null) {
presetTags[tag] = maxOf(0, count - 1)
}
}
}
}
fun markChatTagRead(chat: Chat) {
if (chat.unreadTag) {
chat.chatInfo.chatTags?.let { tags ->
markChatTagRead_(chat, tags)
}
}
}
fun updateChatTagRead(chat: Chat, wasUnread: Boolean) {
val tags = chat.chatInfo.chatTags ?: return
val nowUnread = chat.unreadTag
if (nowUnread && !wasUnread) {
tags.forEach { tag ->
unreadTags[tag] = (unreadTags[tag] ?: 0) + 1
}
} else if (!nowUnread && wasUnread) {
markChatTagRead_(chat, tags)
}
}
fun moveChatTagUnread(chat: Chat, oldTags: List<Long>?, newTags: List<Long>) {
if (chat.unreadTag) {
oldTags?.forEach { t ->
val oldCount = unreadTags[t]
if (oldCount != null) {
unreadTags[t] = maxOf(0, oldCount - 1)
}
}
newTags.forEach { t ->
unreadTags[t] = (unreadTags[t] ?: 0) + 1
}
}
}
private fun markChatTagRead_(chat: Chat, tags: List<Long>) {
for (tag in tags) {
val count = unreadTags[tag]
if (count != null) {
unreadTags[tag] = maxOf(0, count - 1)
}
}
}
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
// TODO pass rhId?
@@ -280,6 +397,7 @@ object ChatModel {
updateChatInfo(rhId, cInfo)
} else if (addMissing) {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
addPresetChatTags(cInfo)
}
}
@@ -329,6 +447,7 @@ object ChatModel {
}
else -> cItem
}
val wasUnread = chat.unreadTag
chats[i] = chat.copy(
chatItems = arrayListOf(newPreviewItem),
chatStats =
@@ -339,6 +458,8 @@ object ChatModel {
else
chat.chatStats
)
updateChatTagRead(chats[i], wasUnread)
if (appPlatform.isDesktop && cItem.chatDir.sent) {
reorderChat(chats[i], 0)
} else {
@@ -455,6 +576,7 @@ object ChatModel {
if (i >= 0) {
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
markChatTagRead(chats[i])
}
// clear current chat
if (chatId.value == cInfo.id) {
@@ -522,11 +644,13 @@ object ChatModel {
val chat = chats[chatIdx]
val lastId = chat.chatItems.lastOrNull()?.id
if (lastId != null) {
val wasUnread = chat.unreadTag
val unreadCount = if (itemIds != null) chat.chatStats.unreadCount - markedRead else 0
decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
chats[chatIdx] = chat.copy(
chatStats = chat.chatStats.copy(unreadCount = unreadCount)
)
updateChatTagRead(chats[chatIdx], wasUnread)
}
}
}
@@ -537,16 +661,29 @@ object ChatModel {
val chat = chats[chatIndex]
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
val wasUnread = chat.unreadTag
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
chats[chatIndex] = chat.copy(
chatStats = chat.chatStats.copy(
unreadCount = unreadCount,
)
)
updateChatTagRead(chats[chatIndex], wasUnread)
}
fun removeChat(rhId: Long?, id: String) {
chats.removeAll { it.id == id && it.remoteHostId == rhId }
var removed: ChatInfo? = null
chats.removeAll {
val found = it.id == id && it.remoteHostId == rhId
if (found) {
removed = it.chatInfo
}
found
}
removed?.let {
removePresetChatTags(it)
}
}
suspend fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
@@ -977,6 +1114,8 @@ data class Chat(
else -> false
}
val unreadTag: Boolean get() = chatInfo.ntfsEnabled && (chatStats.unreadCount > 0 || chatStats.unreadChat)
val id: String get() = chatInfo.id
fun groupFeatureEnabled(feature: GroupFeature): Boolean =
@@ -1189,6 +1328,12 @@ sealed class ChatInfo: SomeChat, NamedChat {
else -> false
}
val chatTags: List<Long>?
get() = when (this) {
is Direct -> contact.chatTags
is Group -> groupInfo.chatTags
else -> null
}
}
@Serializable
@@ -1232,6 +1377,7 @@ data class Contact(
val chatTs: Instant?,
val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean,
val chatTags: List<Long>,
override val chatDeleted: Boolean,
val uiThemes: ThemeModeOverrides? = null,
): SomeChat, NamedChat {
@@ -1315,6 +1461,7 @@ data class Contact(
contactGrpInvSent = false,
chatDeleted = false,
uiThemes = null,
chatTags = emptyList()
)
}
}
@@ -1476,6 +1623,7 @@ data class GroupInfo (
override val updatedAt: Instant,
val chatTs: Instant?,
val uiThemes: ThemeModeOverrides? = null,
val chatTags: List<Long>
): SomeChat, NamedChat {
override val chatType get() = ChatType.Group
override val id get() = "#$groupId"
@@ -1520,6 +1668,7 @@ data class GroupInfo (
updatedAt = Clock.System.now(),
chatTs = Clock.System.now(),
uiThemes = null,
chatTags = emptyList()
)
}
}
@@ -1575,6 +1724,13 @@ data class GroupMember (
var activeConn: Connection? = null
): NamedChat {
val id: String get() = "#$groupId @$groupMemberId"
val ready get() = activeConn?.connStatus == ConnStatus.Ready
val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady
val sendMsgEnabled get() =
sndReady
&& memberCurrent
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false)
&& !(activeConn?.connDisabled ?: true)
override val displayName: String
get() {
val p = memberProfile
@@ -3843,6 +3999,13 @@ sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
}
}
@Serializable
data class ChatTag(
val chatTagId: Long,
val chatTagText: String,
val chatTagEmoji: String?
)
@Serializable
class ChatItemInfo(
val itemVersions: List<ChatItemVersion>,
@@ -80,6 +80,7 @@ class AppPreferences {
if (!runServiceInBackground.get()) NotificationsMode.OFF else NotificationsMode.default
) { NotificationsMode.values().firstOrNull { it.name == this } }
val notificationPreviewMode = mkStrPreference(SHARED_PREFS_NOTIFICATION_PREVIEW_MODE, NotificationPreviewMode.default.name)
val canAskToEnableNotifications = mkBoolPreference(SHARED_PREFS_CAN_ASK_TO_ENABLE_NOTIFICATIONS, true)
val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
val autoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
@@ -132,6 +133,7 @@ class AppPreferences {
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val logLevel = mkEnumPreference(SHARED_PREFS_LOG_LEVEL, LogLevel.WARNING) { LogLevel.entries.firstOrNull { it.name == this } }
val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
@@ -357,6 +359,7 @@ class AppPreferences {
private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
private const val SHARED_PREFS_NOTIFICATIONS_MODE = "NotificationsMode"
private const val SHARED_PREFS_NOTIFICATION_PREVIEW_MODE = "NotificationPreviewMode"
private const val SHARED_PREFS_CAN_ASK_TO_ENABLE_NOTIFICATIONS = "CanAskToEnableNotifications"
private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown"
private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown"
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
@@ -393,6 +396,7 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_LOG_LEVEL = "LogLevel"
private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors"
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
@@ -622,6 +626,9 @@ object ChatController {
val chats = apiGetChats(rhId)
updateChats(chats)
}
chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList()
chatModel.activeChatTagFilter.value = null
chatModel.updateChatTags(rhId)
}
private fun startReceiver() {
@@ -877,6 +884,16 @@ object ChatController {
return emptyList()
}
private suspend fun apiGetChatTags(rh: Long?): List<ChatTag>?{
val userId = currentUserId("apiGetChatTags")
val r = sendCmd(rh, CC.ApiGetChatTags(userId))
if (r is CR.ChatTags) return r.userTags
Log.e(TAG, "apiGetChatTags bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_loading_chat_tags), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
if (r is CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo
@@ -889,6 +906,28 @@ object ChatController {
return null
}
suspend fun apiCreateChatTag(rh: Long?, tag: ChatTagData): List<ChatTag>? {
val r = sendCmd(rh, CC.ApiCreateChatTag(tag))
if (r is CR.ChatTags) return r.userTags
Log.e(TAG, "apiCreateChatTag bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_creating_chat_tags), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiSetChatTags(rh: Long?, type: ChatType, id: Long, tagIds: List<Long>): Pair<List<ChatTag>, List<Long>>? {
val r = sendCmd(rh, CC.ApiSetChatTags(type, id, tagIds))
if (r is CR.TagsUpdated) return r.userTags to r.chatTags
Log.e(TAG, "apiSetChatTags bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_updating_chat_tags), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiDeleteChatTag(rh: Long?, tagId: Long) = sendCommandOkResp(rh, CC.ApiDeleteChatTag(tagId))
suspend fun apiUpdateChatTag(rh: Long?, tagId: Long, tag: ChatTagData) = sendCommandOkResp(rh, CC.ApiUpdateChatTag(tagId, tag))
suspend fun apiReorderChatTags(rh: Long?, tagIds: List<Long>) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds))
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages)
return processSendMessageCmd(rh, cmd)
@@ -966,6 +1005,7 @@ object ChatController {
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, mc, live))
when {
r is CR.ChatItemUpdated -> return r.chatItem
r is CR.ChatItemNotChanged -> return r.chatItem
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.LargeMsg -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.maximum_message_size_title),
@@ -3149,10 +3189,16 @@ sealed class CC {
class TestStorageEncryption(val key: String): CC()
class ApiSaveSettings(val settings: AppSettings): CC()
class ApiGetSettings(val settings: AppSettings): CC()
class ApiGetChatTags(val userId: Long): CC()
class ApiGetChats(val userId: Long): CC()
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
class ApiCreateChatTag(val tag: ChatTagData): CC()
class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List<Long>): CC()
class ApiDeleteChatTag(val tagId: Long): CC()
class ApiUpdateChatTag(val tagId: Long, val tagData: ChatTagData): CC()
class ApiReorderChatTags(val tagIds: List<Long>): CC()
class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List<ComposedMessage>): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
@@ -3304,6 +3350,7 @@ sealed class CC {
is TestStorageEncryption -> "/db test key $key"
is ApiSaveSettings -> "/_save app settings ${json.encodeToString(settings)}"
is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}"
is ApiGetChatTags -> "/_get tags $userId"
is ApiGetChats -> "/_get chats $userId pcc=on"
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
@@ -3312,6 +3359,11 @@ sealed class CC {
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
}
is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}"
is ApiSetChatTags -> "/_tags ${chatRef(type, id)} ${tagIds.joinToString(",")}"
is ApiDeleteChatTag -> "/_delete tag $tagId"
is ApiUpdateChatTag -> "/_update tag $tagId ${json.encodeToString(tagData)}"
is ApiReorderChatTags -> "/_reorder tags ${tagIds.joinToString(",")}"
is ApiCreateChatItems -> {
val msgs = json.encodeToString(composedMessages)
"/_create *$noteFolderId json $msgs"
@@ -3468,10 +3520,16 @@ sealed class CC {
is TestStorageEncryption -> "testStorageEncryption"
is ApiSaveSettings -> "apiSaveSettings"
is ApiGetSettings -> "apiGetSettings"
is ApiGetChatTags -> "apiGetChatTags"
is ApiGetChats -> "apiGetChats"
is ApiGetChat -> "apiGetChat"
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
is ApiSendMessages -> "apiSendMessages"
is ApiCreateChatTag -> "apiCreateChatTag"
is ApiSetChatTags -> "apiSetChatTags"
is ApiDeleteChatTag -> "apiDeleteChatTag"
is ApiUpdateChatTag -> "apiUpdateChatTag"
is ApiReorderChatTags -> "apiReorderChatTags"
is ApiCreateChatItems -> "apiCreateChatItems"
is ApiUpdateChatItem -> "apiUpdateChatItem"
is ApiDeleteChatItem -> "apiDeleteChatItem"
@@ -3654,6 +3712,9 @@ sealed class ChatPagination {
@Serializable
class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent)
@Serializable
class ChatTagData(val emoji: String?, val text: String)
@Serializable
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
@@ -3754,7 +3815,7 @@ data class ServerOperatorConditionsDetail(
@Serializable()
sealed class ConditionsAcceptance {
@Serializable @SerialName("accepted") data class Accepted(val acceptedAt: Instant?) : ConditionsAcceptance()
@Serializable @SerialName("accepted") data class Accepted(val acceptedAt: Instant?, val autoAccepted: Boolean) : ConditionsAcceptance()
@Serializable @SerialName("required") data class Required(val deadline: Instant?) : ConditionsAcceptance()
val conditionsAccepted: Boolean
@@ -3798,7 +3859,7 @@ data class ServerOperator(
tradeName = "SimpleX Chat",
legalName = "SimpleX Chat Ltd",
serverDomains = listOf("simplex.im"),
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null),
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false),
enabled = true,
smpRoles = ServerRoles(storage = true, proxy = true),
xftpRoles = ServerRoles(storage = true, proxy = true)
@@ -3880,7 +3941,7 @@ data class UserOperatorServers(
tradeName = "",
legalName = null,
serverDomains = emptyList(),
conditionsAcceptance = ConditionsAcceptance.Accepted(null),
conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false),
enabled = false,
smpRoles = ServerRoles(storage = true, proxy = true),
xftpRoles = ServerRoles(storage = true, proxy = true)
@@ -5387,6 +5448,7 @@ sealed class CR {
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
@Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR()
@Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR()
@Serializable @SerialName("chatTags") class ChatTags(val user: UserRef, val userTags: List<ChatTag>): CR()
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
@Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR()
@@ -5413,6 +5475,7 @@ sealed class CR {
@Serializable @SerialName("contactCode") class ContactCode(val user: UserRef, val contact: Contact, val connectionCode: String): CR()
@Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR()
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
@Serializable @SerialName("tagsUpdated") class TagsUpdated(val user: UserRef, val userTags: List<ChatTag>, val chatTags: List<Long>): CR()
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
@Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR()
@@ -5571,6 +5634,7 @@ sealed class CR {
is ChatStopped -> "chatStopped"
is ApiChats -> "apiChats"
is ApiChat -> "apiChat"
is ChatTags -> "chatTags"
is ApiChatItemInfo -> "chatItemInfo"
is ServerTestResult -> "serverTestResult"
is ServerOperatorConditions -> "serverOperatorConditions"
@@ -5597,6 +5661,7 @@ sealed class CR {
is ContactCode -> "contactCode"
is GroupMemberCode -> "groupMemberCode"
is ConnectionVerified -> "connectionVerified"
is TagsUpdated -> "tagsUpdated"
is Invitation -> "invitation"
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
is ConnectionUserChanged -> "ConnectionUserChanged"
@@ -5745,6 +5810,7 @@ sealed class CR {
is ChatStopped -> noDetails()
is ApiChats -> withUser(user, json.encodeToString(chats))
is ApiChat -> withUser(user, "chat: ${json.encodeToString(chat)}\nnavInfo: ${navInfo}")
is ChatTags -> withUser(user, "userTags: ${json.encodeToString(userTags)}")
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
is ServerOperatorConditions -> "conditions: ${json.encodeToString(conditions)}"
@@ -5771,6 +5837,7 @@ sealed class CR {
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
is TagsUpdated -> withUser(user, "userTags: ${json.encodeToString(userTags)}\nchatTags: ${json.encodeToString(chatTags)}")
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection")
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" )
@@ -6030,6 +6097,9 @@ class ConnectionStats(
val ratchetSyncSendProhibited: Boolean get() =
listOf(RatchetSyncState.Required, RatchetSyncState.Started, RatchetSyncState.Agreed).contains(ratchetSyncState)
val ratchetSyncInProgress: Boolean get() =
listOf(RatchetSyncState.Started, RatchetSyncState.Agreed).contains(ratchetSyncState)
}
@Serializable
@@ -2,6 +2,10 @@ package chat.simplex.common.platform
const val TAG = "SIMPLEX"
enum class LogLevel {
DEBUG, INFO, WARNING, ERROR
}
expect object Log {
fun d(tag: String, text: String)
fun e(tag: String, text: String)
@@ -10,6 +10,7 @@ import chat.simplex.common.model.ChatId
import chat.simplex.common.model.NotificationsMode
import chat.simplex.common.ui.theme.CurrentColors
import kotlinx.coroutines.Job
import java.io.Closeable
interface PlatformInterface {
suspend fun androidServiceStart() {}
@@ -26,6 +27,7 @@ interface PlatformInterface {
fun androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {}
fun androidRestartNetworkObserver() {}
fun androidCreateActiveCallState(): Closeable = Closeable { }
fun androidIsXiaomiDevice(): Boolean = false
val androidApiLevel: Int? get() = null
@Composable fun androidLockPortraitOrientation() {}
@@ -43,6 +43,7 @@ class CallManager(val chatModel: ChatModel) {
private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) {
with (chatModel) {
activeCall.value?.androidCallState?.close()
activeCall.value = Call(
remoteHostId = invitation.remoteHostId,
userProfile = userProfile,
@@ -51,6 +52,7 @@ class CallManager(val chatModel: ChatModel) {
callState = CallState.InvitationAccepted,
initialCallType = invitation.callType.media,
sharedKey = invitation.sharedKey,
androidCallState = platform.androidCreateActiveCallState()
)
showCallView.value = true
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
@@ -78,6 +80,7 @@ class CallManager(val chatModel: ChatModel) {
// Don't destroy WebView if you plan to accept next call right after this one
if (!switchingCall.value) {
showCallView.value = false
activeCall.value?.androidCallState?.close()
activeCall.value = null
activeCallViewIsCollapsed.value = false
platform.androidCallEnded()
@@ -7,6 +7,7 @@ import chat.simplex.res.MR
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.Closeable
import java.net.URI
import kotlin.collections.ArrayList
@@ -27,7 +28,9 @@ data class Call(
// When a user has audio call, and then he wants to enable camera but didn't grant permissions for using camera yet,
// we show permissions view without enabling camera before permissions are granted. After they are granted, enabling camera
val wantsToEnableCamera: Boolean = false
val wantsToEnableCamera: Boolean = false,
val androidCallState: Closeable
) {
val encrypted: Boolean get() = localEncrypted && sharedKey != null
private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
@@ -131,26 +131,14 @@ fun ChatInfoView(
},
syncContactConnection = {
withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
connStats.value = cStats
if (cStats != null) {
withChats {
updateContactConnectionStats(chatRh, contact, cStats)
}
}
syncContactConnection(chatRh, contact, connStats, force = false)
close.invoke()
}
},
syncContactConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true)
connStats.value = cStats
if (cStats != null) {
withChats {
updateContactConnectionStats(chatRh, contact, cStats)
}
}
syncContactConnection(chatRh, contact, connStats, force = true)
close.invoke()
}
})
@@ -189,6 +177,16 @@ fun ChatInfoView(
}
}
suspend fun syncContactConnection(rhId: Long?, contact: Contact, connectionStats: MutableState<ConnectionStats?>, force: Boolean) {
val cStats = chatModel.controller.apiSyncContactRatchet(rhId, contact.contactId, force = force)
connectionStats.value = cStats
if (cStats != null) {
withChats {
updateContactConnectionStats(rhId, contact, cStats)
}
}
}
sealed class SendReceipts {
object Yes: SendReceipts()
object No: SendReceipts()
@@ -505,7 +503,7 @@ fun ChatInfoLayout(
currentUser: User,
sendReceipts: State<SendReceipts>,
setSendReceipts: (SendReceipts) -> Unit,
connStats: State<ConnectionStats?>,
connStats: MutableState<ConnectionStats?>,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
@@ -553,8 +551,8 @@ fun ChatInfoLayout(
verticalAlignment = Alignment.CenterVertically
) {
SearchButton(modifier = Modifier.fillMaxWidth(0.25f), chat, contact, close, onSearchClicked)
AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact)
VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact)
AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact, connStats)
VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, connStats)
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, contact)
}
}
@@ -699,13 +697,19 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary)
}
)
val clipboard = LocalClipboardManager.current
val copyNameToClipboard = {
clipboard.setText(AnnotatedString(contact.profile.displayName))
showToast(generalGetString(MR.strings.copied))
}
Text(
text,
inlineContent = inlineContent,
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
textAlign = TextAlign.Center,
maxLines = 3,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
)
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
Text(
@@ -713,7 +717,8 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
color = MaterialTheme.colors.onBackground,
textAlign = TextAlign.Center,
maxLines = 4,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
)
}
}
@@ -825,12 +830,14 @@ fun MuteButton(
fun AudioCallButton(
modifier: Modifier,
chat: Chat,
contact: Contact
contact: Contact,
connectionStats: MutableState<ConnectionStats?>
) {
CallButton(
modifier = modifier,
chat,
contact,
connectionStats,
icon = painterResource(MR.images.ic_call),
title = generalGetString(MR.strings.info_view_call_button),
mediaType = CallMediaType.Audio
@@ -841,12 +848,14 @@ fun AudioCallButton(
fun VideoButton(
modifier: Modifier,
chat: Chat,
contact: Contact
contact: Contact,
connectionStats: MutableState<ConnectionStats?>
) {
CallButton(
modifier = modifier,
chat,
contact,
connectionStats,
icon = painterResource(MR.images.ic_videocam),
title = generalGetString(MR.strings.info_view_video_button),
mediaType = CallMediaType.Video
@@ -858,6 +867,7 @@ fun CallButton(
modifier: Modifier,
chat: Chat,
contact: Contact,
connectionStats: MutableState<ConnectionStats?>,
icon: Painter,
title: String,
mediaType: CallMediaType
@@ -879,7 +889,23 @@ fun CallButton(
disabledLook = !canCall,
onClick =
when {
canCall -> { { startChatCall(chat.remoteHostId, chat.chatInfo, mediaType) } }
canCall -> { {
val connStats = connectionStats.value
if (connStats != null) {
if (connStats.ratchetSyncState == RatchetSyncState.Ok) {
startChatCall(chat.remoteHostId, chat.chatInfo, mediaType)
} else if (connStats.ratchetSyncAllowed) {
showFixConnectionAlert(syncConnection = {
withBGApi { syncContactConnection(chat.remoteHostId, contact, connectionStats, force = false) }
})
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.cant_call_contact_alert_title),
generalGetString(MR.strings.encryption_renegotiation_in_progress)
)
}
}
} }
contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } }
!contact.active -> { { showCantCallContactDeletedAlert() } }
!contact.ready -> { { showCantCallContactConnectingAlert() } }
@@ -1265,6 +1291,15 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
)
}
fun showFixConnectionAlert(syncConnection: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.sync_connection_question),
text = generalGetString(MR.strings.sync_connection_desc),
confirmText = generalGetString(MR.strings.sync_connection_confirm),
onConfirm = syncConnection,
)
}
fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String {
val (rcvMsgInfo, qInfo) = info
val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none)
@@ -296,6 +296,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -309,6 +310,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
QuotedMsgView(qi)
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -324,6 +326,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
ForwardedFromView(forwardedFromItem)
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -395,6 +398,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -433,12 +437,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
Column {
if (numTabs() > 1) {
Column(
Box(
Modifier
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
.fillMaxHeight()
) {
Column(Modifier.weight(1f)) {
Column {
when (val sel = selection.value) {
is CIInfoTab.Delivery -> {
DeliveryTab(sel.memberDeliveryStatuses)
@@ -479,7 +482,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
Box(Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
TabRow(
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
Modifier.height(AppBarHeight * fontSizeSqrtMultiplier),
@@ -29,7 +29,9 @@ import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.CIDirection.GroupRcv
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.activeCall
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.markChatTagRead
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
@@ -573,7 +575,8 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
if (chatInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile)
activeCall.value?.androidCallState?.close()
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile, androidCallState = platform.androidCreateActiveCallState())
chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
@@ -663,13 +666,18 @@ fun ChatLayout(
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
if (chatInfo != null) {
Box(Modifier.fillMaxSize()) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
// disables scrolling to top of chat item on click inside the bubble
CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec {
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
}) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
}
}
}
Box(
@@ -937,6 +945,7 @@ fun BoxScope.ChatItemsList(
linkMode: SimplexLinkMode,
selectedChatItems: MutableState<Set<Long>?>,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit,
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit,
@@ -982,6 +991,7 @@ fun BoxScope.ChatItemsList(
})
val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
val loadingMoreItems = remember { mutableStateOf(false) }
val animatedScrollingInProgress = remember { mutableStateOf(false) }
val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf<Long>() }
if (!loadingMoreItems.value) {
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
@@ -1002,7 +1012,7 @@ fun BoxScope.ChatItemsList(
val chatInfoUpdated = rememberUpdatedState(chatInfo)
val highlightedItems = remember { mutableStateOf(setOf<Long>()) }
val scope = rememberCoroutineScope()
val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) }
val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) }
val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) }
LoadLastItems(loadingMoreItems, remoteHostId, chatInfo)
@@ -1063,7 +1073,7 @@ fun BoxScope.ChatItemsList(
highlightedItems.value = setOf()
}
}
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
}
}
@@ -1312,7 +1322,7 @@ fun BoxScope.ChatItemsList(
}
}
}
FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState)
FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState)
FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(true)).align(Alignment.TopCenter), mergedItems, listState)
LaunchedEffect(Unit) {
@@ -1321,6 +1331,15 @@ fun BoxScope.ChatItemsList(
chatViewScrollState.value = it
}
}
LaunchedEffect(Unit) {
snapshotFlow { listState.value.isScrollInProgress }
.filter { !it }
.collect {
if (animatedScrollingInProgress.value) {
animatedScrollingInProgress.value = false
}
}
}
}
@Composable
@@ -1398,6 +1417,7 @@ private fun NotifyChatListOnFinishingComposition(
@Composable
fun BoxScope.FloatingButtons(
loadingMoreItems: MutableState<Boolean>,
animatedScrollingInProgress: MutableState<Boolean>,
mergedItems: State<MergedItems>,
unreadCount: State<Int>,
maxHeight: State<Int>,
@@ -1437,8 +1457,14 @@ fun BoxScope.FloatingButtons(
bottomUnreadCount,
showBottomButtonWithCounter,
showBottomButtonWithArrow,
animatedScrollingInProgress,
composeViewHeight,
onClick = { scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } } }
onClick = {
scope.launch {
animatedScrollingInProgress.value = true
tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) }
}
}
)
// Don't show top FAB if is in search
if (searchValue.value.isNotEmpty()) return
@@ -1449,11 +1475,15 @@ fun BoxScope.FloatingButtons(
TopEndFloatingButton(
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(true)).align(Alignment.TopEnd),
topUnreadCount,
animatedScrollingInProgress,
onClick = {
val index = mergedItems.value.items.indexOfLast { it.hasUnread() }
if (index != -1) {
// scroll to the top unread item
scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) } }
scope.launch {
animatedScrollingInProgress.value = true
tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) }
}
}
},
onLongClick = { showDropDown.value = true }
@@ -1593,10 +1623,11 @@ fun MemberImage(member: GroupMember) {
private fun TopEndFloatingButton(
modifier: Modifier = Modifier,
unreadCount: State<Int>,
animatedScrollingInProgress: State<Boolean>,
onClick: () -> Unit,
onLongClick: () -> Unit
) {
if (unreadCount.value > 0) {
if (remember { derivedStateOf { unreadCount.value > 0 && !animatedScrollingInProgress.value } }.value) {
val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
FloatingActionButton(
{}, // no action here
@@ -1837,6 +1868,7 @@ private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State<Int>, de
private fun scrollToItem(
searchValue: State<String>,
loadingMoreItems: MutableState<Boolean>,
animatedScrollingInProgress: MutableState<Boolean>,
highlightedItems: MutableState<Set<Long>>,
chatInfo: State<ChatInfo>,
maxHeight: State<Int>,
@@ -1874,6 +1906,7 @@ private fun scrollToItem(
highlightedItems.value = setOf(itemId)
} else {
withContext(scope.coroutineContext) {
animatedScrollingInProgress.value = true
listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value)
highlightedItems.value = setOf(itemId)
}
@@ -1935,10 +1968,11 @@ private fun BoxScope.BottomEndFloatingButton(
unreadCount: State<Int>,
showButtonWithCounter: State<Boolean>,
showButtonWithArrow: State<Boolean>,
animatedScrollingInProgress: State<Boolean>,
composeViewHeight: State<Dp>,
onClick: () -> Unit
) = when {
showButtonWithCounter.value -> {
showButtonWithCounter.value && !animatedScrollingInProgress.value -> {
FloatingActionButton(
onClick = onClick,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
@@ -1952,7 +1986,7 @@ private fun BoxScope.BottomEndFloatingButton(
)
}
}
showButtonWithArrow.value -> {
showButtonWithArrow.value && !animatedScrollingInProgress.value -> {
FloatingActionButton(
onClick = onClick,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
@@ -2073,6 +2107,7 @@ private fun markUnreadChatAsRead(chatId: String) {
if (success) {
withChats {
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
markChatTagRead(chat)
}
}
}
@@ -9,6 +9,7 @@ import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material.*
@@ -17,6 +18,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -446,12 +449,18 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
horizontalAlignment = Alignment.CenterHorizontally
) {
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
val clipboard = LocalClipboardManager.current
val copyNameToClipboard = {
clipboard.setText(AnnotatedString(cInfo.displayName))
showToast(generalGetString(MR.strings.copied))
}
Text(
cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
textAlign = TextAlign.Center,
maxLines = 4,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
)
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
Text(
@@ -459,7 +468,8 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
color = MaterialTheme.colors.onBackground,
textAlign = TextAlign.Center,
maxLines = 8,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
)
}
}
@@ -8,8 +8,7 @@ import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview
import java.net.URI
import androidx.compose.foundation.*
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
@@ -58,6 +57,19 @@ fun GroupMemberInfoView(
val developerTools = chatModel.controller.appPrefs.developerTools.get()
var progressIndicator by remember { mutableStateOf(false) }
fun syncMemberConnection() {
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
connStats.value = r.second
withChats {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
close.invoke()
}
}
}
if (chat != null) {
val newRole = remember { mutableStateOf(member.memberRole) }
GroupMemberInfoLayout(
@@ -78,19 +90,35 @@ fun GroupMemberInfoView(
}
},
createMemberContact = {
withBGApi {
progressIndicator = true
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
if (memberContact != null) {
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
withChats {
addChat(memberChat)
openLoadedChat(memberChat)
if (member.sendMsgEnabled) {
withBGApi {
progressIndicator = true
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
if (memberContact != null) {
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
withChats {
addChat(memberChat)
openLoadedChat(memberChat)
}
closeAll()
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
}
closeAll()
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
progressIndicator = false
}
} else if (connectionStats != null) {
if (connectionStats.ratchetSyncAllowed) {
showFixConnectionAlert(syncConnection = { syncMemberConnection() })
} else if (connectionStats.ratchetSyncInProgress) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.cant_send_message_to_member_alert_title),
generalGetString(MR.strings.encryption_renegotiation_in_progress)
)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.cant_send_message_to_member_alert_title),
generalGetString(MR.strings.connection_not_ready)
)
}
progressIndicator = false
}
},
connectViaAddress = { connReqUri ->
@@ -149,16 +177,7 @@ fun GroupMemberInfoView(
})
},
syncMemberConnection = {
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
connStats.value = r.second
withChats {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
close.invoke()
}
}
syncMemberConnection()
},
syncMemberConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
@@ -335,14 +354,29 @@ fun GroupMemberInfoLayout(
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
if (knownChat != null) {
val (chat, contact) = knownChat
val knownContactConnectionStats: MutableState<ConnectionStats?> = remember { mutableStateOf(null) }
LaunchedEffect(contact.contactId) {
withBGApi {
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, chat.chatInfo.apiId)
if (contactInfo != null) {
knownContactConnectionStats.value = contactInfo.first
}
}
}
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contact.contactId) })
AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact)
VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact)
AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, knownContactConnectionStats)
VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact, knownContactConnectionStats)
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
if (contactId != null) {
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group
} else {
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { createMemberContact() })
OpenChatButton(
modifier = Modifier.fillMaxWidth(0.33f),
disabledLook = !(member.sendMsgEnabled || (member.activeConn?.connectionStats?.ratchetSyncAllowed ?: false)),
onClick = { createMemberContact() }
)
}
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
showSendMessageToEnableCallsAlert()
@@ -413,12 +447,12 @@ fun GroupMemberInfoLayout(
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || !member.sendMsgEnabled,
switchAddress = switchMemberAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || !member.sendMsgEnabled,
abortSwitchAddress = abortSwitchMemberAddress
)
}
@@ -504,13 +538,19 @@ fun GroupMemberInfoHeader(member: GroupMember) {
Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary)
}
)
val clipboard = LocalClipboardManager.current
val copyNameToClipboard = {
clipboard.setText(AnnotatedString(member.displayName))
showToast(generalGetString(MR.strings.copied))
}
Text(
text,
inlineContent = inlineContent,
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
textAlign = TextAlign.Center,
maxLines = 3,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
)
if (member.fullName != "" && member.fullName != member.displayName) {
Text(
@@ -518,7 +558,8 @@ fun GroupMemberInfoHeader(member: GroupMember) {
color = MaterialTheme.colors.onBackground,
textAlign = TextAlign.Center,
maxLines = 4,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
)
}
}
@@ -578,6 +619,7 @@ fun RemoveMemberButton(onClick: () -> Unit) {
@Composable
fun OpenChatButton(
modifier: Modifier,
disabledLook: Boolean = false,
onClick: () -> Unit
) {
InfoViewActionButton(
@@ -585,7 +627,7 @@ fun OpenChatButton(
icon = painterResource(MR.images.ic_chat_bubble),
title = generalGetString(MR.strings.info_view_message_button),
disabled = false,
disabledLook = false,
disabledLook = disabledLook,
onClick = onClick
)
}
@@ -1,12 +1,12 @@
package chat.simplex.common.views.chat.item
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
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
@@ -184,14 +184,26 @@ fun CIFileView(
}
}
val showOpenSaveMenu = rememberSaveable(file?.fileId) { mutableStateOf(false) }
val ext = file?.fileSource?.filePath?.substringAfterLast(".")?.takeIf { it.isNotBlank() }
val loadedFilePath = if (appPlatform.isAndroid && file?.fileSource != null) getLoadedFilePath(file) else null
if (loadedFilePath != null && file?.fileSource != null) {
val encrypted = file.fileSource.cryptoArgs != null
SaveOrOpenFileMenu(showOpenSaveMenu, encrypted, ext, File(loadedFilePath).toURI(), file.fileSource, saveFile = { fileAction() })
}
Row(
Modifier
.combinedClickable(
onClick = { fileAction() },
onClick = {
if (appPlatform.isAndroid && loadedFilePath != null) {
showOpenSaveMenu.value = true
} else {
fileAction()
}
},
onLongClick = { showMenu.value = true }
)
.padding(if (smallView) PaddingValues() else PaddingValues(top = 4.sp.toDp(), bottom = 6.sp.toDp(), start = 6.sp.toDp(), end = 12.sp.toDp())),
//Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(2.sp.toDp())
) {
@@ -223,6 +235,16 @@ fun CIFileView(
fun fileSizeValid(file: CIFile): Boolean = file.fileSize <= getMaxFileSize(file.fileProtocol)
@Composable
expect fun SaveOrOpenFileMenu(
showMenu: MutableState<Boolean>,
encrypted: Boolean,
ext: String?,
encryptedUri: URI,
fileSource: CryptoFile,
saveFile: () -> Unit
)
@Composable
fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher =
rememberFileChooserLauncher(false, ciFile) { to: URI? ->
@@ -24,12 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.currentUser
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlin.math.*
@@ -51,6 +51,12 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString =
withStyle(chatEventStyle) { append("$eventText $ts") }
}
data class ChatItemReactionMenuItem (
val name: String,
val image: String?,
val onClick: (() -> Unit)?
)
@Composable
fun ChatItemView(
rhId: Long?,
@@ -87,6 +93,7 @@ fun ChatItemView(
showItemDetails: (ChatInfo, ChatItem) -> Unit,
reveal: (Boolean) -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit,
developerTools: Boolean,
showViaProxy: Boolean,
showTimestamp: Boolean,
@@ -120,7 +127,7 @@ fun ChatItemView(
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) {
cItem.reactions.forEach { r ->
val showReactionMenu = remember { mutableStateOf(false) }
val reactionMembers = remember { mutableStateOf(emptyList<MemberReaction>()) }
val reactionMenuItems = remember { mutableStateOf(emptyList<ChatItemReactionMenuItem>()) }
val interactionSource = remember { MutableInteractionSource() }
val enterInteraction = remember { HoverInteraction.Enter() }
KeyChangeEffect(highlighted.value) {
@@ -134,18 +141,39 @@ fun ChatItemView(
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
if (cInfo.featureEnabled(ChatFeature.Reactions)) {
fun showReactionsMenu() {
if (cInfo is ChatInfo.Group) {
withBGApi {
try {
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
if (members != null) {
showReactionMenu.value = true
reactionMembers.value = members
when (cInfo) {
is ChatInfo.Group -> {
withBGApi {
try {
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
if (members != null) {
showReactionMenu.value = true
reactionMenuItems.value = members.map {
val enabled = cInfo.groupInfo.membership.groupMemberId != it.groupMember.groupMemberId
val click = if (enabled) ({ showMemberInfo(cInfo.groupInfo, it.groupMember) }) else null
ChatItemReactionMenuItem(it.groupMember.displayName, it.groupMember.image, click)
}
}
} catch (e: Exception) {
Log.d(TAG, "chatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}")
}
} catch (e: Exception) {
Log.d(TAG, "hatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}")
}
}
is ChatInfo.Direct -> {
showReactionMenu.value = true
val reactions = mutableListOf<ChatItemReactionMenuItem>()
if (!r.userReacted || r.totalReacted > 1) {
val contact = cInfo.contact
reactions.add(ChatItemReactionMenuItem(contact.displayName, contact.image, showChatInfo))
}
if (r.userReacted) {
reactions.add(ChatItemReactionMenuItem(generalGetString(MR.strings.sender_you_pronoun), currentUser.value?.image, null))
}
reactionMenuItems.value = reactions
}
else -> {}
}
}
modifier = modifier
@@ -166,19 +194,19 @@ fun ChatItemView(
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
ReactionIcon(r.reaction.text, fontSize = 12.sp)
DefaultDropdownMenu(showMenu = showReactionMenu) {
reactionMembers.value.forEach { m ->
reactionMenuItems.value.forEach { m ->
ItemAction(
text = m.groupMember.displayName,
composable = { ProfileImage(44.dp, m.groupMember.image) },
text = m.name,
composable = { ProfileImage(44.dp, m.image) },
onClick = {
if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.groupMemberId != m.groupMember.groupMemberId) {
showMemberInfo(cInfo.groupInfo, m.groupMember)
showReactionMenu.value = false
} else {
val click = m.onClick
if (click != null) {
click()
showReactionMenu.value = false
}
},
lineLimit = 1
lineLimit = 1,
color = if (m.onClick == null) MaterialTheme.colors.secondary else MenuTextColor
)
}
}
@@ -839,6 +867,32 @@ fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, on
}
}
@Composable
fun ItemAction(text: String, icon: ImageBitmap, textColor: Color = Color.Unspecified, iconColor: Color = Color.Unspecified, onClick: () -> Unit) {
val finalColor = if (textColor == Color.Unspecified) {
MenuTextColor
} else textColor
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = finalColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (iconColor == Color.Unspecified) {
Image(icon, text, Modifier.size(22.dp))
} else {
Icon(icon, text, Modifier.size(22.dp), tint = iconColor)
}
}
}
}
@Composable
fun ItemAction(
text: String,
@@ -1188,6 +1242,7 @@ fun PreviewChatItemView(
showItemDetails = { _, _ -> },
reveal = {},
showMemberInfo = { _, _ ->},
showChatInfo = {},
developerTools = false,
showViaProxy = false,
showTimestamp = true,
@@ -1233,6 +1288,7 @@ fun PreviewChatItemViewDeletedContent() {
showItemDetails = { _, _ -> },
reveal = {},
showMemberInfo = { _, _ ->},
showChatInfo = {},
developerTools = false,
showViaProxy = false,
preview = true,
@@ -4,28 +4,34 @@ import SectionItemView
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.*
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.markChatTagRead
import chat.simplex.common.model.ChatModel.updateChatTagRead
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.group.deleteGroupDialog
import chat.simplex.common.views.chat.group.leaveGroupDialog
import chat.simplex.common.views.chat.group.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.contacts.onRequestAccepted
import chat.simplex.common.views.helpers.*
@@ -33,7 +39,6 @@ import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.datetime.Clock
import kotlin.math.min
@Composable
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
@@ -252,6 +257,7 @@ fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMen
}
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
TagListAction(chat, showMenu)
ClearChatAction(chat, showMenu)
}
DeleteContactAction(chat, chatModel, showMenu)
@@ -291,6 +297,7 @@ fun GroupMenuItems(
}
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
TagListAction(chat, showMenu)
ClearChatAction(chat, showMenu)
if (groupInfo.membership.memberCurrent) {
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
@@ -337,6 +344,28 @@ fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableStat
)
}
@Composable
fun TagListAction(
chat: Chat,
showMenu: MutableState<Boolean>
) {
val userTags = remember { chatModel.userTags }
ItemAction(
stringResource(MR.strings.list_menu),
painterResource(MR.images.ic_label),
onClick = {
ModalManager.start.showModalCloseable { close ->
if (userTags.value.isEmpty()) {
TagListEditor(rhId = chat.remoteHostId, chat = chat, close = close)
} else {
TagListView(rhId = chat.remoteHostId, chat = chat, close = close)
}
}
showMenu.value = false
}
)
}
@Composable
fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState<Boolean>) {
ItemAction(
@@ -557,6 +586,7 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
if (success) {
withChats {
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
markChatTagRead(chat)
}
}
}
@@ -568,6 +598,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
if (chat.chatStats.unreadChat) return
withApi {
val wasUnread = chat.unreadTag
val success = chatModel.controller.apiChatUnread(
chat.remoteHostId,
chat.chatInfo.chatType,
@@ -577,6 +608,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
if (success) {
withChats {
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
updateChatTagRead(chat, wasUnread)
}
}
}
@@ -826,12 +858,20 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
else -> false
}
if (res && newChatInfo != null) {
val chat = chatModel.getChat(chatInfo.id)
val wasUnread = chat?.unreadTag ?: false
val wasFavorite = chatInfo.chatSettings?.favorite ?: false
chatModel.updateChatFavorite(favorite = chatSettings.favorite, wasFavorite)
withChats {
updateChatInfo(remoteHostId, newChatInfo)
}
if (chatSettings.enableNtfs != MsgFilter.All) {
ntfManager.cancelNotificationsForChat(chatInfo.id)
}
val updatedChat = chatModel.getChat(chatInfo.id)
if (updatedChat != null) {
chatModel.updateChatTagRead(updatedChat, wasUnread)
}
val current = currentState?.value
if (current != null) {
currentState.value = !current
@@ -16,11 +16,13 @@ import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.AppLock
import chat.simplex.common.model.*
@@ -31,22 +33,30 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.Call
import chat.simplex.common.views.chat.item.CIFileViewScope
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.mkValidName
import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.showInvalidNameAlert
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.views.usersettings.networkAndServers.ConditionsLinkButton
import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.json.Json
import kotlin.time.Duration.Companion.seconds
enum class PresetTagKind { FAVORITES, CONTACTS, GROUPS, BUSINESS }
sealed class ActiveFilter {
data class PresetTag(val tag: PresetTagKind) : ActiveFilter()
data class UserTag(val tag: ChatTag) : ActiveFilter()
data object Unread: ActiveFilter()
}
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
ModalManager.start.closeModals()
ModalManager.end.closeModals()
@@ -187,6 +197,12 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
}
}
if (appPlatform.isAndroid) {
val wasAllowedToSetupNotifications = rememberSaveable { mutableStateOf(false) }
val canEnableNotifications = remember { derivedStateOf { chatModel.chatRunning.value == true } }
if (wasAllowedToSetupNotifications.value || canEnableNotifications.value) {
SetNotificationsModeAdditions()
LaunchedEffect(Unit) { wasAllowedToSetupNotifications.value = true }
}
tryOrShowError("UserPicker", error = {}) {
UserPicker(
chatModel = chatModel,
@@ -557,17 +573,24 @@ private fun BoxScope.unreadBadge(text: String? = "") {
@Composable
private fun ToggleFilterEnabledButton() {
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
IconButton(onClick = { pref.set(!pref.get()) }) {
val showUnread = remember { chatModel.activeChatTagFilter }.value == ActiveFilter.Unread
IconButton(onClick = {
if (showUnread) {
chatModel.activeChatTagFilter.value = null
} else {
chatModel.activeChatTagFilter.value = ActiveFilter.Unread
}
}) {
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
Icon(
painterResource(MR.images.ic_filter_list),
null,
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
tint = if (showUnread) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
modifier = Modifier
.padding(3.dp)
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
.background(color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
.border(width = 1.dp, color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
.padding(3.dp)
.size(sp16)
)
@@ -731,6 +754,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
val oneHandUI = remember { appPrefs.oneHandUI.state }
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
val activeFilter = remember { chatModel.activeChatTagFilter }
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
val currentIndex = listState.firstVisibleItemIndex
@@ -753,14 +777,13 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
DisposableEffect(Unit) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
}
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
val allChats = remember { chatModel.chats }
// In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side
// which is related to [derivedStateOf]. Using safe alternative instead
// val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } }
val searchShowingSimplexLink = remember { mutableStateOf(false) }
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
val chats = filteredChats(searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList(), activeFilter.value)
val topPaddingToContent = topPaddingToContent(false)
val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent
LazyColumnWithScrollBar(
@@ -791,11 +814,15 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
) {
if (oneHandUI.value) {
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
Divider()
TagsView()
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}
} else {
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
TagsView()
Divider()
}
}
}
@@ -815,8 +842,8 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
}
}
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
Box(Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center) {
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
Box(Modifier.fillMaxSize().imePadding().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
NoChatsView(searchText = searchText)
}
}
if (oneHandUI.value) {
@@ -839,6 +866,41 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
}
}
}
LaunchedEffect(activeFilter.value) {
searchText.value = TextFieldValue("")
}
}
@Composable
private fun NoChatsView(searchText: MutableState<TextFieldValue>) {
val activeFilter = remember { chatModel.activeChatTagFilter }.value
if (searchText.value.text.isBlank()) {
when (activeFilter) {
is ActiveFilter.PresetTag -> Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) // this should not happen
is ActiveFilter.UserTag -> Text(String.format(generalGetString(MR.strings.no_chats_in_list), activeFilter.tag.chatTagText), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
is ActiveFilter.Unread -> {
Row(
Modifier.clip(shape = CircleShape).clickable { chatModel.activeChatTagFilter.value = null }.padding(DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(MR.images.ic_filter_list),
null,
tint = MaterialTheme.colors.secondary
)
Text(generalGetString(MR.strings.no_unread_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
}
}
null -> {
Text(generalGetString(MR.strings.no_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
}
}
} else {
Text(generalGetString(MR.strings.no_chats_found), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
}
}
@Composable
@@ -860,31 +922,301 @@ private fun ChatListFeatureCards() {
}
}
private val TAG_MIN_HEIGHT = 35.dp
@Composable
private fun TagsView() {
val userTags = remember { chatModel.userTags }
val presetTags = remember { chatModel.presetTags }
val activeFilter = remember { chatModel.activeChatTagFilter }
val unreadTags = remember { chatModel.unreadTags }
val rhId = chatModel.remoteHostId()
fun showTagList() {
ModalManager.start.showCustomModal { close ->
val editMode = remember { stateGetOrPut("editMode") { false } }
ModalView(close, showClose = true, endButtons = {
TextButton(onClick = { editMode.value = !editMode.value }, modifier = Modifier.clip(shape = CircleShape)) {
Text(stringResource(if (editMode.value) MR.strings.cancel_verb else MR.strings.edit_verb))
}
}) {
TagListView(rhId = rhId, close = close, editMode = editMode)
}
}
}
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
TagsRow {
if (presetTags.size > 1) {
if (presetTags.size + userTags.value.size <= 3) {
PresetTagKind.entries.filter { t -> (presetTags[t] ?: 0) > 0 }.forEach { tag ->
ExpandedTagFilterView(tag)
}
} else {
CollapsedTagsFilterView()
}
}
userTags.value.forEach { tag ->
val current = when (val af = activeFilter.value) {
is ActiveFilter.UserTag -> af.tag == tag
else -> false
}
val interactionSource = remember { MutableInteractionSource() }
Row(
rowSizeModifier
.clip(shape = CircleShape)
.combinedClickable(
onClick = {
if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) {
chatModel.activeChatTagFilter.value = null
} else {
chatModel.activeChatTagFilter.value = ActiveFilter.UserTag(tag)
}
},
onLongClick = { showTagList() },
interactionSource = interactionSource,
indication = LocalIndication.current
)
.onRightClick { showTagList() }
.padding(4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
if (tag.chatTagEmoji != null) {
ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp)
} else {
Icon(
painterResource(if (current) MR.images.ic_label_filled else MR.images.ic_label),
null,
Modifier.size(18.sp.toDp()),
tint = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground
)
}
Spacer(Modifier.width(4.dp))
Box {
val badgeText = if ((unreadTags[tag.chatTagId] ?: 0) > 0) "" else ""
val invisibleText = buildAnnotatedString {
append(tag.chatTagText)
withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold)) {
append(badgeText)
}
}
Text(
text = invisibleText,
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color.Transparent,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Visible text with styles
val visibleText = buildAnnotatedString {
append(tag.chatTagText)
withStyle(SpanStyle(fontSize = 12.5.sp, color = MaterialTheme.colors.primary)) {
append(badgeText)
}
}
Text(
text = visibleText,
fontWeight = if (current) FontWeight.Medium else FontWeight.Normal,
fontSize = 15.sp,
color = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
val plusClickModifier = Modifier
.clickable {
ModalManager.start.showModalCloseable { close ->
TagListEditor(rhId = rhId, close = close)
}
}
if (userTags.value.isEmpty()) {
Row(rowSizeModifier.clip(shape = CircleShape).then(plusClickModifier).padding(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(2.dp))
Text(stringResource(MR.strings.chat_list_add_list), color = MaterialTheme.colors.secondary, fontSize = 15.sp)
}
} else {
Box(rowSizeModifier, contentAlignment = Alignment.Center) {
Icon(
painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.clip(shape = CircleShape).then(plusClickModifier).padding(2.dp), tint = MaterialTheme.colors.secondary
)
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun TagsRow(content: @Composable() (() -> Unit)) {
if (appPlatform.isAndroid) {
Row(
modifier = Modifier
.padding(horizontal = 14.dp)
.horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
content()
}
} else {
FlowRow(modifier = Modifier.padding(horizontal = 14.dp)) { content() }
}
}
@Composable
private fun ExpandedTagFilterView(tag: PresetTagKind) {
val activeFilter = remember { chatModel.activeChatTagFilter }
val active = when (val af = activeFilter.value) {
is ActiveFilter.PresetTag -> af.tag == tag
else -> false
}
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
val (icon, text) = presetTagLabel(tag, active)
val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
Row(
modifier = rowSizeModifier
.clip(shape = CircleShape)
.clickable {
if (activeFilter.value == ActiveFilter.PresetTag(tag)) {
chatModel.activeChatTagFilter.value = null
} else {
chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(tag)
}
}
.padding(horizontal = 5.dp, vertical = 4.dp)
,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(
painterResource(icon),
stringResource(text),
Modifier.size(18.sp.toDp()),
tint = color
)
Spacer(Modifier.width(4.dp))
Box {
Text(
stringResource(text),
color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
fontWeight = if (active) FontWeight.Medium else FontWeight.Normal,
fontSize = 15.sp
)
Text(
stringResource(text),
color = Color.Transparent,
fontWeight = FontWeight.Medium,
fontSize = 15.sp
)
}
}
}
@Composable
private fun CollapsedTagsFilterView() {
val activeFilter = remember { chatModel.activeChatTagFilter }
val presetTags = remember { chatModel.presetTags }
val showMenu = remember { mutableStateOf(false) }
val selectedPresetTag = when (val af = activeFilter.value) {
is ActiveFilter.PresetTag -> af.tag
else -> null
}
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
Box(rowSizeModifier
.padding(vertical = 4.dp)
.clip(shape = CircleShape)
.size(30.sp.toDp())
.clickable { showMenu.value = true },
contentAlignment = Alignment.Center
) {
if (selectedPresetTag != null) {
val (icon, text) = presetTagLabel(selectedPresetTag, true)
Icon(
painterResource(icon),
stringResource(text),
Modifier.size(18.sp.toDp()),
tint = MaterialTheme.colors.secondary
)
} else {
Icon(
painterResource(MR.images.ic_menu),
stringResource(MR.strings.chat_list_all),
tint = MaterialTheme.colors.secondary
)
}
DefaultDropdownMenu(showMenu = showMenu) {
if (selectedPresetTag != null) {
ItemAction(
stringResource(MR.strings.chat_list_all),
painterResource(MR.images.ic_menu),
onClick = {
chatModel.activeChatTagFilter.value = null
showMenu.value = false
}
)
}
PresetTagKind.entries.forEach { tag ->
if ((presetTags[tag] ?: 0) > 0) {
ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu)
}
}
}
}
}
@Composable
fun ItemPresetFilterAction(
presetTag: PresetTagKind,
active: Boolean,
showMenu: MutableState<Boolean>
) {
val (icon, text) = presetTagLabel(presetTag, active)
ItemAction(
stringResource(text),
painterResource(icon),
onClick = {
chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag)
showMenu.value = false
}
)
}
fun filteredChats(
showUnreadAndFavorites: Boolean,
searchShowingSimplexLink: State<Boolean>,
searchChatFilteredBySimplexLink: State<String?>,
searchText: String,
chats: List<Chat>
chats: List<Chat>,
activeFilter: ActiveFilter? = null,
): List<Chat> {
val linkChatId = searchChatFilteredBySimplexLink.value
return if (linkChatId != null) {
chats.filter { it.id == linkChatId }
} else {
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
if (s.isEmpty() && !showUnreadAndFavorites)
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD }
if (s.isEmpty())
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD && filtered(chat, activeFilter) }
else {
chats.filter { chat ->
when (val cInfo = chat.chatInfo) {
is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && (
if (s.isEmpty()) {
chat.id == chatModel.chatId.value || filtered(chat)
chat.id == chatModel.chatId.value || filtered(chat, activeFilter)
} else {
cInfo.anyNameContains(s)
})
is ChatInfo.Group -> if (s.isEmpty()) {
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
chat.id == chatModel.chatId.value || filtered(chat, activeFilter) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
} else {
cInfo.anyNameContains(s)
}
@@ -898,10 +1230,41 @@ fun filteredChats(
}
}
private fun filtered(chat: Chat): Boolean =
(chat.chatInfo.chatSettings?.favorite ?: false) ||
chat.chatStats.unreadChat ||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
private fun filtered(chat: Chat, activeFilter: ActiveFilter?): Boolean =
when (activeFilter) {
is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo)
is ActiveFilter.UserTag -> chat.chatInfo.chatTags?.contains(activeFilter.tag.chatTagId) ?: false
is ActiveFilter.Unread -> chat.chatStats.unreadChat || chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0
else -> true
}
fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo): Boolean =
when (tag) {
PresetTagKind.FAVORITES -> chatInfo.chatSettings?.favorite == true
PresetTagKind.CONTACTS -> when (chatInfo) {
is ChatInfo.Direct -> !(chatInfo.contact.activeConn == null && chatInfo.contact.profile.contactLink != null && chatInfo.contact.active) && !chatInfo.contact.chatDeleted
is ChatInfo.ContactRequest -> true
is ChatInfo.ContactConnection -> true
is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Customer
else -> false
}
PresetTagKind.GROUPS -> when (chatInfo) {
is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null
else -> false
}
PresetTagKind.BUSINESS -> when (chatInfo) {
is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Business
else -> false
}
}
private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair<ImageResource, StringResource> =
when (tag) {
PresetTagKind.FAVORITES -> (if (active) MR.images.ic_star_filled else MR.images.ic_star) to MR.strings.chat_list_favorites
PresetTagKind.CONTACTS -> (if (active) MR.images.ic_person_filled else MR.images.ic_person) to MR.strings.chat_list_contacts
PresetTagKind.GROUPS -> (if (active) MR.images.ic_group_filled else MR.images.ic_group) to MR.strings.chat_list_groups
PresetTagKind.BUSINESS -> (if (active) MR.images.ic_work_filled else MR.images.ic_work) to MR.strings.chat_list_businesses
}
fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) {
scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } }
@@ -191,7 +191,7 @@ private fun ShareList(
val chats by remember(search) {
derivedStateOf {
val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local }
filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted)
filteredChats(mutableStateOf(false), mutableStateOf(null), search, sorted)
}
}
val topPaddingToContent = topPaddingToContent(false)
@@ -0,0 +1,500 @@
package chat.simplex.common.views.chatlist
import SectionCustomFooter
import SectionDivider
import SectionItemView
import TextIconSpaced
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.apiDeleteChatTag
import chat.simplex.common.model.ChatController.apiSetChatTags
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chat.item.ReactionIcon
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: MutableState<Boolean> = remember { mutableStateOf(false) }) {
if (remember { editMode }.value) {
BackHandler {
editMode.value = false
}
}
val userTags = remember { chatModel.userTags }
val oneHandUI = remember { appPrefs.oneHandUI.state }
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
val saving = remember { mutableStateOf(false) }
val chatTagIds = derivedStateOf { chat?.chatInfo?.chatTags ?: emptyList() }
fun reorderTags(tagIds: List<Long>) {
saving.value = true
withBGApi {
try {
chatModel.controller.apiReorderChatTags(rhId, tagIds)
} catch (e: Exception) {
Log.d(TAG, "ChatListTag reorderTags error: ${e.message}")
} finally {
saving.value = false
}
}
}
val dragDropState =
rememberDragDropState(listState) { fromIndex, toIndex ->
userTags.value = userTags.value.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
reorderTags(userTags.value.map { it.chatTagId })
}
val topPaddingToContent = topPaddingToContent(false)
LazyColumnWithScrollBar(
modifier = if (editMode.value) Modifier.dragContainer(dragDropState) else Modifier,
contentPadding = PaddingValues(
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
),
state = listState,
verticalArrangement = if (oneHandUI.value) Arrangement.Bottom else Arrangement.Top,
) {
@Composable fun CreateList() {
SectionItemView({
ModalManager.start.showModalCloseable { close ->
TagListEditor(rhId = rhId, close = close, chat = chat)
}
}) {
Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.create_list), tint = MaterialTheme.colors.primary)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(MR.strings.create_list), color = MaterialTheme.colors.primary)
}
}
if (oneHandUI.value && !editMode.value) {
item {
CreateList()
}
}
itemsIndexed(userTags.value, key = { _, item -> item.chatTagId }) { index, tag ->
DraggableItem(dragDropState, index) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
Card(
elevation = elevation,
backgroundColor = if (isDragging) colors.surface else Color.Unspecified
) {
Column {
val showMenu = remember { mutableStateOf(false) }
val selected = chatTagIds.value.contains(tag.chatTagId)
Row(
Modifier
.fillMaxWidth()
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
.combinedClickable(
enabled = !saving.value,
onClick = {
if (chat == null) {
ModalManager.start.showModalCloseable { close ->
TagListEditor(
rhId = rhId,
tagId = tag.chatTagId,
close = close,
emoji = tag.chatTagEmoji,
name = tag.chatTagText,
)
}
} else {
saving.value = true
setTag(rhId = rhId, tagId = if (selected) null else tag.chatTagId, chat = chat, close = {
saving.value = false
close()
})
}
},
onLongClick = if (editMode.value) null else {
{ showMenu.value = true }
},
interactionSource = remember { MutableInteractionSource() },
indication = LocalIndication.current
)
.onRightClick { showMenu.value = true }
.padding(PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)),
verticalAlignment = Alignment.CenterVertically
) {
if (tag.chatTagEmoji != null) {
ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp)
} else {
Icon(painterResource(MR.images.ic_label), null, Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.onBackground)
}
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
tag.chatTagText,
color = MenuTextColor,
fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal
)
if (selected) {
Spacer(Modifier.weight(1f))
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
} else if (editMode.value) {
Spacer(Modifier.weight(1f))
Icon(painterResource(MR.images.ic_drag_handle), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
}
DefaultDropdownMenu(showMenu, dropdownMenuItems = {
EditTagAction(rhId, tag, showMenu)
DeleteTagAction(rhId, tag, showMenu, saving)
})
}
SectionDivider()
}
}
}
}
if (!oneHandUI.value && !editMode.value) {
item {
CreateList()
}
}
}
}
@Composable
fun ModalData.TagListEditor(
rhId: Long?,
chat: Chat? = null,
tagId: Long? = null,
emoji: String? = null,
name: String = "",
close: () -> Unit
) {
val userTags = remember { chatModel.userTags }
val oneHandUI = remember { appPrefs.oneHandUI.state }
val newEmoji = remember { stateGetOrPutNullable("chatTagEmoji") { emoji } }
val newName = remember { stateGetOrPut("chatTagName") { name } }
val saving = remember { mutableStateOf<Boolean?>(null) }
val trimmedName = remember { derivedStateOf { newName.value.trim() } }
val isDuplicateEmojiOrName = remember {
derivedStateOf {
userTags.value.any { tag ->
tag.chatTagId != tagId &&
((newEmoji.value != null && tag.chatTagEmoji == newEmoji.value) || tag.chatTagText == trimmedName.value)
}
}
}
fun createTag() {
saving.value = true
withBGApi {
try {
val updatedTags = chatModel.controller.apiCreateChatTag(rhId, ChatTagData(newEmoji.value, trimmedName.value))
if (updatedTags != null) {
saving.value = false
userTags.value = updatedTags
close()
} else {
saving.value = null
return@withBGApi
}
if (chat != null) {
val createdTag = updatedTags.firstOrNull() { it.chatTagText == trimmedName.value && it.chatTagEmoji == newEmoji.value }
if (createdTag != null) {
setTag(rhId, createdTag.chatTagId, chat, close = {
saving.value = false
close()
})
}
}
} catch (e: Exception) {
Log.d(TAG, "createChatTag tag error: ${e.message}")
saving.value = null
}
}
}
fun updateTag() {
saving.value = true
withBGApi {
try {
if (chatModel.controller.apiUpdateChatTag(rhId, tagId!!, ChatTagData(newEmoji.value, trimmedName.value))) {
userTags.value = userTags.value.map { tag ->
if (tag.chatTagId == tagId) {
tag.copy(chatTagEmoji = newEmoji.value, chatTagText = trimmedName.value)
} else {
tag
}
}
} else {
saving.value = null
return@withBGApi
}
saving.value = false
close()
} catch (e: Exception) {
Log.d(TAG, "ChatListTagEditor updateChatTag tag error: ${e.message}")
saving.value = null
}
}
}
val showError = derivedStateOf { isDuplicateEmojiOrName.value && saving.value != false }
ColumnWithScrollBar(Modifier.consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) WindowInsets.ime.asPaddingValues().calculateBottomPadding().coerceIn(0.dp, WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) else 0.dp))) {
if (oneHandUI.value) {
Spacer(Modifier.weight(1f))
}
ChatTagInput(newName, showError, newEmoji)
val disabled = saving.value == true ||
(trimmedName.value == name && newEmoji.value == emoji) ||
trimmedName.value.isEmpty() ||
isDuplicateEmojiOrName.value
SectionItemView(click = { if (tagId == null) createTag() else updateTag() }, disabled = disabled) {
Text(
generalGetString(if (chat != null) MR.strings.add_to_list else if (tagId == null) MR.strings.create_list else MR.strings.save_list),
color = if (disabled) colors.secondary else colors.primary
)
}
val showErrorMessage = isDuplicateEmojiOrName.value && saving.value != false
SectionCustomFooter {
Row(
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(MR.images.ic_error),
contentDescription = stringResource(MR.strings.error),
tint = if (showErrorMessage) Color.Red else Color.Transparent,
modifier = Modifier
.size(19.sp.toDp())
.offset(x = 2.sp.toDp())
)
TextIconSpaced()
Text(
generalGetString(MR.strings.duplicated_list_error),
color = if (showErrorMessage) colors.secondary else Color.Transparent,
lineHeight = 18.sp,
fontSize = 14.sp
)
}
}
}
}
@Composable
private fun DeleteTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>, saving: MutableState<Boolean>) {
ItemAction(
stringResource(MR.strings.delete_chat_list_menu_action),
painterResource(MR.images.ic_delete),
onClick = {
deleteTagDialog(rhId, tag, saving)
showMenu.value = false
},
color = Color.Red
)
}
@Composable
private fun EditTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>) {
ItemAction(
stringResource(MR.strings.edit_chat_list_menu_action),
painterResource(MR.images.ic_edit),
onClick = {
showMenu.value = false
ModalManager.start.showModalCloseable { close ->
TagListEditor(
rhId = rhId,
tagId = tag.chatTagId,
close = close,
emoji = tag.chatTagEmoji,
name = tag.chatTagText
)
}
},
color = MenuTextColor
)
}
@Composable
expect fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>)
@Composable
fun TagListNameTextField(name: MutableState<String>, showError: State<Boolean>) {
var focused by rememberSaveable { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val interactionSource = remember { MutableInteractionSource() }
val colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,
focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f),
unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f),
cursorColor = MaterialTheme.colors.secondary,
)
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
interactionSource = interactionSource,
modifier = Modifier
.fillMaxWidth()
.indicatorLine(true, showError.value, interactionSource, colors)
.heightIn(min = TextFieldDefaults.MinHeight)
.onFocusChanged { focused = it.isFocused }
.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 18.sp, color = MaterialTheme.colors.onBackground),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = name.value,
innerTextField = innerTextField,
placeholder = {
Text(generalGetString(MR.strings.list_name_field_placeholder), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp))
},
contentPadding = PaddingValues(),
label = null,
visualTransformation = VisualTransformation.None,
leadingIcon = null,
singleLine = true,
enabled = true,
isError = false,
interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
)
}
)
}
private fun setTag(rhId: Long?, tagId: Long?, chat: Chat, close: () -> Unit) {
withBGApi {
val tagIds: List<Long> = if (tagId == null) {
emptyList()
} else {
listOf(tagId)
}
try {
val result = apiSetChatTags(rh = rhId, type = chat.chatInfo.chatType, id = chat.chatInfo.apiId, tagIds = tagIds)
if (result != null) {
val oldTags = chat.chatInfo.chatTags
chatModel.userTags.value = result.first
when (val cInfo = chat.chatInfo) {
is ChatInfo.Direct -> {
val contact = cInfo.contact.copy(chatTags = result.second)
withChats {
updateContact(rhId, contact)
}
}
is ChatInfo.Group -> {
val group = cInfo.groupInfo.copy(chatTags = result.second)
withChats {
updateGroup(rhId, group)
}
}
else -> {}
}
chatModel.moveChatTagUnread(chat, oldTags, result.second)
close()
}
} catch (e: Exception) {
Log.d(TAG, "setChatTag error: ${e.message}")
}
}
}
private fun deleteTag(rhId: Long?, tag: ChatTag, saving: MutableState<Boolean>) {
withBGApi {
saving.value = true
try {
val tagId = tag.chatTagId
if (apiDeleteChatTag(rhId, tagId)) {
chatModel.userTags.value = chatModel.userTags.value.filter { it.chatTagId != tagId }
if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) {
chatModel.activeChatTagFilter.value = null
}
chatModel.chats.value.forEach { c ->
when (val cInfo = c.chatInfo) {
is ChatInfo.Direct -> {
val contact = cInfo.contact.copy(chatTags = cInfo.contact.chatTags.filter { it != tagId })
withChats {
updateContact(rhId, contact)
}
}
is ChatInfo.Group -> {
val group = cInfo.groupInfo.copy(chatTags = cInfo.groupInfo.chatTags.filter { it != tagId })
withChats {
updateGroup(rhId, group)
}
}
else -> {}
}
}
}
} catch (e: Exception) {
Log.d(TAG, "deleteTag error: ${e.message}")
} finally {
saving.value = false
}
}
}
private fun deleteTagDialog(rhId: Long?, tag: ChatTag, saving: MutableState<Boolean>) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_chat_list_question),
text = String.format(generalGetString(MR.strings.delete_chat_list_warning), tag.chatTagText),
buttons = {
SectionItemView({
AlertManager.shared.hideAlert()
deleteTag(rhId, tag, saving)
}) {
Text(
generalGetString(MR.strings.confirm_verb),
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = colors.error
)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(
stringResource(MR.strings.cancel_verb),
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = colors.primary
)
}
}
)
}
@@ -30,6 +30,7 @@ import kotlinx.datetime.*
import java.io.*
import java.net.URI
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
@@ -44,11 +45,14 @@ fun DatabaseView() {
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
val stopped = remember { m.chatRunning }.value == false
val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? ->
val file = chatArchiveFile.value
if (file != null && to != null) {
copyFileToFile(File(file), to) {
chatArchiveFile.value = null
}
val archive = chatArchiveFile.value
if (archive != null && to != null) {
copyFileToFile(File(archive), to) {}
}
// delete no matter the database was exported or canceled the export process
if (archive != null) {
File(archive).delete()
chatArchiveFile.value = null
}
}
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
@@ -56,8 +60,7 @@ fun DatabaseView() {
if (to != null) {
importArchiveAlert {
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
importArchive(to, appFilesCountAndSize, progressIndicator)
true
importArchive(to, appFilesCountAndSize, progressIndicator, false)
}
}
}
@@ -641,6 +644,7 @@ suspend fun importArchive(
importedArchiveURI: URI,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
progressIndicator: MutableState<Boolean>,
migration: Boolean
): Boolean {
val m = chatModel
progressIndicator.value = true
@@ -662,12 +666,13 @@ suspend fun importArchive(
if (chatModel.localUserCreated.value == false) {
chatModel.chatRunning.value = false
}
return true
} else {
operationEnded(m, progressIndicator) {
showArchiveImportedWithErrorsAlert(archiveErrors)
}
return migration
}
return true
} catch (e: Error) {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_importing_database), e.toString())
@@ -680,6 +685,8 @@ suspend fun importArchive(
} finally {
File(archivePath).delete()
}
} else {
progressIndicator.value = false
}
return false
}
@@ -691,14 +698,15 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? {
if (inputStream != null && archiveName != null) {
val archivePath = "$databaseExportDir${File.separator}$archiveName"
val destFile = File(archivePath)
Files.copy(inputStream, destFile.toPath())
Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
archivePath
} else {
Log.e(TAG, "saveArchiveFromURI null inputStream")
null
}
} catch (e: Exception) {
Log.e(TAG, "saveArchiveFromURI error: ${e.message}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_database), e.stackTraceToString())
Log.e(TAG, "saveArchiveFromURI error: ${e.stackTraceToString()}")
null
}
}
@@ -0,0 +1,177 @@
package chat.simplex.common.views.helpers
/*
* This was adapted from google example of drag and drop for Jetpack Compose
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
*/
import androidx.compose.animation.core.*
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.lazy.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
@Composable
fun rememberDragDropState(lazyListState: LazyListState, onMove: (Int, Int) -> Unit): DragDropState {
val scope = rememberCoroutineScope()
val state =
remember(lazyListState) {
DragDropState(state = lazyListState, onMove = onMove, scope = scope)
}
LaunchedEffect(state) {
while (true) {
val diff = state.scrollChannel.receive()
lazyListState.scrollBy(diff)
}
}
return state
}
class DragDropState
internal constructor(
private val state: LazyListState,
private val scope: CoroutineScope,
private val onMove: (Int, Int) -> Unit
) {
var draggingItemIndex by mutableStateOf<Int?>(null)
private set
internal val scrollChannel = Channel<Float>()
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
private var draggingItemInitialOffset by mutableIntStateOf(0)
internal val draggingItemOffset: Float
get() =
draggingItemLayoutInfo?.let { item ->
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
} ?: 0f
private val draggingItemLayoutInfo: LazyListItemInfo?
get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
private set
internal var previousItemOffset = Animatable(0f)
private set
internal fun onDragStart(offset: Offset) {
val touchY = offset.y.toInt()
val item = state.layoutInfo.visibleItemsInfo.minByOrNull {
val itemCenter = (it.offset - state.layoutInfo.viewportStartOffset) + it.size / 2
kotlin.math.abs(touchY - itemCenter) // Find the item closest to the touch position, needs to take viewportStartOffset into account
}
if (item != null) {
draggingItemIndex = item.index
draggingItemInitialOffset = item.offset
}
}
internal fun onDragInterrupted() {
if (draggingItemIndex != null) {
previousIndexOfDraggedItem = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
previousItemOffset.snapTo(startOffset)
previousItemOffset.animateTo(
0f,
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f)
)
previousIndexOfDraggedItem = null
}
}
draggingItemDraggedDelta = 0f
draggingItemIndex = null
draggingItemInitialOffset = 0
}
internal fun onDrag(offset: Offset) {
draggingItemDraggedDelta += offset.y
val draggingItem = draggingItemLayoutInfo ?: return
val startOffset = draggingItem.offset + draggingItemOffset
val endOffset = startOffset + draggingItem.size
val middleOffset = startOffset + (endOffset - startOffset) / 2f
val targetItem =
state.layoutInfo.visibleItemsInfo.find { item ->
middleOffset.toInt() in item.offset..item.offsetEnd &&
draggingItem.index != item.index
}
if (targetItem != null) {
if (
draggingItem.index == state.firstVisibleItemIndex ||
targetItem.index == state.firstVisibleItemIndex
) {
state.requestScrollToItem(
state.firstVisibleItemIndex,
state.firstVisibleItemScrollOffset
)
}
onMove.invoke(draggingItem.index, targetItem.index)
draggingItemIndex = targetItem.index
} else {
val overscroll =
when {
draggingItemDraggedDelta > 0 ->
(endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
draggingItemDraggedDelta < 0 ->
(startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
else -> 0f
}
if (overscroll != 0f) {
scrollChannel.trySend(overscroll)
}
}
}
private val LazyListItemInfo.offsetEnd: Int
get() = this.offset + this.size
}
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
return pointerInput(dragDropState) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
dragDropState.onDrag(offset = offset)
},
onDragStart = { offset -> dragDropState.onDragStart(offset) },
onDragEnd = { dragDropState.onDragInterrupted() },
onDragCancel = { dragDropState.onDragInterrupted() }
)
}
}
@Composable
fun LazyItemScope.DraggableItem(
dragDropState: DragDropState,
index: Int,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
) {
val dragging = index == dragDropState.draggingItemIndex
val draggingModifier =
if (dragging) {
Modifier.zIndex(1f).graphicsLayer { translationY = dragDropState.draggingItemOffset }
} else if (index == dragDropState.previousIndexOfDraggedItem) {
Modifier.zIndex(1f).graphicsLayer {
translationY = dragDropState.previousItemOffset.value
}
} else {
Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
}
Column(modifier = modifier.then(draggingModifier)) { content(dragging) }
}
@@ -174,7 +174,7 @@ private fun SectionByState(
is MigrationFromState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath)
is MigrationFromState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value)
is MigrationFromState.LinkCreation -> LinkCreationView()
is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl)
is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl, chatReceiver.value)
is MigrationFromState.Finished -> migrationState.FinishedView(s.chatDeletion)
}
}
@@ -335,7 +335,7 @@ private fun LinkCreationView() {
}
@Composable
private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) {
private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl, chatReceiver: MigrationFromChatReceiver?) {
SectionView {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_close),
@@ -356,7 +356,7 @@ private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: S
confirmText = generalGetString(MR.strings.continue_to_next_step),
destructive = true,
onConfirm = {
finishMigration(fileId, ctrl)
finishMigration(fileId, ctrl, chatReceiver)
}
)
}
@@ -450,6 +450,7 @@ private fun MutableState<MigrationFromState>.stopChat() {
try {
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationFromState.PassphraseNotSet else MigrationFromState.PassphraseConfirmation
platform.androidChatStopped()
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.migrate_from_device_error_saving_settings),
@@ -617,9 +618,11 @@ private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) {
}
}
private fun MutableState<MigrationFromState>.finishMigration(fileId: Long, ctrl: ChatCtrl) {
private fun MutableState<MigrationFromState>.finishMigration(fileId: Long, ctrl: ChatCtrl, chatReceiver: MigrationFromChatReceiver?) {
withBGApi {
cancelUploadedArchive(fileId, ctrl)
chatReceiver?.stopAndCleanUp()
getMigrationTempFilesDirectory().deleteRecursively()
state = MigrationFromState.Finished(false)
}
}
@@ -655,6 +658,7 @@ private suspend fun startChatAndDismiss(dismiss: Boolean = true) {
} else if (user != null) {
startChat(user)
}
platform.androidChatStartedAfterBeingOff()
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.error_starting_chat),
@@ -239,7 +239,7 @@ private fun ArchiveImportView(progressIndicator: MutableState<Boolean>, close: (
val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? ->
if (to != null) {
withLongRunningApi {
val success = importArchive(to, mutableStateOf(0 to 0), progressIndicator)
val success = importArchive(to, mutableStateOf(0 to 0), progressIndicator, true)
if (success) {
startChat(
chatModel,
@@ -691,6 +691,7 @@ private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit)
if (user != null) {
startChat(user)
}
platform.androidChatStartedAfterBeingOff()
hideView(close)
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_chat_migrated), generalGetString(MR.strings.migrate_to_device_finalize_migration))
} catch (e: Exception) {
@@ -14,10 +14,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
@@ -55,7 +53,7 @@ fun ModalData.ChooseServerOperators(
Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) {
OnboardingInformationButton(
stringResource(MR.strings.how_it_helps_privacy),
onClick = { modalManager.showModal { ChooseServerOperatorsInfoView() } }
onClick = { modalManager.showModal { ChooseServerOperatorsInfoView(modalManager) } }
)
}
@@ -346,7 +344,9 @@ private fun enabledOperators(operators: List<ServerOperator>, selectedOperatorId
}
@Composable
private fun ChooseServerOperatorsInfoView() {
private fun ChooseServerOperatorsInfoView(
modalManager: ModalManager
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.onboarding_network_operators))
@@ -362,7 +362,7 @@ private fun ChooseServerOperatorsInfoView() {
SectionView(title = stringResource(MR.strings.onboarding_network_about_operators).uppercase()) {
chatModel.conditions.value.serverOperators.forEach { op ->
ServerOperatorRow(op)
ServerOperatorRow(op, modalManager)
}
}
SectionBottomSpacer()
@@ -371,11 +371,12 @@ private fun ChooseServerOperatorsInfoView() {
@Composable()
private fun ServerOperatorRow(
operator: ServerOperator
operator: ServerOperator,
modalManager: ModalManager
) {
SectionItemView(
{
ModalManager.start.showModalCloseable { close ->
modalManager.showModalCloseable { close ->
OperatorInfoView(operator)
}
}
@@ -14,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -44,6 +45,12 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
if (devTools.value) {
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) {
DefaultSwitch(
checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG,
onCheckedChange = { appPrefs.logLevel.set(if (it) LogLevel.DEBUG else LogLevel.WARNING) }
)
}
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
if (appPlatform.isDesktop) {
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
@@ -78,7 +78,7 @@ fun NotificationsSettingsLayout(
)
}
if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) {
SectionTextFooter(stringResource(MR.strings.xiaomi_ignore_battery_optimization))
SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization))
}
}
SectionBottomSpacer()
@@ -95,7 +95,7 @@ fun NotificationsModeView(
AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current))
SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected)
if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) {
SectionTextFooter(stringResource(MR.strings.xiaomi_ignore_battery_optimization))
SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization))
}
}
}
@@ -735,7 +735,10 @@ private fun ConditionsAppliedToOtherOperatorsText(userServers: List<UserOperator
}
if (otherOperatorsToApply.value.isNotEmpty()) {
ReadableText(MR.strings.operator_conditions_will_be_applied)
ReadableText(
MR.strings.operator_conditions_will_be_applied,
args = otherOperatorsToApply.value.joinToString(", ") { it.legalName_ }
)
}
}
@@ -12,10 +12,10 @@
<string name="about_simplex">عن SimpleX</string>
<string name="above_then_preposition_continuation">أعلاه، ثم:</string>
<string name="accept_call_on_lock_screen">اقبل</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف تعريفك وجهات اتصالك ورسائلك وملفاتك بشكل نهائي.</string>
<string name="alert_message_no_group">هذه المجموعة لم تعد موجودة.</string>
<string name="this_QR_code_is_not_a_link">رمز QR هذا ليس رابطًا!</string>
<string name="next_generation_of_private_messaging">الجيل القادم من \nالرسائل الخاصة</string>
<string name="next_generation_of_private_messaging">مستقبل المُراسلة</string>
<string name="delete_files_and_media_desc">لا يمكن التراجع عن هذا الإجراء - سيتم حذف جميع الملفات والوسائط المستلمة والمرسلة. ستبقى الصور منخفضة الدقة.</string>
<string name="enable_automatic_deletion_message">لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق.</string>
<string name="messages_section_description">ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك</string>
@@ -30,7 +30,7 @@
<string name="smp_servers_preset_add">أضِف خوادم مُعدة مسبقًا</string>
<string name="smp_servers_add_to_another_device">أضِف إلى جهاز آخر</string>
<string name="users_delete_all_chats_deleted">سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</string>
<string name="network_enable_socks_info">الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار.</string>
<string name="network_enable_socks_info">الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تفعيل هذا الخيار.</string>
<string name="smp_servers_add">أضِف خادم</string>
<string name="network_settings">إعدادات الشبكة المتقدمة</string>
<string name="all_group_members_will_remain_connected">سيبقى جميع أعضاء المجموعة على اتصال.</string>
@@ -42,7 +42,7 @@
<string name="accept_contact_incognito_button">قبول التخفي</string>
<string name="button_add_welcome_message">أضِف رسالة ترحيب</string>
<string name="v4_3_improved_server_configuration_desc">أضف الخوادم عن طريق مسح رموز QR.</string>
<string name="v4_2_group_links_desc">يمكّن للمشرفين إنشاء روابط للانضمام إلى المجموعات.</string>
<string name="v4_2_group_links_desc">يمكن للمشرفين إنشاء روابط للانضمام إلى المجموعات.</string>
<string name="accept_connection_request__question">قبول طلب الاتصال؟</string>
<string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string>
<string name="callstatus_accepted">مكالمة مقبولة</string>
@@ -67,17 +67,17 @@
<string name="notifications_mode_service">دائِماً مُتاح</string>
<string name="notifications_mode_off_desc">يمكن للتطبيق استلام الإشعارات فقط عند تشغيله، ولن يتم بدء تشغيل أي خدمة في الخلفية</string>
<string name="allow_voice_messages_question">السماح بالرسائل الصوتية؟</string>
<string name="all_your_contacts_will_remain_connected">ستبقى جميع جهات الاتصال الخاصة بك متصلة.</string>
<string name="all_your_contacts_will_remain_connected">ستبقى جميع جهات اتصالك متصلة.</string>
<string name="always_use_relay">استخدم التتابع دائمًا</string>
<string name="full_backup">النسخ الاحتياطي لبيانات التطبيق</string>
<string name="all_app_data_will_be_cleared">حُذفت جميع بيانات التطبيق.</string>
<string name="allow_to_delete_messages">السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string>
<string name="allow_your_contacts_to_send_voice_messages">اسمح لجهات اتصالك بإرسال رسائل صوتية.</string>
<string name="learn_more_about_address">حول عنوان SimpleX</string>
<string name="learn_more_about_address">عن عنوان SimpleX</string>
<string name="app_version_code">بناء التطبيق: %s</string>
<string name="appearance_settings">المظهر</string>
<string name="add_address_to_your_profile">أضف عنوانًا إلى ملف التعريف الخاص بك ، حتى تتمكن جهات الاتصال الخاصة بك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك.</string>
<string name="all_your_contacts_will_remain_connected_update_sent">ستبقى جميع جهات الاتصال الخاصة بك متصلة. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك.</string>
<string name="add_address_to_your_profile">أضف عنوانًا إلى ملف تعريفك، حتى تتمكن جهات اتصالك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</string>
<string name="all_your_contacts_will_remain_connected_update_sent">ستبقى جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</string>
<string name="settings_section_title_icon">رمز التطبيق</string>
<string name="address_section_title">عنوان</string>
<string name="allow_your_contacts_irreversibly_delete">اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string>
@@ -96,7 +96,7 @@
<string name="both_you_and_your_contact_can_add_message_reactions">يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل.</string>
<string name="both_you_and_your_contact_can_send_disappearing">يمكنك أنت وجهة اتصالك إرسال رسائل تختفي.</string>
<string name="icon_descr_call_progress">مكالمتك تحت الإجراء</string>
<string name="cannot_receive_file">لا يمكّن استلام الملف</string>
<string name="cannot_receive_file">لا يمكن استلام الملف</string>
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>جيد للبطارية</b>. يتحقق التطبيق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]></string>
<string name="bold_text">عريض</string>
<string name="audio_call_no_encryption">مكالمات الصوت (ليست مُعمّاة بين الطرفين)</string>
@@ -135,7 +135,7 @@
<string name="app_passcode_replaced_with_self_destruct">يتم استبدال رمز مرور التطبيق برمز مرور التدمير الذاتي.</string>
<string name="v4_6_audio_video_calls">مكالمات الصوت والفيديو</string>
<string name="callstatus_error">خطأ في الاتصال</string>
<string name="turning_off_service_and_periodic">تحسين البطارية نشط ، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تمكينها عبر الإعدادات.</string>
<string name="turning_off_service_and_periodic">تحسين البطارية نشط، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تفعيلها عبر الإعدادات.</string>
<string name="database_initialization_error_title">لا يمكن تهيئة قاعدة البيانات</string>
<string name="attach">إرفاق</string>
<string name="icon_descr_asked_to_receive">طلب لاستلام الصورة</string>
@@ -182,7 +182,7 @@
<string name="encrypted_database">قاعدة البيانات مُعمّاة</string>
<string name="rcv_group_event_changed_member_role">غيرت دور %s إلى %s</string>
<string name="switch_receiving_address">تغيير عنوان الاستلام</string>
<string name="failed_to_create_user_title">خطأ في إنشاء الملف الشخصي!</string>
<string name="failed_to_create_user_title">خطأ في إنشاء ملف التعريف!</string>
<string name="connection_error">خطأ في الإتصال</string>
<string name="connection_timeout">انتهت مهلة الاتصال</string>
<string name="contact_already_exists">جهة الاتصال موجودة بالفعل</string>
@@ -303,7 +303,7 @@
<string name="la_enter_app_passcode">أدخل عبارة المرور</string>
<string name="your_chats">الدردشات</string>
<string name="icon_descr_server_status_connected">متصل</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا الإجراء!</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا!</string>
<string name="maximum_supported_file_size">الحد الأقصى لحجم الملف المدعوم حاليًا هو %1$s.</string>
<string name="connect_via_link_or_qr">تواصل عبر الرابط / رمز QR</string>
<string name="share_one_time_link">إنشاء رابط دعوة لمرة واحدة</string>
@@ -314,7 +314,7 @@
<string name="colored_text">ملون</string>
<string name="status_contact_has_e2e_encryption">لدى جهة الاتصال التعمية بين الطريفين</string>
<string name="create_profile_button">إنشاء</string>
<string name="create_your_profile">إنشاء ملف تعريف</string>
<string name="create_your_profile">أنشئ ملف تعريفك</string>
<string name="icon_descr_call_connecting">مكالمة جارية...</string>
<string name="enable_self_destruct">تفعيل التدمير الذاتي</string>
<string name="conn_event_ratchet_sync_started">الموافقة على التعمية…</string>
@@ -409,11 +409,11 @@
<string name="icon_descr_expand_role">توسيع تحديد الدور</string>
<string name="group_invitation_expired">انتهت صلاحية دعوة المجموعة</string>
<string name="alert_title_no_group">المجموعة غير موجودة!</string>
<string name="export_theme">تصدير السمة</string>
<string name="export_theme">صدّر السمة</string>
<string name="files_and_media">الملفات والوسائط</string>
<string name="icon_descr_flip_camera">قلب الكاميرا</string>
<string name="delete_group_for_all_members_cannot_undo_warning">سيتم حذف المجموعة لجميع الأعضاء - لا يمكن التراجع عن هذا!</string>
<string name="group_members_can_send_dms">يمكن لأعضاء المجموعة إرسال رسائل مباشرة.</string>
<string name="group_members_can_send_dms">يمكن للأعضاء إرسال رسائل مباشرة.</string>
<string name="failed_to_parse_chats_title">فشل تحميل الدردشات</string>
<string name="email_invite_body">أهلاً!
\nتواصل معي عبر SimpleX Chat: %s</string>
@@ -422,8 +422,8 @@
<string name="icon_descr_file">الملف</string>
<string name="snd_group_event_group_profile_updated">حُدّث ملف تعريف المجموعة</string>
<string name="group_display_name_field">أدخل اسم المجموعة:</string>
<string name="group_members_can_send_voice">يمكن لأعضاء المجموعة إرسال رسائل صوتية.</string>
<string name="files_are_prohibited_in_group">الملفات والوسائط ممنوعة في هذه المجموعة.</string>
<string name="group_members_can_send_voice">يمكن للأعضاء إرسال رسائل صوتية.</string>
<string name="files_are_prohibited_in_group">الملفات والوسائط ممنوعة.</string>
<string name="v4_6_group_welcome_message">رسالة ترحيب المجموعة</string>
<string name="v4_6_reduced_battery_usage">مزيد من تقليل استخدام البطارية</string>
<string name="info_row_group">المجموعة</string>
@@ -433,16 +433,16 @@
<string name="v4_4_french_interface">الواجهة الفرنسية</string>
<string name="settings_section_title_help">المساعدة</string>
<string name="group_member_status_group_deleted">حُذِفت المجموعة</string>
<string name="group_members_can_send_disappearing">يمكن لأعضاء المجموعة إرسال رسائل تختفي.</string>
<string name="group_members_can_send_disappearing">يمكن للأعضاء إرسال رسائل تختفي.</string>
<string name="v4_6_group_moderation">إشراف المجموعة</string>
<string name="v5_1_message_reactions_descr">أخيرا، لدينا منهم! 🚀</string>
<string name="export_database">تصدير قاعدة البيانات</string>
<string name="export_database">صدّر قاعدة البيانات</string>
<string name="section_title_for_console">لوحدة التحكم</string>
<string name="settings_experimental_features">الميزات التجريبية</string>
<string name="settings_section_title_experimenta">تجريبي</string>
<string name="icon_descr_group_inactive">المجموعة غير نشطة</string>
<string name="files_and_media_section">الملفات والوسائط</string>
<string name="group_members_can_delete">يمكن لأعضاء المجموعة حذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string>
<string name="group_members_can_delete">يمكن للأعضاء حذف الرسائل المُرسلة بشكل لا رجعة فيه. (24 ساعة)</string>
<string name="fix_connection_not_supported_by_contact">الإصلاح غير مدعوم من قبل جهة الاتصال</string>
<string name="group_profile_is_stored_on_members_devices">يُخزّن ملف تعريف المجموعة على أجهزة الأعضاء، وليس على الخوادم.</string>
<string name="v4_2_group_links">روابط المجموعة</string>
@@ -450,25 +450,25 @@
<string name="full_name__field">الاسم الكامل:</string>
<string name="alert_message_group_invitation_expired">لم تعد دعوة المجموعة صالحة، تمت أُزيلت بواسطة المرسل.</string>
<string name="group_link">رابط المجموعة</string>
<string name="file_will_be_received_when_contact_is_online">سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string>
<string name="file_will_be_received_when_contact_is_online">سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string>
<string name="group_full_name_field">الاسم الكامل للمجموعة:</string>
<string name="simplex_link_mode_full">رابط كامل</string>
<string name="choose_file">ملف</string>
<string name="delete_group_for_self_cannot_undo_warning">سيتم حذف المجموعة لك - لا يمكن التراجع عن هذا!</string>
<string name="failed_to_parse_chat_title">فشل تحميل الدردشة</string>
<string name="group_members_can_add_message_reactions">يمكن لأعضاء المجموعة إضافة ردود فعل الرسالة.</string>
<string name="group_members_can_add_message_reactions">يمكن للأعضاء إضافة ردود الفعل على الرسائل.</string>
<string name="favorite_chat">المفضل</string>
<string name="notification_preview_mode_hidden">مخفي</string>
<string name="file_saved">حُفظ الملف</string>
<string name="revoke_file__message">سيتم حذف الملف من الخوادم.</string>
<string name="file_will_be_received_when_contact_completes_uploading">سيتم استلام الملف عند اكتمال تحميل جهة الاتصال الخاصة بك.</string>
<string name="file_will_be_received_when_contact_completes_uploading">سيتم استلام الملف عندما يكتمل جهة اتصالك من رفعِها.</string>
<string name="icon_descr_help">المساعدة</string>
<string name="file_with_path">الملف: %s</string>
<string name="fix_connection_confirm">إصلاح</string>
<string name="fix_connection">إصلاح الاتصال</string>
<string name="fix_connection_question">إصلاح الاتصال؟</string>
<string name="fix_connection_not_supported_by_group_member">الإصلاح غير مدعوم من قبل أعضاء المجموعة</string>
<string name="group_members_can_send_files">يمكن لأعضاء المجموعة إرسال الملفات والوسائط.</string>
<string name="group_members_can_send_files">يمكن للأعضاء إرسال الملفات والوسائط.</string>
<string name="group_preferences">تفضيلات المجموعة</string>
<string name="v5_0_large_files_support_descr">سريع ولا تنتظر حتى يصبح المرسل متصلاً بالإنترنت!</string>
<string name="hide_verb">إخفاء</string>
@@ -483,7 +483,7 @@
<string name="import_database">استيراد قاعدة بيانات</string>
<string name="custom_time_unit_hours">ساعات</string>
<string name="edit_history">السجل</string>
<string name="image_will_be_received_when_contact_completes_uploading">سيتم استلام الصورة عند اكتمال تحميل جهة اتصالك.</string>
<string name="image_will_be_received_when_contact_completes_uploading">سيتم استلام الصورة عندما يكتمل جهة اتصالك من رفعِها.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، <b>اعرض رمز QR في مكالمة الفيديو</b>، أو شارك الرابط.]]></string>
<string name="install_simplex_chat_for_terminal">ثبّت SimpleX Chat لطرفية</string>
<string name="network_disable_socks_info">إذا قمت بالتأكيد، فستتمكن خوادم المراسلة من رؤية عنوان IP الخاص بك ومزود الخدمة الخاص بك - أي الخوادم التي تتصل بها.</string>
@@ -511,7 +511,7 @@
<string name="description_via_one_time_link_incognito">التخفي عبر رابط لمرة واحدة</string>
<string name="icon_descr_image_snd_complete">أرسلت صورة</string>
<string name="image_descr">صورة</string>
<string name="image_will_be_received_when_contact_is_online">سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string>
<string name="image_will_be_received_when_contact_is_online">سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string>
<string name="image_saved">حُفظت الصورة في المعرض</string>
<string name="gallery_image_button">صورة</string>
<string name="if_you_cant_meet_in_person">إذا لم تتمكن من الالتقاء شخصيًا، اعرض رمز QR في مكالمة الفيديو، أو شارك الرابط.</string>
@@ -526,7 +526,7 @@
<string name="onboarding_notifications_mode_service">فوري</string>
<string name="host_verb">المضيف</string>
<string name="hide_notification">إخفاء</string>
<string name="turn_off_battery_optimization"><![CDATA[من أجل استخدامها، <b>يُرجى السماح لSimpleX للتشغيل في الخلفية</b> في مربع الحوار التالي. وإلا، سيتم تعطيل الإشعارات.]]></string>
<string name="turn_off_battery_optimization"><![CDATA[<b>السماح بذلك</b> في مربع الحوار التالي لتلقي الإشعارات على الفور.]]></string>
<string name="in_reply_to">ردًا على</string>
<string name="icon_descr_instant_notifications">إشعارات فورية</string>
<string name="enter_one_ICE_server_per_line">خوادم ICE (واحد لكل سطر)</string>
@@ -534,7 +534,7 @@
<string name="hide_profile">إخفاء ملف التعريف</string>
<string name="how_to_use_markdown">كيفية استخدام ماركداون</string>
<string name="if_you_enter_self_destruct_code">إذا أدخلت رمز مرور التدمير الذاتي أثناء فتح التطبيق:</string>
<string name="onboarding_notifications_mode_subtitle">يمكن تغييره لاحقًا عبر الإعدادات.</string>
<string name="onboarding_notifications_mode_subtitle">كيف يؤثر على البطارية</string>
<string name="join_group_button">انضمام</string>
<string name="theme_light">فاتح</string>
<string name="display_name_invited_to_connect">مدعو للتواصل</string>
@@ -554,13 +554,13 @@
<string name="invite_friends">دعوة الأصدقاء</string>
<string name="keychain_error">خطأ في Keychain</string>
<string name="invite_to_group_button">دعوة للمجموعة</string>
<string name="message_deletion_prohibited_in_chat">يٌمنع حذف الرسائل بشكل لا رجعة فيه في هذه المجموعة.</string>
<string name="message_deletion_prohibited_in_chat">يٌمنع حذف الرسائل بشكل لا رجعة فيه.</string>
<string name="invalid_message_format">تنسيق الرسالة غير صالح</string>
<string name="invalid_data">البيانات غير صالحة</string>
<string name="users_delete_data_only">بيانات الملف الشخصي المحلية فقط</string>
<string name="users_delete_data_only">بيانات ملف التعريف المحلية فقط</string>
<string name="message_deletion_prohibited">يٌمنع حذف الرسائل بشكل لا رجعة فيه في هذه الدردشة.</string>
<string name="button_add_members">دعوة الأعضاء</string>
<string name="button_leave_group">مغادرة المجموعة</string>
<string name="button_leave_group">غادِر المجموعة</string>
<string name="info_row_local_name">الاسم المحلي:</string>
<string name="rcv_group_event_member_left">غادر</string>
<string name="incognito_info_allows">يسمح بوجود العديد من الاتصالات المجهولة دون مشاركة أي بيانات بينهم في ملف تعريف دردشة واحد.</string>
@@ -603,7 +603,7 @@
<string name="smp_server_test_download_file">نزّل الملف</string>
<string name="auth_disable_simplex_lock">تعطيل قفل SimpleX</string>
<string name="edit_verb">تحرير</string>
<string name="display_name__field">اسم الملف الشخصي:</string>
<string name="display_name__field">اسم ملف التعريف:</string>
<string name="icon_descr_email">البريد الإلكتروني</string>
<string name="display_name">أدخل أسمك:</string>
<string name="integrity_msg_duplicate">كرر الرسالة</string>
@@ -613,7 +613,7 @@
<string name="icon_descr_edited">حُرر</string>
<string name="downgrade_and_open_chat">الرجوع إلى إصدار سابق وفتح الدردشة</string>
<string name="direct_messages">رسائل مباشرة</string>
<string name="disappearing_messages_are_prohibited">الرسائل المختفية ممنوعة في هذه المجموعة.</string>
<string name="disappearing_messages_are_prohibited">الرسائل المختفية ممنوعة.</string>
<string name="button_edit_group_profile">تحرير ملف تعريف المجموعة</string>
<string name="dont_show_again">لا تُظهر مرة أخرى</string>
<string name="settings_section_title_device">الجهاز</string>
@@ -651,7 +651,7 @@
<string name="dont_create_address">لا تنشئ عنوانًا</string>
<string name="error_setting_network_config">خطأ في تحديث تضبيط الشبكة</string>
<string name="error_receiving_file">خطأ في استلام الملف</string>
<string name="failed_to_active_user_title">خطأ في تبديل الملف الشخصي!</string>
<string name="failed_to_active_user_title">خطأ في تبديل ملف التعريف!</string>
<string name="v5_2_fix_encryption">حافظ على اتصالاتك</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">تأكد من أن عناوين خادم XFTP بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة.</string>
<string name="marked_deleted_description">عُلّم محذوف</string>
@@ -683,7 +683,7 @@
<string name="error_starting_chat">خطأ في بدء الدردشة</string>
<string name="error_exporting_chat_database">خطأ في تصدير قاعدة بيانات الدردشة</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">ستتم إزالة العضو من المجموعة - لا يمكن التراجع عن هذا!</string>
<string name="make_profile_private">اجعل الملف الشخصي خاصًا!</string>
<string name="make_profile_private">اجعل ملف التعريف خاصًا!</string>
<string name="v5_2_favourites_filter_descr">تصفية الدردشات غير المقروءة والمفضلة.</string>
<string name="v5_2_favourites_filter">البحث عن الدردشات بشكل أسرع</string>
<string name="enable_receipts_all">تفعيل</string>
@@ -734,7 +734,7 @@
\n- و اكثر!</string>
<string name="network_status">حالة الشبكة</string>
<string name="user_mute">كتم</string>
<string name="message_reactions_are_prohibited">ردود الفعل الرسائل ممنوعة في هذه المجموعة.</string>
<string name="message_reactions_are_prohibited">ردود الفعل الرسائل ممنوعة.</string>
<string name="icon_descr_more_button">المزيد</string>
<string name="network_settings_title">إعدادات متقدّمة</string>
<string name="icon_descr_call_missed">مكالمة فائتة</string>
@@ -757,9 +757,7 @@
<string name="network_use_onion_hosts_prefer_desc">سيتم استخدام مضيفات البصل عند توفرها.</string>
<string name="network_use_onion_hosts_no_desc">لن يتم استخدام مضيفات البصل.</string>
<string name="no_contacts_selected">لم تٌحدد جهات اتصال</string>
<string name="v4_6_group_moderation_descr">يمكّن للمشرف الآن:
\n- حذف رسائل الأعضاء.
\n- تعطيل الأعضاء (دور "المراقب")</string>
<string name="v4_6_group_moderation_descr">يمكن للمشرف الآن:\n- حذف رسائل الأعضاء.\n- تعطيل الأعضاء (دور المراقب)</string>
<string name="settings_notifications_mode_title">خدمة الإشعار</string>
<string name="chat_preferences_off">غير مفعّل</string>`
<string name="chat_preferences_on">مفعل</string>
@@ -793,7 +791,7 @@
<string name="passcode_set">تم تعيين كلمة المرور!</string>
<string name="group_member_role_owner">المالك</string>
<string name="only_your_contact_can_send_disappearing">فقط جهة اتصالك يمكنها إرسال رسائل تختفي.</string>
<string name="only_your_contact_can_add_message_reactions">جهة اتصالك فقط يمكنها إضافة تفاعلات على الرسالة</string>
<string name="only_your_contact_can_add_message_reactions">جهة اتصالك فقط يمكنها إضافة ردود الفعل على الرسالة</string>
<string name="only_group_owners_can_change_prefs">فقط مالكي المجموعة يمكنهم تغيير تفضيلات المجموعة.</string>
<string name="only_your_contact_can_delete">جهة اتصالك فقط يمكنها حذف الرسائل بشكل لا رجعة فيه (يمكنك تعليم الرسالة للحذف). (24 ساعة)</string>
<string name="only_you_can_send_voice">أنت فقط يمكنك إرسال رسائل صوتية.</string>
@@ -806,7 +804,7 @@
<string name="restore_passphrase_not_found_desc">كلمة المرور غير موجودة في مخزن المفاتيح، يرجى إدخالها يدوياً. قد يحدث هذا إذا قمت باستعادة ملفات التطبيق باستخدام أداة استرجاع بيانات. إذا لم يكن الأمر كذلك، تواصل مع المبرمجين رجاء</string>
<string name="open_chat">افتح الدردشة</string>
<string name="simplex_link_mode_browser_warning">فتح الرابط في المتصفح قد يقلل خصوصية وحماية اتصالك. الروابط غير الموثوقة من SimpleX ستكون باللون الأحمر</string>
<string name="only_you_can_add_message_reactions">أنت فقط يمكنك إضافة تفاعل على الرسالة.</string>
<string name="only_you_can_add_message_reactions">أنت فقط يمكنك إضافة ردود الفعل على الرسالة.</string>
<string name="only_you_can_delete_messages">أنت فقط يمكنك حذف الرسائل بشكل لا رجعة فيه (يمكن للمستلم تعليمها للحذف). (24 ساعة)</string>
<string name="only_you_can_send_disappearing">أنت فقط يمكنك إرسال رسائل تختفي</string>
<string name="only_you_can_make_calls">أنت فقط يمكنك إجراء المكالمات.</string>
@@ -819,7 +817,7 @@
<string name="call_connection_peer_to_peer">ندّ لِندّ</string>
<string name="people_can_connect_only_via_links_you_share">أنت تقرر من يمكنه الاتصال.</string>
<string name="icon_descr_call_pending_sent">مكالمة قيد الانتظار</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل المُرسلة باستخدام <b>تعمية ثنائية الطبقات من بين الطريفين</b>.]]></string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل.</string>
<string name="reset_color">صفّر الألوان</string>
<string name="save_verb">حفظ</string>
<string name="smp_servers_preset_address">عنوان الخادم المُعد مسبقًا</string>
@@ -829,7 +827,7 @@
<string name="receiving_via">الاستلام عبر</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">يُرجى التحقق من استخدامك للرابط الصحيح أو اطلب من جهة اتصالك أن ترسل لك رابطًا آخر.</string>
<string name="periodic_notifications_disabled">الإشعارات الدورية مُعطَّلة</string>
<string name="image_descr_profile_image">صورة الملف الشخصي</string>
<string name="image_descr_profile_image">صورة ملف التعريف</string>
<string name="onboarding_notifications_mode_title">الإشعارات خاصة</string>
<string name="store_passphrase_securely_without_recover">يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها.</string>
<string name="contact_developers">يُرجى تحديث التطبيق والتواصل مع المطورين.</string>
@@ -862,7 +860,7 @@
<string name="restore_database_alert_desc">الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء.</string>
<string name="restore_database_alert_title">استعادة النسخة الاحتياطية لقاعدة البيانات؟</string>
<string name="network_options_save">حفظ</string>
<string name="users_delete_with_connections">اتصالات الملف الشخصي والخادم</string>
<string name="users_delete_with_connections">اتصالات ملف التعريف والخادم</string>
<string name="prohibit_message_reactions">منع ردود فعل الرسالة.</string>
<string name="prohibit_sending_voice">منع إرسال الرسائل الصوتية.</string>
<string name="prohibit_message_reactions_group">منع ردود فعل الرسائل.</string>
@@ -884,7 +882,7 @@
<string name="v4_4_live_messages_desc">يرى المستلمون التحديثات أثناء كتابتها.</string>
<string name="feature_received_prohibited">استلمت، ممنوع</string>
<string name="save_servers_button">حفظ</string>
<string name="profile_update_will_be_sent_to_contacts">سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك.</string>
<string name="profile_update_will_be_sent_to_contacts">سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</string>
<string name="save_and_notify_contacts">حفظ وإشعار جهات الاتصال</string>
<string name="save_and_update_group_profile">حفظ وتحديث ملف تعريف المجموعة</string>
<string name="network_option_ping_count">عدد البينج</string>
@@ -900,7 +898,7 @@
<string name="remove_member_confirmation">إزالة</string>
<string name="network_options_reset_to_defaults">صفّر إلى الإعدادات الافتراضية</string>
<string name="network_option_ping_interval">بينج الفاصل الزمني</string>
<string name="profile_password">كلمة مرور الملف الشخصي</string>
<string name="profile_password">كلمة مرور ملف التعريف</string>
<string name="prohibit_sending_disappearing_messages">منع إرسال الرسائل التي تختفي.</string>
<string name="network_option_protocol_timeout">مهلة البروتوكول</string>
<string name="network_option_protocol_timeout_per_kb">مهلة البروتوكول لكل كيلوبايت</string>
@@ -924,7 +922,7 @@
<string name="revoke_file__title">سحب وصول الملف؟</string>
<string name="toast_permission_denied">رٌفض الإذن!</string>
<string name="ask_your_contact_to_enable_voice">يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية.</string>
<string name="icon_descr_profile_image_placeholder">العنصر النائب لصورة الملف الشخصي</string>
<string name="icon_descr_profile_image_placeholder">العنصر النائب لصورة ملف التعريف</string>
<string name="image_descr_qr_code">رمز QR</string>
<string name="reset_verb">صفّر</string>
<string name="network_proxy_port">المنفذ %d</string>
@@ -1016,7 +1014,7 @@
<string name="smp_servers">خوادم SMP</string>
<string name="share_image">مشاركة الوسائط…</string>
<string name="ntf_channel_messages">رسائل SimpleX Chat</string>
<string name="lock_not_enabled">لم يتم تمكين قفل SimpleX!</string>
<string name="lock_not_enabled">قفل SimpleX غير مفعّل!</string>
<string name="auth_stop_chat">إيقاف الدردشة</string>
<string name="stop_rcv_file__title">التوقف عن استلام الملف؟</string>
<string name="share_file">مشاركة الملف…</string>
@@ -1056,7 +1054,7 @@
<string name="show_developer_options">عرض خيارات المطور</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="error_smp_test_server_auth">يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور</string>
<string name="error_xftp_test_server_auth">يتطلب الخادم إذنًا للتحميل، تحقق من كلمة المرور</string>
<string name="error_xftp_test_server_auth">يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور</string>
<string name="notification_preview_mode_contact_desc">عرض جهة الاتصال فقط</string>
<string name="ntf_channel_calls">مكالمات SimpleX Chat</string>
<string name="simplex_service_notification_title">خدمة SimpleX Chat</string>
@@ -1084,14 +1082,13 @@
<string name="connection_you_accepted_will_be_cancelled">سيتم إلغاء الاتصال الذي قبلته!</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">لن تتمكن جهة الاتصال التي شاركت هذا الرابط معها من الاتصال!</string>
<string name="this_text_is_available_in_settings">هذا النص متاح في الإعدادات</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">لحماية الخصوصية، بدلاً من معرفات المستخدم التي تستخدمها جميع الأنظمة الأساسية الأخرى, يحتوي SimpleX على معرفات لقوائم انتظار الرسائل، منفصلة لكل جهة من جهات اتصالك.</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">لحماية معلوماتك، قم بتشغيل قفل SimpleX
\nسيُطلب منك إكمال المصادقة قبل تمكين هذه الميزة.</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">لحماية خصوصيتك، يستخدم SimpleX معرّفات منفصلة لكل جهة اتصال لديك.</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">لحماية معلوماتك، فعّل قفل SimpleX \nسيُطلب منك إكمال المصادقة قبل تفعيل هذه الميزة.</string>
<string name="network_session_mode_transport_isolation">عزل النقل</string>
<string name="v4_4_french_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string>
<string name="v4_6_audio_video_calls_descr">دعم البلوتوث وتحسينات أخرى.</string>
<string name="v5_0_polish_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[للحفاظ على خصوصيتك، بدلاً من دفع الإشعارات، يحتوي التطبيق على <b>خدمة SimpleX تعمل في الخلفية</b> – يستخدم نسبة قليلة من البطارية يوميًا.]]></string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[لتحسين الخصوصية، <b>يتم تشغيل SimpleX في الخلفية</b> بدلاً من استخدام إشعارات push.]]></string>
<string name="tap_to_start_new_chat">انقر لبدء محادثة جديدة</string>
<string name="to_share_with_your_contact">(للمشاركة مع جهة اتصالك)</string>
<string name="to_connect_via_link_title">للتواصل عبر الرابط</string>
@@ -1103,17 +1100,17 @@
<string name="color_title">العنوان الرئيسي</string>
<string name="moderate_message_will_be_marked_warning">سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء.</string>
<string name="group_invitation_tap_to_join">انقر للانضمام</string>
<string name="to_reveal_profile_enter_password">للكشف عن ملف التعريف المخفي الخاص بك، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك.</string>
<string name="to_reveal_profile_enter_password">للكشف عن ملف تعريفك المخفي، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك.</string>
<string name="group_invitation_tap_to_join_incognito">انقر للانضمام إلى وضع التخفي</string>
<string name="la_mode_system">النظام</string>
<string name="settings_section_title_themes">السمات</string>
<string name="v4_6_chinese_spanish_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string>
<string name="database_initialization_error_desc">قاعدة البيانات لا تعمل بشكل صحيح. انقر لمعرفة المزيد</string>
<string name="theme_colors_section_title">ألوان الواجهة</string>
<string name="tap_to_activate_profile">انقر لتنشيط الملف الشخصي.</string>
<string name="tap_to_activate_profile">انقر لتنشيط ملف التعريف.</string>
<string name="v4_5_transport_isolation">عزل النقل</string>
<string name="this_string_is_not_a_connection_link">هذه السلسلة ليست رابط اتصال!</string>
<string name="receipts_section_description">هذه الإعدادات لملف التعريف الحالي الخاص بك</string>
<string name="receipts_section_description">هذه الإعدادات لملف تعريفك الحالي</string>
<string name="receipts_section_description_1">يمكن تجاوزها في إعدادات الاتصال و المجموعة.</string>
<string name="network_option_tcp_connection_timeout">انتهت مهلة اتصال TCP</string>
<string name="v4_5_private_filenames_descr">لحماية المنطقة الزمنية، تستخدم ملفات الصور / الصوت التوقيت العالمي المنسق (UTC).</string>
@@ -1140,7 +1137,7 @@
<string name="trying_to_connect_to_server_to_receive_messages_with_error">محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s).</string>
<string name="la_notice_turn_on">تشغيل</string>
<string name="webrtc_ice_servers">خوادم WebRTC ICE</string>
<string name="alert_title_cant_invite_contacts_descr">أنت تستخدم ملفًا شخصيًا متخفيًا لهذه المجموعة - لمنع مشاركة ملفك الشخصي الرئيسي الذي يدعو جهات الاتصال غير مسموح به</string>
<string name="alert_title_cant_invite_contacts_descr">أنت تستخدم ملف تعريف متخفي لهذه المجموعة - لمنع مشاركة ملفك التعريفي الرئيسي الذي يدعو جهات الاتصال غير مسموح به</string>
<string name="snd_group_event_changed_member_role">غيّرتَ دور %s إلى %s</string>
<string name="chat_preferences_yes">نعم</string>
<string name="connected_to_server_to_receive_messages_from_contact">أنت متصل بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه.</string>
@@ -1155,23 +1152,23 @@
<string name="call_connection_via_relay">عبر المُرحل</string>
<string name="you_joined_this_group">لقد انضممت إلى هذه المجموعة</string>
<string name="you_rejected_group_invitation">لقد رفضت دعوة المجموعة</string>
<string name="incognito_info_share">عندما تشارك ملفًا شخصيًا متخفيًا مع شخص ما، فسيتم استخدام هذا الملف الشخصي للمجموعات التي يدعوك إليها.</string>
<string name="incognito_info_share">عندما تشارك ملف تعريف متخفي مع شخص ما، فسيتم استخدام هذا الملف التعريفي للمجموعات التي يدعوك إليها.</string>
<string name="failed_to_create_user_duplicate_desc">لديك بالفعل ملف تعريف دردشة بنفس اسم العرض. الرجاء اختيار اسم آخر.</string>
<string name="you_are_already_connected_to_vName_via_this_link">أنت متصل بالفعل بـ%1$s.</string>
<string name="waiting_for_video">في انتظار الفيديو</string>
<string name="video_will_be_received_when_contact_completes_uploading">سيتم استلام الفيديو عند اكتمال تحميل جهة اتصالك.</string>
<string name="video_will_be_received_when_contact_completes_uploading">سيتم استلام الفيديو عند اكتمال رفع جهة اتصالك.</string>
<string name="verify_security_code">تحقق من رمز الأمان</string>
<string name="v4_3_voice_messages">رسائل صوتية</string>
<string name="you_can_accept_or_reject_connection">عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه.</string>
<string name="you_will_be_connected_when_group_host_device_is_online">سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">سوف تكون متصلاً عندما يتم قبول طلب الاتصال الخاص بك، يرجى الانتظار أو التحقق لاحقًا!</string>
<string name="you_will_be_connected_when_group_host_device_is_online">سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">سوف تكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا!</string>
<string name="using_simplex_chat_servers">تستخدم خوادم SimpleX Chat.</string>
<string name="network_socks_toggle_use_socks_proxy">استخدم وكيل SOCKS</string>
<string name="network_use_onion_hosts">استخدم مضيفي onion.</string>
<string name="network_enable_socks">استخدام وكيل SOCKS؟</string>
<string name="network_use_onion_hosts_prefer">عندما تكون متاحة</string>
<string name="your_contacts_will_remain_connected">ستبقى جهات اتصالك متصلة.</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">لا نقوم بتخزين أي من جهات الاتصال أو الرسائل الخاصة بك (بمجرد تسليمها) على الخوادم.</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">لا نقوم بتخزين أي من جهات اتصالك أو رسائلك (بمجرد تسليمها) على الخوادم.</string>
<string name="you_can_use_markdown_to_format_messages__prompt">يمكنك استخدام تخفيض السعر لتنسيق الرسائل:</string>
<string name="use_chat">استخدم الدردشة</string>
<string name="settings_section_title_you">أنت</string>
@@ -1206,10 +1203,10 @@
<string name="description_via_one_time_link">عبر رابط لمرة واحدة</string>
<string name="video_call_no_encryption">مكالمة الفيديو ليست مُعمّاة بين الطريفين</string>
<string name="snd_conn_event_switch_queue_phase_completed">غيّرتَ العنوان</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">سوف تكون متصلاً عندما يكون جهاز جهة الاتصال الخاصة بك متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">سوف تكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string>
<string name="snd_group_event_user_left">غادرت</string>
<string name="you_must_use_the_most_recent_version_of_database">يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال.</string>
<string name="video_will_be_received_when_contact_is_online">سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string>
<string name="video_will_be_received_when_contact_is_online">سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string>
<string name="you_can_share_this_address_with_your_contacts">يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s.</string>
<string name="snd_group_event_member_deleted">أُزيلت %1$s</string>
<string name="update_database">تحديث</string>
@@ -1238,11 +1235,11 @@
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">سيتم حذف قاعدة بيانات الدردشة الحالية واستبدالها بالقاعدة المستوردة.
\nلا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string>
<string name="update_database_passphrase">تحديث عبارة مرور قاعدة البيانات</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ سجل الدردشة.</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ بسجل الدردشة.</string>
<string name="custom_time_unit_weeks">أسابيع</string>
<string name="you_can_hide_or_mute_user_profile">يمكنك إخفاء أو كتم ملف تعريف المستخدم - اضغط مطولاً للقائمة.</string>
<string name="whats_new">ما هو الجديد</string>
<string name="your_current_profile">ملفك الشخصي الحالي</string>
<string name="your_current_profile">ملف تعريفك الحالي</string>
<string name="simplex_link_connection">عبر %1$s</string>
<string name="icon_descr_received_msg_status_unread">غير مقروءة</string>
<string name="welcome">مرحبًا!</string>
@@ -1252,7 +1249,7 @@
<string name="gallery_video_button">فيديو</string>
<string name="you_can_share_your_address">يمكنك مشاركة عنوانك كرابط أو رمز QR - يمكن لأي شخص الاتصال بك.</string>
<string name="you_can_create_it_later">يمكنك إنشاؤه لاحقًا</string>
<string name="invite_prohibited_description">أنت تحاول دعوة جهة اتصال قمت بمشاركة ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملفك الشخصي الرئيسي</string>
<string name="invite_prohibited_description">أنت تحاول دعوة جهة اتصال شاركت ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملف تعريفك الرئيسي</string>
<string name="user_unmute">ألغِ الكتم</string>
<string name="unmute_chat">ألغِ الكتم</string>
<string name="you_accepted_connection">لقد قبلت الاتصال</string>
@@ -1274,7 +1271,7 @@
\n- الوقت المخصص لتختفي.
\n- تحرير التاريخ.</string>
<string name="you_can_enable_delivery_receipts_later">يمكنك تفعيلة لاحقًا عبر الإعدادات</string>
<string name="you_can_enable_delivery_receipts_later_alert">يمكنك تمكينها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق.</string>
<string name="you_can_enable_delivery_receipts_later_alert">يمكنك تفعيلها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق.</string>
<string name="description_via_group_link">عبر رابط المجموعة</string>
<string name="description_you_shared_one_time_link_incognito">لقد شاركت رابط لمرة واحدة متخفي</string>
<string name="simplex_link_mode_browser">عبر المتصفح</string>
@@ -1287,11 +1284,11 @@
<string name="your_chat_profile_will_be_sent_to_your_contact">سيتم إرسال ملف تعريف الدردشة الخاص بك
\nإلى جهة اتصالك</string>
<string name="user_unhide">إلغاء الإخفاء</string>
<string name="incognito_random_profile">ملفك الشخصي العشوائي</string>
<string name="you_will_still_receive_calls_and_ntfs">ستستمر في استلام المكالمات والإشعارات من الملفات الشخصية المكتومة عندما تكون نشطة.</string>
<string name="incognito_random_profile">ملفك التعريفي العشوائي</string>
<string name="you_will_still_receive_calls_and_ntfs">ستستمر في استلام المكالمات والإشعارات من الملفات التعريفية المكتومة عندما تكون نشطة.</string>
<string name="chat_preferences_you_allow">انت تسمح بها</string>
<string name="icon_descr_video_call">مكالمة فيديو</string>
<string name="voice_messages_are_prohibited">الرسائل الصوتية ممنوعة في هذه الدردشة.</string>
<string name="voice_messages_are_prohibited">الرسائل الصوتية ممنوعة.</string>
<string name="auth_unlock">فتح القفل</string>
<string name="smp_server_test_upload_file">رفع الملف</string>
<string name="la_could_not_be_verified">لا يمكن التحقق منك؛ الرجاء المحاولة مرة اخرى.</string>
@@ -1299,7 +1296,7 @@
<string name="voice_message_send_text">رسالة صوتية…</string>
<string name="group_preview_you_are_invited">أنت مدعو إلى المجموعة</string>
<string name="observer_cant_send_message_title">لا يمكنك إرسال رسائل!</string>
<string name="you_need_to_allow_to_send_voice">تحتاج إلى السماح لجهة الاتصال الخاصة بك بإرسال رسائل صوتية لتتمكن من إرسالها.</string>
<string name="you_need_to_allow_to_send_voice">تحتاج إلى السماح لجهة اتصالك بإرسال رسائل صوتية لتتمكن من إرسالها.</string>
<string name="contact_sent_large_file">أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s).</string>
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[يمكنك <font color="#0088ff">الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.]]></string>
<string name="smp_servers_your_server">خادمك</string>
@@ -1334,7 +1331,7 @@
<string name="system_restricted_background_desc">لا يمكن تشغيل SimpleX في الخلفية. ستستلم الإشعارات فقط عندما يكون التطبيق قيد التشغيل.</string>
<string name="connect__a_new_random_profile_will_be_shared">سيتم مشاركة ملف تعريف عشوائي جديد.</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">ألصق الرابط المُستلَم للتواصل مع جهة اتصالك…</string>
<string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك الشخصي %1$s.</string>
<string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك التعريفي %1$s.</string>
<string name="system_restricted_background_in_call_desc">قد يغلق التطبيق بعد دقيقة واحدة في الخلفية.</string>
<string name="turn_off_battery_optimization_button">سماح</string>
<string name="system_restricted_background_in_call_title">لا مكالمات في الخلفية</string>
@@ -1388,9 +1385,9 @@
<string name="blocked_item_description">محظور</string>
<string name="v5_4_block_group_members">حظر أعضاء المجموعة</string>
<string name="rcv_direct_event_contact_deleted">جهة الاتصال حُذفت</string>
<string name="v5_4_incognito_groups_descr">أنشِئ مجموعة باستخدام ملف تعريف عشوائي.</string>
<string name="create_group_button">أنشِئ مجموعة</string>
<string name="create_another_profile_button">أنشِئ ملف تعريف</string>
<string name="v5_4_incognito_groups_descr">أنشئ مجموعة باستخدام ملف تعريف عشوائي.</string>
<string name="create_group_button">أنشئ مجموعة</string>
<string name="create_another_profile_button">أنشئ ملف تعريف</string>
<string name="connected_desktop">سطح المكتب متصل</string>
<string name="multicast_connect_automatically">اتصل تلقائيًا</string>
<string name="desktop_address">عنوان سطح المكتب</string>
@@ -1485,9 +1482,7 @@
<string name="verify_code_with_desktop">تحقق من الرمز مع سطح المكتب</string>
<string name="scan_qr_code_from_desktop">مسح رمز QR من سطح المكتب</string>
<string name="unblock_member_confirmation">إلغاء الحظر</string>
<string name="v5_4_more_things_descr">- إشعار اختياريًا جهات الاتصال المحذوفة.
\n- أسماء الملفات الشخصية بمسافات.
\n- و اكثر!</string>
<string name="v5_4_more_things_descr">- إشعار اختياريًا جهات الاتصال المحذوفة. \n- أسماء الملفات التعريفية بمسافات. \n- و اكثر!</string>
<string name="non_content_uri_alert_title">مسار الملف غير صالح</string>
<string name="connect_plan_you_have_already_requested_connection_via_this_address">لقد طلبت بالفعل الاتصال عبر هذا العنوان!</string>
<string name="terminal_always_visible">إظهار وحدة التحكم في نافذة جديدة</string>
@@ -1522,7 +1517,7 @@
<string name="you_can_view_invitation_link_again">يمكنك عرض رابط الدعوة مرة أخرى في تفاصيل الاتصال.</string>
<string name="keep_unused_invitation_question">أبقِ الدعوة غير المستخدمة؟</string>
<string name="share_this_1_time_link">شارك رابط الدعوة هذا لمرة واحدة</string>
<string name="create_group_button_to_create_new_group"><![CDATA[<b>أنشِئ مجموعة</b>: لإنشاء مجموعة جديدة.]]></string>
<string name="create_group_button_to_create_new_group"><![CDATA[<b>أنشئ مجموعة</b>: لإنشاء مجموعة جديدة.]]></string>
<string name="recent_history">التاريخ المرئي</string>
<string name="la_app_passcode">رمز مرور التطبيق</string>
<string name="new_chat">دردشة جديدة</string>
@@ -1550,7 +1545,7 @@
<string name="remote_host_error_inactive"><![CDATA[الجوال <b>%s</b> غير نشط]]></string>
<string name="show_slow_api_calls">أظهر مكالمات API البطيئة</string>
<string name="group_member_status_unknown_short">غير معروف</string>
<string name="profile_update_event_updated_profile">حدّثت الملف الشخصي</string>
<string name="profile_update_event_updated_profile">حدّثت ملف التعريف</string>
<string name="remote_host_error_missing"><![CDATA[الجوال <b>%s</b> مفقود]]></string>
<string name="remote_host_error_bad_version"><![CDATA[الجوال <b>%s</b> لديه إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين]]></string>
<string name="remote_host_error_bad_state"><![CDATA[الاتصال بالجوال <b>%s</b> في حالة سيئة]]></string>
@@ -1575,9 +1570,9 @@
<string name="developer_options_section">خيارات المطور</string>
<string name="profile_update_event_member_name_changed">تغيّر العضو %1$s إلى %2$s</string>
<string name="profile_update_event_removed_address">أزلت عنوان الاتصال</string>
<string name="profile_update_event_removed_picture">أزلت الصورة الشخصية</string>
<string name="profile_update_event_removed_picture">أزلت صورة ملف التعريف</string>
<string name="profile_update_event_set_new_address">عيّن عنوان جهة اتصال جديد</string>
<string name="profile_update_event_set_new_picture">عيّن صورة شخصية جديدة</string>
<string name="profile_update_event_set_new_picture">عيّن صورة تعريفية جديدة</string>
<string name="group_member_status_unknown">حالة غير معروفة</string>
<string name="profile_update_event_contact_name_changed">تغيّر جهة الاتصال %1$s إلى %2$s</string>
<string name="possible_slow_function_desc">يستغرق تنفيذ الوظيفة وقتًا طويلاً جدًا: %1$d ثانية: %2$s</string>
@@ -1624,7 +1619,7 @@
<string name="v5_6_safer_groups_descr">يمكن للمشرفين حظر عضو للجميع.</string>
<string name="v5_6_app_data_migration">ترحيل بيانات التطبيق</string>
<string name="migrate_from_device_archiving_database">جارِ أرشفة قاعدة البيانات</string>
<string name="migrate_from_device_all_data_will_be_uploaded">سيتم تعمية جميع جهات الاتصال والمحادثات والملفات الخاصة بك بشكل آمن وتحميلها في أجزاء إلى مُرحلات XFTP التي ضبطت.</string>
<string name="migrate_from_device_all_data_will_be_uploaded">سيتم تعمية جميع جهات الاتصال والمحادثات والملفات الخاصة بك بشكل آمن ورفعها في أجزاء إلى مُرحلات XFTP التي ضُبطت.</string>
<string name="migrate_to_device_apply_onion">طبّق</string>
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>يُرجى ملاحظة</b>: استخدام نفس قاعدة البيانات على جهازين سيؤدي إلى كسر فك تعمية الرسائل من اتصالاتك، كحماية أمنية.]]></string>
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>تحذير</b>: سيتم حذف الأرشيف.]]></string>
@@ -1658,7 +1653,7 @@
<string name="paste_archive_link">ألصق رابط الأرشيف</string>
<string name="migrate_to_device_try_again">يمكنك إعطاء محاولة أخرى.</string>
<string name="migrate_to_device_error_downloading_archive">حدث خطأ أثناء تنزيل الأرشيف</string>
<string name="migrate_from_device_exported_file_doesnt_exist">الملف المُصدر غير موجود</string>
<string name="migrate_from_device_exported_file_doesnt_exist">الملف المُصدّر غير موجود</string>
<string name="migrate_from_device_verify_passphrase">تحقق من عبارة المرور</string>
<string name="migrate_from_device_confirm_you_remember_passphrase">تأكد من أنك تتذكر عبارة مرور قاعدة البيانات لترحيلها.</string>
<string name="migrate_from_device_verify_database_passphrase">التحقق من عبارة مرور قاعدة البيانات</string>
@@ -1718,8 +1713,8 @@
<string name="allow_to_send_simplex_links">السماح بإرسال روابط SimpleX.</string>
<string name="prohibit_sending_simplex_links">منع إرسال روابط SimpleX</string>
<string name="feature_roles_all_members">كل الأعضاء</string>
<string name="group_members_can_send_simplex_links">يمكن لأعضاء المجموعة إرسال روابط SimpleX.</string>
<string name="simplex_links_are_prohibited_in_group">روابط SimpleX محظورة في هذه المجموعة.</string>
<string name="group_members_can_send_simplex_links">يمكن للأعضاء إرسال روابط SimpleX.</string>
<string name="simplex_links_are_prohibited_in_group">روابط SimpleX محظورة.</string>
<string name="feature_roles_admins">المشرفين</string>
<string name="feature_enabled_for">مفعّل لـ</string>
<string name="feature_roles_owners">المالكون</string>
@@ -1746,8 +1741,8 @@
<string name="v5_7_call_sounds_descr">عند اتصال بمكالمات الصوت والفيديو.</string>
<string name="v5_7_network">إدارة الشبكة</string>
<string name="v5_7_network_descr">اتصال شبكة أكثر موثوقية.</string>
<string name="settings_section_title_profile_images">صور الملف الشخصي</string>
<string name="v5_7_shape_profile_images">شكل الصور الشخصية</string>
<string name="settings_section_title_profile_images">صور ملف التعريف</string>
<string name="v5_7_shape_profile_images">شكل الصور التعريفية</string>
<string name="v5_7_new_interface_languages">واجهة المستخدم الليتوانية</string>
<string name="v5_7_shape_profile_images_descr">مربع أو دائرة أو أي شيء بينهما.</string>
<string name="srv_error_host">عنوان الخادم غير متوافق مع إعدادات الشبكة.</string>
@@ -1814,7 +1809,7 @@
<string name="wallpaper_preview_hello_bob">صباح الخير!</string>
<string name="color_wallpaper_background">صورة خلفية الشاشة</string>
<string name="chat_theme_apply_to_light_mode">الوضع الفاتح</string>
<string name="settings_section_title_user_theme">السمة الملف الشخصي</string>
<string name="settings_section_title_user_theme">سمة ملف التعريف</string>
<string name="color_mode_light">فاتح</string>
<string name="chat_theme_apply_to_mode">طبّق لِ</string>
<string name="wallpaper_scale_fill">ملء</string>
@@ -1833,8 +1828,7 @@
\nآخر رسالة تم استلامها: %2$s</string>
<string name="info_row_debug_delivery">تسليم التصحيح</string>
<string name="message_queue_info">معلومات قائمة انتظار الرسائل</string>
<string name="v5_8_private_routing_descr">احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات الاتصال الخاصة بك.
\nفعّل في إعدادات *الشبكة والخوادم*.</string>
<string name="v5_8_private_routing_descr">احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات اتصالك. \nفعّل في إعدادات *الشبكة والخوادم*.</string>
<string name="v5_8_chat_themes">سمات دردشة جديدة</string>
<string name="error_initializing_web_view">حدث خطأ أثناء تهيئة WebView. حدّث نظامك إلى الإصدار الجديد. يُرجى التواصل بالمطورين.
\nError: %s</string>
@@ -1999,7 +1993,7 @@
<string name="you_can_still_send_messages_to_contact">بإمكانك إرسال رسائل إلى %1$s من جهات الاتصال المؤرشفة.</string>
<string name="paste_link">ألصق الرابط</string>
<string name="contact_list_header_title">جهات اتصالك</string>
<string name="one_hand_ui">شريط أدوات الدردشة القابل للوصول</string>
<string name="one_hand_ui">شريط أدوات التطبيق القابلة للوصول</string>
<string name="cant_call_contact_deleted_alert_text">حُذفت جهة الاتصال.</string>
<string name="allow_calls_question">السماح بالمكالمات؟</string>
<string name="cant_call_member_send_message_alert_text">أرسل رسالة لتفعيل المكالمات.</string>
@@ -2040,7 +2034,7 @@
<string name="v6_0_private_routing_descr">يحمي عنوان IP الخاص بك واتصالاتك.</string>
<string name="network_option_tcp_connection">اتصال TCP</string>
<string name="network_options_save_and_reconnect">حفظ وإعادة الاتصال</string>
<string name="create_address_button">أنشِئ</string>
<string name="create_address_button">أنشئ</string>
<string name="v6_0_new_chat_experience">تجربة دردشة جديدة 🎉</string>
<string name="v6_0_privacy_blur">تمويه من أجل خصوصية أفضل.</string>
<string name="v6_0_increase_font_size">كبّر حجم الخط</string>
@@ -2075,9 +2069,9 @@
<string name="forward_files_in_progress_desc">لا يزال يتم تنزيل %1$d ملفًا.</string>
<string name="network_proxy_auth_mode_no_auth">لا تستخدم بيانات الاعتماد مع الوكيل.</string>
<string name="error_forwarding_messages">خطأ في تحويل الرسائل</string>
<string name="switching_profile_error_title">خطأ في تبديل الملف الشخصي</string>
<string name="switching_profile_error_title">خطأ في تبديل ملف التعريف</string>
<string name="select_chat_profile">حدد ملف تعريف الدردشة</string>
<string name="switching_profile_error_message">لقد تم نقل اتصالك إلى %s ولكن حدث خطأ غير متوقع أثناء إعادة توجيهك إلى الملف الشخصي.</string>
<string name="switching_profile_error_message">لقد تم نقل اتصالك إلى %s ولكن حدث خطأ غير متوقع أثناء إعادة توجيهك إلى ملف التعريف.</string>
<string name="forward_alert_title_messages_to_forward">تحويل %1$s رسالة؟</string>
<string name="forward_files_messages_deleted_after_selection_title">لم يحوّل %1$s من الرسائل</string>
<string name="compose_forward_messages_n">جارِ تحويل %1$s رسالة</string>
@@ -2117,7 +2111,7 @@
<string name="for_chat_profile">لملف تعريف الدردشة %s:</string>
<string name="no_media_servers_configured">لا يوجد وسائط أو خوادم ملفات.</string>
<string name="no_media_servers_configured_for_sending">لا يوجد خوادم لإرسال الملفات.</string>
<string name="connection_error_quota_desc">لقد وصل الاتصال إلى الحد الأقصى من الرسائل غير المُسلمة، قد يكون جهة الاتصال الخاصة بك غير متصلة بالإنترنت.</string>
<string name="connection_error_quota_desc">لقد وصل الاتصال إلى الحد الأقصى من الرسائل غير المُسلمة، قد يكون جهة اتصالك غير متصلة بالإنترنت.</string>
<string name="connection_error_quota">الرسائل غير المُسلَّمة</string>
<string name="share_1_time_link_with_a_friend">شارك رابطًا لمرة واحدة مع صديق</string>
<string name="connection_security">أمان الاتصال</string>
@@ -2182,11 +2176,11 @@
<string name="address_or_1_time_link">عنوان أو رابط لمرة واحدة؟</string>
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[يمكن استخدام الرابط لمرة واحدة <i>مع جهة اتصال واحدة فقط</i> - المشاركة شخصيًا أو عبر أي مُراسل.]]></string>
<string name="onboarding_network_operators_conditions_will_be_accepted">سيتم قبول الشروط للمُشغلين المفعّلين بعد 30 يومًا.</string>
<string name="onboarding_choose_server_operators">اختر المُشغلين</string>
<string name="onboarding_choose_server_operators">مُشغلي الخادم</string>
<string name="operator_conditions_failed_to_load">لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط:</string>
<string name="error_accepting_operator_conditions">خطأ في قبول الشروط</string>
<string name="failed_to_save_servers">خطأ في حفظ الخوادم</string>
<string name="onboarding_network_operators_app_will_use_for_routing">على سبيل المثال، إذا تلقيت رسائل عبر خادم SimpleX Chat، فسيستخدم التطبيق أحد خوادم Flux للتوجيه الخاص.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسوف يقوم تطبيقك بتسليمها عبر خادم Flux.</string>
<string name="no_message_servers_configured_for_private_routing">لا يوجد خوادم لتوجيه الرسائل الخاصة.</string>
<string name="no_message_servers_configured">لا يوجد خوادم رسائل.</string>
<string name="no_media_servers_configured_for_private_routing">لا يوجد خوادم لاستقبال الملفات.</string>
@@ -2202,8 +2196,49 @@
<string name="address_creation_instruction">انقر فوق أنشئ عنوان SimpleX في القائمة لإنشائه لاحقًا.</string>
<string name="message_deleted_or_not_received_error_desc">حُذفت هذه الرسالة أو لم يتم استلامها بعد.</string>
<string name="operator_use_for_messages">استخدم للرسائل</string>
<string name="onboarding_network_operators_app_will_use_different_operators">عندما تفعّل أكثر من مُشغل شبكة واحد، سيستخدم التطبيق خوادم مُشغلين مختلفين لكل مُحادثة.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">يحمي التطبيق خصوصيتك من خلال استخدام مُشغلين مختلفين في كل محادثة.</string>
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[سيتم قبول الشروط للمُشغل/ين: <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_applied"><![CDATA[ستطبق هذه الشروط أيضًا على: <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_be_applied"><![CDATA[ستطبق نفس الشروط على المُشغل <b>%s</b>.]]></string>
<string name="business_address">عنوان العمل التجاري</string>
<string name="onboarding_notifications_mode_service_desc_short">يتم تشغيل التطبيق دائمًا في الخلفية</string>
<string name="v6_2_business_chats">دردشات العمل التجاري</string>
<string name="button_add_team_members">أضف أعضاء الفريق</string>
<string name="button_add_friends">أضف أصدقاء</string>
<string name="add_your_team_members_to_conversations">أضف أعضاء فريقك إلى المحادثات.</string>
<string name="direct_messages_are_prohibited_in_chat">يُحظر إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة.</string>
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>أجهزة Xiaomi</b>: يُرجى تفعيل التشغيل التلقائي (Autostart) في إعدادات النظام لكي تعمل الإشعارات.]]></string>
<string name="all_message_and_files_e2e_encrypted"><![CDATA[يتم إرسال جميع الرسائل والملفات <b>مُعمَّاة بين الطرفين</b>، مع أمان ما بعد الكم في الرسائل المباشرة.]]></string>
<string name="onboarding_notifications_mode_periodic_desc_short">تحقق من الرسائل كل 10 دقائق</string>
<string name="direct_messages_are_prohibited">يُمنع إرسال الرسائل المباشرة بين الأعضاء.</string>
<string name="info_row_chat">الدردشة</string>
<string name="how_it_helps_privacy">كيف يساعد على الخصوصية</string>
<string name="delete_chat_for_all_members_cannot_undo_warning">سيتم حذف الدردشة لجميع الأعضاء - لا يمكن التراجع عن هذا!</string>
<string name="delete_chat_for_self_cannot_undo_warning">سيتم حذف الدردشة لديك - لا يمكن التراجع عن هذا!</string>
<string name="button_delete_chat">احذف الدردشة</string>
<string name="connect_plan_chat_already_exists">الدردشة موجودة بالفعل!</string>
<string name="delete_chat_question">حذف الدردشة؟</string>
<string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[لقد تم توصيلك بالفعل بـ <b>%1$s</b>.]]></string>
<string name="chat_archive">أو استورد ملف الأرشيف</string>
<string name="onboarding_notifications_mode_off_desc_short">لا توجد خدمة خلفية</string>
<string name="onboarding_notifications_mode_battery">الإشعارات والبطارية</string>
<string name="only_chat_owners_can_change_prefs">يمكن فقط لأصحاب الدردشة تغيير التفضيلات.</string>
<string name="v6_2_business_chats_descr">الخصوصية لعملائك.</string>
<string name="remote_hosts_section">الجوالات عن بُعد</string>
<string name="invite_to_chat_button">ادعُ للدردشة</string>
<string name="leave_chat_question">مغادرة المجموعة؟</string>
<string name="member_will_be_removed_from_chat_cannot_be_undone">سيتم إزالة العضو من الدردشة - لا يمكن التراجع عن هذا!</string>
<string name="button_leave_chat">غادِر الدردشة</string>
<string name="maximum_message_size_title">الرسالة كبيرة جدًا!</string>
<string name="maximum_message_size_reached_text">يُرجى تقليل حجم الرسالة وإرسالها مرة أخرى.</string>
<string name="chat_bottom_bar">شريط أداة الدردشة القابلة للوصول</string>
<string name="display_name_accepted_invitation">الدعوة قُبلت</string>
<string name="display_name_requested_to_connect">طلبت الاتصال</string>
<string name="maximum_message_size_reached_non_text">يُرجى تقليل حجم الرسالة أو إزالة الوسائط ثم إرسالها مرة أخرى.</string>
<string name="maximum_message_size_reached_forwarding">يمكنك نسخ الرسالة وتقليل حجمها لإرسالها.</string>
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">عندما يتم تفعيل أكثر من مُشغل واحد، لن يكون لدى أي منهم بيانات تعريفية لمعرفة من يتواصل مع من.</string>
<string name="member_role_will_be_changed_with_notification_chat">سيتم تغيير الدور إلى %s. وسيتم إشعار الجميع في الدردشة.</string>
<string name="chat_main_profile_sent">سيتم إرسال ملف تعريفك للدردشة إلى أعضاء الدردشة</string>
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">سوف تتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة.</string>
<string name="onboarding_network_about_operators">عن المُشغلين</string>
</resources>
@@ -188,6 +188,9 @@
<string name="error_updating_user_privacy">Error updating user privacy</string>
<string name="possible_slow_function_title">Slow function</string>
<string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string>
<string name="error_updating_chat_tags">Error updating chat list</string>
<string name="error_creating_chat_tags">Error creating chat list</string>
<string name="error_loading_chat_tags">Error loading chat lists</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Instant notifications</string>
@@ -361,6 +364,7 @@
<string name="revoke_file__confirm">Revoke</string>
<string name="forward_chat_item">Forward</string>
<string name="download_file">Download</string>
<string name="list_menu">List</string>
<string name="message_forwarded_title">Message forwarded</string>
<string name="message_forwarded_desc">No direct connection yet, message is forwarded by admin.</string>
@@ -390,6 +394,10 @@
<string name="you_have_no_chats">You have no chats</string>
<string name="loading_chats">Loading chats…</string>
<string name="no_filtered_chats">No filtered chats</string>
<string name="no_chats_in_list">No chats in list %s.</string>
<string name="no_unread_chats">No unread chats</string>
<string name="no_chats">No chats</string>
<string name="no_chats_found">No chats found</string>
<string name="contact_tap_to_connect">Tap to Connect</string>
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
<string name="search_or_paste_simplex_link">Search or paste SimpleX link</string>
@@ -409,6 +417,12 @@
<string name="forward_files_missing_desc">%1$d file(s) were deleted.</string>
<string name="forward_files_not_accepted_receive_files">Download</string>
<string name="forward_files_messages_deleted_after_selection_title">%1$s messages not forwarded</string>
<string name="chat_list_favorites">Favorites</string>
<string name="chat_list_contacts">Contacts</string>
<string name="chat_list_groups">Groups</string>
<string name="chat_list_businesses">Businesses</string>
<string name="chat_list_all">All</string>
<string name="chat_list_add_list">Add list</string>
<!-- ShareListView.kt -->
<string name="share_message">Share message…</string>
@@ -482,6 +496,7 @@
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
<string name="file_error">File error</string>
<string name="temporary_file_error">Temporary file error</string>
<string name="open_with_app">Open with %s</string>
<!-- Voice messages -->
<string name="voice_message">Voice message</string>
@@ -524,6 +539,10 @@
<string name="sync_connection_force_question">Renegotiate encryption?</string>
<string name="sync_connection_force_desc">The encryption is working and the new encryption agreement is not required. It may result in connection errors!</string>
<string name="sync_connection_force_confirm">Renegotiate</string>
<string name="sync_connection_question">Fix connection?</string>
<string name="sync_connection_desc">Connection requires encryption renegotiation.</string>
<string name="sync_connection_confirm">Fix</string>
<string name="encryption_renegotiation_in_progress">Encryption renegotiation in progress.</string>
<string name="view_security_code">View security code</string>
<string name="verify_security_code">Verify security code</string>
@@ -622,6 +641,16 @@
<string name="favorite_chat">Favorite</string>
<string name="unfavorite_chat">Unfavorite</string>
<!-- Tags - ChatListNavLinkView.kt -->
<string name="create_list">Create list</string>
<string name="add_to_list">Add to list</string>
<string name="save_list">Save list</string>
<string name="list_name_field_placeholder">List name...</string>
<string name="duplicated_list_error">List name and emoji should be different for all lists.</string>
<string name="delete_chat_list_menu_action">Delete</string>
<string name="delete_chat_list_question">Delete list?</string>
<string name="delete_chat_list_warning">All chats will be removed from the list %s, and the list deleted</string>
<string name="edit_chat_list_menu_action">Edit</string>
<!-- Pending contact connection alert dialogues -->
<string name="you_invited_a_contact">You invited a contact</string>
@@ -903,6 +932,7 @@
<string name="show_dev_options">Show:</string>
<string name="hide_dev_options">Hide:</string>
<string name="show_developer_options">Show developer options</string>
<string name="debug_logs">Enable logs</string>
<string name="developer_options">Database IDs and Transport isolation option.</string>
<string name="developer_options_section">Developer options</string>
<string name="show_internal_errors">Show internal errors</string>
@@ -1334,6 +1364,7 @@
<string name="chat_database_exported_migrate">You may migrate the exported database.</string>
<string name="chat_database_exported_not_all_files">Some file(s) were not exported</string>
<string name="chat_database_exported_continue">Continue</string>
<string name="error_saving_database">Error saving database</string>
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
@@ -1690,6 +1721,7 @@
<string name="cant_call_member_alert_title">Can\'t call group member</string>
<string name="cant_call_member_send_message_alert_text">Send message to enable calls.</string>
<string name="cant_send_message_to_member_alert_title">Can\'t message group member</string>
<string name="connection_not_ready">Connection not ready.</string>
<!-- GroupWelcomeView.kt -->
<string name="group_welcome_title">Welcome message</string>
@@ -1752,7 +1784,6 @@
<string name="operator_same_conditions_will_be_applied"><![CDATA[The same conditions will apply to operator <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[The same conditions will apply to operator(s): <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_applied"><![CDATA[These conditions will also apply for: <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_be_applied"><![CDATA[The same conditions will apply to operator: <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Conditions will be accepted for operator(s): <b>%s</b>.]]></string>
<string name="operators_conditions_will_also_apply"><![CDATA[These conditions will also apply for: <b>%s</b>.]]></string>
<string name="view_conditions">View conditions</string>
@@ -2403,7 +2434,7 @@
<string name="servers_info_messages_sent">Messages sent</string>
<string name="servers_info_messages_received">Messages received</string>
<string name="servers_info_details">Details</string>
<string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is private to your device.</string>
<string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is kept private on your device..</string>
<string name="servers_info_subscriptions_section_header">Message reception</string>
<string name="servers_info_subscriptions_connections_subscribed">Active connections</string>
<string name="servers_info_subscriptions_connections_pending">Pending</string>
@@ -62,7 +62,7 @@
<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="contact_already_exists">Kontakt besteht bereits</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit %1$s 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>
@@ -88,9 +88,9 @@
<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"><![CDATA[Um Ihre Privatsphäre zu schützen, kann statt der Push-Benachrichtigung der <b>SimpleX-Hintergrunddienst genutzt werden</b> dieser benötigt ein paar Prozent Akkuleistung am Tag.]]></string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Um Ihre Privatsphäre zu schützen, <b>läuft SimpleX im Hintergrund ab</b>, anstatt Push-Benachrichtigungen zu nutzen.]]></string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Diese können über die Einstellungen deaktiviert werden</b> solange die App läuft, werden Benachrichtigungen weiterhin angezeigt.]]></string>
<string name="turn_off_battery_optimization"><![CDATA[Um diese Funktion zu nutzen, wählen Sie im nächsten Dialog bitte die Einstellung <b>Erlauben Sie SimpleX im Hintergrund abzulaufen</b>. Ansonsten werden die Benachrichtigungen deaktiviert.]]></string>
<string name="turn_off_battery_optimization"><![CDATA[Um Benachrichtigungen sofort zu erhalten, <b>Erlauben Sie es</b> im nächsten Dialog.]]></string>
<string name="turning_off_service_and_periodic">Die Akkuoptimierung ist aktiv, der Hintergrunddienst und die periodische Nachfrage nach neuen Nachrichten ist abgeschaltet. Sie können diese Funktion in den Einstellungen wieder aktivieren.</string>
<string name="periodic_notifications">Periodische Benachrichtigungen</string>
<string name="periodic_notifications_disabled">Periodische Benachrichtigungen sind deaktiviert!</string>
@@ -462,7 +462,7 @@
<string name="callstate_connected">Verbunden</string>
<string name="callstate_ended">Beendet</string>
<!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">Die nächste Generation \ndes privaten Messagings</string>
<string name="next_generation_of_private_messaging">Die Zukunft des Messagings</string>
<string name="privacy_redefined">Datenschutz neu definiert</string>
<string name="first_platform_without_user_ids">Keine Benutzerkennungen.</string>
<string name="immune_to_spam_and_abuse">Immun gegen Spam</string>
@@ -474,8 +474,8 @@
<string name="how_it_works">Wie es funktioniert</string>
<!-- How SimpleX Works -->
<string name="how_simplex_works">Wie SimpleX funktioniert</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Zum Schutz Ihrer Privatsphäre verwendet SimpleX anstelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind.</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine <b>zweischichtige Ende-zu-Ende-Verschlüsselung</b> gesendet werden.]]></string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">SimpleX nutzt individuelle Kennungen für jeden Ihrer Kontakte, um Ihre Privatsphäre zu schützen.</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Nur die Endgeräte speichern Benutzerprofile, Kontakte, Gruppen und Nachrichten.</string>
<string name="read_more_in_github_with_link"><![CDATA[Erfahren Sie in unserem <font color="#0088ff">GitHub-Repository</font> mehr dazu.]]></string>
<!-- MakeConnection -->
<string name="paste_the_link_you_received">Fügen Sie den erhaltenen Link ein</string>
@@ -683,7 +683,7 @@
<string name="alert_title_group_invitation_expired">Die Einladung ist abgelaufen!</string>
<string name="alert_message_group_invitation_expired">Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde.</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_message_no_group">Diese Gruppe ist nicht mehr vorhanden.</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 -->
@@ -781,8 +781,8 @@
<string name="change_verb">Ändern</string>
<string name="switch_verb">Wechseln</string>
<string name="change_member_role_question">Die Mitgliederrolle ändern?</string>
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf "%s" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf "%s" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
<string name="member_role_will_be_changed_with_notification">Die Rolle wird auf %s geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
<string name="member_role_will_be_changed_with_invitation">Die Rolle wird auf %s geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
<string name="error_removing_member">Fehler beim Entfernen des Mitglieds</string>
<string name="error_changing_role">Fehler beim Ändern der Rolle</string>
<string name="info_row_group">Gruppe</string>
@@ -872,12 +872,12 @@
<string name="prohibit_message_deletion">Unwiederbringliches Löschen von Nachrichten nicht erlauben.</string>
<string name="allow_to_send_voice">Das Senden von Sprachnachrichten erlauben.</string>
<string name="prohibit_sending_voice">Das Senden von Sprachnachrichten nicht erlauben.</string>
<string name="group_members_can_send_dms">Gruppenmitglieder können Direktnachrichten versenden.</string>
<string name="group_members_can_send_dms">Mitglieder können Direktnachrichten versenden.</string>
<string name="direct_messages_are_prohibited_in_group">In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</string>
<string name="group_members_can_delete">Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen (bis zu 24 Stunden).</string>
<string name="message_deletion_prohibited_in_chat">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string>
<string name="group_members_can_send_voice">Gruppenmitglieder können Sprachnachrichten versenden.</string>
<string name="voice_messages_are_prohibited">In dieser Gruppe sind Sprachnachrichten nicht erlaubt.</string>
<string name="group_members_can_delete">Mitglieder können gesendete Nachrichten unwiederbringlich löschen (bis zu 24 Stunden).</string>
<string name="message_deletion_prohibited_in_chat">Das unwiederbringliche Löschen von Nachrichten ist nicht erlaubt.</string>
<string name="group_members_can_send_voice">Mitglieder können Sprachnachrichten versenden.</string>
<string name="voice_messages_are_prohibited">Sprachnachrichten sind nicht erlaubt.</string>
<string name="live">LIVE</string>
<string name="view_security_code">Schauen Sie sich den Sicherheitscode an</string>
<string name="onboarding_notifications_mode_service">Sofort</string>
@@ -887,7 +887,7 @@
<string name="is_verified">%s wurde erfolgreich überprüft</string>
<string name="clear_verification">Verifikation zurücknehmen</string>
<string name="onboarding_notifications_mode_off">Solange die App abläuft</string>
<string name="onboarding_notifications_mode_subtitle">Kann später über die Einstellungen geändert werden.</string>
<string name="onboarding_notifications_mode_subtitle">Auswirkung auf den Akku</string>
<string name="delete_after">Löschen nach</string>
<string name="ttl_hour">%d Stunde</string>
<string name="ttl_hours">%d Stunden</string>
@@ -922,8 +922,8 @@
<string name="create_group_link">Gruppenlink erstellen</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten.</string>
<string name="prohibit_sending_disappearing_messages">Das Senden von verschwindenden Nachrichten nicht erlauben.</string>
<string name="disappearing_messages_are_prohibited">In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt.</string>
<string name="group_members_can_send_disappearing">Gruppenmitglieder können verschwindende Nachrichten senden.</string>
<string name="disappearing_messages_are_prohibited">Verschwindende Nachrichten sind nicht erlaubt.</string>
<string name="group_members_can_send_disappearing">Mitglieder können verschwindende Nachrichten senden.</string>
<string name="v4_3_improved_server_configuration_desc">Fügen Sie Server durch Scannen der QR-Codes hinzu.</string>
<string name="v4_4_disappearing_messages">Verschwindende Nachrichten</string>
<string name="accept_feature">Übernehmen</string>
@@ -978,7 +978,7 @@
<string name="users_delete_profile_for">Chat-Profil löschen für</string>
<string name="network_option_ping_count">PING-Zähler</string>
<string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string>
<string name="smp_servers_per_user">Mögliche Server für neue Verbindungen über Ihr aktuelles Chat-Profil</string>
<string name="smp_servers_per_user">Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil</string>
<string name="files_and_media_section">Dateien &amp; Medien</string>
<string name="network_session_mode_transport_isolation">Transport-Isolation</string>
<string name="users_delete_question">Chat-Profil löschen\?</string>
@@ -1248,7 +1248,7 @@
<string name="if_you_enter_passcode_data_removed">Wenn Sie diesen Zugangscode während des Öffnens der App eingeben, werden alle App-Daten unwiederbringlich gelöscht!</string>
<string name="self_destruct_passcode">Selbstzerstörungs-Zugangscode</string>
<string name="set_passcode">Zugangscode einstellen</string>
<string name="message_reactions_are_prohibited">In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt.</string>
<string name="message_reactions_are_prohibited">Reaktionen auf Nachrichten sind nicht erlaubt.</string>
<string name="error_loading_details">Fehler beim Laden von Details</string>
<string name="received_message">Empfangene Nachricht</string>
<string name="info_menu">Information</string>
@@ -1279,7 +1279,7 @@
<string name="only_your_contact_can_add_message_reactions">Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben.</string>
<string name="allow_message_reactions">Reaktionen auf Nachrichten erlauben.</string>
<string name="prohibit_message_reactions_group">Reaktionen auf Nachrichten nicht erlauben.</string>
<string name="group_members_can_add_message_reactions">Gruppenmitglieder können eine Reaktion auf Nachrichten geben.</string>
<string name="group_members_can_add_message_reactions">Mitglieder können eine Reaktion auf Nachrichten geben.</string>
<string name="whats_new_read_more">Mehr erfahren</string>
<string name="v5_1_message_reactions_descr">Endlich haben wir sie! 🚀</string>
<string name="v5_1_message_reactions">Reaktionen auf Nachrichten</string>
@@ -1294,9 +1294,7 @@
<string name="v5_1_custom_themes_descr">Farbdesigns anpassen und weitergeben.</string>
<string name="custom_time_unit_days">Tage</string>
<string name="custom_time_unit_hours">Stunden</string>
<string name="v5_1_better_messages_descr">- Bis zu 5 Minuten lange Sprachnachrichten
\n- Zeitdauer für verschwindende Nachrichten anpassen
\n- Nachrichtenverlauf bearbeiten</string>
<string name="v5_1_better_messages_descr">- Bis zu 5 Minuten lange Sprachnachrichten\n- Zeitdauer für verschwindende Nachrichten anpassen\n- Nachrichtenverlauf bearbeiten</string>
<string name="custom_time_picker_custom">benutzerdefiniert</string>
<string name="custom_time_unit_months">Monate</string>
<string name="custom_time_picker_select">Auswählen</string>
@@ -1325,9 +1323,9 @@
<string name="abort_switch_receiving_address_question">Wechsel der Empfängeradresse beenden?</string>
<string name="files_and_media_prohibited">Dateien und Medien sind nicht erlaubt!</string>
<string name="only_owners_can_enable_files_and_media">Nur Gruppenbesitzer können Dateien und Medien aktivieren.</string>
<string name="group_members_can_send_files">Gruppenmitglieder können Dateien und Medien senden.</string>
<string name="group_members_can_send_files">Mitglieder können Dateien und Medien senden.</string>
<string name="abort_switch_receiving_address_desc">Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet.</string>
<string name="files_are_prohibited_in_group">In dieser Gruppe sind Dateien und Medien nicht erlaubt.</string>
<string name="files_are_prohibited_in_group">Dateien und Medien sind nicht erlaubt.</string>
<string name="unfavorite_chat">Favorit entfernen</string>
<string name="favorite_chat">Favorit</string>
<string name="no_filtered_chats">Keine gefilterten Chats</string>
@@ -1385,9 +1383,7 @@
<string name="v5_2_fix_encryption_descr">Reparatur der Verschlüsselung nach Wiedereinspielen von Backups.</string>
<string name="v5_2_more_things">Ein paar weitere Dinge</string>
<string name="v5_2_disappear_one_message_descr">Auch wenn sie in den Unterhaltungen deaktiviert sind.</string>
<string name="v5_2_more_things_descr">- stabilere Zustellung von Nachrichten.
\n- ein bisschen verbesserte Gruppen.
\n- und mehr!</string>
<string name="v5_2_more_things_descr">- Stabilere Zustellung von Nachrichten.\n- Ein bisschen verbesserte Gruppen.\n- Und mehr!</string>
<string name="dont_enable_receipts">Nicht aktivieren</string>
<string name="sending_delivery_receipts_will_be_enabled">Das Senden von Empfangsbestätigungen an alle Kontakte wird aktiviert.</string>
<string name="you_can_enable_delivery_receipts_later_alert">Sie können diese später in den Datenschutz- und Sicherheits-Einstellungen der App aktivieren.</string>
@@ -1457,9 +1453,7 @@
<string name="v5_3_new_interface_languages_descr">Arabisch, Bulgarisch, Finnisch, Hebräisch, Thailändisch und Ukrainisch - Dank der Nutzer und Weblate.</string>
<string name="v5_3_new_desktop_app_descr">Erstellen eines neuen Profils in der Desktop-App. 💻</string>
<string name="v5_3_simpler_incognito_mode_descr">Inkognito beim Verbinden einschalten.</string>
<string name="v5_3_discover_join_groups_descr">- Verbindung mit dem Directory-Service (BETA)!
\n- Empfangsbestätigungen (für bis zu 20 Mitglieder).
\n- Schneller und stabiler.</string>
<string name="v5_3_discover_join_groups_descr">- Verbindung mit dem Directory-Service (BETA)!\n- Empfangsbestätigungen (für bis zu 20 Mitglieder).\n- Schneller und stabiler.</string>
<string name="member_contact_send_direct_message">Direktnachricht senden</string>
<string name="rcv_group_event_member_created_contact">Direkt miteinander verbunden</string>
<string name="expand_verb">Erweitern</string>
@@ -1568,9 +1562,7 @@
<string name="paste_desktop_address">Desktop-Adresse einfügen</string>
<string name="verify_code_with_desktop">Code mit dem Desktop überprüfen</string>
<string name="scan_qr_code_from_desktop">Den QR-Code vom Desktop scannen</string>
<string name="v5_4_more_things_descr">- Optionale Benachrichtigung von gelöschten Kontakten.
\n- Profilnamen mit Leerzeichen.
\n- Und mehr!</string>
<string name="v5_4_more_things_descr">- Optionale Benachrichtigung von gelöschten Kontakten.\n- Profilnamen mit Leerzeichen.\n- Und mehr!</string>
<string name="scan_from_mobile">Vom Mobiltelefon scannen</string>
<string name="verify_connections">Verbindungen überprüfen</string>
<string name="loading_remote_file_desc">Bitte warten Sie, solange die Datei von dem verknüpften Mobiltelefon geladen wird</string>
@@ -1799,12 +1791,12 @@
<string name="simplex_links_not_allowed">SimpleX-Links sind nicht erlaubt</string>
<string name="voice_messages_not_allowed">Sprachnachrichten sind nicht erlaubt</string>
<string name="simplex_links">SimpleX-Links</string>
<string name="group_members_can_send_simplex_links">Gruppenmitglieder können SimpleX-Links senden.</string>
<string name="group_members_can_send_simplex_links">Mitglieder können SimpleX-Links senden.</string>
<string name="feature_roles_admins">Administratoren</string>
<string name="feature_roles_all_members">Alle Mitglieder</string>
<string name="feature_enabled_for">Aktiviert für</string>
<string name="feature_roles_owners">Eigentümer</string>
<string name="simplex_links_are_prohibited_in_group">In dieser Gruppe sind SimpleX-Links nicht erlaubt.</string>
<string name="simplex_links_are_prohibited_in_group">SimpleX-Links sind nicht erlaubt.</string>
<string name="prohibit_sending_simplex_links">Das Senden von SimpleX-Links nicht erlauben.</string>
<string name="allow_to_send_simplex_links">Das Senden von SimpleX-Links erlauben.</string>
<string name="audio_device_speaker">Lautsprecher</string>
@@ -2077,7 +2069,7 @@
<string name="deleted_chats">Archivierte Kontakte</string>
<string name="no_filtered_contacts">Keine gefilterten Kontakte</string>
<string name="contact_list_header_title">Ihre Kontakte</string>
<string name="one_hand_ui">Chat-Symbolleiste unten</string>
<string name="one_hand_ui">App-Symbolleiste unten</string>
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren.</string>
<string name="you_need_to_allow_calls">Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können.</string>
<string name="allow_calls_question">Anrufe erlauben?</string>
@@ -2157,8 +2149,7 @@
<string name="network_proxy_auth_mode_isolate_by_auth_user">Verwenden Sie für jedes Profil unterschiedliche Proxy-Anmeldeinformationen.</string>
<string name="network_proxy_random_credentials">Verwenden Sie zufällige Anmeldeinformationen</string>
<string name="network_proxy_username">Benutzername</string>
<string name="n_file_errors">%1$d Datei-Fehler:
\n%2$s</string>
<string name="n_file_errors">%1$d Datei-Fehler:\n%2$s</string>
<string name="forward_files_in_progress_desc">%1$d Datei(en) wird/werden immer noch heruntergeladen.</string>
<string name="forward_files_failed_to_receive_desc">Bei %1$d Datei(en) ist das Herunterladen fehlgeschlagen.</string>
<string name="error_forwarding_messages">Fehler beim Weiterleiten der Nachrichten</string>
@@ -2215,11 +2206,11 @@
<string name="for_social_media">Für soziale Medien</string>
<string name="or_to_share_privately">Oder zum privaten Teilen</string>
<string name="simplex_address_or_1_time_link">SimpleX-Adresse oder Einmal-Link?</string>
<string name="onboarding_choose_server_operators">Betreiber auswählen</string>
<string name="onboarding_choose_server_operators">Server-Betreiber</string>
<string name="onboarding_network_operators">Netzwerk-Betreiber</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Wenn mehr als ein Netzwerk-Betreiber aktiviert ist, verwendet die App für jede Unterhaltung Server der verschiedenen Betreiber.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Die App verwendet für jede Unterhaltung Server von unterschiedlichen Betreibern, um Ihre Privatsphäre zu schützen.</string>
<string name="onboarding_network_operators_conditions_will_be_accepted">Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Wenn Sie beispielsweise Nachrichten über einen SimpleX-Chatserver empfangen, verwendet die App einen der Server von Flux für die private Weiterleitung.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chat-Server empfängt, wird Ihre App diese über einen Flux-Server versenden.</string>
<string name="onboarding_network_operators_review_later">Später einsehen</string>
<string name="onboarding_select_network_operators_to_use">Wählen sie die zu nutzenden Netzwerk-Betreiber aus.</string>
<string name="onboarding_network_operators_conditions_you_can_configure">Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren.</string>
@@ -2246,7 +2237,7 @@
<string name="operator_use_for_messages">Für Nachrichten verwenden</string>
<string name="operator_added_message_servers">Nachrichtenserver hinzugefügt</string>
<string name="operator_use_for_messages_private_routing">Für privates Routing</string>
<string name="xftp_servers_per_user">Die Server Deines aktuellen Chat-Profils für neue Dateien</string>
<string name="xftp_servers_per_user">Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil</string>
<string name="operator_use_for_sending">Für das Senden</string>
<string name="operator_use_for_files">Für Dateien verwenden</string>
<string name="error_adding_server">Fehler beim Hinzufügen des Servers</string>
@@ -2271,7 +2262,7 @@
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Ein Einmal-Link kann <i>nur mit einem Kontakt</i> genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger.]]></string>
<string name="operator_conditions_accepted_for_some"><![CDATA[Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: <b>%s</b>.]]></string>
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_accepted_on">Die Nutzungsbedingungen werden akzeptiert am: %s.</string>
<string name="operator_conditions_will_be_accepted_on">Die Nutzungsbedingungen wurden akzeptiert am: %s</string>
<string name="operator_conditions_will_be_applied"><![CDATA[Diese Nutzungsbedingungen gelten auch für: <b>%s</b>.]]></string>
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[Um die Server von <b>%s</b> zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren.]]></string>
<string name="error_accepting_operator_conditions">Fehler beim Akzeptieren der Nutzungsbedingungen</string>
@@ -2285,10 +2276,50 @@
<string name="connection_error_quota_desc">Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline.</string>
<string name="message_deleted_or_not_received_error_desc">Diese Nachricht wurde gelöscht oder bisher noch nicht empfangen.</string>
<string name="to_protect_against_your_link_replaced_compare_codes">Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen.</string>
<string name="operators_conditions_accepted_for"><![CDATA[Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: <b>%s</b>.]]></string>
<string name="operators_conditions_accepted_for"><![CDATA[Die Nutzungsbedingungen der/des Betreiber(s) wurden akzeptiert: <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: <b>%s</b>.]]></string>
<string name="operator_conditions_accepted_on">Die Nutzungsbedingungen wurden akzeptiert am: %s.</string>
<string name="operator_conditions_failed_to_load">Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen:</string>
<string name="remote_hosts_section">Ferngesteuerte Mobiltelefone</string>
<string name="chat_archive">Oder importieren Sie eine Archiv-Datei</string>
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Hinweis für Geräte von Xiaomi</b>: Bitte aktivieren Sie in den System-Einstellungen die Option "Autostart", damit Benachrichtigungen funktionieren.]]></string>
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Alle Nachrichten und Dateien werden <b>Ende-zu-Ende-verschlüsselt</b> versendet. In Direktnachrichten sogar mit Post-Quantum-Security.]]></string>
<string name="button_add_team_members">Team-Mitglieder aufnehmen</string>
<string name="button_add_friends">Freunde aufnehmen</string>
<string name="display_name_accepted_invitation">Einladung akzeptiert</string>
<string name="business_address">Geschäftliche Adresse</string>
<string name="v6_2_business_chats">Geschäftliche Chats</string>
<string name="add_your_team_members_to_conversations">Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf.</string>
<string name="onboarding_notifications_mode_service_desc_short">Die App läuft immer im Hintergrund ab</string>
<string name="direct_messages_are_prohibited_in_chat">In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</string>
<string name="onboarding_notifications_mode_off_desc_short">Kein Hintergrund-Service</string>
<string name="onboarding_notifications_mode_periodic_desc_short">Nachrichten alle 10 Minuten überprüfen</string>
<string name="onboarding_notifications_mode_battery">Benachrichtigungen und Akku</string>
<string name="invite_to_chat_button">Zum Chat einladen</string>
<string name="connect_plan_chat_already_exists">Chat besteht bereits!</string>
<string name="chat_bottom_bar">Chat-Symbolleiste unten</string>
<string name="button_leave_chat">Chat verlassen</string>
<string name="member_will_be_removed_from_chat_cannot_be_undone">Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!</string>
<string name="chat_main_profile_sent">Ihr Chat-Profil wird an die Chat-Mitglieder gesendet.</string>
<string name="direct_messages_are_prohibited">Direktnachrichten zwischen Mitgliedern sind nicht erlaubt.</string>
<string name="how_it_helps_privacy">Wie die Privatsphäre geschützt wird</string>
<string name="leave_chat_question">Chat verlassen?</string>
<string name="delete_chat_question">Chat löschen?</string>
<string name="delete_chat_for_all_members_cannot_undo_warning">Der Chat wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
<string name="v6_2_business_chats_descr">Schutz der Privatsphäre Ihrer Kunden.</string>
<string name="display_name_requested_to_connect">Zur Verbindung aufgefordert</string>
<string name="maximum_message_size_reached_non_text">Bitte verkleinern Sie die Nachrichten-Größe oder entfernen Sie Medien und versenden Sie diese erneut.</string>
<string name="only_chat_owners_can_change_prefs">Nur Chat-Eigentümer können die Präferenzen ändern.</string>
<string name="maximum_message_size_reached_text">Bitte verkleinern Sie die Nachrichten-Größe und versenden Sie diese erneut.</string>
<string name="member_role_will_be_changed_with_notification_chat">Die Rolle wird auf %s geändert. Im Chat wird Jeder darüber informiert.</string>
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten.</string>
<string name="maximum_message_size_reached_forwarding">Sie können die Nachricht kopieren und verkleinern, um sie zu versenden.</string>
<string name="button_delete_chat">Chat löschen</string>
<string name="delete_chat_for_self_cannot_undo_warning">Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
<string name="maximum_message_size_title">Die Nachricht ist zu umfangreich!</string>
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Wenn mehr als ein Betreiber aktiviert ist, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert.</string>
<string name="info_row_chat">Chat</string>
<string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Sie sind bereits mit <b>%1$s</b> verbunden.]]></string>
<string name="onboarding_network_about_operators">Über Betreiber</string>
<string name="onboarding_network_operators_simplex_flux_agreement">SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen.</string>
</resources>
@@ -94,7 +94,7 @@
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo.</string>
<string name="no_call_on_lock_screen">Desactivar</string>
<string name="disappearing_prohibited_in_this_chat">Los mensajes temporales no están permitidos en este chat.</string>
<string name="disappearing_messages_are_prohibited">Los mensajes temporales no están permitidos en este grupo.</string>
<string name="disappearing_messages_are_prohibited">Mensajes temporales no permitidos.</string>
<string name="display_name_cannot_contain_whitespace">El nombre mostrado no puede contener espacios en blanco.</string>
<string name="encrypted_video_call">Videollamada con cifrado de extremo a extremo</string>
<string name="display_name_connection_established">conexión establecida</string>
@@ -337,7 +337,7 @@
<string name="enter_passphrase">Introduce la contraseña…</string>
<string name="icon_descr_group_inactive">Grupo inactivo</string>
<string name="group_member_status_group_deleted">grupo eliminado</string>
<string name="group_members_can_send_disappearing">Los miembros del grupo pueden enviar mensajes temporales.</string>
<string name="group_members_can_send_disappearing">Los miembros pueden enviar mensajes temporales.</string>
<string name="v4_2_group_links">Enlaces de grupo</string>
<string name="invalid_connection_link">Enlace de conexión no válido</string>
<string name="error_accepting_contact_request">Error al aceptar solicitud del contacto</string>
@@ -357,7 +357,7 @@
<string name="error_deleting_database">Error al eliminar base de datos</string>
<string name="encrypted_database">Base de datos cifrada</string>
<string name="error_removing_member">Error al eliminar miembro</string>
<string name="group_members_can_send_voice">Los miembros del grupo pueden enviar mensajes de voz.</string>
<string name="group_members_can_send_voice">Los miembros pueden enviar mensajes de voz.</string>
<string name="description_via_contact_address_link_incognito">en modo incógnito mediante enlace de dirección del contacto</string>
<string name="failed_to_create_user_title">¡Error al crear perfil!</string>
<string name="failed_to_parse_chat_title">No se pudo cargar el chat</string>
@@ -407,8 +407,8 @@
<string name="conn_stats_section_title_servers">SERVIDORES</string>
<string name="group_display_name_field">Nombre del grupo:</string>
<string name="group_preferences">Preferencias del grupo</string>
<string name="group_members_can_send_dms">Los miembros del grupo pueden enviar mensajes directos.</string>
<string name="group_members_can_delete">Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)</string>
<string name="group_members_can_send_dms">Los miembros pueden enviar mensajes directos.</string>
<string name="group_members_can_delete">Los miembros pueden eliminar mensajes enviados de forma irreversible. (24 horas)</string>
<string name="v4_3_improved_privacy_and_security_desc">Ocultar pantalla de aplicaciones en aplicaciones recientes.</string>
<string name="encrypt_database">Cifrar</string>
<string name="icon_descr_expand_role">Ampliar la selección de roles</string>
@@ -431,7 +431,7 @@
<string name="how_it_works">Cómo funciona</string>
<string name="delete_message_cannot_be_undone_warning">El mensaje será eliminado. ¡No podrá deshacerse!</string>
<string name="incognito_info_protects">El modo incógnito protege tu privacidad creando un perfil aleatorio por cada contacto.</string>
<string name="turn_off_battery_optimization"><![CDATA[Para usar SimpleX, por favor <b>permite que SimpleX se ejecute en segundo plano</b> en el siguiente cuadro de diálogo. De lo contrario las notificaciones se desactivarán.]]></string>
<string name="turn_off_battery_optimization"><![CDATA[<b>Da permiso</b> en el siguiente diálogo para recibir notificaciones instantáneas.]]></string>
<string name="install_simplex_chat_for_terminal">Instalar terminal de SimpleX Chat</string>
<string name="group_invitation_item_description">invitación al grupo %1$s</string>
<string name="rcv_group_event_member_added">ha invitado a %1$s</string>
@@ -443,7 +443,7 @@
<string name="icon_descr_instant_notifications">Notificación instantánea</string>
<string name="network_settings_title">Configuración avanzada</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes.</string>
<string name="onboarding_notifications_mode_subtitle">Puedes cambiar estos ajustes más tarde en Configuración.</string>
<string name="onboarding_notifications_mode_subtitle">Cómo afecta a la batería</string>
<string name="onboarding_notifications_mode_service">Instantánea</string>
<string name="join_group_button">Unirte</string>
<string name="join_group_incognito_button">Unirte en modo incógnito</string>
@@ -451,7 +451,7 @@
<string name="theme_light">Claro</string>
<string name="chat_preferences_on">Activado</string>
<string name="message_deletion_prohibited">La eliminación irreversible de mensajes no está permitida en este chat.</string>
<string name="message_deletion_prohibited_in_chat">La eliminación irreversible de mensajes no está permitida en este grupo.</string>
<string name="message_deletion_prohibited_in_chat">Eliminación irreversible no permitida.</string>
<string name="v4_3_improved_server_configuration">Configuración del servidor mejorada</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Esto puede ocurrir cuando:
\n1. Los mensajes hayan caducado en el cliente saliente tras 2 días o en el servidor tras 30 días.
@@ -555,7 +555,7 @@
<string name="snd_conn_event_switch_queue_phase_completed_for_member">has cambiado el servidor para %s</string>
<string name="rcv_group_event_member_left">ha salido</string>
<string name="button_leave_group">Salir del grupo</string>
<string name="only_group_owners_can_change_prefs">Sólo los propietarios pueden modificar las preferencias del grupo.</string>
<string name="only_group_owners_can_change_prefs">Sólo los propietarios del grupo pueden cambiar las preferencias.</string>
<string name="users_delete_data_only">Eliminar sólo el perfil</string>
<string name="chat_preferences_no">no</string>
<string name="thousand_abbreviation">k</string>
@@ -642,7 +642,7 @@
<string name="icon_descr_profile_image_placeholder">Espacio reservado para la imagen del perfil</string>
<string name="image_descr_qr_code">Código QR</string>
<string name="chat_with_the_founder">Consultas y sugerencias</string>
<string name="smp_servers_preset_address">Dirección del servidor predefinida</string>
<string name="smp_servers_preset_address">Dirección predefinida del servidor</string>
<string name="send_us_an_email">Contacta vía email</string>
<string name="rate_the_app">Valora la aplicación</string>
<string name="save_servers_button">Guardar</string>
@@ -718,7 +718,7 @@
<string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string>
<string name="set_contact_name">Escribe un nombre para el contacto</string>
<string name="unknown_error">Error desconocido</string>
<string name="member_role_will_be_changed_with_notification">El rol del miembro cambiará a "%s" y se notificará al grupo.</string>
<string name="member_role_will_be_changed_with_notification">El rol cambiará a %s. Todos serán notificados.</string>
<string name="v4_2_security_assessment_desc">La seguridad de SimpleX Chat ha sido auditada por Trail of Bits.</string>
<string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string>
<string name="ntf_channel_messages">Mensajes de chat SimpleX</string>
@@ -736,12 +736,12 @@
<string name="connection_you_accepted_will_be_cancelled">¡La conexión que has aceptado se cancelará!</string>
<string name="database_initialization_error_desc">La base de datos no funciona correctamente. Pulsa para conocer más</string>
<string name="moderate_message_will_be_marked_warning">El mensaje será marcado como moderado para todos los miembros.</string>
<string name="next_generation_of_private_messaging">La nueva generación \nde mensajería privada</string>
<string name="next_generation_of_private_messaging">El futuro de la mensajería</string>
<string name="delete_files_and_media_desc">Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</string>
<string name="enable_automatic_deletion_message">Esta acción es irreversible. Los mensajes enviados y recibidos anteriores a la selección serán eliminados. Podría tardar varios minutos.</string>
<string name="messages_section_description">Esta configuración se aplica a los mensajes del perfil actual</string>
<string name="this_string_is_not_a_connection_link">¡Esta cadena no es un enlace de conexión!</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Para preservar tu privacidad, en lugar de notificaciones automáticas la aplicación cuenta con un <b>servicio en segundo planoSimpleX</b>, usa un pequeño porcentaje de la batería al día.]]></string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Para mejorar la privacidad <b>SimpleX se ejecuta en segundo plano</b> en lugar de usar notificaciones push.]]></string>
<string name="icon_descr_settings">Configuración</string>
<string name="icon_descr_speaker_off">Altavoz desactivado</string>
<string name="add_contact_or_create_group">Inciar chat nuevo</string>
@@ -785,7 +785,7 @@
<string name="smp_servers_test_server">Probar servidor</string>
<string name="smp_servers_test_servers">Probar servidores</string>
<string name="star_on_github">Estrella en GitHub</string>
<string name="smp_servers_per_user">Lista de servidores para las conexiones nuevas de tu perfil actual</string>
<string name="smp_servers_per_user">Lista de servidores para las conexiones nuevas del perfil</string>
<string name="network_disable_socks">¿Usar conexión directa a Internet\?</string>
<string name="profile_is_only_shared_with_your_contacts">El perfil sólo se comparte con tus contactos.</string>
<string name="callstate_starting">inicializando…</string>
@@ -804,7 +804,7 @@
<string name="update_database_passphrase">Actualizar contraseña base de datos</string>
<string name="group_invitation_tap_to_join_incognito">Pulsa para unirte en modo incógnito</string>
<string name="switch_verb">Cambiar</string>
<string name="member_role_will_be_changed_with_invitation">El rol del miembro cambiará a "%s" y recibirá una invitación nueva.</string>
<string name="member_role_will_be_changed_with_invitation">El rol cambiará a %s y el miembro recibirá una invitación nueva.</string>
<string name="update_network_settings_confirmation">Actualizar</string>
<string name="update_network_settings_question">¿Actualizar la configuración de red\?</string>
<string name="trying_to_connect_to_server_to_receive_messages">Intentando conectar con el servidor para recibir mensajes de este contacto.</string>
@@ -840,9 +840,9 @@
<string name="v4_3_voice_messages">Mensajes de voz</string>
<string name="v4_3_irreversible_message_deletion_desc">Tus contactos pueden permitir la eliminación completa de mensajes.</string>
<string name="voice_messages">Mensajes de voz</string>
<string name="voice_messages_are_prohibited">Los mensajes de voz no están permitidos en este grupo.</string>
<string name="voice_messages_are_prohibited">Mensajes de voz no permitidos.</string>
<string name="v4_4_verify_connection_security">Comprobar la seguridad de la conexión</string>
<string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado a %1$s.</string>
<string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado con %1$s.</string>
<string name="welcome">¡Bienvenido!</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil será enviado
\na tu contacto</string>
@@ -1028,7 +1028,7 @@
<string name="your_XFTP_servers">Servidores XFTP</string>
<string name="port_verb">Puerto</string>
<string name="network_proxy_port">puerto %d</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Configura <i>Usar hosts .onion</i> como <i>No</i> si el proxy SOCKS no los admite.]]></string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[<i>Usar hosts .onion</i> debe estar a <i>No</i> si el proxy SOCKS no los admite.]]></string>
<string name="smp_server_test_download_file">Descargar archivo</string>
<string name="network_socks_toggle_use_socks_proxy">Usar proxy SOCKS</string>
<string name="host_verb">Host</string>
@@ -1208,7 +1208,7 @@
<string name="custom_time_unit_weeks">semanas</string>
<string name="error_loading_details">Error al cargar detalles</string>
<string name="group_members_can_add_message_reactions">Los miembros pueden añadir reacciones a los mensajes.</string>
<string name="message_reactions_are_prohibited">Las reacciones a los mensajes no están permitidas en este grupo.</string>
<string name="message_reactions_are_prohibited">Reacciones a los mensajes no permitidas.</string>
<string name="only_your_contact_can_add_message_reactions">Sólo tu contacto puede añadir reacciones a los mensajes.</string>
<string name="send_disappearing_message_1_minute">1 minuto</string>
<string name="info_row_updated_at">Registro actualiz</string>
@@ -1228,12 +1228,12 @@
<string name="v5_1_custom_themes_descr">Personalizar y compartir temas de color.</string>
<string name="v5_1_message_reactions_descr">¡Por fin los tenemos! 🚀</string>
<string name="v5_1_message_reactions">Reacciones a los mensajes</string>
<string name="whats_new_read_more">Conoce más</string>
<string name="whats_new_read_more">Saber más</string>
<string name="v5_1_japanese_portuguese_interface">Interfaz en japonés y portugués</string>
<string name="item_info_no_text">sin texto</string>
<string name="non_fatal_errors_occured_during_import">Han ocurrido algunos errores no críticos durante la importación:</string>
<string name="shutdown_alert_question">¿Cerrar\?</string>
<string name="settings_section_title_app">Aplicación</string>
<string name="settings_section_title_app">APLICACIÓN</string>
<string name="settings_restart_app">Reiniciar</string>
<string name="settings_shutdown">Cerrar</string>
<string name="shutdown_alert_desc">Las notificaciones dejarán de funcionar hasta que reinicies la aplicación</string>
@@ -1248,8 +1248,8 @@
<string name="abort_switch_receiving_address">Cancelar cambio de dirección</string>
<string name="files_and_media">Archivos y multimedia</string>
<string name="prohibit_sending_files">No se permite el envío de archivos y multimedia.</string>
<string name="files_are_prohibited_in_group">Los archivos y multimedia no están permitidos en este grupo.</string>
<string name="group_members_can_send_files">Los miembros del grupo pueden enviar archivos y multimedia.</string>
<string name="files_are_prohibited_in_group">Archivos y multimedia no permitidos.</string>
<string name="group_members_can_send_files">Los miembros pueden enviar archivos y multimedia.</string>
<string name="allow_to_send_files">Se permite enviar archivos y multimedia</string>
<string name="favorite_chat">Favorito</string>
<string name="only_owners_can_enable_files_and_media">Sólo los propietarios del grupo pueden activar los archivos y multimedia.</string>
@@ -1356,7 +1356,7 @@
<string name="you_can_change_it_later">La contraseña aleatoria se almacenará en Configuración como texto plano.
\nPuedes cambiarlo más tarde.</string>
<string name="database_encryption_will_be_updated_in_settings">La contraseña para el cifrado de la base de datos se actualizará y almacenará en Configuración</string>
<string name="remove_passphrase_from_settings">Eliminar contraseña de configuración\?</string>
<string name="remove_passphrase_from_settings">¿Eliminar contraseña de configuración?</string>
<string name="use_random_passphrase">Usar contraseña aleatoria</string>
<string name="save_passphrase_in_settings">Guardar contraseña en configuración</string>
<string name="setup_database_passphrase">Configuración contraseña base de datos</string>
@@ -1715,8 +1715,8 @@
<string name="simplex_links_not_allowed">Enlaces SimpleX no permitidos</string>
<string name="voice_messages_not_allowed">Mensajes de voz no permitidos</string>
<string name="simplex_links">Enlaces SimpleX</string>
<string name="group_members_can_send_simplex_links">Los miembros del grupo pueden enviar enlaces SimpleX.</string>
<string name="simplex_links_are_prohibited_in_group">Los enlaces SimpleX no se permiten en este grupo.</string>
<string name="group_members_can_send_simplex_links">Los miembros pueden enviar enlaces SimpleX.</string>
<string name="simplex_links_are_prohibited_in_group">Enlaces SimpleX no permitidos.</string>
<string name="feature_roles_owners">propietarios</string>
<string name="network_type_cellular">Móvil</string>
<string name="network_type_no_network_connection">Sin conexión de red</string>
@@ -1897,7 +1897,7 @@
<string name="decryption_errors">errores de descifrado</string>
<string name="deleted">Eliminadas</string>
<string name="deletion_errors">Errores de eliminación</string>
<string name="member_info_member_disabled">desactivado</string>
<string name="member_info_member_disabled">inactivo</string>
<string name="message_forwarded_title">Mensaje reenviado</string>
<string name="member_inactive_desc">El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo.</string>
<string name="member_inactive_title">Miembro inactivo</string>
@@ -1990,7 +1990,7 @@
<string name="privacy_media_blur_radius_medium">Medio</string>
<string name="privacy_media_blur_radius_soft">Suave</string>
<string name="one_hand_ui">Barra de herramientas accesible</string>
<string name="info_view_call_button">llamada</string>
<string name="info_view_call_button">llamar</string>
<string name="info_view_connect_button">conectar</string>
<string name="delete_members_messages__question">¿Eliminar %d mensajes de miembros?</string>
<string name="info_view_message_button">mensaje</string>
@@ -2083,8 +2083,8 @@
<string name="network_proxy_incorrect_config_title">Error guardando proxy</string>
<string name="network_proxy_password">Contraseña</string>
<string name="network_proxy_auth">Autenticación proxy</string>
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Credenciales proxy diferentes para cada conexión.</string>
<string name="network_proxy_auth_mode_isolate_by_auth_user">Credenciales proxy diferentes para cada perfil.</string>
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Se usan credenciales proxy diferentes para cada conexión.</string>
<string name="network_proxy_auth_mode_isolate_by_auth_user">Se usan credenciales proxy diferentes para cada perfil.</string>
<string name="network_proxy_random_credentials">Credenciales aleatorias</string>
<string name="network_proxy_username">Nombre de usuario</string>
<string name="network_proxy_auth_mode_username_password">Tus credenciales podrían ser enviadas sin cifrar.</string>
@@ -2125,12 +2125,12 @@
<string name="no_media_servers_configured_for_sending">Ningún servidor para enviar archivos.</string>
<string name="connection_security">Seguridad de conexión</string>
<string name="share_1_time_link_with_a_friend">Compartir enlace de un uso con un amigo</string>
<string name="share_simplex_address_on_social_media">Compartir dirección SimpleX en redes sociales.</string>
<string name="share_simplex_address_on_social_media">Comparte tu dirección SimpleX en redes sociales.</string>
<string name="address_settings">Configuración de dirección</string>
<string name="create_1_time_link">Crear enlace de un uso</string>
<string name="for_social_media">Para redes sociales</string>
<string name="simplex_address_or_1_time_link">Dirección SimpleX o enlace de un uso?</string>
<string name="onboarding_choose_server_operators">Selecciona operadores</string>
<string name="simplex_address_or_1_time_link">¿Dirección SimpleX o enlace de un uso?</string>
<string name="onboarding_choose_server_operators">Operadores de servidores</string>
<string name="onboarding_network_operators">Operadores de red</string>
<string name="onboarding_network_operators_conditions_will_be_accepted">Las condiciones de los operadores habilitados serán aceptadas después de 30 días.</string>
<string name="onboarding_network_operators_review_later">Revisar más tarde</string>
@@ -2162,10 +2162,10 @@
<string name="operator_conditions_accepted_for_enabled_operators_on">Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %s.</string>
<string name="onboarding_network_operators_continue">Continuar</string>
<string name="operator_conditions_failed_to_load">El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace:</string>
<string name="v6_2_network_decentralization_enable_flux">Habilitar Flux</string>
<string name="v6_2_network_decentralization_enable_flux">Habilita Flux</string>
<string name="error_accepting_operator_conditions">Error al aceptar las condiciones</string>
<string name="error_updating_server_title">Error al actualizar el servidor</string>
<string name="v6_2_network_decentralization_enable_flux_reason">para mayor privacidad de los metadatos.</string>
<string name="v6_2_network_decentralization_enable_flux_reason">para mejorar la privacidad de los metadatos.</string>
<string name="message_deleted_or_not_received_error_title">Ningún mensaje</string>
<string name="smp_servers_new_server">Servidor nuevo</string>
<string name="no_media_servers_configured">Ningún servidor de archivos y multimedia.</string>
@@ -2175,7 +2175,7 @@
<string name="or_to_share_privately">O para compartir en privado</string>
<string name="onboarding_select_network_operators_to_use">Selecciona los operadores de red a utilizar</string>
<string name="share_address_publicly">Campartir dirección públicamente</string>
<string name="simplex_address_and_1_time_links_are_safe_to_share">Compartir enlaces de un uso y direcciones SimpleX es seguro a través de cualquier medio.</string>
<string name="simplex_address_and_1_time_links_are_safe_to_share">Compartir los enlaces de un uso y las direcciones SimpleX es seguro a través de cualquier medio.</string>
<string name="onboarding_network_operators_update">Actualizar</string>
<string name="operator_website">Sitio web</string>
<string name="your_servers">Tus servidores</string>
@@ -2191,25 +2191,65 @@
<string name="connection_error_quota">Mensajes no entregados</string>
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Los enlaces de un uso pueden ser usados <i>solamente con un contacto</i> - comparte en persona o mediante cualquier aplicación de mensajería.]]></string>
<string name="you_can_set_connection_name_to_remember">Puedes añadir un nombre a la conexión para recordar a quién corresponde.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Cuando está habilitado más de un operador de red, la aplicación usa servidores de diferentes operadores para cada conversación.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación.</string>
<string name="onboarding_network_operators_conditions_you_can_configure">Puedes configurar los operadores desde Servidores y Redes.</string>
<string name="operators_conditions_accepted_for"><![CDATA[Las condiciones se han aceptado para el(los) operador(s): <b>%s</b>.]]></string>
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Las condiciones serán aceptadas para el/los operador(es): <b>%s</b>.]]></string>
<string name="operator_conditions_accepted_for_some"><![CDATA[Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_be_applied"><![CDATA[Las mismas condiciones se aplican al operador <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_be_applied"><![CDATA[Las mismas condiciones se aplican al operador <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Las condiciones serán aceptadas para el/los operador(es): <b>%s</b>.]]></string>
<string name="operators_conditions_will_also_apply"><![CDATA[Estas condiciones también se aplican para: <b>%s</b>.]]></string>
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[Para usar los servidores de <b>%s</b>, acepta las condiciones de uso.]]></string>
<string name="xftp_servers_per_user">Los servidores para archivos nuevos en tu perfil actual</string>
<string name="v6_2_network_decentralization_descr">El segundo operador predefinido!</string>
<string name="v6_2_network_decentralization_descr">¡Segundo operador predefinido!</string>
<string name="onboarding_network_operators_configure_via_settings">Puedes configurar los servidores a través de su configuración.</string>
<string name="to_protect_against_your_link_replaced_compare_codes">Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto.</string>
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Las mismas condiciones se aplican a el/los operador(es) <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Las mismas condiciones se aplican a el/los operador(es) <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_applied"><![CDATA[Estas condiciones también se aplican para: <b>%s</b>.]]></string>
<string name="onboarding_network_operators_app_will_use_for_routing">Si por ejemplo recibes los mensajes a través de un servidor de SimpleX Chat, la aplicación usará uno de Flux para el enrutamiento privado.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Por ejemplo, si tu contacto recibe a través de un servidor de SimpleX Chat, tu aplicación enviará a través de un servidor de Flux.</string>
<string name="address_creation_instruction">Pulsa Crear dirección SimpleX en el menú para crearla más tarde.</string>
<string name="connection_error_quota_desc">La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado.</string>
<string name="message_deleted_or_not_received_error_desc">El mensaje ha sido borrado o aún no se ha recibido.</string>
<string name="remote_hosts_section">Móvil remoto</string>
<string name="chat_archive">O importa desde un archivo</string>
<string name="direct_messages_are_prohibited_in_chat">Mensajes directos entre miembros de este chat no permitidos.</string>
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>En dispositivos Xiaomi:</b> por favor, habilita el Autoinicio en los ajustes del sistema para que las notificaciones funcionen.]]></string>
<string name="maximum_message_size_reached_text">Por favor, reduce el tamaño del mensaje y envíalo de nuevo.</string>
<string name="maximum_message_size_reached_non_text">Por favor, reduce el tamaño del mensaje o elimina los archivos y envíalo de nuevo.</string>
<string name="maximum_message_size_reached_forwarding">Puedes copiar y reducir el tamaño del mensaje para enviarlo.</string>
<string name="add_your_team_members_to_conversations">Añade a los miembros de tu equipo a las conversaciones.</string>
<string name="onboarding_notifications_mode_battery">Notificaciones y batería</string>
<string name="invite_to_chat_button">Invitar al chat</string>
<string name="button_add_friends">Añadir amigos</string>
<string name="button_add_team_members">Añadir miembros del equipo</string>
<string name="delete_chat_for_all_members_cannot_undo_warning">El chat será eliminado para todos los miembros. ¡No podrá deshacerse!</string>
<string name="button_delete_chat">Eliminar chat</string>
<string name="delete_chat_question">¿Eliminar chat?</string>
<string name="button_leave_chat">Salir del chat</string>
<string name="delete_chat_for_self_cannot_undo_warning">El chat será eliminado para tí. ¡No podrá deshacerse!</string>
<string name="only_chat_owners_can_change_prefs">Sólo los propietarios del chat pueden cambiar las preferencias.</string>
<string name="member_will_be_removed_from_chat_cannot_be_undone">El miembro será eliminado del chat. ¡No podrá deshacerse!</string>
<string name="member_role_will_be_changed_with_notification_chat">El rol cambiará a %s. Todos serán notificados.</string>
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Dejarás de recibir mensajes de este chat. El historial del chat se conserva.</string>
<string name="how_it_helps_privacy">Cómo ayuda a la privacidad</string>
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién.</string>
<string name="chat_main_profile_sent">Tu perfil de chat será enviado a los miembros de chat</string>
<string name="v6_2_business_chats">Chats empresariales</string>
<string name="leave_chat_question">¿Salir del chat?</string>
<string name="v6_2_business_chats_descr">Privacidad para tus clientes.</string>
<string name="display_name_accepted_invitation">invitación aceptada</string>
<string name="display_name_requested_to_connect">solicitado para conectar</string>
<string name="business_address">Dirección empresarial</string>
<string name="onboarding_notifications_mode_periodic_desc_short">Comprobar mensajes cada 10 min.</string>
<string name="onboarding_notifications_mode_off_desc_short">Sin servicio en segundo plano</string>
<string name="info_row_chat">Chat</string>
<string name="chat_bottom_bar">Barra de herramientas accesible</string>
<string name="direct_messages_are_prohibited">Mensajes directos entre miembros no permitidos.</string>
<string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Ya estás conectado con <b>%1$s</b>.]]></string>
<string name="connect_plan_chat_already_exists">¡El chat ya existe!</string>
<string name="onboarding_network_about_operators">Acerca de los operadores</string>
<string name="onboarding_notifications_mode_service_desc_short">La aplicación siempre funciona en segundo plano</string>
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Todos los mensajes y archivos son enviados <b>cifrados de extremo a extremo</b> y con seguridad postcuántica en mensajes directos.]]></string>
<string name="maximum_message_size_title">¡Mensaje demasiado largo!</string>
<string name="onboarding_network_operators_simplex_flux_agreement">Simplex Chat y Flux han acordado incluir servidores operados por Flux en la aplicación.</string>
</resources>
@@ -15,7 +15,7 @@
<string name="send_disappearing_message_30_seconds">30 másodperc</string>
<string name="one_time_link_short">Egyszer használható meghívó-hivatkozás</string>
<string name="contact_wants_to_connect_via_call">%1$s szeretne kapcsolatba lépni Önnel ezen keresztül:</string>
<string name="about_simplex_chat">A SimpleX Chatről</string>
<string name="about_simplex_chat">SimpleX Chat névjegye</string>
<string name="chat_item_ttl_day">1 nap</string>
<string name="abort_switch_receiving_address">Címváltoztatás megszakítása</string>
<string name="about_simplex">A SimpleXről</string>
@@ -1754,7 +1754,7 @@
<string name="network_smp_proxy_fallback_prohibit_description">Ne küldjön üzeneteket közvetlenül, még akkor sem, ha az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</string>
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Tor vagy VPN nélkül az IP-címe látható lesz a fájlkiszolgálók számára.</string>
<string name="settings_section_title_files">FÁJLOK</string>
<string name="protect_ip_address">IP-cím védelem</string>
<string name="protect_ip_address">IP-cím védelme</string>
<string name="app_will_ask_to_confirm_unknown_file_servers">Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van).</string>
<string name="file_not_approved_title">Ismeretlen kiszolgálók!</string>
<string name="file_not_approved_descr">Tor vagy VPN nélkül az IP-címe látható lesz az XFTP-közvetítő-kiszolgálók számára:\n%1$s.</string>
@@ -2109,13 +2109,13 @@
<string name="create_1_time_link">Egyszer használható meghívó-hivatkozás létrehozása</string>
<string name="onboarding_choose_server_operators">Kiszolgáló-üzemeltetők</string>
<string name="onboarding_network_operators">Hálózati üzemeltetők</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Amikor egynél több hálózati üzemeltető van engedélyezve, akkor az alkalmazás minden egyes beszélgetéshez a különböző üzemeltetők kiszolgálóit használja.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Ha például a SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, az alkalmazás a Flux egyik kiszolgálóját használja a privát útválasztáshoz.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetésben más-más üzemeltetőt használ.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Például, ha az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni.</string>
<string name="onboarding_select_network_operators_to_use">Válassza ki a használni kívánt hálózati üzemeltetőket.</string>
<string name="onboarding_network_operators_review_later">Felülvizsgálat később</string>
<string name="onboarding_network_operators_configure_via_settings">A kiszolgálókat a beállításokon keresztül konfigurálhatja.</string>
<string name="onboarding_network_operators_configure_via_settings">A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja.</string>
<string name="onboarding_network_operators_conditions_will_be_accepted">A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltetők számára.</string>
<string name="onboarding_network_operators_conditions_you_can_configure">Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja.</string>
<string name="onboarding_network_operators_conditions_you_can_configure">Az üzemeltetőket a „Hálózat és kiszolgálók” menüben konfigurálhatja.</string>
<string name="onboarding_network_operators_update">Frissítés</string>
<string name="onboarding_network_operators_continue">Folytatás</string>
<string name="operator_review_conditions">Feltételek felülvizsgálata</string>
@@ -2133,14 +2133,14 @@
<string name="use_servers_of_operator_x">%s használata</string>
<string name="operator_conditions_failed_to_load">A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket ezen a hivatkozáson keresztül vizsgálhatja felül:</string>
<string name="operator_conditions_accepted_for_some"><![CDATA[A feltételek már el lettek fogadva a következő üzemeltető(k) számára: <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_be_applied"><![CDATA[Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_be_applied"><![CDATA[Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára: <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k) számára: <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_applied"><![CDATA[Ezek a feltételek lesznek elfogadva a következő számára is: <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[A feltételek el lesznek fogadva a következő üzemeltető(k) számára: <b>%s</b>.]]></string>
<string name="operators_conditions_will_also_apply"><![CDATA[Ezek a feltételek lesznek elfogadva a következő számára is: <b>%s</b>.]]></string>
<string name="accept_conditions">Feltételek elfogadása</string>
<string name="operator_conditions_of_use">Használati feltételek</string>
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[A(z) <b>%s</b> kiszolgálóinak használatához fogadja el a használati feltételeket.]]></string>
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[A(z) <b>%s</b> kiszolgálók használatához fogadja el a használati feltételeket.]]></string>
<string name="operator_use_for_messages">Használat az üzenetekhez</string>
<string name="operator_use_for_messages_receiving">A fogadáshoz</string>
<string name="operator_use_for_messages_private_routing">A privát útválasztáshoz</string>
@@ -2204,4 +2204,9 @@
<string name="maximum_message_size_title">Az üzenet túl nagy!</string>
<string name="maximum_message_size_reached_non_text">Csökkentse az üzenet méretét vagy távolítsa el a médiát, és küldje el újra.</string>
<string name="direct_messages_are_prohibited_in_chat">A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben.</string>
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Amikor egynél több üzemeltető van engedélyezve, akkor egyik sem rendelkezik olyan metaadatokkal, amelyekből megtudható, hogy ki kivel kommunikál.</string>
<string name="display_name_accepted_invitation">elfogadott meghívó</string>
<string name="display_name_requested_to_connect">kérelmezve a kapcsolódáshoz</string>
<string name="onboarding_network_about_operators">Az üzemeltetőkről</string>
<string name="onboarding_network_operators_simplex_flux_agreement">A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba.</string>
</resources>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 -960 960 960" width="22px" fill="#5f6368"><path d="M262.5-285q11.5 0 20.25-8.5t8.75-20q0-11.5-8.75-20.25t-20.25-8.75q-11.5 0-20 8.75T234-313.5q0 11.5 8.5 20t20 8.5Zm-29-168.5H291v-227h-57.5v227Zm217.5 174h275.5V-337H451v57.5Zm0-174h275.5V-511H451v57.5Zm0-169.5h275.5v-57.5H451v57.5ZM134.5-124.5q-22.97 0-40.23-17.27Q77-159.03 77-182v-596q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h691q22.97 0 40.23 17.27Q883-800.97 883-778v596q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27h-691Zm0-57.5h691v-596h-691v596Zm0 0v-596 596Z"/></svg>

After

Width:  |  Height:  |  Size: 592 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M170-368v-75h620v75H170Zm0-150v-75h620v75H170Z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M54.5-279q0-33 16.75-60.25T116.5-381q61-30 123.5-45.25t126-15.25q63.5 0 125.75 15.25T615-381q28.5 14.5 45.25 41.75T677-279v31q0 31-22 53t-53 22H129.5q-31 0-53-22t-22-53v-31Zm677 106q10-17 15.25-36t5.25-39v-35q0-43.5-22.5-83.75T663-434.5q48.5 6 91.25 19.75t80.25 34.25Q869-362 887.25-338t18.25 52v38q0 31-22 53t-53 22h-99ZM366-479q-64 0-109-45t-45-109q0-64 45-109t109-45q64 0 109 45t45 109q0 64-45 109t-109 45Zm382-154.5q0 63.5-45 108.75T594-479.5q-9.5 0-25.25-2.25T543-487q26.5-30.5 40.75-68T598-633.5q0-40.5-14.25-78.25T543-780q12.5-4.5 25.5-5.75T594-787q64 0 109 45t45 108.5Z"/></svg>

After

Width:  |  Height:  |  Size: 683 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m859-406.5-304 305Q545.93-93 535.21-89q-10.71 4-21.71 4t-21.25-4.25Q482-93.5 473.5-101.5L102.5-474q-8-7.5-12.75-17.97Q85-502.44 85-514v-303.5q0-23.72 16.89-40.61T142.5-875h305q11.41 0 22.11 4.4 10.71 4.39 18.89 12.6L859-488q8.91 8.92 13.21 19.52 4.29 10.6 4.29 21.21 0 11.27-4.5 22.27t-13 18.5Zm-343 266L820-446 447.49-817.5H142.5v301.77L516-140.5ZM246.75-664q20.5 0 35.63-15.04 15.12-15.03 15.12-35.37 0-20.34-15.06-35.47Q267.38-765 247-765q-20.75 0-35.62 15.04-14.88 15.03-14.88 35.37 0 20.34 14.88 35.46Q226.25-664 246.75-664ZM481.5-479Z"/></svg>

After

Width:  |  Height:  |  Size: 646 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m859-406.5-304 305Q545.93-93 535.21-89q-10.71 4-21.71 4t-21.25-4.25Q482-93.5 473.5-101.5L102.5-474q-8-7.5-12.75-17.97Q85-502.44 85-514v-303.5q0-23.72 16.89-40.61T142.5-875h305q11.41 0 22.11 4.4 10.71 4.39 18.89 12.6L859-488q8.91 8.92 13.21 19.52 4.29 10.6 4.29 21.21 0 11.27-4.5 22.27t-13 18.5ZM246.75-664q20.5 0 35.63-15.04 15.12-15.03 15.12-35.37 0-20.34-15.06-35.47Q267.38-765 247-765q-20.75 0-35.62 15.04-14.88 15.03-14.88 35.37 0 20.34 14.88 35.46Q226.25-664 246.75-664Z"/></svg>

After

Width:  |  Height:  |  Size: 581 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480-479q-64.5 0-109.75-45.25T325-634q0-64.5 45.25-109.75T480-789q64.5 0 109.75 45.25T635-634q0 64.5-45.25 109.75T480-479ZM169-248v-31.03q0-32.97 16.75-60.22t45.27-41.76Q292-411 354.25-426.25 416.5-441.5 480-441.5t125.75 15.25Q668-411 728.98-381.01q28.52 14.51 45.27 41.76Q791-312 791-279.03V-248q0 30.94-22.03 52.97Q746.94-173 716-173H244q-30.94 0-52.97-22.03Q169-217.06 169-248Z"/></svg>

After

Width:  |  Height:  |  Size: 486 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-130q-30.94 0-52.97-22.03Q90-174.06 90-205v-440q0-30.94 22.03-52.97Q134.06-720 165-720h161v-75.04Q326-826 348.03-848T401-870h158q30.94 0 52.97 22.03Q634-825.94 634-795v75h161q30.94 0 52.97 22.03Q870-675.94 870-645v440q0 30.94-22.03 52.97Q825.94-130 795-130H165Zm236-590h158v-75H401v75Z"/></svg>

After

Width:  |  Height:  |  Size: 395 B

@@ -363,7 +363,7 @@
<string name="icon_descr_audio_off">Audio spento</string>
<string name="icon_descr_audio_on">Audio acceso</string>
<string name="settings_audio_video_calls">Chiamate audio e video</string>
<string name="auto_accept_images">Auto-accetta immagini</string>
<string name="auto_accept_images">Auto-accetta le immagini</string>
<string name="integrity_msg_bad_hash">hash del messaggio errato</string>
<string name="integrity_msg_bad_id">ID messaggio errato</string>
<string name="icon_descr_call_ended">Chiamata terminata</string>
@@ -470,7 +470,7 @@
<string name="feature_enabled_for_contact">attivato per il contatto</string>
<string name="feature_enabled_for_you">attivato per te</string>
<string name="group_preferences">Preferenze del gruppo</string>
<string name="v4_2_auto_accept_contact_requests">Auto-accetta richieste di contatto</string>
<string name="v4_2_auto_accept_contact_requests">Auto-accetta le richieste di contatto</string>
<string name="ttl_d">%dg</string>
<string name="ttl_day">%d giorno</string>
<string name="ttl_days">%d giorni</string>
@@ -538,7 +538,7 @@
<string name="invalid_QR_code">Codice QR non valido</string>
<string name="image_descr_link_preview">immagine di anteprima link</string>
<string name="mark_read">Segna come già letto</string>
<string name="mark_unread">Segna come non letto</string>
<string name="mark_unread">Segna come non letta</string>
<string name="icon_descr_more_button">Altro</string>
<string name="mute_chat">Silenzia</string>
<string name="image_descr_profile_image">immagine del profilo</string>
@@ -704,7 +704,7 @@
<string name="restart_the_app_to_create_a_new_chat_profile">Riavvia l\'app per creare un profilo di chat nuovo.</string>
<string name="restart_the_app_to_use_imported_chat_database">Riavvia l\'app per usare il database della chat importato.</string>
<string name="run_chat_section">AVVIA CHAT</string>
<string name="send_link_previews">Invia anteprime dei link</string>
<string name="send_link_previews">Invia le anteprime dei link</string>
<string name="set_password_to_export">Imposta la password per esportare</string>
<string name="settings_section_title_settings">IMPOSTAZIONI</string>
<string name="settings_section_title_socks">PROXY SOCKS</string>
@@ -918,7 +918,7 @@
<string name="v4_5_italian_interface_descr">Grazie agli utenti contribuite via Weblate!</string>
<string name="v4_4_french_interface">Interfaccia francese</string>
<string name="v4_5_italian_interface">Interfaccia italiana</string>
<string name="v4_5_message_draft">Bozza dei messaggi</string>
<string name="v4_5_message_draft">Bozza del messaggio</string>
<string name="v4_5_message_draft_descr">Conserva la bozza dell\'ultimo messaggio, con gli allegati.</string>
<string name="v4_5_private_filenames">Nomi di file privati</string>
<string name="v4_5_transport_isolation_descr">Per profilo di chat (predefinito) o per connessione (BETA).</string>
@@ -1348,7 +1348,7 @@
<string name="rcv_group_event_2_members_connected">%s e %s si sono connessi/e</string>
<string name="rcv_group_event_n_members_connected">%s, %s e altri %d membri si sono connessi</string>
<string name="rcv_group_event_3_members_connected">%s, %s e %s si sono connessi/e</string>
<string name="privacy_message_draft">Bozza</string>
<string name="privacy_message_draft">Bozza del messaggio</string>
<string name="privacy_show_last_messages">Mostra gli ultimi messaggi</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Il database verrà crittografato e la password conservata nelle impostazioni.</string>
<string name="you_can_change_it_later">La password casuale viene conservata nelle impostazioni come testo normale.
@@ -1615,8 +1615,7 @@
<string name="snd_group_event_member_blocked">hai bloccato %s</string>
<string name="welcome_message_is_too_long">Il messaggio di benvenuto è troppo lungo</string>
<string name="message_too_large">Messaggio troppo grande</string>
<string name="database_migration_in_progress">Migrazione database in corso.
\nPuò richiedere qualche minuto.</string>
<string name="database_migration_in_progress">Migrazione del database in corso.\nPuò richiedere qualche minuto.</string>
<string name="call_service_notification_audio_call">Chiamata audio</string>
<string name="call_service_notification_end_call">Termina chiamata</string>
<string name="call_service_notification_video_call">Videochiamata</string>
@@ -1980,7 +1979,7 @@
<string name="smp_proxy_error_connecting">Errore di connessione al server di inoltro %1$s. Riprova più tardi.</string>
<string name="smp_proxy_error_broker_version">La versione server di inoltro è incompatibile con le impostazioni di rete: %1$s.</string>
<string name="privacy_media_blur_radius_off">Off</string>
<string name="privacy_media_blur_radius">Sfocatura file multimediali</string>
<string name="privacy_media_blur_radius">Sfocatura dei file multimediali</string>
<string name="privacy_media_blur_radius_soft">Leggera</string>
<string name="privacy_media_blur_radius_medium">Media</string>
<string name="privacy_media_blur_radius_strong">Forte</string>
@@ -2131,18 +2130,18 @@
<string name="for_social_media">Per i social media</string>
<string name="or_to_share_privately">O per condividere in modo privato</string>
<string name="onboarding_network_operators">Operatori di rete</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Quando più di un operatore di rete è attivato, l\'app userà i server di diversi operatori per ogni conversazione.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">L\'app protegge la tua privacy usando diversi operatori per ogni conversazione.</string>
<string name="onboarding_network_operators_conditions_you_can_configure">Puoi configurare gli operatori nelle impostazioni di rete e server.</string>
<string name="onboarding_choose_server_operators">Operatori del server</string>
<string name="onboarding_select_network_operators_to_use">Seleziona gli operatori di rete da usare.</string>
<string name="onboarding_network_operators_continue">Continua</string>
<string name="onboarding_network_operators_update">Aggiorna</string>
<string name="onboarding_network_operators_review_later">Esamina più tardi</string>
<string name="onboarding_network_operators_review_later">Leggi più tardi</string>
<string name="network_preset_servers_title">Server preimpostati</string>
<string name="operator_conditions_accepted">Condizioni accettate</string>
<string name="operator_conditions_accepted_for_enabled_operators_on">Le condizioni verranno accettate automaticamente per gli operatori attivati il: %s.</string>
<string name="your_servers">I tuoi server</string>
<string name="operator_review_conditions">Esamina le condizioni</string>
<string name="operator_review_conditions">Leggi le condizioni</string>
<string name="operators_conditions_accepted_for"><![CDATA[Condizioni accettate per gli operatori: <b>%s</b>.]]></string>
<string name="operator_conditions_accepted_for_some"><![CDATA[Condizioni già accettate per i seguenti operatori: <b>%s</b>.]]></string>
<string name="operator_conditions_failed_to_load">Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link:</string>
@@ -2192,13 +2191,13 @@
<string name="error_accepting_operator_conditions">Errore di accettazione delle condizioni</string>
<string name="failed_to_save_servers">Errore di salvataggio dei server</string>
<string name="v6_2_network_decentralization_enable_flux_reason">per una migliore privacy dei metadati.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Ad esempio, se ricevi messaggi tramite il server di SimpleX Chat, l\'app userà uno dei server Flux per l\'instradamento privato.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Ad esempio, se il tuo contatto riceve i messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server di Flux.</string>
<string name="v6_2_improved_chat_navigation">Navigazione della chat migliorata</string>
<string name="smp_servers_new_server">Nuovo server</string>
<string name="operator_use_for_files">Usa per i file</string>
<string name="simplex_address_or_1_time_link">Indirizzo SimpleX o link una tantum?</string>
<string name="message_deleted_or_not_received_error_desc">Questo messaggio è stato eliminato o non ancora ricevuto.</string>
<string name="address_creation_instruction">Tocca \"Crea indirizzo SimpleX\" nel menu per crearlo più tardi.</string>
<string name="address_creation_instruction">Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi.</string>
<string name="connection_error_quota_desc">La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline.</string>
<string name="operator_use_operator_toggle_description">Usa i server</string>
<string name="onboarding_network_operators_configure_via_settings">Puoi configurare i server nelle impostazioni.</string>
@@ -2208,7 +2207,7 @@
<string name="no_media_servers_configured_for_sending">Nessun server per inviare file.</string>
<string name="v6_2_improved_chat_navigation_descr">- Apri la chat sul primo messaggio non letto.\n- Salta ai messaggi citati.</string>
<string name="share_address_publicly">Condividi indirizzo pubblicamente</string>
<string name="share_simplex_address_on_social_media">Condividi indirizzo SimpleX sui social media.</string>
<string name="share_simplex_address_on_social_media">Condividi l\'indirizzo SimpleX sui social media.</string>
<string name="chat_archive">O importa file archivio</string>
<string name="remote_hosts_section">Telefoni remoti</string>
<string name="direct_messages_are_prohibited_in_chat">I messaggi diretti tra i membri sono vietati in questa chat.</string>
@@ -2246,4 +2245,8 @@
<string name="member_role_will_be_changed_with_notification_chat">Il ruolo verrà cambiato in %s. Verrà notificato a tutti nella chat.</string>
<string name="chat_main_profile_sent">Il tuo profilo di chat verrà inviato ai membri della chat</string>
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata.</string>
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Quando più di un operatore è attivato, nessuno di essi ha metadati per capire chi comunica con chi.</string>
<string name="display_name_accepted_invitation">invito accettato</string>
<string name="display_name_requested_to_connect">richiesto di connettersi</string>
<string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell\'app</string>
</resources>
@@ -2147,7 +2147,7 @@
<string name="onboarding_network_operators_review_later">Later beoordelen</string>
<string name="onboarding_select_network_operators_to_use">Selecteer welke netwerkoperators u wilt gebruiken.</string>
<string name="onboarding_network_operators_update">Update</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Wanneer er meer dan één netwerkoperator is ingeschakeld, gebruikt de app voor elk gesprek de servers van verschillende operators.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">De app beschermt uw privacy door in elk gesprek verschillende operators te gebruiken.</string>
<string name="onboarding_network_operators_conditions_you_can_configure">U kunt operators configureren in Netwerk- en serverinstellingen.</string>
<string name="onboarding_network_operators_continue">Doorgaan</string>
<string name="operator_review_conditions">Voorwaarden bekijken</string>
@@ -2183,7 +2183,7 @@
<string name="v6_2_improved_chat_navigation">Verbeterde chatnavigatie</string>
<string name="v6_2_network_decentralization">Netwerk decentralisatie</string>
<string name="v6_2_network_decentralization_descr">De tweede vooraf ingestelde operator in de app!</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Als u bijvoorbeeld berichten ontvangt via de SimpleX Chat-server, gebruikt de app een van de Flux-servers voor privéroutering.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden.</string>
<string name="v6_2_network_decentralization_enable_flux">Flux inschakelen</string>
<string name="message_deleted_or_not_received_error_title">Geen bericht</string>
<string name="appearance_app_toolbars">App-werkbalken</string>
@@ -2242,4 +2242,9 @@
<string name="onboarding_notifications_mode_off_desc_short">Geen achtergrondservice</string>
<string name="only_chat_owners_can_change_prefs">Alleen chateigenaren kunnen voorkeuren wijzigen.</string>
<string name="maximum_message_size_reached_non_text">Verklein de berichtgrootte of verwijder de media en verzend het bericht opnieuw.</string>
<string name="display_name_accepted_invitation">geaccepteerde uitnodiging</string>
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert.</string>
<string name="display_name_requested_to_connect">gevraagd om verbinding te maken</string>
<string name="onboarding_network_about_operators">Over operatoren</string>
<string name="onboarding_network_operators_simplex_flux_agreement">Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen.</string>
</resources>
@@ -1882,8 +1882,7 @@
<string name="message_queue_info">Информация об очереди сообщений</string>
<string name="v5_8_persian_ui">Персидский интерфейс</string>
<string name="protect_ip_address">Защитить IP адрес</string>
<string name="v5_8_private_routing_descr">Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами.
\nВключите в настройках Сеть и серверы.</string>
<string name="v5_8_private_routing_descr">Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. \nВключите в настройках Сети и серверов.</string>
<string name="network_smp_proxy_fallback_allow_description">Отправьте сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку.</string>
<string name="network_smp_proxy_mode_private_routing">Конфиденциальная доставка</string>
<string name="network_smp_proxy_mode_unknown_description">Использовать конфиденциальную доставку с неизвестными серверами.</string>
@@ -2265,7 +2264,7 @@
<string name="how_it_helps_privacy">Как это улучшает конфиденциальность</string>
<string name="onboarding_network_operators">Операторы серверов</string>
<string name="onboarding_select_network_operators_to_use">Выберите операторов сети.</string>
<string name="onboarding_network_operators_conditions_you_can_configure">Вы можете настроить операторов в настройках Сеть и серверы.</string>
<string name="onboarding_network_operators_conditions_you_can_configure">Вы можете настроить операторов в настройках Сети и серверов.</string>
<string name="onboarding_network_operators_continue">Продолжить</string>
<string name="onboarding_network_operators_review_later">Посмотреть позже</string>
<string name="onboarding_network_operators_update">Обновить</string>
@@ -2305,7 +2304,7 @@
<string name="error_updating_server_title">Ошибка сохранения сервера</string>
<string name="operator_use_for_messages_private_routing">Для доставки сообщений</string>
<string name="operator_open_changes">Открыть изменения</string>
<string name="error_server_operator_changed">Оператор серверов изменен.</string>
<string name="error_server_operator_changed">Оператор сервера изменен.</string>
<string name="error_server_protocol_changed">Протокол сервера изменен.</string>
<string name="xftp_servers_per_user">Серверы для новых файлов Вашего текущего профиля</string>
<string name="operator_use_for_messages_receiving">Для получения</string>
@@ -219,7 +219,7 @@
<string name="ok">OK</string>
<string name="copied">Скопійовано в буфер обміну</string>
<string name="to_connect_via_link_title">Для підключення через посилання</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобільний телефон: натисніть <b>Відкрити у мобільному додатку</b>, а потім торкніться <b>Підключити</b> в додатку.]]></string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобільний: натисніть <b>Відкрити у мобільному додатку</b>, а потім торкніться <b>Підключити</b> в додатку.]]></string>
<string name="mute_chat">Приглушити</string>
<string name="unmute_chat">Скасувати приглушення</string>
<string name="you_invited_a_contact">Ви запросили контакт</string>
@@ -233,7 +233,7 @@
<string name="one_time_link">Одноразове запрошення</string>
<string name="incorrect_code">Невірний код безпеки!</string>
<string name="to_verify_compare">Для перевірки end-to-end шифрування порівняйте (або скануйте) код на своїх пристроях.</string>
<string name="your_settings">Ваші налаштування</string>
<string name="your_settings">Налаштування</string>
<string name="your_simplex_contact_address">Ваша SimpleX-адреса</string>
<string name="markdown_help">Допомога з Markdown</string>
<string name="chat_lock">Блокування SimpleX</string>
@@ -268,7 +268,7 @@
<string name="first_platform_without_user_ids">Ніяких ідентифікаторів користувачів.</string>
<string name="decentralized">Децентралізована</string>
<string name="use_chat">Використовувати чат</string>
<string name="onboarding_notifications_mode_subtitle">Це можна змінити пізніше в налаштуваннях.</string>
<string name="onboarding_notifications_mode_subtitle">Як це впливає на батарею</string>
<string name="onboarding_notifications_mode_service">Миттєво</string>
<string name="call_already_ended">Виклик вже завершено!</string>
<string name="your_calls">Ваші виклики</string>
@@ -496,7 +496,7 @@
<string name="only_your_contact_can_send_voice">Тільки ваш контакт може надсилати голосові повідомлення.</string>
<string name="prohibit_sending_disappearing">Забороняйте надсилання повідомлень, які зникають.</string>
<string name="prohibit_message_deletion">Забороняйте невідворотне видалення повідомлень.</string>
<string name="group_members_can_send_voice">Учасники групи можуть надсилати голосові повідомлення.</string>
<string name="group_members_can_send_voice">Учасники можуть надсилати голосові повідомлення.</string>
<string name="ttl_m">%dm</string>
<string name="new_in_version">Нове в %s</string>
<string name="v5_1_self_destruct_passcode">Самознищуючий пароль</string>
@@ -544,7 +544,7 @@
<string name="smp_server_test_create_file">Створити файл</string>
<string name="error_deleting_user">Помилка видалення користувача</string>
<string name="error_updating_user_privacy">Помилка оновлення конфіденційності користувача</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Для збереження вашої конфіденційності, замість торкання сповіщень, програма використовує <b>фоновий сервіс SimpleX</b> – він використовує кілька відсотків батареї щодня.]]></string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Для покращення конфіденційності, <b>SimpleX працює у фоновому режимі</b> замість використання пуш-повідомлень.]]></string>
<string name="periodic_notifications">Періодичні сповіщення</string>
<string name="simplex_service_notification_title">Служба чату SimpleX</string>
<string name="notifications_mode_periodic_desc">Перевіряє нові повідомлення кожні 10 хвилин протягом 1 хвилини</string>
@@ -649,7 +649,7 @@
<string name="you_can_use_markdown_to_format_messages__prompt">Ви можете використовувати markdown для форматування повідомлень:</string>
<string name="create_your_profile">Створіть свій профіль</string>
<string name="make_private_connection">Створіть приватне підключення</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Тільки пристрої клієнта зберігають профілі користувачів, контакти, групи та повідомлення, відправлені за допомогою <b>шифрування на двох рівнях</b>.]]></string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Тільки клієнтські пристрої зберігають профілі, контакти, групи та повідомлення.</string>
<string name="onboarding_notifications_mode_title">Приватні сповіщення</string>
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Споживає більше акумулятора</b>! Додаток завжди працює у фоновому режимі – сповіщення відображаються миттєво.]]></string>
<string name="paste_the_link_you_received">Вставте отримане посилання</string>
@@ -746,14 +746,14 @@
<string name="only_you_can_send_voice">Тільки ви можете надсилати голосові повідомлення.</string>
<string name="only_you_can_add_message_reactions">Тільки ви можете додавати реакції на повідомлення.</string>
<string name="prohibit_message_reactions_group">Заборонити реакції на повідомлення.</string>
<string name="disappearing_messages_are_prohibited">Самознищувальні повідомлення заборонені в цій групі.</string>
<string name="group_members_can_send_dms">Учасники групи можуть надсилати приватні повідомлення.</string>
<string name="disappearing_messages_are_prohibited">Повідомлення, що зникають, заборонені.</string>
<string name="group_members_can_send_dms">Учасники можуть надсилати прямі повідомлення.</string>
<string name="direct_messages_are_prohibited_in_group">Приватні повідомлення між учасниками заборонені в цій групі.</string>
<string name="group_members_can_delete">Учасники групи можуть назавжди видаляти відправлені повідомлення. (24 години)</string>
<string name="message_deletion_prohibited_in_chat">Назавжди видалення повідомлень заборонене в цій групі.</string>
<string name="voice_messages_are_prohibited">Голосові повідомлення заборонені в цій групі.</string>
<string name="group_members_can_add_message_reactions">Учасники групи можуть додавати реакції на повідомлення.</string>
<string name="message_reactions_are_prohibited">Реакції на повідомлення заборонені в цій групі.</string>
<string name="group_members_can_delete">Учасники можуть необоротно видаляти надіслані повідомлення (протягом 24 годин).</string>
<string name="message_deletion_prohibited_in_chat">Заборонено необоротне видалення повідомлень.</string>
<string name="voice_messages_are_prohibited">Голосові повідомлення заборонені</string>
<string name="group_members_can_add_message_reactions">Учасники можуть додавати реакції на повідомлення.</string>
<string name="message_reactions_are_prohibited">Реакції на повідомлення заборонені.</string>
<string name="ttl_hour">%d година</string>
<string name="ttl_week">%d тиждень</string>
<string name="ttl_weeks">%d тижні</string>
@@ -805,7 +805,7 @@
<string name="smp_server_test_secure_queue">Безпечна черга</string>
<string name="smp_server_test_delete_queue">Видалити чергу</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Будь ласка, перевірте, що ви використали правильне посилання або попросіть вашого контакту вислати інше.</string>
<string name="turn_off_battery_optimization"><![CDATA[Щоб його використовувати, будь ласка, <b>дозвольте SimpleX працювати в фоновому режимі</b> в наступному діалозі. В іншому випадку сповіщення будуть вимкнені.]]></string>
<string name="turn_off_battery_optimization"><![CDATA[<b>Дозвольте це</b> в наступному діалозі, щоб отримувати сповіщення миттєво.]]></string>
<string name="icon_descr_instant_notifications">Миттєві сповіщення</string>
<string name="notification_preview_somebody">Контакт прихований:</string>
<string name="notification_preview_new_message">нове повідомлення</string>
@@ -892,7 +892,7 @@
<string name="v4_6_chinese_spanish_interface_descr">Дякуємо користувачам – приєднуйтеся через Weblate!</string>
<string name="la_lock_mode">Режим блокування SimpleX</string>
<string name="la_lock_mode_system">Системна аутентифікація</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Для захисту приватності, замість ідентифікаторів користувачів, які використовуються всіма іншими платформами, у SimpleX є ідентифікатори черг повідомлень, окремі для кожного з ваших контактів.</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Для захисту вашої конфіденційності SimpleX використовує окремі ID для кожного вашого контакту.</string>
<string name="onboarding_notifications_mode_off">Коли додаток запущено</string>
<string name="onboarding_notifications_mode_periodic">Періодично</string>
<string name="status_contact_has_no_e2e_encryption">контакт не має зашифрування e2e</string>
@@ -919,7 +919,7 @@
<string name="snd_group_event_member_deleted">ви видалили %1$s</string>
<string name="tap_to_activate_profile">Торкніться для активації профілю.</string>
<string name="prohibit_sending_voice">Забороняйте надсилання голосових повідомлень.</string>
<string name="group_members_can_send_disappearing">Учасники групи можуть надсилати самознищувальні повідомлення.</string>
<string name="group_members_can_send_disappearing">Учасники можуть надсилати повідомлення, що зникають.</string>
<string name="ttl_min">%d хв</string>
<string name="v4_5_reduced_battery_usage">Зменшене споживання енергії батареї</string>
<string name="edit_image">Редагувати зображення</string>
@@ -1012,7 +1012,7 @@
<string name="clear_verification">Очистити перевірку</string>
<string name="is_verified">%s перевірено</string>
<string name="is_not_verified">%s не перевірено</string>
<string name="send_us_an_email">Надішліть нам електронного листа</string>
<string name="send_us_an_email">Написати нам ел. листа</string>
<string name="smp_servers_test_server">Тестовий сервер</string>
<string name="smp_save_servers_question">Зберегти сервери\?</string>
<string name="your_ICE_servers">Ваші сервери ICE</string>
@@ -1089,7 +1089,7 @@
<string name="chat_preferences_no">ні</string>
<string name="chat_preferences_off">вимк</string>
<string name="set_group_preferences">Встановити налаштування групи</string>
<string name="your_preferences">Ваші налаштування</string>
<string name="your_preferences">Налаштування</string>
<string name="direct_messages">Прямі повідомлення</string>
<string name="icon_descr_server_status_error">Помилка</string>
<string name="add_contact">Одноразове запрошення</string>
@@ -1161,7 +1161,7 @@
<string name="colored_text">кольоровий</string>
<string name="callstatus_ended">дзвінок завершено %1$s</string>
<string name="callstatus_error">помилка дзвінка</string>
<string name="next_generation_of_private_messaging">Наступне покоління \nприватних повідомлень</string>
<string name="next_generation_of_private_messaging">Майбутнє обміну повідомленнями</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Кожен може хостити сервери.</string>
<string name="settings_developer_tools">Інструменти розробника</string>
<string name="settings_experimental_features">Експериментальні функції</string>
@@ -1178,7 +1178,7 @@
<string name="to_share_with_your_contact">(щоб поділитися з вашим контактом)</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(сканувати або вставити з буферу обміну)</string>
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Ви можете <font color="#0088ff">підключитися до розробників SimpleX Chat, щоб задати будь-які питання і отримувати оновлення</font>.]]></string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 настільний комп\'ютер: скануйте відображений QR-код з додатка за допомогою <b>Сканувати QR-код</b>.]]></string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 на комп\'ютері: скануйте відображений QR-код з додатка за допомогою <b>Сканувати QR-код</b>.]]></string>
<string name="icon_descr_address">Адреса SimpleX</string>
<string name="show_QR_code">Показати QR-код</string>
<string name="joining_group">Приєднання до групи</string>
@@ -1249,7 +1249,7 @@
<string name="error_aborting_address_change">Помилка відміни зміни адреси</string>
<string name="abort_switch_receiving_address">Перервати зміну адреси</string>
<string name="allow_to_send_files">Дозволити надсилання файлів та медіафайлів.</string>
<string name="files_are_prohibited_in_group">Файли та медіафайли заборонені в цій групі.</string>
<string name="files_are_prohibited_in_group">Файли та медіа заборонені.</string>
<string name="connect_via_link_incognito">Підключити інкогніто</string>
<string name="connect_use_current_profile">Використовувати поточний профіль</string>
<string name="turn_off_battery_optimization_button">Дозволити</string>
@@ -1347,7 +1347,7 @@
<string name="abort_switch_receiving_address_desc">Зміна адреси буде скасована. Буде використовуватися стара адреса для отримання.</string>
<string name="sync_connection_force_question">Повторно узгодити шифрування?</string>
<string name="sync_connection_force_desc">Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок підключення!</string>
<string name="group_members_can_send_files">Учасники групи можуть надсилати файли та медіафайли.</string>
<string name="group_members_can_send_files">Учасники можуть надсилати файли та медіа.</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">База даних буде зашифрована, і ключова фраза буде збережена в налаштуваннях.</string>
<string name="expand_verb">Розгорнути</string>
<string name="connect_plan_repeat_connection_request">Повторити запит на підключення?</string>
@@ -1441,7 +1441,7 @@
<string name="multicast_connect_automatically">Підключати автоматично</string>
<string name="desktop_address">Адреса робочого столу</string>
<string name="only_one_device_can_work_at_the_same_time">Одночасно може працювати лише один пристрій</string>
<string name="v5_4_link_mobile_desktop">Посилання на мобільний та комп\'ютерний додатки! 🔗</string>
<string name="v5_4_link_mobile_desktop">Підключіть мобільний і десктопний додатки! 🔗</string>
<string name="v5_4_link_mobile_desktop_descr">Через безпечний квантовостійкий протокол.</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Відкрийте <i>Використовувати з робочого столу</i> у мобільному додатку і скануйте QR-код.]]></string>
<string name="v5_4_block_group_members_descr">Щоб приховати небажані повідомлення.</string>
@@ -1734,7 +1734,7 @@
<string name="forward_chat_item">Переслати</string>
<string name="forwarded_chat_item_info_tab">Переслано</string>
<string name="forwarded_from_chat_item_info_title">Переслано з</string>
<string name="group_members_can_send_simplex_links">Учасники групи можуть надсилати посилання SimpleX.</string>
<string name="group_members_can_send_simplex_links">Учасники можуть надсилати посилання SimpleX.</string>
<string name="v5_7_call_sounds">Звуки вхідного дзвінка</string>
<string name="chat_theme_apply_to_light_mode">Світлий режим</string>
<string name="update_network_smp_proxy_fallback_question">Запасний варіант маршрутизації повідомлень</string>
@@ -1828,7 +1828,7 @@
<string name="network_smp_proxy_fallback_allow_protected">Коли IP приховано</string>
<string name="network_smp_proxy_fallback_allow">Так</string>
<string name="network_option_rcv_concurrency">Отримання паралелізму</string>
<string name="simplex_links_are_prohibited_in_group">У цій групі заборонені посилання на SimpleX.</string>
<string name="simplex_links_are_prohibited_in_group">Посилання SimpleX заборонені.</string>
<string name="v5_7_shape_profile_images">Сформуйте зображення профілю</string>
<string name="v5_7_call_sounds_descr">При підключенні аудіо та відеодзвінків.</string>
<string name="reset_single_color">Скинути колір</string>
@@ -2011,7 +2011,7 @@
<string name="app_check_for_updates_button_download">Завантажити %s (%s)</string>
<string name="app_check_for_updates_button_open">Відкрити розташування файлу</string>
<string name="app_check_for_updates_button_skip">Пропустити цю версію</string>
<string name="one_hand_ui">Доступна панель чату</string>
<string name="one_hand_ui">Доступні панелі додатка</string>
<string name="cant_call_contact_alert_title">Не можна зателефонувати контакту</string>
<string name="cant_call_contact_connecting_wait_alert_text">Підключення до контакту, будь ласка, зачекайте або перевірте пізніше!</string>
<string name="calls_prohibited_alert_title">Дзвінки заборонені!</string>
@@ -2129,10 +2129,10 @@
<string name="to_protect_against_your_link_replaced_compare_codes">Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів.</string>
<string name="for_social_media">Для соціальних мереж</string>
<string name="or_to_share_privately">Або поділитися приватно</string>
<string name="onboarding_choose_server_operators">Обирайте операторів</string>
<string name="onboarding_choose_server_operators">Оператори серверів</string>
<string name="onboarding_network_operators">Мережеві оператори</string>
<string name="onboarding_network_operators_conditions_will_be_accepted">Умови будуть прийняті для ввімкнених операторів через 30 днів.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Наприклад, якщо ви отримуєте повідомлення через сервер SimpleX Chat, програма використовуватиме один із серверів Flux для приватної маршрутизації.</string>
<string name="onboarding_network_operators_app_will_use_for_routing">Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux.</string>
<string name="onboarding_select_network_operators_to_use">Виберіть мережевих операторів для використання.</string>
<string name="onboarding_network_operators_configure_via_settings">Ви можете налаштувати сервери за допомогою налаштувань.</string>
<string name="onboarding_network_operators_review_later">Перегляньте пізніше</string>
@@ -2145,7 +2145,7 @@
<string name="your_servers">Ваші сервери</string>
<string name="use_servers_of_operator_x">Використовуйте %s</string>
<string name="operator_use_operator_toggle_description">Використовуйте сервери</string>
<string name="operator_same_conditions_will_be_applied"><![CDATA[Ті самі умови стосуватимуться оператора <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_be_applied"><![CDATA[Ті ж умови будуть застосовуватись до оператора <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_applied"><![CDATA[Ці умови також застосовуватимуться для: <b>%s</b>.]]></string>
<string name="accept_conditions">Прийняти умови</string>
<string name="view_conditions">Умови перегляду</string>
@@ -2177,7 +2177,7 @@
<string name="operator_conditions_accepted_for_some"><![CDATA[Умови вже прийняті для наступних операторів: <b>%s</b>.]]></string>
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Умови будуть прийняті для оператора(ів): <b>%s</b>.]]></string>
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Умови будуть прийняті для оператора(ів): <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Ті самі умови застосовуватимуться до оператора(ів): <b>%s</b>.]]></string>
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Ті ж умови будуть застосовуватись до оператора(ів): <b>%s</b>.]]></string>
<string name="operators_conditions_will_also_apply"><![CDATA[Ці умови також застосовуватимуться для: <b>%s</b>.]]></string>
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[Щоб використовувати сервери <b>%s</b>, прийміть умови використання.]]></string>
<string name="operator_conditions_failed_to_load">Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням:</string>
@@ -2203,9 +2203,48 @@
<string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер.</string>
<string name="connection_error_quota_desc">З\'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн.</string>
<string name="address_creation_instruction">Натисніть Створити адресу SimpleX у меню, щоб створити її пізніше.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Якщо увімкнено більше одного оператора, програма використовуватиме сервери різних операторів для кожної розмови.</string>
<string name="onboarding_network_operators_app_will_use_different_operators">Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові.</string>
<string name="operator_use_for_messages">Використовуйте для повідомлень</string>
<string name="onboarding_network_operators_conditions_you_can_configure">Ви можете налаштувати операторів у налаштуваннях Мережі та серверів.</string>
<string name="chat_archive">Або імпортуйте архівний файл</string>
<string name="remote_hosts_section">Віддалені мобільні</string>
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Пристрої Xiaomi</b>: будь ласка, увімкніть Автозапуск у налаштуваннях системи, щоб сповіщення працювали.]]></string>
<string name="maximum_message_size_title">Повідомлення занадто велике!</string>
<string name="maximum_message_size_reached_non_text">Будь ласка, зменшіть розмір повідомлення або видаліть медіа та надішліть знову.</string>
<string name="add_your_team_members_to_conversations">Додайте учасників команди до розмов.</string>
<string name="business_address">Бізнес адреса</string>
<string name="onboarding_notifications_mode_periodic_desc_short">Перевіряти повідомлення кожні 10 хвилин.</string>
<string name="onboarding_notifications_mode_off_desc_short">Без фонової служби</string>
<string name="onboarding_notifications_mode_battery">Сповіщення та батарея</string>
<string name="onboarding_notifications_mode_service_desc_short">Додаток завжди працює у фоні.</string>
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Усі повідомлення та файли надсилаються <b>зашифрованими end-to-end</b>, з пост-квантовою безпекою в особистих повідомленнях.]]></string>
<string name="leave_chat_question">Покинути чат?</string>
<string name="member_will_be_removed_from_chat_cannot_be_undone">Учасник буде видалений з чату — це неможливо скасувати!</string>
<string name="v6_2_business_chats">Бізнес чати</string>
<string name="v6_2_business_chats_descr">Конфіденційність для ваших клієнтів.</string>
<string name="chat_bottom_bar">Доступна панель чату</string>
<string name="button_add_friends">Додати друзів</string>
<string name="button_add_team_members">Додати учасників команди</string>
<string name="invite_to_chat_button">Запросити до чату</string>
<string name="delete_chat_for_all_members_cannot_undo_warning">Чат буде видалений для всіх учасників — це неможливо скасувати!</string>
<string name="button_delete_chat">Видалити чат</string>
<string name="delete_chat_question">Видалити чат?</string>
<string name="only_chat_owners_can_change_prefs">Тільки власники чату можуть змінювати налаштування.</string>
<string name="member_role_will_be_changed_with_notification_chat">Роль буде змінена на %s. Усі учасники чату отримають повідомлення.</string>
<string name="direct_messages_are_prohibited">Прямі повідомлення між учасниками заборонені.</string>
<string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Ви вже підключені до <b>%1$s</b>.]]></string>
<string name="connect_plan_chat_already_exists">Чат вже існує!</string>
<string name="how_it_helps_privacy">Як це допомагає зберігати конфіденційність</string>
<string name="direct_messages_are_prohibited_in_chat">Прямі повідомлення між учасниками заборонені в цьому чаті.</string>
<string name="button_leave_chat">Покинути чат</string>
<string name="info_row_chat">Чат</string>
<string name="delete_chat_for_self_cannot_undo_warning">Чат буде видалений для вас — це неможливо скасувати!</string>
<string name="maximum_message_size_reached_text">Будь ласка, зменшіть розмір повідомлення та надішліть знову.</string>
<string name="maximum_message_size_reached_forwarding">Скопіюйте та зменшіть розмір повідомлення для відправки.</string>
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Ви припините отримувати повідомлення з цього чату. Історія чату буде збережена.</string>
<string name="chat_main_profile_sent">Ваш профіль чату буде надіслано учасникам чату.</string>
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Коли увімкнено більше ніж одного оператора, жоден з них не має метаданих, щоб дізнатися, хто спілкується з ким.</string>
<string name="display_name_accepted_invitation">прийнято запрошення</string>
<string name="display_name_requested_to_connect">запит на підключення</string>
<string name="onboarding_network_about_operators">Про операторів</string>
</resources>
@@ -2157,8 +2157,8 @@
<string name="v6_2_network_decentralization_descr">应用中的第二个预设运营者!</string>
<string name="v6_2_improved_chat_navigation">改进了聊天导航</string>
<string name="view_updated_conditions">查看更新后的条款</string>
<string name="onboarding_network_operators_app_will_use_for_routing">比如,如果你通过 SimpleX 服务器收到消息,应用会使用 Flux 服务器中的一台进行私密路由</string>
<string name="onboarding_network_operators_app_will_use_different_operators">启用了多于一个网络运营者时,应用会为每个对话使用不同运营者的服务器</string>
<string name="onboarding_network_operators_app_will_use_for_routing">比如,如果你通过 SimpleX 服务器收到消息,应用会通过 Flux 服务器传送它们</string>
<string name="onboarding_network_operators_app_will_use_different_operators">应用通过在每个对话使用不同运营者保护你的隐私</string>
<string name="accept_conditions">接受条款</string>
<string name="appearance_bars_blur_radius">模糊</string>
<string name="address_or_1_time_link">地址或一次性链接?</string>
@@ -2229,4 +2229,8 @@
<string name="info_row_chat">聊天</string>
<string name="member_will_be_removed_from_chat_cannot_be_undone">将从聊天中删除成员 - 此操作无法撤销!</string>
<string name="maximum_message_size_reached_text">请减小消息尺寸并再次发送。</string>
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">当启用了超过一个运营者时,没有一个运营者拥有了解谁和谁联络的元数据。</string>
<string name="display_name_accepted_invitation">已接受邀请</string>
<string name="display_name_requested_to_connect">被请求连接</string>
<string name="onboarding_network_about_operators">关于运营者</string>
</resources>
@@ -12,26 +12,60 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 30%;
max-height: 30%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 80px;
right: 0;
@media (orientation: portrait) {
#remote-video-stream.collapsed {
position: absolute;
width: 30%;
max-width: 30%;
height: 39.9vw;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 80px;
right: 0;
}
}
#remote-video-stream.collapsed-pip {
position: absolute;
max-width: 50%;
max-height: 50%;
object-fit: cover;
margin: 8px;
border-radius: 8px;
bottom: 0;
right: 0;
@media (orientation: landscape) {
#remote-video-stream.collapsed {
position: absolute;
width: 20%;
max-width: 20%;
height: 15.03vw;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 80px;
right: 0;
}
}
@media (orientation: portrait) {
#remote-video-stream.collapsed-pip {
position: absolute;
width: 50%;
max-width: 50%;
height: 66.5vw;
object-fit: cover;
margin: 8px;
border-radius: 8px;
bottom: 0;
right: 0;
}
}
@media (orientation: landscape) {
#remote-video-stream.collapsed-pip {
position: absolute;
width: 50%;
max-width: 50%;
height: 37.59vw;
object-fit: cover;
margin: 8px;
border-radius: 8px;
bottom: 0;
right: 0;
}
}
#remote-screen-video-stream.inline {
@@ -41,15 +75,32 @@ body {
object-fit: cover;
}
#local-video-stream.inline {
position: absolute;
width: 30%;
max-width: 30%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
top: 0;
right: 0;
@media (orientation: portrait) {
#local-video-stream.inline {
position: absolute;
width: 30%;
max-width: 30%;
height: 39.9vw;
object-fit: cover;
margin: 16px;
border-radius: 16px;
top: 0;
right: 0;
}
}
@media (orientation: landscape) {
#local-video-stream.inline {
position: absolute;
width: 20%;
max-width: 20%;
height: 15.03vw;
object-fit: cover;
margin: 16px;
border-radius: 16px;
top: 0;
right: 0;
}
}
#local-screen-video-stream.inline {
@@ -301,6 +301,7 @@ const processCommand = (function () {
localStream = await getLocalMediaStream(true, command.media == CallMediaType.Video && (await browserHasCamera()), VideoCamera.User);
const videos = getVideoElements();
if (videos) {
setupLocalVideoRatio(videos.local);
videos.local.srcObject = localStream;
videos.local.play().catch((e) => console.log(e));
}
@@ -330,9 +331,12 @@ const processCommand = (function () {
console.log("starting incoming call - create webrtc session");
if (activeCall)
endCall();
// It can be already defined on Android when switching calls (if the previous call was outgoing)
notConnectedCall = undefined;
inactiveCallMediaSources.mic = true;
inactiveCallMediaSources.camera = command.media == CallMediaType.Video;
inactiveCallMediaSourcesChanged(inactiveCallMediaSources);
setupLocalVideoRatio(getVideoElements().local);
const { media, iceServers, relay } = command;
const encryption = supportsInsertableStreams(useWorker);
const aesKey = encryption ? command.aesKey : undefined;
@@ -547,13 +551,13 @@ const processCommand = (function () {
}
function endCall() {
var _a;
shutdownCameraAndMic();
try {
(_a = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection) === null || _a === void 0 ? void 0 : _a.close();
}
catch (e) {
console.log(e);
}
shutdownCameraAndMic();
activeCall = undefined;
resetVideoElements();
}
@@ -642,27 +646,21 @@ const processCommand = (function () {
}
// Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture
videos.local.play().catch((e) => console.log(e));
setupLocalVideoRatio(videos.local);
}
function setupLocalVideoRatio(local) {
const ratio = isDesktop ? 1.33 : 1 / 1.33;
const currentRect = local.getBoundingClientRect();
// better to get percents from here than to hardcode values from styles (the styles can be changed)
const screenWidth = currentRect.left + currentRect.width;
const percents = currentRect.width / screenWidth;
local.style.width = `${percents * 100}%`;
local.style.height = `${(percents / ratio) * 100}vw`;
local.addEventListener("loadedmetadata", function () {
console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px");
if (local.videoWidth == 0 || local.videoHeight == 0)
return;
local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw`;
const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3;
local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw`;
});
local.onresize = function () {
console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight);
if (local.videoWidth == 0 || local.videoHeight == 0)
return;
local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw`;
const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3;
local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw`;
};
}
function setupEncryptionForLocalStream(call) {
@@ -1128,8 +1126,9 @@ const processCommand = (function () {
(!!useWorker && "RTCRtpScriptTransform" in window));
}
function shutdownCameraAndMic() {
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localStream) {
if (activeCall) {
activeCall.localStream.getTracks().forEach((track) => track.stop());
activeCall.localScreenStream.getTracks().forEach((track) => track.stop());
}
}
function resetVideoElements() {
@@ -1295,6 +1294,9 @@ function changeLayout(layout) {
break;
}
videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden";
if (!isDesktop && !localSources.camera) {
resetLocalVideoElementHeight(videos.local);
}
}
function getVideoElements() {
const local = document.getElementById("local-video-stream");
@@ -1312,6 +1314,11 @@ function getVideoElements() {
return;
return { local, localScreen, remote, remoteScreen };
}
// Allow CSS to figure out the size of view by itself on Android because rotating to different orientation
// without dropping override will cause the view to have not normal proportion while no video is present
function resetLocalVideoElementHeight(local) {
local.style.height = "";
}
function desktopShowPermissionsAlert(mediaType) {
if (!isDesktop)
return;
@@ -15,8 +15,9 @@ body {
#remote-video-stream.collapsed {
position: absolute;
width: 20%;
max-width: 20%;
max-height: 20%;
height: 15.03vw;
object-fit: cover;
margin: 16px;
border-radius: 16px;
@@ -47,6 +48,7 @@ body {
position: absolute;
width: 20%;
max-width: 20%;
height: 15.03vw;
object-fit: cover;
margin: 16px;
border-radius: 16px;
@@ -67,7 +67,7 @@ object NtfManager {
ntf.second.close()
} catch (e: Exception) {
// Can be java.lang.UnsupportedOperationException, for example. May do nothing
println("Failed to close notification: ${e.stackTraceToString()}")
Log.e(TAG, "Failed to close notification: ${e.stackTraceToString()}")
}*/
}
}
@@ -85,7 +85,8 @@ object NtfManager {
}
fun cancelAllNotifications() {
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } }
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { Log.e(TAG, "Failed to close notification: ${e
// .stackTraceToString()}") } }
withBGApi {
prevNtfsMutex.withLock {
prevNtfs.clear()
@@ -153,7 +154,7 @@ object NtfManager {
ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream())
newFile.absolutePath
} catch (e: Exception) {
println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
Log.e(TAG, "Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
null
}
} else null
@@ -1,8 +1,10 @@
package chat.simplex.common.platform
import chat.simplex.common.model.ChatController.appPrefs
actual object Log {
actual fun d(tag: String, text: String) = println("D: $text")
actual fun e(tag: String, text: String) = println("E: $text")
actual fun i(tag: String, text: String) = println("I: $text")
actual fun w(tag: String, text: String) = println("W: $text")
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) println("D: $text") }
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) println("E: $text") }
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) println("I: $text") }
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) println("W: $text") }
}
@@ -0,0 +1,18 @@
package chat.simplex.common.views.chat.item
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import chat.simplex.common.model.CryptoFile
import java.net.URI
@Composable
actual fun SaveOrOpenFileMenu(
showMenu: MutableState<Boolean>,
encrypted: Boolean,
ext: String?,
encryptedUri: URI,
fileSource: CryptoFile,
saveFile: () -> Unit
) {
}
@@ -4,8 +4,7 @@ import SectionDivider
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -0,0 +1,72 @@
package chat.simplex.common.views.chatlist
import SectionItemView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.ui.theme.*
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
import chat.simplex.common.views.chat.item.isHeartEmoji
import chat.simplex.common.views.chat.item.isShortEmoji
import chat.simplex.common.views.helpers.toDp
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable
actual fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>) {
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SingleEmojiInput(emoji)
TagListNameTextField(name, showError = showError)
}
}
@Composable
private fun SingleEmojiInput(
emoji: MutableState<String?>
) {
val state = remember { mutableStateOf(TextFieldValue(emoji.value ?: "")) }
val colors = TextFieldDefaults.textFieldColors(
textColor = if (isHeartEmoji(emoji.value ?: "")) Color(0xffD63C31) else MaterialTheme.colors.onPrimary,
backgroundColor = Color.Unspecified,
focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f),
unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f),
cursorColor = MaterialTheme.colors.secondary,
)
TextField(
value = state.value,
onValueChange = { newValue ->
if (newValue.text == emoji.value) {
state.value = newValue
return@TextField
}
val newValueClamped = newValue.text.replace(emoji.value ?: "", "")
val isEmoji = isShortEmoji(newValueClamped)
emoji.value = if (isEmoji) newValueClamped else null
state.value = if (isEmoji) newValue else TextFieldValue()
},
singleLine = true,
modifier = Modifier
.padding(4.dp)
.size(width = TextFieldDefaults.MinHeight.value.sp.toDp(), height = TextFieldDefaults.MinHeight),
textStyle = LocalTextStyle.current.copy(fontFamily = EmojiFont, textAlign = TextAlign.Center),
placeholder = {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(
painter = painterResource(MR.images.ic_add_reaction),
contentDescription = null,
tint = MaterialTheme.colors.secondary
)
}
},
colors = colors,
)
}
+4 -4
View File
@@ -24,11 +24,11 @@ android.nonTransitiveRClass=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.2-beta.6
android.version_code=258
android.version_name=6.2.3
android.version_code=265
desktop.version_name=6.2-beta.6
desktop.version_code=81
desktop.version_name=6.2.3
desktop.version_code=85
kotlin.version=1.9.23
gradle.plugin.version=8.2.0