Merge branch 'stable' into stable-android
@@ -2,6 +2,7 @@ package chat.simplex.app
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
@@ -83,7 +84,7 @@ class CallService: Service() {
|
||||
generalGetString(MR.strings.notification_preview_somebody)
|
||||
else
|
||||
call?.contact?.profile?.displayName ?: ""
|
||||
val text = generalGetString(if (call?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
|
||||
val text = generalGetString(if (call?.hasVideo == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
|
||||
val image = call?.contact?.image
|
||||
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
|
||||
BitmapFactory.decodeResource(resources, R.drawable.icon)
|
||||
@@ -105,7 +106,7 @@ class CallService: Service() {
|
||||
0
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (call.supportsVideo()) {
|
||||
if (call.hasVideo && ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
|
||||
@@ -150,12 +150,9 @@ fun processIntent(intent: Intent?) {
|
||||
"android.intent.action.VIEW" -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
val transformedUri = uri.toURIOrNull()
|
||||
if (transformedUri != null) {
|
||||
chatModel.appOpenUrl.value = null to transformedUri
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), generalGetString(MR.strings.error_parsing_uri_desc))
|
||||
}
|
||||
chatModel.appOpenUrl.value = null to uri.toString()
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), generalGetString(MR.strings.error_parsing_uri_desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import chat.simplex.common.platform.Log
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -65,7 +66,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
}
|
||||
}
|
||||
context = this
|
||||
initHaskell()
|
||||
initHaskell(packageName)
|
||||
initMultiplatform()
|
||||
runMigrations()
|
||||
tmpDir.deleteRecursively()
|
||||
@@ -259,7 +260,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
|
||||
override fun androidSetNightModeIfSupported() {
|
||||
if (Build.VERSION.SDK_INT < 31) return
|
||||
|
||||
val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) {
|
||||
null
|
||||
} else {
|
||||
@@ -274,6 +274,41 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
uiModeManager.setApplicationNightMode(mode)
|
||||
}
|
||||
|
||||
override fun androidSetDrawerStatusAndNavBarColor(
|
||||
isLight: Boolean,
|
||||
drawerShadingColor: Color,
|
||||
toolbarOnTop: Boolean,
|
||||
navBarColor: Color,
|
||||
) {
|
||||
val window = mainActivity.get()?.window ?: return
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
|
||||
// Blend status bar color to the animated color
|
||||
val colors = CurrentColors.value.colors
|
||||
val baseBackgroundColor = if (toolbarOnTop) colors.background.mixWith(colors.onBackground, 0.97f) else colors.background
|
||||
var statusBar = baseBackgroundColor.mixWith(drawerShadingColor.copy(1f), 1 - drawerShadingColor.alpha).toArgb()
|
||||
var statusBarLight = isLight
|
||||
|
||||
// SimplexGreen while in call
|
||||
if (window.statusBarColor == SimplexGreen.toArgb()) {
|
||||
statusBarColorAfterCall.intValue = statusBar
|
||||
statusBar = SimplexGreen.toArgb()
|
||||
statusBarLight = false
|
||||
}
|
||||
window.statusBarColor = statusBar
|
||||
val navBar = navBarColor.toArgb()
|
||||
if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) {
|
||||
windowInsetController?.isAppearanceLightStatusBars = statusBarLight
|
||||
}
|
||||
if (window.navigationBarColor != navBar) {
|
||||
window.navigationBarColor = navBar
|
||||
}
|
||||
if (windowInsetController?.isAppearanceLightNavigationBars != isLight) {
|
||||
windowInsetController?.isAppearanceLightNavigationBars = isLight
|
||||
}
|
||||
}
|
||||
|
||||
override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {
|
||||
val window = mainActivity.get()?.window ?: return
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -288,11 +323,13 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
backgroundColor
|
||||
}
|
||||
}).toArgb()
|
||||
var statusBarLight = isLight
|
||||
|
||||
// SimplexGreen while in call
|
||||
if (window.statusBarColor == SimplexGreen.toArgb()) {
|
||||
statusBarColorAfterCall.intValue = statusBar
|
||||
statusBar = SimplexGreen.toArgb()
|
||||
statusBarLight = false
|
||||
}
|
||||
val navBar = (if (hasBottom && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
||||
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
|
||||
@@ -302,8 +339,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
if (window.statusBarColor != statusBar) {
|
||||
window.statusBarColor = statusBar
|
||||
}
|
||||
if (windowInsetController?.isAppearanceLightStatusBars != isLight) {
|
||||
windowInsetController?.isAppearanceLightStatusBars = isLight
|
||||
if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) {
|
||||
windowInsetController?.isAppearanceLightStatusBars = statusBarLight
|
||||
}
|
||||
if (window.navigationBarColor != navBar) {
|
||||
window.navigationBarColor = navBar
|
||||
|
||||
@@ -24,8 +24,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -37,7 +36,9 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.app.model.NtfManager.AcceptCallAction
|
||||
import chat.simplex.common.helpers.applyAppLocale
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -49,6 +50,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import chat.simplex.common.platform.chatModel as m
|
||||
|
||||
class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
@@ -56,6 +58,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
var boundService: CallService? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyAppLocale(appPrefs.appLanguage)
|
||||
super.onCreate(savedInstanceState)
|
||||
callActivity = WeakReference(this)
|
||||
when (intent?.action) {
|
||||
@@ -80,6 +83,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
(mainActivity.get() ?: this).applyAppLocale(appPrefs.appLanguage)
|
||||
if (isOnLockScreenNow()) {
|
||||
lockAfterIncomingCall()
|
||||
}
|
||||
@@ -116,7 +120,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
|
||||
private fun hasGrantedPermissions(): Boolean {
|
||||
val grantedAudio = ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|
||||
val grantedCamera = !callSupportsVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
val grantedCamera = !callHasVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
return grantedAudio && grantedCamera
|
||||
}
|
||||
|
||||
@@ -124,7 +128,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
override fun onBackPressed() {
|
||||
if (isOnLockScreenNow()) {
|
||||
super.onBackPressed()
|
||||
} else if (!hasGrantedPermissions() && !callSupportsVideo()) {
|
||||
} else if (!hasGrantedPermissions() && !callHasVideo()) {
|
||||
val call = m.activeCall.value
|
||||
if (call != null) {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
@@ -142,7 +146,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
// On Android 12+ PiP is enabled automatically when a user hides the app
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callHasVideo() && platform.androidPictureInPictureAllowed()) {
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
@@ -198,7 +202,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
fun getKeyguardManager(context: Context): KeyguardManager =
|
||||
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
|
||||
private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
|
||||
private fun callHasVideo() = m.activeCall.value?.hasVideo == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
|
||||
|
||||
@Composable
|
||||
fun CallActivityView() {
|
||||
@@ -212,7 +216,7 @@ fun CallActivityView() {
|
||||
.collect { collapsed ->
|
||||
when {
|
||||
collapsed -> {
|
||||
if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) {
|
||||
if (!platform.androidPictureInPictureAllowed() || !callHasVideo()) {
|
||||
activity.moveTaskToBack(true)
|
||||
activity.startActivity(Intent(activity, MainActivity::class.java))
|
||||
} else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) {
|
||||
@@ -221,7 +225,7 @@ fun CallActivityView() {
|
||||
activity.enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> {
|
||||
callHasVideo() && !platform.androidPictureInPictureAllowed() -> {
|
||||
// PiP disabled by user
|
||||
platform.androidStartCallActivity(false)
|
||||
}
|
||||
@@ -233,7 +237,7 @@ fun CallActivityView() {
|
||||
}
|
||||
SimpleXTheme {
|
||||
var prevCall by remember { mutableStateOf(call) }
|
||||
KeyChangeEffect(m.activeCall.value) {
|
||||
KeyChangeEffect(m.activeCall.value, remember { appPrefs.appLanguage.state }.value) {
|
||||
if (m.activeCall.value != null) {
|
||||
prevCall = m.activeCall.value
|
||||
activity.boundService?.updateNotification()
|
||||
@@ -242,28 +246,43 @@ fun CallActivityView() {
|
||||
Box(Modifier.background(Color.Black)) {
|
||||
if (call != null) {
|
||||
val permissionsState = rememberMultiplePermissionsState(
|
||||
permissions = if (callSupportsVideo()) {
|
||||
permissions = if (callHasVideo()) {
|
||||
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
|
||||
} else {
|
||||
listOf(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
)
|
||||
if (permissionsState.allPermissionsGranted) {
|
||||
// callState == connected is needed in a situation when a peer enabled camera in audio call while a user didn't grant camera permission yet,
|
||||
// so no need to hide active call view in this case
|
||||
if (permissionsState.allPermissionsGranted || call.callState == CallState.Connected) {
|
||||
ActiveCallView()
|
||||
LaunchedEffect(Unit) {
|
||||
activity.startServiceAndBind()
|
||||
}
|
||||
} else {
|
||||
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
|
||||
}
|
||||
if ((!permissionsState.allPermissionsGranted && call.callState != CallState.Connected) || call.wantsToEnableCamera) {
|
||||
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callHasVideo() || call.wantsToEnableCamera) {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
val cameraAndMicPermissions = rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
|
||||
DisposableEffect(cameraAndMicPermissions.allPermissionsGranted) {
|
||||
onDispose {
|
||||
if (call.wantsToEnableCamera && cameraAndMicPermissions.allPermissionsGranted) {
|
||||
val activeCall = chatModel.activeCall.value
|
||||
if (activeCall != null && activeCall.contact.apiId == call.contact.apiId) {
|
||||
chatModel.activeCall.value = activeCall.copy(wantsToEnableCamera = false)
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (callSupportsVideo()) {
|
||||
if (callHasVideo()) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch {
|
||||
activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height))
|
||||
activity.setPipParams(callHasVideo(), viewRatio = Rational(view.width, view.height))
|
||||
activity.trackPipAnimationHintView(view)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ private fun Activity.applyLocale(locale: Locale) {
|
||||
Locale.setDefault(locale)
|
||||
val appConf = Configuration(androidAppContext.resources.configuration).apply { setLocale(locale) }
|
||||
val activityConf = Configuration(resources.configuration).apply { setLocale(locale) }
|
||||
@Suppress("DEPRECATION")
|
||||
androidAppContext.resources.updateConfiguration(appConf, resources.displayMetrics)
|
||||
androidAppContext = androidAppContext.createConfigurationContext(appConf)
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(activityConf, resources.displayMetrics)
|
||||
}
|
||||
|
||||
@@ -31,22 +31,19 @@ lateinit var androidAppContext: Context
|
||||
var mainActivity: WeakReference<FragmentActivity> = WeakReference(null)
|
||||
var callActivity: WeakReference<ComponentActivity> = WeakReference(null)
|
||||
|
||||
fun initHaskell() {
|
||||
val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000)
|
||||
fun initHaskell(packageName: String) {
|
||||
val s = Semaphore(0)
|
||||
thread(name="stdout/stderr pipe") {
|
||||
Log.d(TAG, "starting server")
|
||||
var server: LocalServerSocket? = null
|
||||
for (i in 0..100) {
|
||||
try {
|
||||
server = LocalServerSocket(socketName + i)
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
if (server == null) {
|
||||
throw Error("Unable to setup local server socket. Contact developers")
|
||||
val server: LocalServerSocket
|
||||
try {
|
||||
server = LocalServerSocket(packageName)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
Log.e(TAG, "Unable to setup local server socket. Contact developers")
|
||||
s.release()
|
||||
// Will not have logs from backend
|
||||
return@thread
|
||||
}
|
||||
Log.d(TAG, "started server")
|
||||
s.release()
|
||||
@@ -60,7 +57,7 @@ fun initHaskell() {
|
||||
Log.d(TAG, "starting receiver loop")
|
||||
while (true) {
|
||||
val line = input.readLine() ?: break
|
||||
Log.w("$TAG (stdout/stderr)", line)
|
||||
Log.w(TAG, "(stdout/stderr) $line")
|
||||
logbuffer.add(line)
|
||||
}
|
||||
Log.w(TAG, "exited receiver loop")
|
||||
@@ -70,7 +67,7 @@ fun initHaskell() {
|
||||
System.loadLibrary("app-lib")
|
||||
|
||||
s.acquire()
|
||||
pipeStdOutToSocket(socketName)
|
||||
pipeStdOutToSocket(packageName)
|
||||
|
||||
initHS()
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
|
||||
Log.d(TAG, "Added audio devices2: ${devices.value.map { it.type }}")
|
||||
|
||||
if (devices.value.size - oldDevices.size > 0) {
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false)
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +116,14 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
|
||||
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
|
||||
super.onAudioDevicesAdded(addedDevices)
|
||||
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false)
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false)
|
||||
}
|
||||
|
||||
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
|
||||
super.onAudioDevicesRemoved(removedDevices)
|
||||
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, true)
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -16,9 +17,12 @@ import android.view.ViewGroup
|
||||
import android.webkit.*
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
@@ -27,17 +31,20 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.*
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
import chat.simplex.common.helpers.applyAppLocale
|
||||
import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -119,26 +126,26 @@ actual fun ActiveCallView() {
|
||||
val callRh = call.remoteHostId
|
||||
when (val r = apiMsg.resp) {
|
||||
is WCallResponse.Capabilities -> withBGApi {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
val callType = CallType(call.initialCallType, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
||||
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()
|
||||
} else {
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.supportsVideo(), true)
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
||||
}
|
||||
CallSoundsPlayer.startConnectingCallSound(scope)
|
||||
activeCallWaitDeliveryReceipt(scope)
|
||||
}
|
||||
is WCallResponse.Offer -> withBGApi {
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities)
|
||||
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()
|
||||
} else {
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.supportsVideo(), true)
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
||||
}
|
||||
}
|
||||
is WCallResponse.Answer -> withBGApi {
|
||||
@@ -162,6 +169,17 @@ actual fun ActiveCallView() {
|
||||
is WCallResponse.Connected -> {
|
||||
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) }
|
||||
}
|
||||
is WCallResponse.PeerMedia -> {
|
||||
updateActiveCall(call) {
|
||||
val sources = it.peerMediaSources
|
||||
when (r.source) {
|
||||
CallMediaSource.Mic -> it.copy(peerMediaSources = sources.copy(mic = r.enabled))
|
||||
CallMediaSource.Camera -> it.copy(peerMediaSources = sources.copy(camera = r.enabled))
|
||||
CallMediaSource.ScreenAudio -> it.copy(peerMediaSources = sources.copy(screenAudio = r.enabled))
|
||||
CallMediaSource.ScreenVideo -> it.copy(peerMediaSources = sources.copy(screenVideo = r.enabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
is WCallResponse.End -> {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
@@ -174,16 +192,23 @@ actual fun ActiveCallView() {
|
||||
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
|
||||
is WCallCommand.Media -> {
|
||||
updateActiveCall(call) {
|
||||
when (cmd.media) {
|
||||
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable)
|
||||
CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable)
|
||||
val sources = it.localMediaSources
|
||||
when (cmd.source) {
|
||||
CallMediaSource.Mic -> {
|
||||
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.isMicrophoneMute = !cmd.enable
|
||||
it.copy(localMediaSources = sources.copy(mic = cmd.enable))
|
||||
}
|
||||
CallMediaSource.Camera -> it.copy(localMediaSources = sources.copy(camera = cmd.enable))
|
||||
CallMediaSource.ScreenAudio -> it.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
|
||||
CallMediaSource.ScreenVideo -> it.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
|
||||
}
|
||||
}
|
||||
}
|
||||
is WCallCommand.Camera -> {
|
||||
updateActiveCall(call) { it.copy(localCamera = cmd.camera) }
|
||||
if (!call.audioEnabled) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
|
||||
if (!call.localMediaSources.mic) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
|
||||
}
|
||||
}
|
||||
is WCallCommand.End -> {
|
||||
@@ -200,7 +225,6 @@ actual fun ActiveCallView() {
|
||||
val showOverlay = when {
|
||||
call == null -> false
|
||||
!platform.androidPictureInPictureAllowed() -> true
|
||||
!call.supportsVideo() -> true
|
||||
!chatModel.activeCallViewIsCollapsed.value -> true
|
||||
else -> false
|
||||
}
|
||||
@@ -208,6 +232,12 @@ actual fun ActiveCallView() {
|
||||
ActiveCallOverlay(call, chatModel, callAudioDeviceManager)
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(call?.localMediaSources?.hasVideo) {
|
||||
if (call != null && call.hasVideo && 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)
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||
@@ -237,9 +267,15 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceM
|
||||
devices = remember { callAudioDeviceManager.devices }.value,
|
||||
currentDevice = remember { callAudioDeviceManager.currentDevice },
|
||||
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
|
||||
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) },
|
||||
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
|
||||
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
|
||||
toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) },
|
||||
toggleVideo = {
|
||||
if (ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = !call.localMediaSources.camera))
|
||||
} else {
|
||||
updateActiveCall(call) { it.copy(wantsToEnableCamera = true) }
|
||||
}
|
||||
},
|
||||
toggleSound = {
|
||||
val enableSpeaker = callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
val preferredInternalDevice = callAudioDeviceManager.devices.value.firstOrNull { it.type == if (enableSpeaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }
|
||||
@@ -293,30 +329,30 @@ private fun ActiveCallOverlayLayout(
|
||||
flipCamera: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
|
||||
if (media == CallMediaType.Video) {
|
||||
if (call.hasVideo) {
|
||||
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
||||
}
|
||||
}
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
@Composable
|
||||
fun SelectSoundDevice() {
|
||||
fun SelectSoundDevice(size: Dp) {
|
||||
if (devices.size == 2 &&
|
||||
devices.all { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE || it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } ||
|
||||
currentDevice.value == null ||
|
||||
devices.none { it.id == currentDevice.value?.id }
|
||||
) {
|
||||
val isSpeaker = currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
||||
ToggleSoundButton(call, enabled, isSpeaker, toggleSound)
|
||||
ToggleSoundButton(enabled, isSpeaker, !call.peerMediaSources.mic, toggleSound, size = size)
|
||||
} else {
|
||||
ExposedDropDownSettingWithIcon(
|
||||
devices.map { Triple(it, it.icon, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) },
|
||||
devices.map { Triple(it, if (call.peerMediaSources.mic) it.icon else MR.images.ic_volume_off, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) },
|
||||
currentDevice,
|
||||
fontSize = 18.sp,
|
||||
iconSize = 40.dp,
|
||||
boxSize = size,
|
||||
listIconSize = 30.dp,
|
||||
iconColor = Color(0xFFFFFFD8),
|
||||
background = controlButtonsBackground(),
|
||||
minWidth = 300.dp,
|
||||
onSelected = {
|
||||
if (it != null) {
|
||||
@@ -327,29 +363,9 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
}
|
||||
|
||||
when (media) {
|
||||
CallMediaType.Video -> {
|
||||
VideoCallInfoView(call)
|
||||
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
||||
DisabledBackgroundCallsButton()
|
||||
}
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
ToggleAudioButton(call, enabled, toggleAudio)
|
||||
SelectSoundDevice()
|
||||
IconButton(onClick = dismiss, enabled = enabled) {
|
||||
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
|
||||
}
|
||||
if (call.videoEnabled) {
|
||||
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera)
|
||||
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo)
|
||||
} else {
|
||||
Spacer(Modifier.size(48.dp))
|
||||
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CallMediaType.Audio -> {
|
||||
when (call.hasVideo) {
|
||||
true -> VideoCallInfoView(call)
|
||||
false -> {
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -359,23 +375,26 @@ private fun ActiveCallOverlayLayout(
|
||||
ProfileImage(size = 192.dp, image = call.contact.profile.image)
|
||||
AudioCallInfoView(call)
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
||||
DisabledBackgroundCallsButton()
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
IconButton(onClick = dismiss, enabled = enabled) {
|
||||
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(start = 32.dp)) {
|
||||
ToggleAudioButton(call, enabled, toggleAudio)
|
||||
}
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
||||
Box(Modifier.padding(end = 32.dp)) {
|
||||
SelectSoundDevice()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
||||
DisabledBackgroundCallsButton()
|
||||
}
|
||||
|
||||
BoxWithConstraints(Modifier.padding(start = 6.dp, end = 6.dp, bottom = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
|
||||
val size = ((maxWidth - DEFAULT_PADDING_HALF * 4) / 5).coerceIn(0.dp, 60.dp)
|
||||
// limiting max width for tablets/wide screens, will be displayed in the center
|
||||
val padding = ((min(420.dp, maxWidth) - size * 5) / 4).coerceAtLeast(0.dp)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(padding), verticalAlignment = Alignment.CenterVertically) {
|
||||
ToggleMicButton(call, enabled, toggleAudio, size = size)
|
||||
SelectSoundDevice(size = size)
|
||||
ControlButton(painterResource(MR.images.ic_call_end_filled), MR.strings.icon_descr_hang_up, enabled = enabled, dismiss, background = Color.Red, size = size, iconPaddingPercent = 0.166f)
|
||||
if (call.localMediaSources.camera) {
|
||||
ControlButton(painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera, size = size)
|
||||
ControlButton(painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo, size = size)
|
||||
} else {
|
||||
Spacer(Modifier.size(size))
|
||||
ControlButton(painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo, size = size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,34 +403,52 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit) {
|
||||
if (call.hasMedia) {
|
||||
IconButton(onClick = action, enabled = enabled) {
|
||||
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.size(40.dp))
|
||||
private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, iconPaddingPercent: Float = 0.2f) {
|
||||
ControlButtonWrap(enabled, action, background, size) {
|
||||
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.padding(size * iconPaddingPercent).fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleAudioButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit) {
|
||||
if (call.audioEnabled) {
|
||||
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio)
|
||||
} else {
|
||||
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio)
|
||||
private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) {
|
||||
Box(
|
||||
Modifier
|
||||
.background(background, CircleShape)
|
||||
.size(size)
|
||||
.clickable(
|
||||
onClick = action,
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)),
|
||||
enabled = enabled
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleSoundButton(call: Call, enabled: Boolean, speaker: Boolean, toggleSound: () -> Unit) {
|
||||
if (speaker) {
|
||||
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound)
|
||||
private fun ToggleMicButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit, size: Dp) {
|
||||
if (call.localMediaSources.mic) {
|
||||
ControlButton(painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio, size = size)
|
||||
} else {
|
||||
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound)
|
||||
ControlButton(painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio, size = size)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleSoundButton(enabled: Boolean, speaker: Boolean, muted: Boolean, toggleSound: () -> Unit, size: Dp) {
|
||||
when {
|
||||
muted -> ControlButton(painterResource(MR.images.ic_volume_off), MR.strings.icon_descr_sound_muted, enabled, toggleSound, size = size)
|
||||
speaker -> ControlButton(painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound, size = size)
|
||||
else -> ControlButton(painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound, size = size)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun controlButtonsBackground(): Color = if (chatModel.activeCall.value?.peerMediaSources?.hasVideo == true) Color.Black.copy(0.2f) else Color.White.copy(0.2f)
|
||||
|
||||
@Composable
|
||||
fun AudioCallInfoView(call: Call) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@@ -553,38 +590,39 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ColumnWithScrollBar(Modifier.fillMaxSize()) {
|
||||
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.permissions_required))
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
val onClick = {
|
||||
if (permissionsState.shouldShowRationale) {
|
||||
context.showAllowPermissionInSettingsAlert()
|
||||
} else {
|
||||
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
|
||||
ModalView(background = Color.Black, showClose = false, close = {}) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxSize()) {
|
||||
AppBarTitle(stringResource(MR.strings.permissions_required))
|
||||
Spacer(Modifier.weight(1f))
|
||||
val onClick = {
|
||||
if (permissionsState.shouldShowRationale) {
|
||||
context.showAllowPermissionInSettingsAlert()
|
||||
} else {
|
||||
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8))
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_camera_and_record_audio)
|
||||
} else if (audioPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_record_audio)
|
||||
} else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_camera)
|
||||
} else ""
|
||||
GrantPermissionButton(text, buttonEnabled.value, onClick)
|
||||
Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8))
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_camera_and_record_audio)
|
||||
} else if (audioPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_record_audio)
|
||||
} else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_camera)
|
||||
} else null
|
||||
if (text != null) {
|
||||
GrantPermissionButton(text, buttonEnabled.value, onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = if (hasVideo) 0.dp else DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.Center) {
|
||||
SimpleButtonFrame(cancel, Modifier.height(64.dp)) {
|
||||
Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8))
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
|
||||
SimpleButtonFrame(cancel, Modifier.height(60.dp)) {
|
||||
Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,9 +708,10 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
|
||||
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
AndroidView(
|
||||
factory = { AndroidViewContext ->
|
||||
factory = {
|
||||
try {
|
||||
(staticWebView ?: WebView(androidAppContext)).apply {
|
||||
reapplyLocale()
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
@@ -706,7 +745,11 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error initializing WebView: ${e.stackTraceToString()}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString()))
|
||||
if (e.stackTraceToString().contains("/lib64")) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view_wrong_arch).format(e.stackTraceToString()))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString()))
|
||||
}
|
||||
return@AndroidView View(androidAppContext)
|
||||
}
|
||||
}
|
||||
@@ -739,6 +782,16 @@ private fun updateActiveCall(initial: Call, transform: (Call) -> Call) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Creating WebView automatically drops user's custom app locale to default system locale.
|
||||
* Preventing it by re-applying custom locale
|
||||
* https://issuetracker.google.com/issues/109833940
|
||||
* */
|
||||
private fun reapplyLocale() {
|
||||
mainActivity.get()?.applyAppLocale(appPrefs.appLanguage)
|
||||
callActivity.get()?.applyAppLocale(appPrefs.appLanguage)
|
||||
}
|
||||
|
||||
private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
@@ -768,8 +821,8 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
userProfile = Profile.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Video,
|
||||
peerMedia = CallMediaType.Video,
|
||||
initialCallType = CallMediaType.Video,
|
||||
peerMediaSources = CallMediaSources(),
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
||||
@@ -798,8 +851,8 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
userProfile = Profile.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Audio,
|
||||
peerMedia = CallMediaType.Audio,
|
||||
initialCallType = CallMediaType.Audio,
|
||||
peerMediaSources = CallMediaSources(),
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
||||
|
||||
@@ -54,8 +54,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
.align(Alignment.BottomCenter),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
if (media == CallMediaType.Video) {
|
||||
if (call.hasVideo) {
|
||||
Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White)
|
||||
} else {
|
||||
Icon(painterResource(MR.images.ic_call_filled), null, Modifier.size(27.dp).offset(x = -0.5.dp, y = 2.dp), tint = Color.White)
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.DrawerDefaults.ScrimOpacity
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.User
|
||||
import chat.simplex.common.model.UserInfo
|
||||
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 kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
private val USER_PICKER_IMAGE_SIZE = 44.dp
|
||||
private val USER_PICKER_ROW_PADDING = 16.dp
|
||||
|
||||
@Composable
|
||||
actual fun UserPickerUsersSection(
|
||||
users: List<UserInfo>,
|
||||
stopped: Boolean,
|
||||
onUserClicked: (user: User) -> Unit,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val screenWidthDp = windowWidth()
|
||||
|
||||
if (users.isNotEmpty()) {
|
||||
SectionItemView(
|
||||
padding = PaddingValues(),
|
||||
disabled = stopped
|
||||
) {
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(scrollState),
|
||||
) {
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(USER_PICKER_ROW_PADDING)) {
|
||||
users.forEach { u ->
|
||||
UserPickerUserBox(u, stopped, modifier = Modifier.userBoxWidth(u.user, users.size, screenWidthDp)) {
|
||||
onUserClicked(it)
|
||||
withBGApi {
|
||||
delay(500)
|
||||
scrollState.scrollTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun UserPickerUserBox(
|
||||
userInfo: UserInfo,
|
||||
stopped: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (user: User) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.userPickerBoxModifier()
|
||||
.clickable (
|
||||
onClick = { onClick(userInfo.user) },
|
||||
enabled = !stopped
|
||||
)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.padding(USER_PICKER_ROW_PADDING),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(USER_PICKER_ROW_PADDING)
|
||||
) {
|
||||
Box {
|
||||
ProfileImageForActiveCall(size = USER_PICKER_IMAGE_SIZE, image = userInfo.user.profile.image, color = MaterialTheme.colors.secondaryVariant)
|
||||
|
||||
if (userInfo.unreadCount > 0 && !userInfo.user.activeUser) {
|
||||
unreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false)
|
||||
}
|
||||
}
|
||||
val user = userInfo.user
|
||||
Text(
|
||||
user.displayName,
|
||||
fontWeight = if (user.activeUser) FontWeight.Bold else FontWeight.Normal,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.userPickerBoxModifier(): Modifier {
|
||||
val percent = remember { appPreferences.profileImageCornerRadius.state }
|
||||
val r = kotlin.math.max(0f, percent.value)
|
||||
|
||||
val cornerSize = when {
|
||||
r >= 50 -> 50
|
||||
r <= 0 -> 0
|
||||
else -> r.toInt()
|
||||
}
|
||||
|
||||
val shape = RoundedCornerShape(CornerSize(cornerSize))
|
||||
return this.clip(shape).border(1.dp, MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 1 - userPickerAlpha() - 0.02f), shape)
|
||||
}
|
||||
|
||||
|
||||
private fun calculateFraction(pos: Float) =
|
||||
(pos / 1f).coerceIn(0f, 1f)
|
||||
|
||||
@Composable
|
||||
actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<AnimatedViewState>, content: @Composable () -> Unit) {
|
||||
val pickerIsVisible = pickerState.collectAsState().value.isVisible()
|
||||
val dismissState = rememberDismissState(initialValue = if (pickerIsVisible) DismissValue.Default else DismissValue.DismissedToEnd) {
|
||||
if (it == DismissValue.DismissedToEnd && pickerState.value.isVisible()) {
|
||||
pickerState.value = AnimatedViewState.HIDING
|
||||
}
|
||||
true
|
||||
}
|
||||
val height = remember { mutableIntStateOf(0) }
|
||||
val heightValue = height.intValue
|
||||
val clickableModifier = if (pickerIsVisible) {
|
||||
Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING })
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.then(clickableModifier)
|
||||
.drawBehind {
|
||||
val pos = when {
|
||||
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
|
||||
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
|
||||
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
|
||||
else -> 1 - dismissState.progress.fraction
|
||||
}
|
||||
val colors = CurrentColors.value.colors
|
||||
val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
|
||||
val adjustedAlpha = resultingColor.alpha * calculateFraction(pos = pos)
|
||||
val shadingColor = resultingColor.copy(alpha = adjustedAlpha)
|
||||
|
||||
if (pickerState.value.isVisible()) {
|
||||
platform.androidSetDrawerStatusAndNavBarColor(
|
||||
isLight = colors.isLight,
|
||||
drawerShadingColor = shadingColor,
|
||||
toolbarOnTop = !appPrefs.oneHandUI.get(),
|
||||
navBarColor = colors.background.mixWith(colors.onBackground, 1 - userPickerAlpha())
|
||||
)
|
||||
} else if (ModalManager.start.modalCount.value == 0) {
|
||||
platform.androidSetDrawerStatusAndNavBarColor(
|
||||
isLight = colors.isLight,
|
||||
drawerShadingColor = shadingColor,
|
||||
toolbarOnTop = !appPrefs.oneHandUI.get(),
|
||||
navBarColor = (if (appPrefs.oneHandUI.get() && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
||||
colors.background.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
|
||||
} else {
|
||||
colors.background
|
||||
})
|
||||
)
|
||||
}
|
||||
drawRect(
|
||||
if (pos != 0f) resultingColor else Color.Transparent,
|
||||
alpha = calculateFraction(pos = pos)
|
||||
)
|
||||
}
|
||||
.graphicsLayer {
|
||||
if (heightValue == 0) {
|
||||
alpha = 0f
|
||||
}
|
||||
translationY = dismissState.offset.value
|
||||
},
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Box(
|
||||
Modifier.onSizeChanged { height.intValue = it.height }
|
||||
) {
|
||||
KeyChangeEffect(pickerIsVisible) {
|
||||
if (pickerState.value.isVisible()) {
|
||||
try {
|
||||
dismissState.animateTo(DismissValue.Default, userPickerAnimSpec())
|
||||
} catch (e: CancellationException) {
|
||||
Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}")
|
||||
pickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec())
|
||||
} catch (e: CancellationException) {
|
||||
Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}")
|
||||
pickerState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
val draggableModifier = if (height.intValue != 0)
|
||||
Modifier.draggableBottomDrawerModifier(
|
||||
state = dismissState,
|
||||
swipeDistance = height.intValue.toFloat(),
|
||||
)
|
||||
else Modifier
|
||||
Box(draggableModifier.then(modifier)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.draggableBottomDrawerModifier(
|
||||
state: DismissState,
|
||||
swipeDistance: Float,
|
||||
): Modifier = this.swipeable(
|
||||
state = state,
|
||||
anchors = mapOf(0f to DismissValue.Default, swipeDistance to DismissValue.DismissedToEnd),
|
||||
thresholds = { _, _ -> FractionalThreshold(0.3f) },
|
||||
orientation = Orientation.Vertical,
|
||||
resistance = null
|
||||
)
|
||||
|
||||
private fun Modifier.userBoxWidth(user: User, totalUsers: Int, windowWidth: Dp): Modifier {
|
||||
return if (totalUsers == 1) {
|
||||
this.width(windowWidth - DEFAULT_PADDING * 2)
|
||||
} else if (user.activeUser) {
|
||||
this.width(windowWidth - DEFAULT_PADDING - (USER_PICKER_ROW_PADDING * 3) - USER_PICKER_IMAGE_SIZE)
|
||||
} else {
|
||||
this.widthIn(max = (windowWidth - (DEFAULT_PADDING * 2)) * 0.618f)
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,9 @@ fun AppearanceScope.AppearanceLayout(
|
||||
SectionDividerSpaced()
|
||||
ThemesSection(systemDarkTheme)
|
||||
|
||||
SectionDividerSpaced()
|
||||
MessageShapeSection()
|
||||
|
||||
SectionDividerSpaced()
|
||||
ProfileImageSection()
|
||||
|
||||
|
||||
@@ -6,14 +6,11 @@ import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
@@ -42,12 +39,6 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.math.sqrt
|
||||
|
||||
data class SettingsViewState(
|
||||
val userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
val scaffoldState: ScaffoldState
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppScreen() {
|
||||
@@ -145,13 +136,11 @@ fun MainScreen() {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
}
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) }
|
||||
SetupClipboardListener()
|
||||
if (appPlatform.isAndroid) {
|
||||
AndroidScreen(settingsState)
|
||||
AndroidScreen(userPickerState)
|
||||
} else {
|
||||
DesktopScreen(settingsState)
|
||||
DesktopScreen(userPickerState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,7 +238,7 @@ fun MainScreen() {
|
||||
val ANDROID_CALL_TOP_PADDING = 40.dp
|
||||
|
||||
@Composable
|
||||
fun AndroidScreen(settingsState: SettingsViewState) {
|
||||
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
BoxWithConstraints {
|
||||
val call = remember { chatModel.activeCall} .value
|
||||
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
|
||||
@@ -262,7 +251,7 @@ fun AndroidScreen(settingsState: SettingsViewState) {
|
||||
}
|
||||
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
|
||||
) {
|
||||
StartPartOfScreen(settingsState)
|
||||
StartPartOfScreen(userPickerState)
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val onComposed: suspend (chatId: String?) -> Unit = { chatId ->
|
||||
@@ -318,15 +307,15 @@ fun AndroidScreen(settingsState: SettingsViewState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StartPartOfScreen(settingsState: SettingsViewState) {
|
||||
fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
if (chatModel.setDeliveryReceipts.value) {
|
||||
SetDeliveryReceiptsView(chatModel)
|
||||
} else {
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
if (chatModel.sharedContent.value == null)
|
||||
ChatListView(chatModel, settingsState, AppLock::setPerformLA, stopped)
|
||||
ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped)
|
||||
else
|
||||
ShareListView(chatModel, settingsState, stopped)
|
||||
ShareListView(chatModel, stopped)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,49 +356,41 @@ fun EndPartOfScreen() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
Box {
|
||||
// 56.dp is a size of unused space of settings drawer
|
||||
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + 56.dp)) {
|
||||
StartPartOfScreen(settingsState)
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
ModalManager.start.showInView()
|
||||
SwitchingUsersView()
|
||||
}
|
||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
||||
CenterPartOfScreen()
|
||||
}
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
VerticalDivider()
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
EndPartOfScreen()
|
||||
}
|
||||
}
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
val scope = rememberCoroutineScope()
|
||||
if (scaffoldState.drawerState.isOpen || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
|
||||
ModalManager.start.closeModals()
|
||||
scope.launch { settingsState.scaffoldState.drawerState.close() }
|
||||
})
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
|
||||
fun DesktopScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
StartPartOfScreen(userPickerState)
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA)
|
||||
}
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
ModalManager.start.showInView()
|
||||
SwitchingUsersView()
|
||||
}
|
||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
||||
CenterPartOfScreen()
|
||||
}
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
VerticalDivider()
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
EndPartOfScreen()
|
||||
}
|
||||
}
|
||||
if (userPickerState.collectAsState().value.isVisible() || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
|
||||
ModalManager.start.closeModals()
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
})
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -71,6 +71,9 @@ object ChatModel {
|
||||
val groupMembers = mutableStateListOf<GroupMember>()
|
||||
val groupMembersIndexes = 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>()
|
||||
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
|
||||
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
||||
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
|
||||
@@ -82,7 +85,7 @@ object ChatModel {
|
||||
val desktopOnboardingRandomPassword = mutableStateOf(false)
|
||||
|
||||
// set when app is opened via contact or invitation URI (rhId, uri)
|
||||
val appOpenUrl = mutableStateOf<Pair<Long?, URI>?>(null)
|
||||
val appOpenUrl = mutableStateOf<Pair<Long?, String>?>(null)
|
||||
|
||||
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android
|
||||
val newChatSheetVisible = mutableStateOf(false)
|
||||
@@ -772,6 +775,16 @@ object ChatModel {
|
||||
|
||||
fun addTerminalItem(item: TerminalItem) {
|
||||
val maxItems = if (appPreferences.developerTools.get()) 500 else 200
|
||||
if (terminalsVisible.isNotEmpty()) {
|
||||
withApi {
|
||||
addTerminalItem(item, maxItems)
|
||||
}
|
||||
} else {
|
||||
addTerminalItem(item, maxItems)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTerminalItem(item: TerminalItem, maxItems: Int) {
|
||||
if (terminalItems.value.size >= maxItems) {
|
||||
terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size)
|
||||
}
|
||||
@@ -785,7 +798,8 @@ object ChatModel {
|
||||
data class ShowingInvitation(
|
||||
val connId: String,
|
||||
val connReq: String,
|
||||
val connChatUsed: Boolean
|
||||
val connChatUsed: Boolean,
|
||||
val conn: PendingContactConnection
|
||||
)
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
@@ -805,6 +819,7 @@ data class User(
|
||||
val profile: LocalProfile,
|
||||
val fullPreferences: FullChatPreferences,
|
||||
override val activeUser: Boolean,
|
||||
val activeOrder: Long,
|
||||
override val showNtfs: Boolean,
|
||||
val sendRcptsContacts: Boolean,
|
||||
val sendRcptsSmallGroups: Boolean,
|
||||
@@ -832,6 +847,7 @@ data class User(
|
||||
profile = LocalProfile.sampleData,
|
||||
fullPreferences = FullChatPreferences.sampleData,
|
||||
activeUser = true,
|
||||
activeOrder = 0,
|
||||
showNtfs = true,
|
||||
sendRcptsContacts = true,
|
||||
sendRcptsSmallGroups = false,
|
||||
@@ -1403,6 +1419,14 @@ class Group (
|
||||
var members: List<GroupMember>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class ForwardConfirmation {
|
||||
@Serializable @SerialName("filesNotAccepted") data class FilesNotAccepted(val fileIds: List<Long>) : ForwardConfirmation()
|
||||
@Serializable @SerialName("filesInProgress") data class FilesInProgress(val filesCount: Int) : ForwardConfirmation()
|
||||
@Serializable @SerialName("filesMissing") data class FilesMissing(val filesCount: Int) : ForwardConfirmation()
|
||||
@Serializable @SerialName("filesFailed") data class FilesFailed(val filesCount: Int) : ForwardConfirmation()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GroupInfo (
|
||||
val groupId: Long,
|
||||
@@ -1865,6 +1889,7 @@ class PendingContactConnection(
|
||||
@Serializable
|
||||
enum class ConnStatus {
|
||||
@SerialName("new") New,
|
||||
@SerialName("prepared") Prepared,
|
||||
@SerialName("joined") Joined,
|
||||
@SerialName("requested") Requested,
|
||||
@SerialName("accepted") Accepted,
|
||||
@@ -1874,6 +1899,7 @@ enum class ConnStatus {
|
||||
|
||||
val initiated: Boolean? get() = when (this) {
|
||||
New -> true
|
||||
Prepared -> false
|
||||
Joined -> false
|
||||
Requested -> true
|
||||
Accepted -> true
|
||||
@@ -2345,7 +2371,8 @@ data class CIMeta (
|
||||
val deletable: Boolean,
|
||||
val editable: Boolean
|
||||
) {
|
||||
val timestampText: String get() = getTimestampText(itemTs)
|
||||
val timestampText: String get() = getTimestampText(itemTs, true)
|
||||
|
||||
val recent: Boolean get() = updatedAt + 10.toDuration(DurationUnit.SECONDS) > Clock.System.now()
|
||||
val isLive: Boolean get() = itemLive == true
|
||||
val disappearing: Boolean get() = !isRcvNew && itemTimed?.deleteAt != null
|
||||
@@ -2409,7 +2436,18 @@ data class CITimed(
|
||||
val deleteAt: Instant?
|
||||
)
|
||||
|
||||
fun getTimestampText(t: Instant): String {
|
||||
fun getTimestampDateText(t: Instant): String {
|
||||
val tz = TimeZone.currentSystemDefault()
|
||||
val time = t.toLocalDateTime(tz).toJavaLocalDateTime()
|
||||
val weekday = time.format(DateTimeFormatter.ofPattern("EEE"))
|
||||
val dayMonthYear = time.format(DateTimeFormatter.ofPattern(
|
||||
if (Clock.System.now().toLocalDateTime(tz).year == time.year) "d MMM" else "d MMM YYYY")
|
||||
)
|
||||
|
||||
return "$weekday, $dayMonthYear"
|
||||
}
|
||||
|
||||
fun getTimestampText(t: Instant, shortFormat: Boolean = false): String {
|
||||
val tz = TimeZone.currentSystemDefault()
|
||||
val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz)
|
||||
val time: LocalDateTime = t.toLocalDateTime(tz)
|
||||
@@ -2417,16 +2455,23 @@ fun getTimestampText(t: Instant): String {
|
||||
val recent = now.date == time.date ||
|
||||
(period.years == 0 && period.months == 0 && period.days == 1 && now.hour < 12 && time.hour >= 18 )
|
||||
val dateFormatter =
|
||||
if (recent) {
|
||||
if (recent || shortFormat) {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
} else {
|
||||
val dayMonthFormat = when (Locale.getDefault().country) {
|
||||
"US" -> "M/dd"
|
||||
"DE" -> "dd.MM"
|
||||
"RU" -> "dd.MM"
|
||||
else -> "dd/MM"
|
||||
}
|
||||
val dayMonthYearFormat = when (Locale.getDefault().country) {
|
||||
"US" -> "M/dd/yy"
|
||||
"DE" -> "dd.MM.yy"
|
||||
"RU" -> "dd.MM.yy"
|
||||
else -> "dd/MM/yy"
|
||||
}
|
||||
DateTimeFormatter.ofPattern(
|
||||
when (Locale.getDefault().country) {
|
||||
"US" -> "M/dd"
|
||||
"DE" -> "dd.MM"
|
||||
"RU" -> "dd.MM"
|
||||
else -> "dd/MM"
|
||||
}
|
||||
if (now.year == time.year) dayMonthFormat else dayMonthYearFormat
|
||||
)
|
||||
// DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package chat.simplex.common.model
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.setNetCfg
|
||||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||
@@ -218,6 +219,8 @@ class AppPreferences {
|
||||
}, settingsThemes)
|
||||
val themeOverrides = mkThemeOverridesPreference()
|
||||
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
|
||||
val chatItemRoundness = mkFloatPreference(SHARED_PREFS_CHAT_ITEM_ROUNDNESS, 0.75f)
|
||||
val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true)
|
||||
val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f)
|
||||
val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f)
|
||||
|
||||
@@ -421,6 +424,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_THEMES_OLD = "Themes"
|
||||
private const val SHARED_PREFS_THEME_OVERRIDES = "ThemeOverrides"
|
||||
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
|
||||
private const val SHARED_PREFS_CHAT_ITEM_ROUNDNESS = "ChatItemRoundness"
|
||||
private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail"
|
||||
private const val SHARED_PREFS_FONT_SCALE = "FontScale"
|
||||
private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale"
|
||||
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
||||
@@ -861,15 +866,15 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSendMessage(rh: Long?, type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
|
||||
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl)
|
||||
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)
|
||||
}
|
||||
|
||||
private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): AChatItem? {
|
||||
private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): List<AChatItem>? {
|
||||
val r = sendCmd(rh, cmd)
|
||||
return when (r) {
|
||||
is CR.NewChatItem -> r.chatItem
|
||||
is CR.NewChatItems -> r.chatItems
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("processSendMessageCmd", generalGetString(MR.strings.error_sending_message), r)
|
||||
@@ -878,13 +883,13 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
}
|
||||
suspend fun apiCreateChatItem(rh: Long?, noteFolderId: Long, file: CryptoFile? = null, mc: MsgContent): AChatItem? {
|
||||
val cmd = CC.ApiCreateChatItem(noteFolderId, file, mc)
|
||||
suspend fun apiCreateChatItems(rh: Long?, noteFolderId: Long, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
||||
val cmd = CC.ApiCreateChatItems(noteFolderId, composedMessages)
|
||||
val r = sendCmd(rh, cmd)
|
||||
return when (r) {
|
||||
is CR.NewChatItem -> r.chatItem
|
||||
is CR.NewChatItems -> r.chatItems
|
||||
else -> {
|
||||
apiErrorAlert("apiCreateChatItem", generalGetString(MR.strings.error_creating_message), r)
|
||||
apiErrorAlert("apiCreateChatItems", generalGetString(MR.strings.error_creating_message), r)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -900,12 +905,20 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long, ttl: Int?): ChatItem? {
|
||||
val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl)
|
||||
return processSendMessageCmd(rh, cmd)?.chatItem
|
||||
suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemIds: List<Long>, ttl: Int?): List<ChatItem>? {
|
||||
val cmd = CC.ApiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl)
|
||||
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
|
||||
}
|
||||
|
||||
|
||||
suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List<Long>): CR.ForwardPlan? {
|
||||
return when (val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds))) {
|
||||
is CR.ForwardPlan -> r
|
||||
else -> {
|
||||
apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mc: MsgContent, live: Boolean = false): AChatItem? {
|
||||
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, mc, live))
|
||||
@@ -1149,9 +1162,30 @@ object ChatController {
|
||||
|
||||
suspend fun apiSetConnectionIncognito(rh: Long?, connId: Long, incognito: Boolean): PendingContactConnection? {
|
||||
val r = sendCmd(rh, CC.ApiSetConnectionIncognito(connId, incognito))
|
||||
if (r is CR.ConnectionIncognitoUpdated) return r.toConnection
|
||||
Log.e(TAG, "apiSetConnectionIncognito bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
|
||||
return when (r) {
|
||||
is CR.ConnectionIncognitoUpdated -> r.toConnection
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiSetConnectionIncognito", generalGetString(MR.strings.error_sending_message), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? {
|
||||
val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId))
|
||||
|
||||
return when (r) {
|
||||
is CR.ConnectionUserChanged -> r.toConnection
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? {
|
||||
@@ -1476,6 +1510,13 @@ object ChatController {
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, itemIds: List<Long>): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, itemIds))
|
||||
if (r is CR.CmdOk) return true
|
||||
Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}")
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiChatUnread(rh: Long?, type: ChatType, id: Long, unreadChat: Boolean): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiChatUnread(type, id, unreadChat))
|
||||
if (r is CR.CmdOk) return true
|
||||
@@ -1513,50 +1554,132 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
|
||||
val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline))
|
||||
return when (r) {
|
||||
is CR.RcvFileAccepted -> r.chatItem
|
||||
is CR.RcvFileAcceptedSndCancelled -> {
|
||||
Log.d(TAG, "apiReceiveFile error: sender cancelled file transfer")
|
||||
if (!auto) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.cannot_receive_file),
|
||||
generalGetString(MR.strings.sender_cancelled_file_transfer)
|
||||
)
|
||||
}
|
||||
null
|
||||
}
|
||||
suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List<Long>, userApprovedRelays: Boolean = false, auto: Boolean = false) {
|
||||
val fileIdsToApprove = mutableListOf<Long>()
|
||||
val srvsToApprove = mutableSetOf<String>()
|
||||
val otherFileErrs = mutableListOf<CR>()
|
||||
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
val maybeChatError = chatError(r)
|
||||
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
|
||||
Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error")
|
||||
} else if (maybeChatError is ChatErrorType.FileNotApproved) {
|
||||
Log.d(TAG, "apiReceiveFile FileNotApproved error")
|
||||
if (!auto) {
|
||||
val srvs = maybeChatError.unknownServers.map{ serverHostname(it) }
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.file_not_approved_title),
|
||||
text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")),
|
||||
confirmText = generalGetString(MR.strings.download_file),
|
||||
onConfirm = {
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
} else if (!auto) {
|
||||
apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r)
|
||||
}
|
||||
for (fileId in fileIds) {
|
||||
val r = sendCmd(
|
||||
rhId, CC.ReceiveFile(
|
||||
fileId,
|
||||
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
|
||||
encrypt = appPrefs.privacyEncryptLocalFiles.get(),
|
||||
inline = null
|
||||
)
|
||||
)
|
||||
if (r is CR.RcvFileAccepted) {
|
||||
chatItemSimpleUpdate(rhId, user, r.chatItem)
|
||||
} else {
|
||||
val maybeChatError = chatError(r)
|
||||
if (maybeChatError is ChatErrorType.FileNotApproved) {
|
||||
fileIdsToApprove.add(maybeChatError.fileId)
|
||||
srvsToApprove.addAll(maybeChatError.unknownServers.map { serverHostname(it) })
|
||||
} else {
|
||||
otherFileErrs.add(r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (!auto) {
|
||||
// If there are not approved files, alert is shown the same way both in case of singular and plural files reception
|
||||
if (fileIdsToApprove.isNotEmpty()) {
|
||||
showFilesToApproveAlert(
|
||||
srvsToApprove = srvsToApprove,
|
||||
otherFileErrs = otherFileErrs,
|
||||
approveFiles = {
|
||||
withBGApi {
|
||||
receiveFiles(
|
||||
rhId = rhId,
|
||||
user = user,
|
||||
fileIds = fileIdsToApprove,
|
||||
userApprovedRelays = true
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (otherFileErrs.size == 1) { // If there is a single other error, we differentiate on it
|
||||
when (val errCR = otherFileErrs.first()) {
|
||||
is CR.RcvFileAcceptedSndCancelled -> {
|
||||
Log.d(TAG, "receiveFiles error: sender cancelled file transfer")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.cannot_receive_file),
|
||||
generalGetString(MR.strings.sender_cancelled_file_transfer)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val maybeChatError = chatError(errCR)
|
||||
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
|
||||
Log.d(TAG, "receiveFiles ignoring FileCancelled or FileAlreadyReceiving error")
|
||||
} else {
|
||||
apiErrorAlert("receiveFiles", generalGetString(MR.strings.error_receiving_file), errCR)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (otherFileErrs.size > 1) { // If there are multiple other errors, we show general alert
|
||||
val errsStr = otherFileErrs.map { json.encodeToString(it) }.joinToString(separator = "\n")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.error_receiving_file),
|
||||
text = String.format(generalGetString(MR.strings.n_file_errors), otherFileErrs.size, errsStr),
|
||||
shareText = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFilesToApproveAlert(
|
||||
srvsToApprove: Set<String>,
|
||||
otherFileErrs: List<CR>,
|
||||
approveFiles: (() -> Unit)
|
||||
) {
|
||||
val srvsToApproveStr = srvsToApprove.sorted().joinToString(separator = ", ")
|
||||
val alertText =
|
||||
generalGetString(MR.strings.file_not_approved_descr).format(srvsToApproveStr) +
|
||||
(if (otherFileErrs.isNotEmpty()) "\n" + generalGetString(MR.strings.n_other_file_errors).format(otherFileErrs.size) else "")
|
||||
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(generalGetString(MR.strings.file_not_approved_title), alertText, belowTextContent = {
|
||||
if (otherFileErrs.isNotEmpty()) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SimpleButtonFrame(click = {
|
||||
clipboard.setText(AnnotatedString(otherFileErrs.map { json.encodeToString(it) }.joinToString(separator = "\n")))
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_content_copy),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(generalGetString(MR.strings.copy_error), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
|
||||
delay(200)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
TextButton(onClick = AlertManager.shared::hideAlert) { Text(generalGetString(MR.strings.cancel_verb)) }
|
||||
TextButton(onClick = {
|
||||
approveFiles.invoke()
|
||||
AlertManager.shared.hideAlert()
|
||||
}, Modifier.focusRequester(focusRequester)) { Text(generalGetString(MR.strings.download_file)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
|
||||
receiveFiles(
|
||||
rhId = rhId,
|
||||
user = user,
|
||||
fileIds = listOf(fileId),
|
||||
userApprovedRelays = userApprovedRelays,
|
||||
auto = auto
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun cancelFile(rh: Long?, user: User, fileId: Long) {
|
||||
@@ -2149,38 +2272,42 @@ object ChatController {
|
||||
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
|
||||
}
|
||||
}
|
||||
is CR.NewChatItem -> withBGApi {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
is CR.NewChatItems -> withBGApi {
|
||||
r.chatItems.forEach { chatItem ->
|
||||
val cInfo = chatItem.chatInfo
|
||||
val cItem = chatItem.chatItem
|
||||
if (active(r.user)) {
|
||||
withChats {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
}
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||
}
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||
}
|
||||
val file = cItem.file
|
||||
val mc = cItem.content.msgContent
|
||||
if (file != null &&
|
||||
val file = cItem.file
|
||||
val mc = cItem.content.msgContent
|
||||
if (file != null &&
|
||||
appPrefs.privacyAcceptImages.get() &&
|
||||
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|
||||
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|
||||
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
|
||||
receiveFile(rhId, r.user, file.fileId, auto = true)
|
||||
}
|
||||
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
|
||||
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
is CR.ChatItemStatusUpdated -> {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (!cItem.isDeletedContent && active(r.user)) {
|
||||
withChats {
|
||||
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))
|
||||
) {
|
||||
receiveFile(rhId, r.user, file.fileId, auto = true)
|
||||
}
|
||||
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
|
||||
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemsStatusesUpdated ->
|
||||
r.chatItems.forEach { chatItem ->
|
||||
val cInfo = chatItem.chatInfo
|
||||
val cItem = chatItem.chatItem
|
||||
if (!cItem.isDeletedContent && active(r.user)) {
|
||||
withChats {
|
||||
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemUpdated ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
is CR.ChatItemReaction -> {
|
||||
@@ -2402,7 +2529,7 @@ object ChatController {
|
||||
// TODO askConfirmation?
|
||||
// TODO check encryption is compatible
|
||||
withCall(r, r.contact) { call ->
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, sharedKey = r.sharedKey)
|
||||
val useRelay = appPrefs.webrtcPolicyRelay.get()
|
||||
val iceServers = getIceServers()
|
||||
Log.d(TAG, ".callOffer iceServers $iceServers")
|
||||
@@ -2657,19 +2784,6 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
|
||||
val chatItem = apiReceiveFile(
|
||||
rhId,
|
||||
fileId,
|
||||
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
|
||||
encrypted = appPrefs.privacyEncryptLocalFiles.get(),
|
||||
auto = auto
|
||||
)
|
||||
if (chatItem != null) {
|
||||
chatItemSimpleUpdate(rhId, user, chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun leaveGroup(rh: Long?, groupId: Long) {
|
||||
val groupInfo = apiLeaveGroup(rh, groupId)
|
||||
if (groupInfo != null) {
|
||||
@@ -2876,13 +2990,14 @@ sealed class 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 ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
|
||||
class ApiCreateChatItem(val noteFolderId: Long, val file: CryptoFile?, val mc: MsgContent): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): 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()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC()
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long, val ttl: Int?): CC()
|
||||
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List<Long>): CC()
|
||||
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List<Long>, val ttl: Int?): CC()
|
||||
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
|
||||
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
|
||||
class ApiJoinGroup(val groupId: Long): CC()
|
||||
@@ -2926,6 +3041,7 @@ sealed class CC {
|
||||
class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC()
|
||||
class APIAddContact(val userId: Long, val incognito: Boolean): CC()
|
||||
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
|
||||
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
|
||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
||||
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
||||
@@ -2955,6 +3071,7 @@ sealed class CC {
|
||||
class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC()
|
||||
class ApiRejectContact(val contactReqId: Long): CC()
|
||||
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
||||
class ApiChatItemsRead(val type: ChatType, val id: Long, val itemIds: List<Long>): CC()
|
||||
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
||||
class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC()
|
||||
class CancelFile(val fileId: Long): CC()
|
||||
@@ -3021,20 +3138,25 @@ sealed class CC {
|
||||
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"
|
||||
is ApiSendMessage -> {
|
||||
is ApiSendMessages -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
|
||||
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
|
||||
}
|
||||
is ApiCreateChatItem -> {
|
||||
"/_create *$noteFolderId json ${json.encodeToString(ComposedMessage(file, null, mc))}"
|
||||
is ApiCreateChatItems -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
"/_create *$noteFolderId json $msgs"
|
||||
}
|
||||
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}"
|
||||
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
|
||||
is ApiForwardChatItem -> {
|
||||
is ApiForwardChatItems -> {
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId ttl=${ttlStr}"
|
||||
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
|
||||
}
|
||||
is ApiPlanForwardChatItems -> {
|
||||
"/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}"
|
||||
}
|
||||
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
|
||||
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
|
||||
@@ -3079,6 +3201,7 @@ sealed class CC {
|
||||
is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else ""
|
||||
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}"
|
||||
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
|
||||
is ApiChangeConnectionUser -> "/_set conn user :$connId $userId"
|
||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||
@@ -3108,6 +3231,7 @@ sealed class CC {
|
||||
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
|
||||
is ApiGetNetworkStatuses -> "/_network_statuses"
|
||||
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
||||
is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}"
|
||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||
is ReceiveFile ->
|
||||
"/freceive $fileId" +
|
||||
@@ -3171,13 +3295,14 @@ sealed class CC {
|
||||
is ApiGetChats -> "apiGetChats"
|
||||
is ApiGetChat -> "apiGetChat"
|
||||
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
|
||||
is ApiSendMessage -> "apiSendMessage"
|
||||
is ApiCreateChatItem -> "apiCreateChatItem"
|
||||
is ApiSendMessages -> "apiSendMessages"
|
||||
is ApiCreateChatItems -> "apiCreateChatItems"
|
||||
is ApiUpdateChatItem -> "apiUpdateChatItem"
|
||||
is ApiDeleteChatItem -> "apiDeleteChatItem"
|
||||
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
|
||||
is ApiChatItemReaction -> "apiChatItemReaction"
|
||||
is ApiForwardChatItem -> "apiForwardChatItem"
|
||||
is ApiForwardChatItems -> "apiForwardChatItems"
|
||||
is ApiPlanForwardChatItems -> "apiPlanForwardChatItems"
|
||||
is ApiNewGroup -> "apiNewGroup"
|
||||
is ApiAddMember -> "apiAddMember"
|
||||
is ApiJoinGroup -> "apiJoinGroup"
|
||||
@@ -3221,6 +3346,7 @@ sealed class CC {
|
||||
is APIVerifyGroupMember -> "apiVerifyGroupMember"
|
||||
is APIAddContact -> "apiAddContact"
|
||||
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
|
||||
is ApiChangeConnectionUser -> "apiChangeConnectionUser"
|
||||
is APIConnectPlan -> "apiConnectPlan"
|
||||
is APIConnect -> "apiConnect"
|
||||
is ApiConnectContactViaAddress -> "apiConnectContactViaAddress"
|
||||
@@ -3250,6 +3376,7 @@ sealed class CC {
|
||||
is ApiCallStatus -> "apiCallStatus"
|
||||
is ApiGetNetworkStatuses -> "apiGetNetworkStatuses"
|
||||
is ApiChatRead -> "apiChatRead"
|
||||
is ApiChatItemsRead -> "apiChatItemsRead"
|
||||
is ApiChatUnread -> "apiChatUnread"
|
||||
is ReceiveFile -> "receiveFile"
|
||||
is CancelFile -> "cancelFile"
|
||||
@@ -3527,9 +3654,10 @@ data class NetCfg(
|
||||
val socksMode: SocksMode = SocksMode.Always,
|
||||
val hostMode: HostMode = HostMode.OnionViaSocks,
|
||||
val requiredHostMode: Boolean = false,
|
||||
val sessionMode: TransportSessionMode = TransportSessionMode.User,
|
||||
val sessionMode: TransportSessionMode = TransportSessionMode.default,
|
||||
val smpProxyMode: SMPProxyMode = SMPProxyMode.Unknown,
|
||||
val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.AllowProtected,
|
||||
val smpWebPort: Boolean = false,
|
||||
val tcpConnectTimeout: Long, // microseconds
|
||||
val tcpTimeout: Long, // microseconds
|
||||
val tcpTimeoutPerKb: Long, // microseconds
|
||||
@@ -3653,10 +3781,13 @@ enum class SMPProxyFallback {
|
||||
@Serializable
|
||||
enum class TransportSessionMode {
|
||||
@SerialName("user") User,
|
||||
@SerialName("session") Session,
|
||||
@SerialName("server") Server,
|
||||
@SerialName("entity") Entity;
|
||||
|
||||
companion object {
|
||||
val default = User
|
||||
val default = Session
|
||||
val safeValues = arrayOf(User, Session, Server)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4793,6 +4924,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): 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()
|
||||
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
|
||||
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@@ -4831,12 +4963,13 @@ sealed class CR {
|
||||
@Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List<MemberSubError>): CR()
|
||||
@Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR()
|
||||
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
|
||||
@Serializable @SerialName("newChatItem") class NewChatItem(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List<AChatItem>): CR()
|
||||
@Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List<AChatItem>): CR()
|
||||
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR()
|
||||
@Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List<ChatItemDeletion>, val byUser: Boolean): CR()
|
||||
@Serializable @SerialName("forwardPlan") class ForwardPlan(val user: UserRef, val itemsCount: Int, val chatItemIds: List<Long>, val forwardConfirmation: ForwardConfirmation? = null): CR()
|
||||
// group events
|
||||
@Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
|
||||
@@ -4971,6 +5104,7 @@ sealed class CR {
|
||||
is ConnectionVerified -> "connectionVerified"
|
||||
is Invitation -> "invitation"
|
||||
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
|
||||
is ConnectionUserChanged -> "ConnectionUserChanged"
|
||||
is CRConnectionPlan -> "connectionPlan"
|
||||
is SentConfirmation -> "sentConfirmation"
|
||||
is SentInvitation -> "sentInvitation"
|
||||
@@ -5007,12 +5141,13 @@ sealed class CR {
|
||||
is MemberSubErrors -> "memberSubErrors"
|
||||
is GroupEmpty -> "groupEmpty"
|
||||
is UserContactLinkSubscribed -> "userContactLinkSubscribed"
|
||||
is NewChatItem -> "newChatItem"
|
||||
is ChatItemStatusUpdated -> "chatItemStatusUpdated"
|
||||
is NewChatItems -> "newChatItems"
|
||||
is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated"
|
||||
is ChatItemUpdated -> "chatItemUpdated"
|
||||
is ChatItemNotChanged -> "chatItemNotChanged"
|
||||
is ChatItemReaction -> "chatItemReaction"
|
||||
is ChatItemsDeleted -> "chatItemsDeleted"
|
||||
is ForwardPlan -> "forwardPlan"
|
||||
is GroupCreated -> "groupCreated"
|
||||
is SentGroupInvitation -> "sentGroupInvitation"
|
||||
is UserAcceptedGroupSent -> "userAcceptedGroupSent"
|
||||
@@ -5139,6 +5274,7 @@ sealed class CR {
|
||||
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
||||
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)}" )
|
||||
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
|
||||
is SentConfirmation -> withUser(user, json.encodeToString(connection))
|
||||
is SentInvitation -> withUser(user, json.encodeToString(connection))
|
||||
@@ -5175,12 +5311,13 @@ sealed class CR {
|
||||
is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors))
|
||||
is GroupEmpty -> withUser(user, json.encodeToString(group))
|
||||
is UserContactLinkSubscribed -> noDetails()
|
||||
is NewChatItem -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemStatusUpdated -> withUser(user, json.encodeToString(chatItem))
|
||||
is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
|
||||
is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
|
||||
is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}")
|
||||
is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser")
|
||||
is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}")
|
||||
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
|
||||
is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member")
|
||||
is UserAcceptedGroupSent -> json.encodeToString(groupInfo)
|
||||
@@ -5590,6 +5727,7 @@ sealed class ChatErrorType {
|
||||
is AgentCommandError -> "agentCommandError"
|
||||
is InvalidFileDescription -> "invalidFileDescription"
|
||||
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
|
||||
is ConnectionUserChangeProhibited -> "connectionUserChangeProhibited"
|
||||
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
|
||||
is InternalError -> "internalError"
|
||||
is CEException -> "exception $message"
|
||||
@@ -5667,6 +5805,7 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
|
||||
@Serializable @SerialName("connectionUserChangeProhibited") object ConnectionUserChangeProhibited: ChatErrorType()
|
||||
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
|
||||
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
|
||||
@@ -6202,6 +6341,8 @@ data class AppSettings(
|
||||
var iosCallKitEnabled: Boolean? = null,
|
||||
var iosCallKitCallsInRecents: Boolean? = null,
|
||||
var uiProfileImageCornerRadius: Float? = null,
|
||||
var uiChatItemRoundness: Float? = null,
|
||||
var uiChatItemTail: Boolean? = null,
|
||||
var uiColorScheme: String? = null,
|
||||
var uiDarkColorScheme: String? = null,
|
||||
var uiCurrentThemeIds: Map<String, String>? = null,
|
||||
@@ -6234,6 +6375,8 @@ data class AppSettings(
|
||||
if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled }
|
||||
if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
||||
if (uiProfileImageCornerRadius != def.uiProfileImageCornerRadius) { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius }
|
||||
if (uiChatItemRoundness != def.uiChatItemRoundness) { empty.uiChatItemRoundness = uiChatItemRoundness }
|
||||
if (uiChatItemTail != def.uiChatItemTail) { empty.uiChatItemTail = uiChatItemTail }
|
||||
if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme }
|
||||
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
||||
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
||||
@@ -6277,6 +6420,8 @@ data class AppSettings(
|
||||
iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) }
|
||||
iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) }
|
||||
uiProfileImageCornerRadius?.let { def.profileImageCornerRadius.set(it) }
|
||||
uiChatItemRoundness?.let { def.chatItemRoundness.set(it) }
|
||||
uiChatItemTail?.let { def.chatItemTail.set(it) }
|
||||
uiColorScheme?.let { def.currentTheme.set(it) }
|
||||
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
||||
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
||||
@@ -6310,6 +6455,8 @@ data class AppSettings(
|
||||
iosCallKitEnabled = true,
|
||||
iosCallKitCallsInRecents = false,
|
||||
uiProfileImageCornerRadius = 22.5f,
|
||||
uiChatItemRoundness = 0.75f,
|
||||
uiChatItemTail = true,
|
||||
uiColorScheme = DefaultTheme.SYSTEM_THEME_NAME,
|
||||
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = null,
|
||||
@@ -6344,6 +6491,8 @@ data class AppSettings(
|
||||
iosCallKitEnabled = def.iosCallKitEnabled.get(),
|
||||
iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(),
|
||||
uiProfileImageCornerRadius = def.profileImageCornerRadius.get(),
|
||||
uiChatItemRoundness = def.chatItemRoundness.get(),
|
||||
uiChatItemTail = def.chatItemTail.get(),
|
||||
uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM_THEME_NAME,
|
||||
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = def.currentThemeIds.get(),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.*
|
||||
@@ -22,6 +21,7 @@ interface PlatformInterface {
|
||||
fun androidIsBackgroundCallAllowed(): Boolean = true
|
||||
fun androidSetNightModeIfSupported() {}
|
||||
fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {}
|
||||
fun androidSetDrawerStatusAndNavBarColor(isLight: Boolean, drawerShadingColor: Color, toolbarOnTop: Boolean, navBarColor: Color) {}
|
||||
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
||||
fun androidPictureInPictureAllowed(): Boolean = true
|
||||
fun androidCallEnded() {}
|
||||
|
||||
@@ -20,11 +20,12 @@ import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
fun TerminalView(floating: Boolean = false, close: () -> Unit) {
|
||||
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
|
||||
val close = {
|
||||
close()
|
||||
@@ -37,6 +38,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
})
|
||||
TerminalLayout(
|
||||
composeState,
|
||||
floating,
|
||||
sendCommand = { sendCommand(chatModel, composeState) },
|
||||
close
|
||||
)
|
||||
@@ -65,6 +67,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
||||
@Composable
|
||||
fun TerminalLayout(
|
||||
composeState: MutableState<ComposeState>,
|
||||
floating: Boolean,
|
||||
sendCommand: () -> Unit,
|
||||
close: () -> Unit
|
||||
) {
|
||||
@@ -109,7 +112,6 @@ fun TerminalLayout(
|
||||
}
|
||||
},
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Surface(
|
||||
@@ -119,19 +121,40 @@ fun TerminalLayout(
|
||||
color = MaterialTheme.colors.background,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
TerminalLog()
|
||||
TerminalLog(floating)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TerminalLog() {
|
||||
fun TerminalLog(floating: Boolean) {
|
||||
val reversedTerminalItems by remember {
|
||||
derivedStateOf { chatModel.terminalItems.value.asReversed() }
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
LazyColumnWithScrollBar(reverseLayout = true) {
|
||||
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
|
||||
LaunchedEffect(Unit) {
|
||||
var autoScrollToBottom = true
|
||||
launch {
|
||||
snapshotFlow { listState.layoutInfo.totalItemsCount }
|
||||
.filter { autoScrollToBottom }
|
||||
.collect {
|
||||
try {
|
||||
listState.scrollToItem(0)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.collect {
|
||||
autoScrollToBottom = listState.firstVisibleItemIndex == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyColumnWithScrollBar(reverseLayout = true, state = listState) {
|
||||
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
||||
val rhId = item.remoteHostId
|
||||
val rhIdStr = if (rhId == null) "" else "$rhId "
|
||||
@@ -143,7 +166,12 @@ fun TerminalLog() {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
ModalManager.start.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
|
||||
val modalPlace = if (floating) {
|
||||
ModalManager.floatingTerminal
|
||||
} else {
|
||||
ModalManager.start
|
||||
}
|
||||
modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
|
||||
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
val details = item.details
|
||||
.let {
|
||||
@@ -157,6 +185,16 @@ fun TerminalLog() {
|
||||
)
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
val terminals = chatModel.terminalsVisible.toMutableSet()
|
||||
terminals += floating
|
||||
chatModel.terminalsVisible = terminals
|
||||
onDispose {
|
||||
val terminals = chatModel.terminalsVisible.toMutableSet()
|
||||
terminals -= floating
|
||||
chatModel.terminalsVisible = terminals
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
@@ -170,6 +208,7 @@ fun PreviewTerminalLayout() {
|
||||
TerminalLayout(
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
|
||||
sendCommand = {},
|
||||
floating = false,
|
||||
close = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
contact = invitation.contact,
|
||||
callUUID = invitation.callUUID,
|
||||
callState = CallState.InvitationAccepted,
|
||||
localMedia = invitation.callType.media,
|
||||
initialCallType = invitation.callType.media,
|
||||
sharedKey = invitation.sharedKey,
|
||||
)
|
||||
showCallView.value = true
|
||||
|
||||
@@ -15,10 +15,11 @@ fun activeCallWaitDeliveryReceipt(scope: CoroutineScope) = scope.launch(Dispatch
|
||||
if (call == null || call.callState > CallState.InvitationSent) break
|
||||
val msg = apiResp.resp
|
||||
if (apiResp.remoteHostId == call.remoteHostId &&
|
||||
msg is CR.ChatItemStatusUpdated &&
|
||||
msg.chatItem.chatInfo.id == call.contact.id &&
|
||||
msg.chatItem.chatItem.content is CIContent.SndCall &&
|
||||
msg.chatItem.chatItem.meta.itemStatus is CIStatus.SndRcvd) {
|
||||
msg is CR.ChatItemsStatusesUpdated &&
|
||||
msg.chatItems.any {
|
||||
it.chatInfo.id == call.contact.id && it.chatItem.content is CIContent.SndCall && it.chatItem.meta.itemStatus is CIStatus.SndRcvd
|
||||
}
|
||||
) {
|
||||
CallSoundsPlayer.startInCallSound(scope)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.common.views.call
|
||||
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -15,18 +16,21 @@ data class Call(
|
||||
val contact: Contact,
|
||||
val callUUID: String?,
|
||||
val callState: CallState,
|
||||
val localMedia: CallMediaType,
|
||||
val initialCallType: CallMediaType,
|
||||
val localMediaSources: CallMediaSources = CallMediaSources(mic = true, camera = initialCallType == CallMediaType.Video),
|
||||
val localCapabilities: CallCapabilities? = null,
|
||||
val peerMedia: CallMediaType? = null,
|
||||
val peerMediaSources: CallMediaSources = CallMediaSources(),
|
||||
val sharedKey: String? = null,
|
||||
val audioEnabled: Boolean = true,
|
||||
val videoEnabled: Boolean = localMedia == CallMediaType.Video,
|
||||
var localCamera: VideoCamera = VideoCamera.User,
|
||||
val connectionInfo: ConnectionInfo? = null,
|
||||
var connectedAt: Instant? = null,
|
||||
|
||||
// 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 encrypted: Boolean get() = localEncrypted && sharedKey != null
|
||||
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
||||
private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
||||
|
||||
val encryptionStatus: String get() = when(callState) {
|
||||
CallState.WaitCapabilities -> ""
|
||||
@@ -35,10 +39,8 @@ data class Call(
|
||||
else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
|
||||
}
|
||||
|
||||
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
|
||||
|
||||
fun supportsVideo(): Boolean = peerMedia == CallMediaType.Video || localMedia == CallMediaType.Video
|
||||
|
||||
val hasVideo: Boolean
|
||||
get() = localMediaSources.hasVideo || peerMediaSources.hasVideo
|
||||
}
|
||||
|
||||
enum class CallState {
|
||||
@@ -68,14 +70,25 @@ enum class CallState {
|
||||
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||
|
||||
@Serializable data class CallMediaSources(
|
||||
val mic: Boolean = false,
|
||||
val camera: Boolean = false,
|
||||
val screenAudio: Boolean = false,
|
||||
val screenVideo: Boolean = false
|
||||
) {
|
||||
val hasVideo: Boolean
|
||||
get() = camera || screenVideo
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class WCallCommand {
|
||||
@Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand()
|
||||
@Serializable @SerialName("permission") data class Permission(val title: String, val chrome: String, val safari: String): WCallCommand()
|
||||
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("media") data class Media(val source: CallMediaSource, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
|
||||
@Serializable @SerialName("layout") data class Layout(val layout: LayoutType): WCallCommand()
|
||||
@@ -90,6 +103,7 @@ sealed class WCallResponse {
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
|
||||
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||
@Serializable @SerialName("peerMedia") data class PeerMedia(val source: CallMediaSource, val enabled: Boolean): WCallResponse()
|
||||
@Serializable @SerialName("end") object End: WCallResponse()
|
||||
@Serializable @SerialName("ended") object Ended: WCallResponse()
|
||||
@Serializable @SerialName("ok") object Ok: WCallResponse()
|
||||
@@ -165,6 +179,14 @@ enum class CallMediaType {
|
||||
@SerialName("audio") Audio
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class CallMediaSource {
|
||||
@SerialName("mic") Mic,
|
||||
@SerialName("camera") Camera,
|
||||
@SerialName("screenAudio") ScreenAudio,
|
||||
@SerialName("screenVideo") ScreenVideo
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class VideoCamera {
|
||||
@SerialName("user") User,
|
||||
|
||||
@@ -24,11 +24,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chat.item.MarkdownText
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.group.MemberProfileImage
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
@@ -75,7 +74,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
Column {
|
||||
Box(
|
||||
Modifier.clip(RoundedCornerShape(18.dp)).background(itemColor).padding(bottom = 3.dp)
|
||||
Modifier.clipChatItem().background(itemColor).padding(bottom = 3.dp)
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
@@ -122,7 +121,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
Column {
|
||||
Box(
|
||||
Modifier.clip(RoundedCornerShape(18.dp)).background(quoteColor).padding(bottom = 3.dp)
|
||||
Modifier.clipChatItem().background(quoteColor).padding(bottom = 3.dp)
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
|
||||
@@ -12,8 +12,10 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -21,8 +23,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
@@ -39,11 +43,14 @@ import chat.simplex.common.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sign
|
||||
|
||||
data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?)
|
||||
|
||||
@Composable
|
||||
// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat
|
||||
// to chat list smooth. Otherwise, chat view will become blank right before the transition starts
|
||||
@@ -172,7 +179,30 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
forwardItems = {
|
||||
val itemIds = selectedChatItems.value
|
||||
|
||||
if (itemIds != null) {
|
||||
withBGApi {
|
||||
val chatItemIds = itemIds.toList()
|
||||
val forwardPlan = controller.apiPlanForwardChatItems(
|
||||
rh = chatRh,
|
||||
fromChatType = chatInfo.chatType,
|
||||
fromChatId = chatInfo.apiId,
|
||||
chatItemIds = chatItemIds
|
||||
)
|
||||
|
||||
if (forwardPlan != null) {
|
||||
if (forwardPlan.chatItemIds.count() < chatItemIds.count() || forwardPlan.forwardConfirmation != null) {
|
||||
handleForwardConfirmation(chatRh, forwardPlan, chatInfo)
|
||||
} else {
|
||||
forwardContent(forwardPlan.chatItemIds, chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -347,9 +377,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
forwardItem = { cItem, cInfo ->
|
||||
forwardItem = { cInfo, cItem ->
|
||||
chatModel.chatId.value = null
|
||||
chatModel.sharedContent.value = SharedContent.Forward(cInfo, cItem)
|
||||
chatModel.sharedContent.value = SharedContent.Forward(listOf(cItem), cInfo)
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withBGApi {
|
||||
@@ -444,8 +474,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
for (apiResp in controller.messagesChannel) {
|
||||
val msg = apiResp.resp
|
||||
if (apiResp.remoteHostId == chatRh &&
|
||||
msg is CR.ChatItemStatusUpdated &&
|
||||
msg.chatItem.chatItem.id == cItem.id
|
||||
msg is CR.ChatItemsStatusesUpdated &&
|
||||
msg.chatItems.any { it.chatItem.id == cItem.id }
|
||||
) {
|
||||
ciInfo = loadChatItemInfo() ?: return@withContext
|
||||
initialCiInfo = ciInfo
|
||||
@@ -551,7 +581,7 @@ 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, localMedia = media, userProfile = profile)
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
@@ -660,7 +690,6 @@ fun ChatLayout(
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
backgroundColor = Color.Unspecified
|
||||
) { contentPadding ->
|
||||
val wallpaperImage = MaterialTheme.wallpaper.type.image
|
||||
@@ -751,30 +780,16 @@ fun ChatInfoToolbar(
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
|
||||
if (activeCall == null) {
|
||||
barButtons.add {
|
||||
if (appPlatform.isAndroid) {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current),
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam),
|
||||
stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current),
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current),
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) {
|
||||
@@ -808,17 +823,10 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) {
|
||||
menuItems.add {
|
||||
if (appPlatform.isAndroid) {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
} else {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), painterResource(MR.images.ic_call_500), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
})
|
||||
}
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) {
|
||||
@@ -1010,25 +1018,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||
) {
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||
if (it == DismissValue.DismissedToStart) {
|
||||
scope.launch {
|
||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
val swipeableModifier = SwipeToDismissModifier(
|
||||
state = dismissState,
|
||||
directions = setOf(DismissDirection.EndToStart),
|
||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||
)
|
||||
val provider = {
|
||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||
scope.launch {
|
||||
@@ -1043,18 +1032,65 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
val revealed = remember { mutableStateOf(false) }
|
||||
|
||||
@Composable
|
||||
fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?) {
|
||||
fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: IntRange?, fillMaxWidth: Boolean = true) {
|
||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||
}) {
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, 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, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, 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, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) {
|
||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||
if (it == DismissValue.DismissedToStart) {
|
||||
scope.launch {
|
||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
val swipeableModifier = SwipeToDismissModifier(
|
||||
state = dismissState,
|
||||
directions = setOf(DismissDirection.EndToStart),
|
||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||
)
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(Modifier.padding(bottom = 4.dp)) {
|
||||
|
||||
@Composable
|
||||
fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) {
|
||||
Box(
|
||||
modifier = modifier.padding(
|
||||
bottom = if (itemSeparation.largeGap) {
|
||||
if (i == 0) {
|
||||
8.dp
|
||||
} else {
|
||||
4.dp
|
||||
}
|
||||
} else 1.dp, top = if (previousItemSeparation?.largeGap == true) 4.dp else 1.dp
|
||||
),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp {
|
||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
|
||||
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
|
||||
|
||||
return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp)
|
||||
}
|
||||
|
||||
Box {
|
||||
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
|
||||
val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
|
||||
val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
|
||||
@@ -1072,84 +1108,112 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
Column(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp),
|
||||
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.fillMaxWidth()
|
||||
.then(swipeableModifier),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
if (cItem.content.showMemberName) {
|
||||
val memberNameStyle = SpanStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
val memberNameString = if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
|
||||
buildAnnotatedString {
|
||||
withStyle(memberNameStyle.copy(fontWeight = FontWeight.Medium)) { append(member.memberRole.text) }
|
||||
append(" ")
|
||||
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
|
||||
}
|
||||
} else {
|
||||
buildAnnotatedString {
|
||||
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
|
||||
@Composable
|
||||
fun MemberNameAndRole() {
|
||||
Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
memberNames(member, prevMember, memCount),
|
||||
Modifier
|
||||
.padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF)
|
||||
.weight(1f, false),
|
||||
fontSize = 13.5.sp,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
|
||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
|
||||
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
|
||||
|
||||
Text(
|
||||
member.memberRole.text,
|
||||
Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp),
|
||||
fontSize = 13.5.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
memberNameString,
|
||||
Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp),
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(
|
||||
swipeableOrSelectionModifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
MemberImage(member)
|
||||
|
||||
@Composable
|
||||
fun Item() {
|
||||
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
|
||||
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
MemberImage(member)
|
||||
}
|
||||
Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range, false)
|
||||
}
|
||||
}
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
if (cItem.content.showMemberName) {
|
||||
DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) {
|
||||
MemberNameAndRole()
|
||||
Item()
|
||||
}
|
||||
} else {
|
||||
Item()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
|
||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||
.then(swipeableOrSelectionModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp)
|
||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||
.then(if (selectionVisible) Modifier else swipeableModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
|
||||
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
|
||||
).then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
|
||||
start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp,
|
||||
end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false),
|
||||
)
|
||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||
.then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1168,17 +1232,30 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
// memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView
|
||||
} else {
|
||||
val (prevHidden, prevItem) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
||||
|
||||
val itemSeparation = getItemSeparation(cItem, nextItem)
|
||||
val previousItemSeparation = if (prevItem != null) getItemSeparation(prevItem, cItem) else null
|
||||
|
||||
if (itemSeparation.date != null) {
|
||||
DateSeparator(itemSeparation.date)
|
||||
}
|
||||
|
||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||
if (revealed.value && range != null) {
|
||||
reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
|
||||
val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1]
|
||||
ChatItemView(ci, null, prev)
|
||||
ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation)
|
||||
}
|
||||
} else {
|
||||
ChatItemView(cItem, range, prevItem)
|
||||
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
|
||||
}
|
||||
|
||||
if (i == reversedChatItems.lastIndex) {
|
||||
DateSeparator(cItem.meta.itemTs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
|
||||
LaunchedEffect(cItem.id) {
|
||||
scope.launch {
|
||||
@@ -1191,6 +1268,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState)
|
||||
|
||||
FloatingDate(
|
||||
Modifier.padding(top = 10.dp).align(Alignment.TopCenter),
|
||||
listState,
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { listState.isScrollInProgress }
|
||||
.collect {
|
||||
@@ -1382,7 +1465,7 @@ private fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean =
|
||||
else -> false
|
||||
}
|
||||
|
||||
val MEMBER_IMAGE_SIZE: Dp = 38.dp
|
||||
val MEMBER_IMAGE_SIZE: Dp = 37.dp
|
||||
|
||||
@Composable
|
||||
fun MemberImage(member: GroupMember) {
|
||||
@@ -1417,6 +1500,179 @@ private fun TopEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingDate(
|
||||
modifier: Modifier,
|
||||
listState: LazyListState,
|
||||
) {
|
||||
var nearBottomIndex by remember { mutableStateOf(-1) }
|
||||
var isNearBottom by remember { mutableStateOf(true) }
|
||||
val lastVisibleItemDate = remember {
|
||||
derivedStateOf {
|
||||
if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) {
|
||||
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
|
||||
val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex)
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
val showDate = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
showDate.value = false
|
||||
isNearBottom = true
|
||||
nearBottomIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
|
||||
.collect { visibleItemsInfo ->
|
||||
if (visibleItemsInfo.find { it.index == 0 } != null) {
|
||||
var elapsedOffset = 0
|
||||
|
||||
for (it in visibleItemsInfo) {
|
||||
if (elapsedOffset >= listState.layoutInfo.viewportSize.height / 2.5) {
|
||||
nearBottomIndex = it.index
|
||||
break;
|
||||
}
|
||||
elapsedOffset += it.size
|
||||
}
|
||||
}
|
||||
|
||||
isNearBottom = if (nearBottomIndex == -1) true else (visibleItemsInfo.firstOrNull()?.index ?: 0) <= nearBottomIndex
|
||||
}
|
||||
}
|
||||
|
||||
fun setDateVisibility(isVisible: Boolean) {
|
||||
if (isVisible) {
|
||||
val now = Clock.System.now()
|
||||
val date = lastVisibleItemDate.value
|
||||
if (!isNearBottom && !showDate.value && date != null && getTimestampDateText(date) != getTimestampDateText(now)) {
|
||||
showDate.value = true
|
||||
}
|
||||
} else if (showDate.value) {
|
||||
showDate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
var hideDateWhenNotScrolling: Job = Job()
|
||||
snapshotFlow { listState.firstVisibleItemScrollOffset }
|
||||
.collect {
|
||||
setDateVisibility(true)
|
||||
hideDateWhenNotScrolling.cancel()
|
||||
hideDateWhenNotScrolling = launch {
|
||||
delay(1000)
|
||||
setDateVisibility(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = showDate.value,
|
||||
enter = fadeIn(tween(durationMillis = 350)),
|
||||
exit = fadeOut(tween(durationMillis = 350))
|
||||
) {
|
||||
val date = lastVisibleItemDate.value
|
||||
Column {
|
||||
Text(
|
||||
text = if (date != null) getTimestampDateText(date) else "",
|
||||
Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colors.secondaryVariant,
|
||||
RoundedCornerShape(25.dp)
|
||||
)
|
||||
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||
.clip(RoundedCornerShape(25.dp)),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadFilesButton(
|
||||
forwardConfirmation: ForwardConfirmation.FilesNotAccepted,
|
||||
rhId: Long?,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding
|
||||
) {
|
||||
val user = chatModel.currentUser.value
|
||||
|
||||
if (user != null) {
|
||||
TextButton(
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
AlertManager.shared.hideAlert()
|
||||
|
||||
withBGApi {
|
||||
controller.receiveFiles(
|
||||
rhId = rhId,
|
||||
fileIds = forwardConfirmation.fileIds,
|
||||
user = user
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(MR.strings.forward_files_not_accepted_receive_files), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ForwardButton(
|
||||
forwardPlan: CR.ForwardPlan,
|
||||
chatInfo: ChatInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
forwardContent(forwardPlan.chatItemIds, chatInfo)
|
||||
AlertManager.shared.hideAlert()
|
||||
},
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding
|
||||
) {
|
||||
Text(stringResource(MR.strings.forward_chat_item), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ButtonRow(horizontalArrangement: Arrangement.Horizontal, content: @Composable() (RowScope.() -> Unit)) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = horizontalArrangement
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateSeparator(date: Instant) {
|
||||
Text(
|
||||
text = getTimestampDateText(date),
|
||||
Modifier.padding(vertical = DEFAULT_PADDING_HALF + 4.dp, horizontal = DEFAULT_PADDING_HALF).fillMaxWidth(),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
val chatViewScrollState = MutableStateFlow(false)
|
||||
|
||||
fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
|
||||
@@ -1713,6 +1969,100 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf
|
||||
override val touchSlop: Float get() = slop
|
||||
}
|
||||
|
||||
private fun forwardContent(chatItemsIds: List<Long>, chatInfo: ChatInfo) {
|
||||
chatModel.chatId.value = null
|
||||
chatModel.sharedContent.value = SharedContent.Forward(
|
||||
chatModel.chatItems.value.filter { chatItemsIds.contains(it.id) },
|
||||
chatInfo
|
||||
)
|
||||
}
|
||||
|
||||
private fun forwardConfirmationAlertDescription(forwardConfirmation: ForwardConfirmation): String {
|
||||
return when (forwardConfirmation) {
|
||||
is ForwardConfirmation.FilesNotAccepted -> String.format(generalGetString(MR.strings.forward_files_not_accepted_desc), forwardConfirmation.fileIds.count())
|
||||
is ForwardConfirmation.FilesInProgress -> String.format(generalGetString(MR.strings.forward_files_in_progress_desc), forwardConfirmation.filesCount)
|
||||
is ForwardConfirmation.FilesFailed -> String.format(generalGetString(MR.strings.forward_files_failed_to_receive_desc), forwardConfirmation.filesCount)
|
||||
is ForwardConfirmation.FilesMissing -> String.format(generalGetString(MR.strings.forward_files_missing_desc), forwardConfirmation.filesCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleForwardConfirmation(
|
||||
rhId: Long?,
|
||||
forwardPlan: CR.ForwardPlan,
|
||||
chatInfo: ChatInfo
|
||||
) {
|
||||
var alertDescription = if (forwardPlan.forwardConfirmation != null) forwardConfirmationAlertDescription(forwardPlan.forwardConfirmation) else ""
|
||||
|
||||
if (forwardPlan.chatItemIds.isNotEmpty()) {
|
||||
alertDescription += "\n${generalGetString(MR.strings.forward_alert_forward_messages_without_files)}"
|
||||
}
|
||||
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = if (forwardPlan.chatItemIds.isNotEmpty())
|
||||
String.format(generalGetString(MR.strings.forward_alert_title_messages_to_forward), forwardPlan.chatItemIds.count()) else
|
||||
generalGetString(MR.strings.forward_alert_title_nothing_to_forward),
|
||||
text = alertDescription,
|
||||
buttons = {
|
||||
if (forwardPlan.chatItemIds.isNotEmpty()) {
|
||||
when (val confirmation = forwardPlan.forwardConfirmation) {
|
||||
is ForwardConfirmation.FilesNotAccepted -> {
|
||||
val fillMaxWidthModifier = Modifier.fillMaxWidth()
|
||||
val contentPadding = PaddingValues(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)
|
||||
Column {
|
||||
ForwardButton(forwardPlan, chatInfo, fillMaxWidthModifier, contentPadding)
|
||||
DownloadFilesButton(confirmation, rhId, fillMaxWidthModifier, contentPadding)
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }, modifier = fillMaxWidthModifier, contentPadding = contentPadding) {
|
||||
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
ButtonRow(Arrangement.SpaceBetween) {
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
ForwardButton(forwardPlan, chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (val confirmation = forwardPlan.forwardConfirmation) {
|
||||
is ForwardConfirmation.FilesNotAccepted -> {
|
||||
ButtonRow(Arrangement.SpaceBetween) {
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
DownloadFilesButton(confirmation, rhId)
|
||||
}
|
||||
}
|
||||
else -> ButtonRow(Arrangement.Center) {
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
Text(stringResource(MR.strings.ok), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getItemSeparation(chatItem: ChatItem, nextItem: ChatItem?): ItemSeparation {
|
||||
if (nextItem == null) {
|
||||
return ItemSeparation(timestamp = true, largeGap = true, date = null)
|
||||
}
|
||||
|
||||
val sameMemberAndDirection = if (nextItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) {
|
||||
chatItem.chatDir.groupMember.groupMemberId == nextItem.chatDir.groupMember.groupMemberId
|
||||
} else chatItem.chatDir.sent == nextItem.chatDir.sent
|
||||
val largeGap = !sameMemberAndDirection || (abs(nextItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60)
|
||||
|
||||
return ItemSeparation(
|
||||
timestamp = largeGap || nextItem.meta.timestampText != chatItem.meta.timestampText,
|
||||
largeGap = largeGap,
|
||||
date = if (getTimestampDateText(chatItem.meta.itemTs) == getTimestampDateText(nextItem.meta.itemTs)) null else nextItem.meta.itemTs
|
||||
)
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -49,7 +48,7 @@ sealed class ComposeContextItem {
|
||||
@Serializable object NoContextItem: ComposeContextItem()
|
||||
@Serializable class QuotedItem(val chatItem: ChatItem): ComposeContextItem()
|
||||
@Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem()
|
||||
@Serializable class ForwardingItem(val chatItem: ChatItem, val fromChatInfo: ChatInfo): ComposeContextItem()
|
||||
@Serializable class ForwardingItems(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): ComposeContextItem()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -85,7 +84,7 @@ data class ComposeState(
|
||||
}
|
||||
val forwarding: Boolean
|
||||
get() = when (contextItem) {
|
||||
is ComposeContextItem.ForwardingItem -> true
|
||||
is ComposeContextItem.ForwardingItems -> true
|
||||
else -> false
|
||||
}
|
||||
val sendEnabled: () -> Boolean
|
||||
@@ -380,55 +379,68 @@ fun ComposeView(
|
||||
|
||||
suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
|
||||
val cInfo = chat.chatInfo
|
||||
val aChatItem = if (chat.chatInfo.chatType == ChatType.Local)
|
||||
chatModel.controller.apiCreateChatItem(rh = chat.remoteHostId, noteFolderId = chat.chatInfo.apiId, file = file, mc = mc)
|
||||
val chatItems = if (chat.chatInfo.chatType == ChatType.Local)
|
||||
chatModel.controller.apiCreateChatItems(
|
||||
rh = chat.remoteHostId,
|
||||
noteFolderId = chat.chatInfo.apiId,
|
||||
composedMessages = listOf(ComposedMessage(file, null, mc))
|
||||
)
|
||||
else
|
||||
chatModel.controller.apiSendMessage(
|
||||
rh = chat.remoteHostId,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
file = file,
|
||||
quotedItemId = quoted,
|
||||
mc = mc,
|
||||
live = live,
|
||||
ttl = ttl
|
||||
)
|
||||
if (aChatItem != null) {
|
||||
withChats {
|
||||
addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
chatModel.controller.apiSendMessages(
|
||||
rh = chat.remoteHostId,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
live = live,
|
||||
ttl = ttl,
|
||||
composedMessages = listOf(ComposedMessage(file, quoted, mc))
|
||||
)
|
||||
if (!chatItems.isNullOrEmpty()) {
|
||||
chatItems.forEach { aChatItem ->
|
||||
withChats {
|
||||
addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
return aChatItem.chatItem
|
||||
return chatItems.first().chatItem
|
||||
}
|
||||
if (file != null) removeFile(file.filePath)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List<ChatItem>? {
|
||||
val cInfo = chat.chatInfo
|
||||
val cs = composeState.value
|
||||
var sent: ChatItem?
|
||||
var sent: List<ChatItem>?
|
||||
val msgText = text ?: cs.message
|
||||
|
||||
fun sending() {
|
||||
composeState.value = composeState.value.copy(inProgress = true)
|
||||
}
|
||||
|
||||
suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo, ttl: Int?): ChatItem? {
|
||||
val chatItem = controller.apiForwardChatItem(
|
||||
suspend fun forwardItem(rhId: Long?, forwardedItem: List<ChatItem>, fromChatInfo: ChatInfo, ttl: Int?): List<ChatItem>? {
|
||||
val chatItems = controller.apiForwardChatItems(
|
||||
rh = rhId,
|
||||
toChatType = chat.chatInfo.chatType,
|
||||
toChatId = chat.chatInfo.apiId,
|
||||
fromChatType = fromChatInfo.chatType,
|
||||
fromChatId = fromChatInfo.apiId,
|
||||
itemId = forwardedItem.id,
|
||||
itemIds = forwardedItem.map { it.id },
|
||||
ttl = ttl
|
||||
)
|
||||
if (chatItem != null) {
|
||||
|
||||
chatItems?.forEach { chatItem ->
|
||||
withChats {
|
||||
addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
}
|
||||
}
|
||||
return chatItem
|
||||
|
||||
if (chatItems != null && chatItems.count() < forwardedItem.count()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = String.format(generalGetString(MR.strings.forward_files_messages_deleted_after_selection_title), forwardedItem.count() - chatItems.count()),
|
||||
text = generalGetString(MR.strings.forward_files_messages_deleted_after_selection_desc)
|
||||
)
|
||||
}
|
||||
|
||||
return chatItems
|
||||
}
|
||||
|
||||
fun checkLinkPreview(): MsgContent {
|
||||
@@ -501,16 +513,25 @@ fun ComposeView(
|
||||
if (chat.nextSendGrpInv) {
|
||||
sendMemberContactInvitation()
|
||||
sent = null
|
||||
} else if (cs.contextItem is ComposeContextItem.ForwardingItem) {
|
||||
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo, ttl = ttl)
|
||||
} else if (cs.contextItem is ComposeContextItem.ForwardingItems) {
|
||||
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItems, cs.contextItem.fromChatInfo, ttl = ttl)
|
||||
if (cs.message.isNotEmpty()) {
|
||||
sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = ttl)
|
||||
sent?.mapIndexed { index, message ->
|
||||
if (index == sent!!.lastIndex) {
|
||||
send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
}
|
||||
else if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
val ei = cs.contextItem.chatItem
|
||||
sent = updateMessage(ei, chat, live)
|
||||
val updatedMessage = updateMessage(ei, chat, live)
|
||||
sent = if (updatedMessage != null) listOf(updatedMessage) else null
|
||||
} else if (liveMessage != null && liveMessage.sent) {
|
||||
sent = updateMessage(liveMessage.chatItem, chat, live)
|
||||
val updatedMessage = updateMessage(liveMessage.chatItem, chat, live)
|
||||
sent = if (updatedMessage != null) listOf(updatedMessage) else null
|
||||
} else {
|
||||
val msgs: ArrayList<MsgContent> = ArrayList()
|
||||
val files: ArrayList<CryptoFile> = ArrayList()
|
||||
@@ -519,6 +540,7 @@ fun ComposeView(
|
||||
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
|
||||
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
|
||||
is ComposePreview.MediaPreview -> {
|
||||
// TODO batch send: batch media previews
|
||||
preview.content.forEachIndexed { index, it ->
|
||||
val file = when (it) {
|
||||
is UploadContent.SimpleImage ->
|
||||
@@ -602,21 +624,23 @@ fun ComposeView(
|
||||
localPath = file.filePath
|
||||
)
|
||||
}
|
||||
sent = send(chat, content, if (index == 0) quotedItemId else null, file,
|
||||
val sendResult = send(chat, content, if (index == 0) quotedItemId else null, file,
|
||||
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false,
|
||||
ttl = ttl
|
||||
)
|
||||
sent = if (sendResult != null) listOf(sendResult) else null
|
||||
}
|
||||
if (sent == null &&
|
||||
(cs.preview is ComposePreview.MediaPreview ||
|
||||
cs.preview is ComposePreview.FilePreview ||
|
||||
cs.preview is ComposePreview.VoicePreview)
|
||||
) {
|
||||
sent = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
|
||||
val sendResult = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
|
||||
sent = if (sendResult != null) listOf(sendResult) else null
|
||||
}
|
||||
}
|
||||
val wasForwarding = cs.forwarding
|
||||
val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItem)?.fromChatInfo?.id
|
||||
val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItems)?.fromChatInfo?.id
|
||||
clearState(live)
|
||||
val draft = chatModel.draft.value
|
||||
if (wasForwarding && chatModel.draftChatId.value == chat.chatInfo.id && forwardingFromChatId != chat.chatInfo.id && draft != null) {
|
||||
@@ -718,8 +742,8 @@ fun ComposeView(
|
||||
val typedMsg = cs.message
|
||||
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage.sent)) {
|
||||
val ci = sendMessageAsync(typedMsg, live = true, ttl = null)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
|
||||
if (!ci.isNullOrEmpty()) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
|
||||
}
|
||||
} else if (cs.liveMessage == null) {
|
||||
val cItem = chatModel.addLiveDummy(chat.chatInfo)
|
||||
@@ -739,8 +763,8 @@ fun ComposeView(
|
||||
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
|
||||
if (sentMsg != null) {
|
||||
val ci = sendMessageAsync(sentMsg, live = true, ttl = null)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
|
||||
if (!ci.isNullOrEmpty()) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
|
||||
}
|
||||
} else if (liveMessage.typedMsg != typedMsg) {
|
||||
composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg))
|
||||
@@ -799,13 +823,13 @@ fun ComposeView(
|
||||
fun contextItemView() {
|
||||
when (val contextItem = composeState.value.contextItem) {
|
||||
ComposeContextItem.NoContextItem -> {}
|
||||
is ComposeContextItem.QuotedItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_reply)) {
|
||||
is ComposeContextItem.QuotedItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_reply), chatType = chat.chatInfo.chatType) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
|
||||
}
|
||||
is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_edit_filled)) {
|
||||
is ComposeContextItem.EditingItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_edit_filled), chatType = chat.chatInfo.chatType) {
|
||||
clearState()
|
||||
}
|
||||
is ComposeContextItem.ForwardingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_forward), showSender = false) {
|
||||
is ComposeContextItem.ForwardingItems -> ContextItemView(contextItem.chatItems, painterResource(MR.images.ic_forward), showSender = false, chatType = chat.chatInfo.chatType) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
|
||||
}
|
||||
}
|
||||
@@ -828,7 +852,7 @@ fun ComposeView(
|
||||
is SharedContent.Media -> composeState.processPickedMedia(shared.uris, shared.text)
|
||||
is SharedContent.File -> composeState.processPickedFile(shared.uri, shared.text)
|
||||
is SharedContent.Forward -> composeState.value = composeState.value.copy(
|
||||
contextItem = ComposeContextItem.ForwardingItem(shared.chatItem, shared.fromChatInfo),
|
||||
contextItem = ComposeContextItem.ForwardingItems(shared.chatItems, shared.fromChatInfo),
|
||||
preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview
|
||||
)
|
||||
null -> {}
|
||||
|
||||
@@ -13,28 +13,31 @@ import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.getLoadedFilePath
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ContextItemView(
|
||||
contextItem: ChatItem,
|
||||
contextItems: List<ChatItem>,
|
||||
contextIcon: Painter,
|
||||
showSender: Boolean = true,
|
||||
cancelContextItem: () -> Unit
|
||||
chatType: ChatType,
|
||||
cancelContextItem: () -> Unit,
|
||||
) {
|
||||
val sent = contextItem.chatDir.sent
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
|
||||
@Composable
|
||||
fun MessageText(attachment: ImageResource?, lines: Int) {
|
||||
fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int) {
|
||||
val inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = if (attachment != null) {
|
||||
remember(contextItem.id) {
|
||||
val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = {
|
||||
@@ -62,19 +65,24 @@ fun ContextItemView(
|
||||
)
|
||||
}
|
||||
|
||||
fun attachment(): ImageResource? =
|
||||
when (contextItem.content.msgContent) {
|
||||
is MsgContent.MCFile -> MR.images.ic_draft_filled
|
||||
fun attachment(contextItem: ChatItem): ImageResource? {
|
||||
val fileIsLoaded = getLoadedFilePath(contextItem.file) != null
|
||||
|
||||
return when (contextItem.content.msgContent) {
|
||||
is MsgContent.MCFile -> if (fileIsLoaded) MR.images.ic_draft_filled else null
|
||||
is MsgContent.MCImage -> MR.images.ic_image
|
||||
is MsgContent.MCVoice -> MR.images.ic_play_arrow_filled
|
||||
is MsgContent.MCVoice -> if (fileIsLoaded) MR.images.ic_play_arrow_filled else null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContextMsgPreview(lines: Int) {
|
||||
MessageText(remember(contextItem.id) { attachment() }, lines)
|
||||
fun ContextMsgPreview(contextItem: ChatItem, lines: Int) {
|
||||
MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines)
|
||||
}
|
||||
|
||||
val sent = contextItems[0].chatDir.sent
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
@@ -97,20 +105,27 @@ fun ContextItemView(
|
||||
contentDescription = stringResource(MR.strings.icon_descr_context),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
val sender = contextItem.memberDisplayName
|
||||
if (showSender && sender != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
sender,
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
)
|
||||
ContextMsgPreview(lines = 2)
|
||||
|
||||
if (contextItems.count() == 1) {
|
||||
val contextItem = contextItems[0]
|
||||
val sender = contextItem.memberDisplayName
|
||||
|
||||
if (showSender && sender != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
sender,
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
)
|
||||
ContextMsgPreview(contextItem, lines = 2)
|
||||
}
|
||||
} else {
|
||||
ContextMsgPreview(contextItem, lines = 3)
|
||||
}
|
||||
} else {
|
||||
ContextMsgPreview(lines = 3)
|
||||
} else if (contextItems.isNotEmpty()) {
|
||||
Text(String.format(generalGetString(if (chatType == ChatType.Local) MR.strings.compose_save_messages_n else MR.strings.compose_forward_messages_n), contextItems.count()), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = cancelContextItem) {
|
||||
@@ -129,8 +144,9 @@ fun ContextItemView(
|
||||
fun PreviewContextItemView() {
|
||||
SimpleXTheme {
|
||||
ContextItemView(
|
||||
contextItem = ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello"),
|
||||
contextIcon = painterResource(MR.images.ic_edit_filled)
|
||||
contextItems = listOf(ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello")),
|
||||
contextIcon = painterResource(MR.images.ic_edit_filled),
|
||||
chatType = ChatType.Direct
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -51,47 +52,60 @@ fun SelectedItemsBottomToolbar(
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
|
||||
moderateItems: () -> Unit,
|
||||
// shareItems: () -> Unit,
|
||||
forwardItems: () -> Unit,
|
||||
) {
|
||||
val deleteEnabled = remember { mutableStateOf(false) }
|
||||
val deleteForEveryoneEnabled = remember { mutableStateOf(false) }
|
||||
val canModerate = remember { mutableStateOf(false) }
|
||||
val moderateEnabled = remember { mutableStateOf(false) }
|
||||
val allButtonsDisabled = remember { mutableStateOf(false) }
|
||||
val forwardEnabled = remember { mutableStateOf(false) }
|
||||
val deleteCountProhibited = remember { mutableStateOf(false) }
|
||||
val forwardCountProhibited = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
|
||||
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {})
|
||||
Row(Modifier.matchParentSize().background(MaterialTheme.colors.background), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !allButtonsDisabled.value) {
|
||||
Row(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colors.background)
|
||||
.pointerInput(Unit) {
|
||||
detectGesture {
|
||||
true
|
||||
}
|
||||
},
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !deleteCountProhibited.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_delete),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!deleteEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
tint = if (!deleteEnabled.value || deleteCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
|
||||
IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !allButtonsDisabled.value) {
|
||||
IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_flag),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!moderateEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
tint = if (!moderateEnabled.value || deleteCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
|
||||
IconButton({ /*shareItems()*/ }, Modifier.alpha(0f), enabled = false/*!allButtonsDisabled.value*/) {
|
||||
IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_share),
|
||||
painterResource(MR.images.ic_forward),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
tint = if (!forwardEnabled.value || forwardCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, allButtonsDisabled)
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,16 +116,20 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
deleteForEveryoneEnabled: MutableState<Boolean>,
|
||||
canModerate: MutableState<Boolean>,
|
||||
moderateEnabled: MutableState<Boolean>,
|
||||
allButtonsDisabled: MutableState<Boolean>
|
||||
forwardEnabled: MutableState<Boolean>,
|
||||
deleteCountProhibited: MutableState<Boolean>,
|
||||
forwardCountProhibited: MutableState<Boolean>
|
||||
) {
|
||||
val count = selectedChatItems.value?.size ?: 0
|
||||
allButtonsDisabled.value = count == 0 || count > 20
|
||||
deleteCountProhibited.value = count == 0 || count > 200
|
||||
forwardCountProhibited.value = count == 0 || count > 20
|
||||
canModerate.value = possibleToModerate(chatInfo)
|
||||
val selected = selectedChatItems.value ?: return
|
||||
var rDeleteEnabled = true
|
||||
var rDeleteForEveryoneEnabled = true
|
||||
var rModerateEnabled = true
|
||||
var rOnlyOwnGroupItems = true
|
||||
var rForwardEnabled = true
|
||||
val rSelectedChatItems = mutableSetOf<Long>()
|
||||
for (ci in chatItems) {
|
||||
if (selected.contains(ci.id)) {
|
||||
@@ -119,6 +137,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
rDeleteForEveryoneEnabled = rDeleteForEveryoneEnabled && ci.meta.deletable && !ci.localNote
|
||||
rOnlyOwnGroupItems = rOnlyOwnGroupItems && ci.chatDir is CIDirection.GroupSnd
|
||||
rModerateEnabled = rModerateEnabled && ci.content.msgContent != null && ci.memberToModerate(chatInfo) != null
|
||||
rForwardEnabled = rForwardEnabled && ci.content.msgContent != null && ci.meta.itemDeleted == null && !ci.isLiveDummy
|
||||
rSelectedChatItems.add(ci.id) // we are collecting new selected items here to account for any changes in chat items list
|
||||
}
|
||||
}
|
||||
@@ -126,6 +145,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
deleteEnabled.value = rDeleteEnabled
|
||||
deleteForEveryoneEnabled.value = rDeleteForEveryoneEnabled
|
||||
moderateEnabled.value = rModerateEnabled
|
||||
forwardEnabled.value = rForwardEnabled
|
||||
selectedChatItems.value = rSelectedChatItems
|
||||
}
|
||||
|
||||
|
||||
@@ -665,9 +665,8 @@ private fun updateMemberRoleDialog(
|
||||
|
||||
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
|
||||
try {
|
||||
val uri = URI(connReqUri)
|
||||
withBGApi {
|
||||
planAndConnect(rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
|
||||
planAndConnect(rhId, connReqUri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
||||
@@ -20,6 +20,7 @@ fun CICallItemView(
|
||||
cItem: ChatItem,
|
||||
status: CICallStatus,
|
||||
duration: Int,
|
||||
showTimestamp: Boolean,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
timedMessagesTTL: Int?
|
||||
) {
|
||||
@@ -47,7 +48,7 @@ fun CICallItemView(
|
||||
CICallStatus.Error -> {}
|
||||
}
|
||||
|
||||
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ fun CIGroupInvitationView(
|
||||
ci: ChatItem,
|
||||
groupInvitation: CIGroupInvitation,
|
||||
memberRole: GroupMemberRole,
|
||||
showTimestamp: Boolean,
|
||||
chatIncognito: Boolean = false,
|
||||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
timedMessagesTTL: Int?
|
||||
@@ -118,7 +119,7 @@ fun CIGroupInvitationView(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join))
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) }
|
||||
},
|
||||
color = if (inProgress.value)
|
||||
MaterialTheme.colors.secondary
|
||||
@@ -129,7 +130,7 @@ fun CIGroupInvitationView(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(groupInvitationStr())
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -145,7 +146,7 @@ fun CIGroupInvitationView(
|
||||
}
|
||||
}
|
||||
|
||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +163,8 @@ fun PendingCIGroupInvitationViewPreview() {
|
||||
groupInvitation = CIGroupInvitation.getSample(),
|
||||
memberRole = GroupMemberRole.Admin,
|
||||
joinGroup = { _, _ -> },
|
||||
timedMessagesTTL = null
|
||||
timedMessagesTTL = null,
|
||||
showTimestamp = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -179,8 +181,9 @@ fun CIGroupInvitationViewAcceptedPreview() {
|
||||
groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted),
|
||||
memberRole = GroupMemberRole.Admin,
|
||||
joinGroup = { _, _ -> },
|
||||
timedMessagesTTL = null
|
||||
)
|
||||
timedMessagesTTL = null,
|
||||
showTimestamp = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +199,8 @@ fun CIGroupInvitationViewLongNamePreview() {
|
||||
),
|
||||
memberRole = GroupMemberRole.Admin,
|
||||
joinGroup = { _, _ -> },
|
||||
timedMessagesTTL = null
|
||||
)
|
||||
timedMessagesTTL = null,
|
||||
showTimestamp = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ fun CIMetaView(
|
||||
},
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean
|
||||
showTimestamp: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
) {
|
||||
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (chatItem.isDeletedContent) {
|
||||
@@ -54,7 +55,8 @@ fun CIMetaView(
|
||||
paleMetaColor,
|
||||
showStatus = showStatus,
|
||||
showEdited = showEdited,
|
||||
showViaProxy = showViaProxy
|
||||
showViaProxy = showViaProxy,
|
||||
showTimestamp = showTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -70,11 +72,11 @@ private fun CIMetaText(
|
||||
paleColor: Color,
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean
|
||||
showTimestamp: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
) {
|
||||
if (showEdited && meta.itemEdited) {
|
||||
StatusIconText(painterResource(MR.images.ic_edit), color)
|
||||
Spacer(Modifier.width(3.dp))
|
||||
}
|
||||
if (meta.disappearing) {
|
||||
StatusIconText(painterResource(MR.images.ic_timer), color)
|
||||
@@ -82,12 +84,13 @@ private fun CIMetaText(
|
||||
if (ttl != chatTTL) {
|
||||
Text(shortTimeText(ttl), color = color, fontSize = 12.sp)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
if (showStatus) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
|
||||
if (statusIcon != null) {
|
||||
val (icon, statusColor) = statusIcon
|
||||
@@ -96,17 +99,19 @@ private fun CIMetaText(
|
||||
} else {
|
||||
StatusIconText(painterResource(icon), statusColor)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
} else if (!meta.disappearing) {
|
||||
StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
}
|
||||
if (encrypted != null) {
|
||||
StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color)
|
||||
}
|
||||
|
||||
if (showTimestamp) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
|
||||
// the conditions in this function should match CIMetaText
|
||||
@@ -117,28 +122,56 @@ fun reserveSpaceForMeta(
|
||||
secondaryColor: Color,
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean = false
|
||||
showViaProxy: Boolean = false,
|
||||
showTimestamp: Boolean
|
||||
): String {
|
||||
val iconSpace = " "
|
||||
var res = ""
|
||||
if (showEdited && meta.itemEdited) res += iconSpace
|
||||
val iconSpace = " \u00A0\u00A0\u00A0"
|
||||
val whiteSpace = "\u00A0"
|
||||
var res = if (showTimestamp) "" else iconSpace
|
||||
var space: String? = null
|
||||
|
||||
fun appendSpace() {
|
||||
if (space != null) {
|
||||
res += space
|
||||
space = null
|
||||
}
|
||||
}
|
||||
|
||||
if (showEdited && meta.itemEdited) {
|
||||
res += iconSpace
|
||||
}
|
||||
if (meta.itemTimed != null) {
|
||||
res += iconSpace
|
||||
val ttl = meta.itemTimed.ttl
|
||||
if (ttl != chatTTL) {
|
||||
res += shortTimeText(ttl)
|
||||
}
|
||||
space = whiteSpace
|
||||
}
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
appendSpace()
|
||||
res += iconSpace
|
||||
}
|
||||
if (showStatus && (meta.statusIcon(secondaryColor) != null || !meta.disappearing)) {
|
||||
res += iconSpace
|
||||
if (showStatus) {
|
||||
appendSpace()
|
||||
if (meta.statusIcon(secondaryColor) != null) {
|
||||
res += iconSpace
|
||||
} else if (!meta.disappearing) {
|
||||
res += iconSpace
|
||||
}
|
||||
space = whiteSpace
|
||||
}
|
||||
|
||||
if (encrypted != null) {
|
||||
appendSpace()
|
||||
res += iconSpace
|
||||
space = whiteSpace
|
||||
}
|
||||
return res + meta.timestampText
|
||||
if (showTimestamp) {
|
||||
appendSpace()
|
||||
res += meta.timestampText
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -154,7 +187,8 @@ fun PreviewCIMetaView() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,7 +201,8 @@ fun PreviewCIMetaViewUnread() {
|
||||
status = CIStatus.RcvNew()
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -180,7 +215,8 @@ fun PreviewCIMetaViewSendFailed() {
|
||||
status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX"))
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -192,7 +228,8 @@ fun PreviewCIMetaViewSendNoAuth() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -204,7 +241,8 @@ fun PreviewCIMetaViewSendSent() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -217,7 +255,8 @@ fun PreviewCIMetaViewEdited() {
|
||||
itemEdited = true
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -231,7 +270,8 @@ fun PreviewCIMetaViewEditedUnread() {
|
||||
status= CIStatus.RcvNew()
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -245,7 +285,8 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
status= CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -255,6 +296,7 @@ fun PreviewCIMetaViewDeletedContent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getDeletedContentSampleData(),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -169,14 +169,14 @@ fun DecryptionErrorItemFixButton(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.fix_connection))
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) }
|
||||
withStyle(reserveTimestampStyle) { append(" ") } // for icon
|
||||
},
|
||||
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,11 +201,11 @@ fun DecryptionErrorItem(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ fun CIVoiceView(
|
||||
ci: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
smallView: Boolean = false,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -86,7 +87,7 @@ fun CIVoiceView(
|
||||
durationText(time / 1000)
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, sizeMultiplier, play, pause, longClick, receiveFile) {
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, showTimestamp, sizeMultiplier, play, pause, longClick, receiveFile) {
|
||||
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath)
|
||||
}
|
||||
if (smallView) {
|
||||
@@ -120,6 +121,7 @@ private fun VoiceLayout(
|
||||
hasText: Boolean,
|
||||
timedMessagesTTL: Int?,
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
sizeMultiplier: Float,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
@@ -200,7 +202,7 @@ private fun VoiceLayout(
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile)
|
||||
}
|
||||
Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier)) {
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +217,7 @@ private fun VoiceLayout(
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier)) {
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ package chat.simplex.common.views.chat.item
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.geometry.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
@@ -26,9 +27,16 @@ import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.*
|
||||
|
||||
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
|
||||
|
||||
private val msgRectMaxRadius = 18.dp
|
||||
private val msgBubbleMaxRadius = msgRectMaxRadius * 1.2f
|
||||
val msgTailWidthDp = 9.dp
|
||||
private val msgTailMinHeightDp = msgTailWidthDp * 1.254f // ~56deg
|
||||
private val msgTailMaxHeightDp = msgTailWidthDp * 1.732f // 60deg
|
||||
|
||||
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary)
|
||||
|
||||
fun chatEventText(ci: ChatItem): AnnotatedString =
|
||||
@@ -51,6 +59,7 @@ fun ChatItemView(
|
||||
revealed: MutableState<Boolean>,
|
||||
range: IntRange?,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
fillMaxWidth: Boolean = true,
|
||||
selectChatItem: () -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
@@ -72,6 +81,8 @@ fun ChatItemView(
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
itemSeparation: ItemSeparation,
|
||||
preview: Boolean = false,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -83,7 +94,7 @@ fun ChatItemView(
|
||||
val live = composeState.value.liveMessage != null
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier,
|
||||
contentAlignment = alignment,
|
||||
) {
|
||||
val info = cItem.meta.itemStatus.statusInto
|
||||
@@ -98,7 +109,7 @@ fun ChatItemView(
|
||||
|
||||
@Composable
|
||||
fun ChatItemReactions() {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) {
|
||||
cItem.reactions.forEach { r ->
|
||||
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
|
||||
if (cInfo.featureEnabled(ChatFeature.Reactions) && (cItem.allowAddReaction || r.userReacted)) {
|
||||
@@ -125,13 +136,13 @@ fun ChatItemView(
|
||||
Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) {
|
||||
Column(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.clipChatItem(cItem, itemSeparation.largeGap, revealed.value)
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick)
|
||||
.onRightClick { showMenu.value = true },
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
@@ -354,14 +365,14 @@ fun ChatItemView(
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else {
|
||||
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
@@ -373,7 +384,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun LegacyDeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
@@ -385,7 +396,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun CallItem(status: CICallStatus, duration: Int) {
|
||||
CICallItemView(cInfo, cItem, status, duration, acceptCall, cInfo.timedMessagesTTL)
|
||||
CICallItemView(cInfo, cItem, status, duration, showTimestamp = showTimestamp, acceptCall, cInfo.timedMessagesTTL)
|
||||
DeleteItemMenu()
|
||||
}
|
||||
|
||||
@@ -430,7 +441,7 @@ fun ChatItemView(
|
||||
|
||||
@Composable
|
||||
fun DeletedItem() {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
|
||||
@@ -473,7 +484,7 @@ fun ChatItemView(
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> if (developerTools) {
|
||||
IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL)
|
||||
IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL)
|
||||
DeleteItemMenu()
|
||||
} else {
|
||||
Box(Modifier.size(0.dp)) {}
|
||||
@@ -483,11 +494,11 @@ fun ChatItemView(
|
||||
DeleteItemMenu()
|
||||
}
|
||||
is CIContent.RcvGroupInvitation -> {
|
||||
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, timedMessagesTTL = cInfo.timedMessagesTTL)
|
||||
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
|
||||
DeleteItemMenu()
|
||||
}
|
||||
is CIContent.SndGroupInvitation -> {
|
||||
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, timedMessagesTTL = cInfo.timedMessagesTTL)
|
||||
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
|
||||
DeleteItemMenu()
|
||||
}
|
||||
is CIContent.RcvDirectEventContent -> {
|
||||
@@ -793,6 +804,154 @@ fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Un
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.chatItemOffset(cItem: ChatItem, tailVisible: Boolean, inverted: Boolean = false, revealed: Boolean): Modifier {
|
||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||
val style = shapeStyle(cItem, chatItemTail.value, tailVisible, revealed)
|
||||
|
||||
val offset = if (style is ShapeStyle.Bubble) {
|
||||
if (style.tailVisible) {
|
||||
if (cItem.chatDir.sent) msgTailWidthDp else -msgTailWidthDp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
} else 0.dp
|
||||
|
||||
return this.offset(x = if (inverted) (-1f * offset) else offset)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = false, revealed: Boolean = false): Modifier {
|
||||
val chatItemRoundness = remember { appPreferences.chatItemRoundness.state }
|
||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||
val style = shapeStyle(chatItem, chatItemTail.value, tailVisible, revealed)
|
||||
val cornerRoundness = chatItemRoundness.value.coerceIn(0f, 1f)
|
||||
|
||||
val shape = when (style) {
|
||||
is ShapeStyle.Bubble -> chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true)
|
||||
is ShapeStyle.RoundRect -> RoundedCornerShape(style.radius * cornerRoundness)
|
||||
}
|
||||
|
||||
return this.clip(shape)
|
||||
}
|
||||
|
||||
private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ ->
|
||||
val (msgTailWidth, msgBubbleMaxRadius) = with(density) { Pair(msgTailWidthDp.toPx(), msgBubbleMaxRadius.toPx()) }
|
||||
val width = if (sent && tailVisible) size.width - msgTailWidth else size.width
|
||||
val height = size.height
|
||||
val rxMax = min(msgBubbleMaxRadius, width / 2)
|
||||
val ryMax = min(msgBubbleMaxRadius, height / 2)
|
||||
val rx = roundness * rxMax
|
||||
val ry = roundness * ryMax
|
||||
val tailHeight = with(density) {
|
||||
min(
|
||||
msgTailMinHeightDp.toPx() + roundness * (msgTailMaxHeightDp.toPx() - msgTailMinHeightDp.toPx()),
|
||||
height / 2
|
||||
)
|
||||
}
|
||||
moveTo(rx, 0f)
|
||||
lineTo(width - rx, 0f) // Top Line
|
||||
if (roundness > 0) {
|
||||
quadraticBezierTo(width, 0f, width, ry) // Top-right corner
|
||||
}
|
||||
if (height > 2 * ry) {
|
||||
lineTo(width, height - ry) // Right side
|
||||
}
|
||||
if (roundness > 0) {
|
||||
quadraticBezierTo(width, height, width - rx, height) // Bottom-right corner
|
||||
}
|
||||
if (tailVisible) {
|
||||
lineTo(0f, height) // Bottom line
|
||||
if (roundness > 0) {
|
||||
val d = tailHeight - msgTailWidth * msgTailWidth / tailHeight
|
||||
val controlPoint = Offset(msgTailWidth, height - tailHeight + d * sqrt(roundness))
|
||||
quadraticBezierTo(controlPoint.x, controlPoint.y, msgTailWidth, height - tailHeight)
|
||||
} else {
|
||||
lineTo(msgTailWidth, height - tailHeight)
|
||||
}
|
||||
|
||||
if (height > ry + tailHeight) {
|
||||
lineTo(msgTailWidth, ry)
|
||||
}
|
||||
} else {
|
||||
lineTo(rx, height) // Bottom line
|
||||
if (roundness > 0) {
|
||||
quadraticBezierTo(0f, height, 0f, height - ry) // Bottom-left corner
|
||||
}
|
||||
if (height > 2 * ry) {
|
||||
lineTo(0f, ry) // Left side
|
||||
}
|
||||
}
|
||||
if (roundness > 0) {
|
||||
val bubbleInitialX = if (tailVisible) msgTailWidth else 0f
|
||||
quadraticBezierTo(bubbleInitialX, 0f, bubbleInitialX + rx, 0f) // Top-left corner
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
val matrix = Matrix()
|
||||
matrix.scale(-1f, 1f)
|
||||
this.transform(matrix)
|
||||
this.translate(Offset(size.width, 0f))
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ShapeStyle {
|
||||
data class Bubble(val tailVisible: Boolean, val startPadding: Boolean) : ShapeStyle()
|
||||
data class RoundRect(val radius: Dp) : ShapeStyle()
|
||||
}
|
||||
|
||||
fun shapeStyle(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Boolean, revealed: Boolean): ShapeStyle {
|
||||
if (chatItem == null) {
|
||||
return ShapeStyle.RoundRect(msgRectMaxRadius)
|
||||
}
|
||||
|
||||
when (chatItem.content) {
|
||||
is CIContent.SndMsgContent,
|
||||
is CIContent.RcvMsgContent,
|
||||
is CIContent.RcvDecryptionError,
|
||||
is CIContent.SndDeleted,
|
||||
is CIContent.RcvDeleted,
|
||||
is CIContent.RcvIntegrityError,
|
||||
is CIContent.SndModerated,
|
||||
is CIContent.RcvModerated,
|
||||
is CIContent.RcvBlocked,
|
||||
is CIContent.InvalidJSON -> {
|
||||
if (chatItem.meta.itemDeleted != null && (!revealed || chatItem.isDeletedContent)) {
|
||||
return ShapeStyle.RoundRect(msgRectMaxRadius)
|
||||
}
|
||||
|
||||
val tail = when (val content = chatItem.content.msgContent) {
|
||||
is MsgContent.MCImage,
|
||||
is MsgContent.MCVideo,
|
||||
is MsgContent.MCVoice -> {
|
||||
if (content.text.isEmpty()) {
|
||||
false
|
||||
} else {
|
||||
tailVisible
|
||||
}
|
||||
}
|
||||
is MsgContent.MCText -> {
|
||||
if (isShortEmoji(content.text)) {
|
||||
false
|
||||
} else {
|
||||
tailVisible
|
||||
}
|
||||
}
|
||||
else -> tailVisible
|
||||
}
|
||||
return if (tailEnabled) {
|
||||
ShapeStyle.Bubble(tail, !chatItem.chatDir.sent)
|
||||
} else {
|
||||
ShapeStyle.RoundRect(msgRectMaxRadius)
|
||||
}
|
||||
}
|
||||
|
||||
is CIContent.RcvGroupInvitation,
|
||||
is CIContent.SndGroupInvitation -> return ShapeStyle.RoundRect(msgRectMaxRadius)
|
||||
else -> return ShapeStyle.RoundRect(8.dp)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(cancelAction.alert.titleId),
|
||||
@@ -927,7 +1086,9 @@ fun PreviewChatItemView(
|
||||
showItemDetails = { _, _ -> },
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showTimestamp = true,
|
||||
preview = true,
|
||||
itemSeparation = ItemSeparation(timestamp = true, largeGap = true, null)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -967,6 +1128,8 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
preview = true,
|
||||
showTimestamp = true,
|
||||
itemSeparation = ItemSeparation(timestamp = true, largeGap = true, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
val sent = ci.chatDir.sent
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
@@ -36,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean)
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@ fun PreviewDeletedItemView() {
|
||||
DeletedItemView(
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,19 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.MREmojiChar
|
||||
import chat.simplex.common.ui.theme.EmojiFont
|
||||
import java.sql.Timestamp
|
||||
|
||||
val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFont)
|
||||
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont)
|
||||
|
||||
@Composable
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
Column(
|
||||
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmojiText(chatItem.content.text)
|
||||
CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
@@ -35,6 +33,8 @@ fun FramedItemView(
|
||||
linkMode: SimplexLinkMode,
|
||||
showViaProxy: Boolean,
|
||||
showMenu: MutableState<Boolean>,
|
||||
showTimestamp: Boolean,
|
||||
tailVisible: Boolean = false,
|
||||
receiveFile: (Long) -> Unit,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
scrollToItem: (Long) -> Unit = {},
|
||||
@@ -47,7 +47,7 @@ fun FramedItemView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) {
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean) {
|
||||
MarkdownText(
|
||||
qi.text,
|
||||
qi.formattedText,
|
||||
@@ -56,7 +56,8 @@ fun FramedItemView(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface),
|
||||
linkMode = linkMode,
|
||||
uriHandler = if (appPlatform.isDesktop) uriHandler else null
|
||||
uriHandler = if (appPlatform.isDesktop) uriHandler else null,
|
||||
showTimestamp = showTimestamp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,10 +77,10 @@ fun FramedItemView(
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary),
|
||||
maxLines = 1
|
||||
)
|
||||
ciQuotedMsgTextView(qi, lines = 2)
|
||||
ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp)
|
||||
}
|
||||
} else {
|
||||
ciQuotedMsgTextView(qi, lines = 3)
|
||||
ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +179,7 @@ fun FramedItemView(
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +189,7 @@ fun FramedItemView(
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Box(Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.clipChatItem(ci, tailVisible, revealed = true)
|
||||
.background(
|
||||
when {
|
||||
transparentBackground -> Color.Transparent
|
||||
@@ -198,7 +199,14 @@ fun FramedItemView(
|
||||
)) {
|
||||
var metaColor = MaterialTheme.colors.secondary
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
Column(Modifier.width(IntrinsicSize.Max)) {
|
||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||
val style = shapeStyle(ci, chatItemTail.value, tailVisible, revealed = true)
|
||||
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
|
||||
Column(
|
||||
Modifier
|
||||
.width(IntrinsicSize.Max)
|
||||
.padding(start = if (tailRendered) msgTailWidthDp else 0.dp, end = if (sent && tailRendered) msgTailWidthDp else 0.dp)
|
||||
) {
|
||||
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
|
||||
if (ci.meta.itemDeleted != null) {
|
||||
when (ci.meta.itemDeleted) {
|
||||
@@ -242,7 +250,7 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
@@ -250,35 +258,41 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
|
||||
if (mc.text != "") {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> ciFileView(ci, mc.text)
|
||||
is MsgContent.MCUnknown ->
|
||||
if (ci.file == null) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
} else {
|
||||
ciFileView(ci, mc.text)
|
||||
}
|
||||
is MsgContent.MCLink -> {
|
||||
ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true })
|
||||
Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
|
||||
CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy)
|
||||
Box(
|
||||
Modifier
|
||||
.padding(
|
||||
bottom = 6.dp,
|
||||
end = 12.dp + if (tailRendered && sent) msgTailWidthDp else 0.dp,
|
||||
)
|
||||
) {
|
||||
CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,19 +305,21 @@ fun CIMarkdownText(
|
||||
linkMode: SimplexLinkMode,
|
||||
uriHandler: UriHandler?,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
showViaProxy: Boolean
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
Box(Modifier.padding(vertical = 7.dp, horizontal = 12.dp)) {
|
||||
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
|
||||
MarkdownText(
|
||||
text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true,
|
||||
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||
const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble"
|
||||
/**
|
||||
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
|
||||
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
|
||||
@@ -311,23 +327,23 @@ const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||
* */
|
||||
const val MAX_SAFE_WIDTH = 0x3FFFF - 1
|
||||
|
||||
/**
|
||||
* Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints]
|
||||
* */
|
||||
private fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31
|
||||
width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height
|
||||
width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height
|
||||
width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height
|
||||
width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height
|
||||
else -> 0x1FFF // shouldn't happen since width is limited already
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PriorityLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
priorityLayoutId: String,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
/**
|
||||
* Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints]
|
||||
* */
|
||||
fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31
|
||||
width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height
|
||||
width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height
|
||||
width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height
|
||||
width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height
|
||||
else -> 0x1FFF // shouldn't happen since width is limited already
|
||||
}
|
||||
|
||||
Layout(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
@@ -352,6 +368,36 @@ fun PriorityLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DependentLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
mainLayoutId: String,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Layout(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
) { measureable, constraints ->
|
||||
// Find important element which should tell what min width it needs to draw itself.
|
||||
// Expecting only one such element. Can be less than one but not more
|
||||
val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }?.measure(constraints)
|
||||
val placeables: List<Placeable> = measureable.map {
|
||||
if (it.layoutId == mainLayoutId)
|
||||
mainPlaceable!!
|
||||
else
|
||||
it.measure(constraints.copy(minWidth = mainPlaceable?.width ?: 0, maxWidth = min(MAX_SAFE_WIDTH, constraints.maxWidth))) }
|
||||
val width = mainPlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width })
|
||||
val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height })
|
||||
layout(width, height) {
|
||||
var y = 0
|
||||
placeables.forEach {
|
||||
it.place(0, y)
|
||||
y += it.measuredHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
class EditedProvider: PreviewParameterProvider<Boolean> {
|
||||
|
||||
@@ -23,8 +23,8 @@ import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
CIMsgError(ci, timedMessagesTTL) {
|
||||
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?) {
|
||||
CIMsgError(ci, showTimestamp, timedMessagesTTL) {
|
||||
when (msgError) {
|
||||
is MsgErrorType.MsgSkipped ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -49,7 +49,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
fun CIMsgError(ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
@@ -68,7 +68,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = false)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = false, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,8 @@ fun IntegrityErrorItemViewPreview() {
|
||||
IntegrityErrorItemView(
|
||||
MsgErrorType.MsgBadHash(),
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
showTimestamp = true,
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean) {
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
@@ -35,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl
|
||||
Box(Modifier.weight(1f, false)) {
|
||||
MergedMarkedDeletedText(ci, revealed)
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,8 @@ fun PreviewMarkedDeletedItemView() {
|
||||
DeletedItemView(
|
||||
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ fun MarkdownText (
|
||||
linkMode: SimplexLinkMode,
|
||||
inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = null,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
showViaProxy: Boolean = false
|
||||
showViaProxy: Boolean = false,
|
||||
showTimestamp: Boolean = true
|
||||
) {
|
||||
val textLayoutDirection = remember (text) {
|
||||
if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
@@ -78,7 +79,7 @@ fun MarkdownText (
|
||||
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
|
||||
"\n"
|
||||
} else if (meta != null) {
|
||||
reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy)
|
||||
reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ 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.unit.*
|
||||
import chat.simplex.common.SettingsViewState
|
||||
import chat.simplex.common.AppLock
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||
@@ -135,7 +135,7 @@ fun ToggleChatListCard() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
@@ -153,18 +153,15 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
VideoPlayerHolder.stopAll()
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!oneHandUI.value) {
|
||||
Column(Modifier.padding(end = endPadding)) {
|
||||
Column {
|
||||
ChatListToolbar(
|
||||
scaffoldState.drawerState,
|
||||
userPickerState,
|
||||
stopped,
|
||||
setPerformLA,
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
@@ -172,33 +169,17 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
},
|
||||
bottomBar = {
|
||||
if (oneHandUI.value) {
|
||||
Column(Modifier.padding(end = endPadding)) {
|
||||
Column {
|
||||
Divider()
|
||||
ChatListToolbar(
|
||||
scaffoldState.drawerState,
|
||||
userPickerState,
|
||||
stopped,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
ModalView(showClose = appPlatform.isDesktop, close = { scope.launch { scaffoldState.drawerState.close() } }) {
|
||||
SettingsView(chatModel, setPerformLA, scaffoldState.drawerState)
|
||||
}
|
||||
setPerformLA,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||
floatingActionButton = {
|
||||
if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
FloatingActionButton(
|
||||
@@ -208,7 +189,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
},
|
||||
Modifier
|
||||
.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp)
|
||||
.padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp)
|
||||
.size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
@@ -224,7 +205,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(Modifier.padding(it).padding(end = endPadding)) {
|
||||
Box(Modifier.padding(it)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -252,11 +233,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
UserPicker(
|
||||
chatModel = chatModel,
|
||||
userPickerState = userPickerState,
|
||||
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart
|
||||
) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
setPerformLA = AppLock::setPerformLA
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,7 +256,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||
private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, setPerformLA: (Boolean) -> Unit) {
|
||||
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val updatingProgress = remember { chatModel.updatingProgress }.value
|
||||
@@ -344,23 +322,22 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt
|
||||
}
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboard = LocalClipboardManager.current
|
||||
DefaultTopAppBar(
|
||||
navigationButton = {
|
||||
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||
NavigationButtonMenu {
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
SettingsView(chatModel, setPerformLA, close)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||
val allRead = users
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
if (users.size == 1 && chatModel.remoteHosts.isEmpty()) {
|
||||
scope.launch { drawerState.open() }
|
||||
} else {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -501,7 +478,7 @@ private fun ToggleFilterEnabledButton() {
|
||||
@Composable
|
||||
expect fun ActiveCallInteractiveArea(call: Call)
|
||||
|
||||
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
if (chatModel.currentUser.value == null) {
|
||||
chatModel.appOpenUrl.value = rhId to uri
|
||||
@@ -589,7 +566,7 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<
|
||||
withBGApi {
|
||||
planAndConnect(
|
||||
chatModel.remoteHostId(),
|
||||
URI.create(link),
|
||||
link,
|
||||
incognito = null,
|
||||
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
|
||||
@@ -256,7 +256,7 @@ fun ChatPreviewView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> SmallContentPreviewVoice() {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, smallView = true, longClick = {}) {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, smallView = true, longClick = {}) {
|
||||
val user = chatModel.currentUser.value ?: return@CIVoiceView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
@@ -332,7 +332,7 @@ fun ChatPreviewView(
|
||||
chatPreviewTitle()
|
||||
}
|
||||
Spacer(Modifier.width(8.sp.toDp()))
|
||||
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs)
|
||||
val ts = getTimestampText(chat.chatItems.lastOrNull()?.meta?.itemTs ?: chat.chatInfo.chatTs)
|
||||
ChatListTimestampView(ts)
|
||||
}
|
||||
Row(Modifier.heightIn(min = 46.sp.toDp()).fillMaxWidth()) {
|
||||
|
||||
@@ -11,31 +11,24 @@ import androidx.compose.ui.graphics.Color
|
||||
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.unit.dp
|
||||
import chat.simplex.common.SettingsViewState
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.newchat.ActiveProfilePicker
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
|
||||
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val (userPickerState, scaffoldState) = settingsState
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
|
||||
Scaffold(
|
||||
Modifier.padding(end = endPadding),
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = {
|
||||
if (!oneHandUI.value) {
|
||||
Column {
|
||||
ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() }
|
||||
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
@@ -44,7 +37,7 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
if (oneHandUI.value) {
|
||||
Column {
|
||||
Divider()
|
||||
ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() }
|
||||
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,11 +58,13 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
hasSimplexLink = hasSimplexLink(sharedContent.text)
|
||||
}
|
||||
is SharedContent.Forward -> {
|
||||
val mc = sharedContent.chatItem.content.msgContent
|
||||
if (mc != null) {
|
||||
isMediaOrFileAttachment = mc.isMediaOrFileAttachment
|
||||
isVoice = mc.isVoice
|
||||
hasSimplexLink = hasSimplexLink(mc.text)
|
||||
sharedContent.chatItems.forEach { ci ->
|
||||
val mc = ci.content.msgContent
|
||||
if (mc != null) {
|
||||
isMediaOrFileAttachment = isMediaOrFileAttachment || mc.isMediaOrFileAttachment
|
||||
isVoice = isVoice || mc.isVoice
|
||||
hasSimplexLink = hasSimplexLink || hasSimplexLink(mc.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
@@ -92,21 +87,6 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
}
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(
|
||||
chatModel,
|
||||
userPickerState,
|
||||
showSettings = false,
|
||||
showCancel = true,
|
||||
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart,
|
||||
cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasSimplexLink(msg: String): Boolean {
|
||||
@@ -122,7 +102,7 @@ private fun EmptyList() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
|
||||
if (showSearch) {
|
||||
@@ -138,7 +118,24 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
ModalView(
|
||||
{ close() },
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||
},
|
||||
content = {
|
||||
ActiveProfilePicker(
|
||||
search = search,
|
||||
rhId = chatModel.remoteHostId,
|
||||
close = close,
|
||||
contactConnection = null,
|
||||
showIncognito = false
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> NavigationButtonBack(onButtonClicked = {
|
||||
@@ -180,11 +177,11 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
when (chatModel.sharedContent.value) {
|
||||
when (val v = chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> stringResource(MR.strings.share_message)
|
||||
is SharedContent.Media -> stringResource(MR.strings.share_image)
|
||||
is SharedContent.File -> stringResource(MR.strings.share_file)
|
||||
is SharedContent.Forward -> stringResource(MR.strings.forward_message)
|
||||
is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message)
|
||||
null -> stringResource(MR.strings.share_message)
|
||||
},
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.animation.core.*
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
@@ -12,42 +12,40 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.CreateProfile
|
||||
import chat.simplex.common.views.localauth.VerticalDivider
|
||||
import chat.simplex.common.views.remote.*
|
||||
import chat.simplex.common.views.usersettings.doWithAuth
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.views.usersettings.AppearanceScope.ColorModeSwitcher
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val USER_PICKER_SECTION_SPACING = 32.dp
|
||||
|
||||
@Composable
|
||||
fun UserPicker(
|
||||
chatModel: ChatModel,
|
||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
showSettings: Boolean = true,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
showCancel: Boolean = false,
|
||||
cancelClicked: () -> Unit = {},
|
||||
useFromDesktopClicked: () -> Unit = {},
|
||||
settingsClicked: () -> Unit = {},
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||
if (newChat.isVisible()) {
|
||||
BackHandler {
|
||||
@@ -58,7 +56,7 @@ fun UserPicker(
|
||||
derivedStateOf {
|
||||
chatModel.users
|
||||
.filter { u -> u.user.activeUser || !u.user.hidden }
|
||||
.sortedByDescending { it.user.activeUser }
|
||||
.sortedByDescending { it.user.activeOrder }
|
||||
}
|
||||
}
|
||||
val remoteHosts by remember {
|
||||
@@ -67,18 +65,32 @@ fun UserPicker(
|
||||
.sortedBy { it.hostDeviceName }
|
||||
}
|
||||
}
|
||||
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
|
||||
|
||||
val view = LocalMultiplatformView()
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
userPickerState.collect {
|
||||
newChat = it
|
||||
if (it.isVisible()) {
|
||||
hideKeyboard(view)
|
||||
}
|
||||
launch {
|
||||
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
|
||||
if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { ModalManager.start.modalCount.value }
|
||||
.filter { it > 0 }
|
||||
.collect {
|
||||
closePicker(userPickerState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { newChat.isVisible() }
|
||||
.distinctUntilChanged()
|
||||
@@ -124,110 +136,116 @@ fun UserPicker(
|
||||
}
|
||||
}
|
||||
}
|
||||
val UsersView: @Composable ColumnScope.() -> Unit = {
|
||||
users.forEach { u ->
|
||||
UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) {
|
||||
|
||||
PlatformUserPicker(
|
||||
modifier = Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth()
|
||||
.then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true) else Modifier)
|
||||
.background(if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface)
|
||||
.padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL),
|
||||
pickerState = userPickerState
|
||||
) {
|
||||
val showCustomModal: (@Composable() (ModalData.(ChatModel, () -> Unit) -> Unit)) -> () -> Unit = { modalView ->
|
||||
{
|
||||
ModalManager.start.showCustomModal { close -> modalView(chatModel, close) }
|
||||
}
|
||||
}
|
||||
val stopped = remember { chatModel.chatRunning }.value == false
|
||||
val onUserClicked: (user: User) -> Unit = { user ->
|
||||
if (!user.activeUser) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
if (!u.user.activeUser) {
|
||||
withBGApi {
|
||||
controller.showProgressIfNeeded {
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null)
|
||||
}
|
||||
withBGApi {
|
||||
controller.showProgressIfNeeded {
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
chatModel.controller.changeActiveUser(user.remoteHostId, user.userId, null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }()
|
||||
withBGApi {
|
||||
closePicker(userPickerState)
|
||||
}
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
|
||||
}
|
||||
}
|
||||
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
|
||||
val maxWidth = with(LocalDensity.current) { windowWidth() * density }
|
||||
Box(Modifier
|
||||
.fillMaxSize()
|
||||
.offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else xOffset, 0) }
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING })
|
||||
.padding(bottom = 10.dp, top = 10.dp)
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (if (appPrefs.oneHandUI.state.value) -1 else 1) * (animatedFloat.value - 1) * xOffset
|
||||
},
|
||||
contentAlignment = contentAlignment
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.widthIn(min = 260.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
.height(IntrinsicSize.Min)
|
||||
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
|
||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
.clip(RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
) {
|
||||
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
|
||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||
if (remoteHosts.isNotEmpty()) {
|
||||
if (currentRemoteHost == null && chatModel.localUserCreated.value == true) {
|
||||
LocalDevicePickerItem(true) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToLocalDevice()
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
} else if (currentRemoteHost != null) {
|
||||
val connecting = rememberSaveable { mutableStateOf(false) }
|
||||
RemoteHostPickerItem(currentRemoteHost,
|
||||
actionButtonClick = {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
stopRemoteHostAndReloadHosts(currentRemoteHost, true)
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToRemoteHost(currentRemoteHost, connecting)
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
}
|
||||
|
||||
UsersView()
|
||||
@Composable
|
||||
fun FirstSection() {
|
||||
if (remoteHosts.isNotEmpty()) {
|
||||
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
|
||||
val localDeviceActive = currentRemoteHost == null && chatModel.localUserCreated.value == true
|
||||
|
||||
if (remoteHosts.isNotEmpty() && currentRemoteHost != null && chatModel.localUserCreated.value == true) {
|
||||
LocalDevicePickerItem(false) {
|
||||
DevicePickerRow(
|
||||
localDeviceActive = localDeviceActive,
|
||||
remoteHosts = remoteHosts,
|
||||
onRemoteHostClick = { h, connecting ->
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToRemoteHost(h, connecting)
|
||||
},
|
||||
onLocalDeviceClick = {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToLocalDevice()
|
||||
},
|
||||
onRemoteHostActionButtonClick = { h ->
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
stopRemoteHostAndReloadHosts(h, true)
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
)
|
||||
}
|
||||
val currentUser = remember { chatModel.currentUser }.value
|
||||
if (appPlatform.isAndroid) {
|
||||
Column(modifier = Modifier.padding(top = USER_PICKER_SECTION_SPACING, bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL - 3.dp)) {
|
||||
UserPickerUsersSection(
|
||||
users = users,
|
||||
onUserClicked = onUserClicked,
|
||||
stopped = stopped
|
||||
)
|
||||
}
|
||||
remoteHosts.filter { !it.activeHost }.forEach { h ->
|
||||
val connecting = rememberSaveable { mutableStateOf(false) }
|
||||
RemoteHostPickerItem(h,
|
||||
actionButtonClick = {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
stopRemoteHostAndReloadHosts(h, false)
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToRemoteHost(h, connecting)
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
} else if (currentUser != null) {
|
||||
SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
|
||||
ProfilePreview(currentUser.profile, stopped = stopped)
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
UseFromDesktopPickerItem {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ConnectDesktopView(close)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SecondSection() {
|
||||
UserPickerOptionRow(
|
||||
painterResource(MR.images.ic_qr_code),
|
||||
if (chatModel.userAddress.value != null) generalGetString(MR.strings.your_simplex_contact_address) else generalGetString(MR.strings.create_simplex_address),
|
||||
showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped
|
||||
)
|
||||
UserPickerOptionRow(
|
||||
painterResource(MR.images.ic_toggle_on),
|
||||
stringResource(MR.strings.chat_preferences),
|
||||
click = if (stopped) null else ({
|
||||
showCustomModal { m, close ->
|
||||
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
|
||||
}()
|
||||
}),
|
||||
disabled = stopped
|
||||
)
|
||||
if (appPlatform.isDesktop) {
|
||||
Divider(Modifier.padding(DEFAULT_PADDING))
|
||||
|
||||
val inactiveUsers = users.filter { !it.user.activeUser }
|
||||
|
||||
if (inactiveUsers.isNotEmpty()) {
|
||||
Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) {
|
||||
UserPickerUsersSection(
|
||||
users = inactiveUsers,
|
||||
onUserClicked = onUserClicked,
|
||||
stopped = stopped
|
||||
)
|
||||
}
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
} else {
|
||||
if (remoteHosts.isEmpty()) {
|
||||
LinkAMobilePickerItem {
|
||||
ModalManager.start.showModal {
|
||||
ConnectMobileView()
|
||||
}
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
if (chatModel.desktopNoUserNoRemote) {
|
||||
CreateInitialProfile {
|
||||
}
|
||||
|
||||
if (chatModel.desktopNoUserNoRemote) {
|
||||
UserPickerOptionRow(
|
||||
painterResource(MR.images.ic_manage_accounts),
|
||||
generalGetString(MR.strings.create_chat_profile),
|
||||
{
|
||||
doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) {
|
||||
ModalManager.center.showModalCloseable { close ->
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -237,16 +255,123 @@ fun UserPicker(
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
}
|
||||
if (showSettings) {
|
||||
SettingsPickerItem(settingsClicked)
|
||||
}
|
||||
if (showCancel) {
|
||||
CancelPickerItem(cancelClicked)
|
||||
)
|
||||
} else {
|
||||
UserPickerOptionRow(
|
||||
painterResource(MR.images.ic_manage_accounts),
|
||||
stringResource(MR.strings.your_chat_profiles),
|
||||
{
|
||||
doWithAuth(
|
||||
generalGetString(MR.strings.auth_open_chat_profiles),
|
||||
generalGetString(MR.strings.auth_log_in_using_credential)
|
||||
) {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
||||
ModalView(
|
||||
{ close() },
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||
},
|
||||
content = { UserProfilesView(chatModel, search, profileHidden) })
|
||||
}
|
||||
}
|
||||
},
|
||||
disabled = stopped
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop || windowOrientation() == WindowOrientation.PORTRAIT) {
|
||||
Column {
|
||||
FirstSection()
|
||||
SecondSection()
|
||||
GlobalSettingsSection(
|
||||
userPickerState = userPickerState,
|
||||
setPerformLA = setPerformLA,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column {
|
||||
FirstSection()
|
||||
Row {
|
||||
Box(Modifier.weight(1f)) {
|
||||
Column {
|
||||
SecondSection()
|
||||
}
|
||||
}
|
||||
VerticalDivider()
|
||||
Box(Modifier.weight(1f)) {
|
||||
Column {
|
||||
GlobalSettingsSection(
|
||||
userPickerState = userPickerState,
|
||||
setPerformLA = setPerformLA,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun userPickerAlpha(): Float {
|
||||
return when (CurrentColors.value.base) {
|
||||
DefaultTheme.LIGHT -> 0.05f
|
||||
DefaultTheme.DARK -> 0.05f
|
||||
DefaultTheme.BLACK -> 0.075f
|
||||
DefaultTheme.SIMPLEX -> 0.035f
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GlobalSettingsSection(
|
||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
) {
|
||||
val stopped = remember { chatModel.chatRunning }.value == false
|
||||
|
||||
if (appPlatform.isAndroid) {
|
||||
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
|
||||
|
||||
UserPickerOptionRow(
|
||||
painterResource(MR.images.ic_desktop),
|
||||
text,
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ConnectDesktopView(close)
|
||||
}
|
||||
},
|
||||
disabled = stopped
|
||||
)
|
||||
} else {
|
||||
UserPickerOptionRow(
|
||||
icon = painterResource(MR.images.ic_smartphone_300),
|
||||
text = stringResource(if (remember { chat.simplex.common.platform.chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles),
|
||||
click = {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
ModalManager.start.showModal {
|
||||
ConnectMobileView()
|
||||
}
|
||||
},
|
||||
disabled = stopped
|
||||
)
|
||||
}
|
||||
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
SettingsView(chatModel, setPerformLA, close)
|
||||
}
|
||||
},
|
||||
padding = if (appPlatform.isDesktop) PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING + 2.dp) else PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||
Icon(painterResource(MR.images.ic_settings), text, tint = MaterialTheme.colors.secondary)
|
||||
TextIconSpaced()
|
||||
Text(text, color = Color.Unspecified)
|
||||
Spacer(Modifier.weight(1f))
|
||||
ColorModeSwitcher()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +379,7 @@ fun UserPicker(
|
||||
fun UserProfilePickerItem(
|
||||
u: User,
|
||||
unreadCount: Int = 0,
|
||||
enabled: Boolean = chatModel.chatRunning.value == true || chatModel.connectedToRemote,
|
||||
enabled: Boolean = remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote,
|
||||
onLongClick: () -> Unit = {},
|
||||
openSettings: () -> Unit = {},
|
||||
onClick: () -> Unit
|
||||
@@ -296,14 +421,14 @@ fun UserProfilePickerItem(
|
||||
}
|
||||
} else if (!u.showNtfs) {
|
||||
Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
|
||||
} else {
|
||||
} else {
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == true || chatModel.connectedToRemote) {
|
||||
fun UserProfileRow(u: User, enabled: Boolean = remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = windowWidth() * 0.7f)
|
||||
@@ -325,136 +450,143 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoteHostPickerItem(h: RemoteHostInfo, onLongClick: () -> Unit = {}, actionButtonClick: () -> Unit = {}, onClick: () -> Unit) {
|
||||
Row(
|
||||
fun UserPickerOptionRow(icon: Painter, text: String, click: (() -> Unit)? = null, disabled: Boolean = false) {
|
||||
SectionItemView(click, disabled = disabled, extraPadding = appPlatform.isDesktop) {
|
||||
Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.secondary)
|
||||
TextIconSpaced()
|
||||
Text(text = text, color = if (disabled) MaterialTheme.colors.secondary else Color.Unspecified)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun DevicePickerRow(
|
||||
localDeviceActive: Boolean,
|
||||
remoteHosts: List<RemoteHostInfo>,
|
||||
onLocalDeviceClick: () -> Unit,
|
||||
onRemoteHostClick: (rh: RemoteHostInfo, connecting: MutableState<Boolean>) -> Unit,
|
||||
onRemoteHostActionButtonClick: (rh: RemoteHostInfo) -> Unit,
|
||||
) {
|
||||
FlowRow(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = if (h.activeHost) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified)
|
||||
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
.onRightClick { onLongClick() }
|
||||
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_PADDING + DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
RemoteHostRow(h)
|
||||
if (h.sessionState is RemoteHostSessionState.Connected) {
|
||||
HostDisconnectButton(actionButtonClick)
|
||||
} else {
|
||||
Box(Modifier.size(20.dp))
|
||||
val activeHost = remoteHosts.firstOrNull { h -> h.activeHost }
|
||||
|
||||
if (activeHost != null) {
|
||||
val connecting = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
DevicePill(
|
||||
active = true,
|
||||
icon = painterResource(MR.images.ic_smartphone_300),
|
||||
text = activeHost.hostDeviceName,
|
||||
actionButtonVisible = activeHost.sessionState is RemoteHostSessionState.Connected,
|
||||
onActionButtonClick = { onRemoteHostActionButtonClick(activeHost) }
|
||||
) {
|
||||
onRemoteHostClick(activeHost, connecting)
|
||||
}
|
||||
}
|
||||
|
||||
DevicePill(
|
||||
active = localDeviceActive,
|
||||
icon = painterResource(MR.images.ic_desktop),
|
||||
text = stringResource(MR.strings.this_device),
|
||||
actionButtonVisible = false
|
||||
) {
|
||||
onLocalDeviceClick()
|
||||
}
|
||||
|
||||
remoteHosts.filter { h -> h.sessionState is RemoteHostSessionState.Connected && !h.activeHost }.forEach { h ->
|
||||
val connecting = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
DevicePill(
|
||||
active = h.activeHost,
|
||||
icon = painterResource(MR.images.ic_smartphone_300),
|
||||
text = h.hostDeviceName,
|
||||
actionButtonVisible = h.sessionState is RemoteHostSessionState.Connected,
|
||||
onActionButtonClick = { onRemoteHostActionButtonClick(h) }
|
||||
) {
|
||||
onRemoteHostClick(h, connecting)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoteHostRow(h: RemoteHostInfo) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = windowWidth() * 0.7f)
|
||||
.padding(start = 17.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Text(
|
||||
h.hostDeviceName,
|
||||
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
||||
color = if (h.activeHost) MaterialTheme.colors.onBackground else MenuTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
expect fun UserPickerUsersSection(
|
||||
users: List<UserInfo>,
|
||||
stopped: Boolean,
|
||||
onUserClicked: (user: User) -> Unit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LocalDevicePickerItem(active: Boolean, onLongClick: () -> Unit = {}, onClick: () -> Unit) {
|
||||
expect fun PlatformUserPicker(
|
||||
modifier: Modifier,
|
||||
pickerState: MutableStateFlow<AnimatedViewState>,
|
||||
content: @Composable () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DevicePill(
|
||||
active: Boolean,
|
||||
icon: Painter,
|
||||
text: String,
|
||||
actionButtonVisible: Boolean,
|
||||
onActionButtonClick: (() -> Unit)? = null,
|
||||
onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = if (active) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified)
|
||||
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
|
||||
.combinedClickable(
|
||||
onClick = if (active) {{}} else onClick,
|
||||
onLongClick = onLongClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = if (!active) LocalIndication.current else null
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.border(
|
||||
BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.onRightClick { onLongClick() }
|
||||
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
.background(if (active) MaterialTheme.colors.secondaryVariant else Color.Transparent)
|
||||
.clickable(
|
||||
enabled = !active,
|
||||
onClick = onClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = LocalIndication.current
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LocalDeviceRow(active)
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocalDeviceRow(active: Boolean) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = windowWidth() * 0.7f)
|
||||
.padding(start = 17.dp, end = DEFAULT_PADDING),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Text(
|
||||
stringResource(MR.strings.this_device),
|
||||
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
||||
color = if (active) MaterialTheme.colors.onBackground else MenuTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
|
||||
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkAMobilePickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.link_a_mobile)
|
||||
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateInitialProfile(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.create_chat_profile)
|
||||
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CancelPickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.cancel_verb)
|
||||
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||
Text(text, color = MenuTextColor)
|
||||
Row(
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
text,
|
||||
Modifier.size(16.dp * fontSizeSqrtMultiplier),
|
||||
tint = MaterialTheme.colors.onSurface
|
||||
)
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier))
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontSize = 12.sp,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
modifier = if (onActionButtonClick != null && actionButtonVisible) Modifier.widthIn(max = 300.dp * fontSizeSqrtMultiplier) else Modifier
|
||||
)
|
||||
if (onActionButtonClick != null && actionButtonVisible) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val hovered = interactionSource.collectIsHoveredAsState().value
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier))
|
||||
IconButton(onActionButtonClick, Modifier.requiredSize(16.dp * fontSizeSqrtMultiplier)) {
|
||||
Icon(
|
||||
painterResource(if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi),
|
||||
null,
|
||||
Modifier.size(16.dp * fontSizeSqrtMultiplier).hoverable(interactionSource),
|
||||
tint = if (hovered) WarningOrange else MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,6 +604,28 @@ fun HostDisconnectButton(onClick: (() -> Unit)?) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.unreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) {
|
||||
Text(
|
||||
if (unreadCount > 0) unreadCountStr(unreadCount) else "",
|
||||
color = Color.White,
|
||||
fontSize = 10.sp,
|
||||
style = TextStyle(textAlign = TextAlign.Center),
|
||||
modifier = Modifier
|
||||
.offset(y = if (hasPadding) 3.sp.toDp() else -4.sp.toDp(), x = if (hasPadding) 0.dp else 4.sp.toDp())
|
||||
.background(if (userMuted) MaterialTheme.colors.primaryVariant else MaterialTheme.colors.secondary, shape = CircleShape)
|
||||
.badgeLayout()
|
||||
.padding(horizontal = 2.sp.toDp())
|
||||
.padding(vertical = 2.sp.toDp())
|
||||
.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun closePicker(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
delay(500)
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
}
|
||||
|
||||
private fun switchToLocalDevice() {
|
||||
withBGApi {
|
||||
chatController.switchUIRemoteHost(null)
|
||||
|
||||
@@ -7,3 +7,5 @@ fun <T> chatListAnimationSpec() = tween<T>(durationMillis = 250, easing = FastOu
|
||||
fun <T> newChatSheetAnimSpec() = tween<T>(256, 0, LinearEasing)
|
||||
|
||||
fun <T> audioProgressBarAnimationSpec() = tween<T>(durationMillis = 30, easing = LinearEasing)
|
||||
|
||||
fun <T> userPickerAnimSpec() = tween<T>(256, 0, FastOutSlowInEasing)
|
||||
|
||||
@@ -137,9 +137,10 @@ fun ProfileImageForActiveCall(
|
||||
size: Dp,
|
||||
image: String? = null,
|
||||
color: Color = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
backgroundColor: Color? = null,
|
||||
) {
|
||||
if (image == null) {
|
||||
Box(Modifier.requiredSize(size).clip(CircleShape)) {
|
||||
Box(Modifier.requiredSize(size).clip(CircleShape).then(if (backgroundColor != null) Modifier.background(backgroundColor) else Modifier)) {
|
||||
Icon(
|
||||
AccountCircleFilled,
|
||||
contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.DevicePill
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlin.math.absoluteValue
|
||||
@@ -157,9 +158,13 @@ private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?
|
||||
@Composable
|
||||
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
|
||||
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
|
||||
Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(hostDevice.second, color = MaterialTheme.colors.secondary)
|
||||
DevicePill(
|
||||
active = true,
|
||||
onClick = {},
|
||||
actionButtonVisible = false,
|
||||
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
|
||||
text = hostDevice.second
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import chat.simplex.common.model.ChatInfo
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
@@ -15,7 +14,7 @@ sealed class SharedContent {
|
||||
data class Text(val text: String): SharedContent()
|
||||
data class Media(val text: String, val uris: List<URI>): SharedContent()
|
||||
data class File(val text: String, val uri: URI): SharedContent()
|
||||
data class Forward(val chatItem: ChatItem, val fromChatInfo: ChatInfo): SharedContent()
|
||||
data class Forward(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): SharedContent()
|
||||
}
|
||||
|
||||
enum class AnimatedViewState {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -9,6 +14,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -85,10 +91,12 @@ fun <T> ExposedDropDownSettingWithIcon(
|
||||
values: List<Triple<T, ImageResource, String>>,
|
||||
selection: State<T>,
|
||||
fontSize: TextUnit = 16.sp,
|
||||
iconSize: Dp = 40.dp,
|
||||
iconPaddingPercent: Float = 0.2f,
|
||||
listIconSize: Dp = 30.dp,
|
||||
boxSize: Dp = 60.dp,
|
||||
iconColor: Color = MenuTextColor,
|
||||
enabled: State<Boolean> = mutableStateOf(true),
|
||||
background: Color,
|
||||
minWidth: Dp = 200.dp,
|
||||
onSelected: (T) -> Unit
|
||||
) {
|
||||
@@ -99,13 +107,21 @@ fun <T> ExposedDropDownSettingWithIcon(
|
||||
expanded.value = !expanded.value && enabled.value
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
Box(
|
||||
Modifier
|
||||
.background(background, CircleShape)
|
||||
.size(boxSize)
|
||||
.clickable(
|
||||
onClick = {},
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)),
|
||||
enabled = enabled.value
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val choice = values.first { it.first == selection.value }
|
||||
Icon(painterResource(choice.second), choice.third, Modifier.size(iconSize), tint = iconColor)
|
||||
Icon(painterResource(choice.second), choice.third, Modifier.padding(boxSize * iconPaddingPercent).fillMaxSize(), tint = iconColor)
|
||||
}
|
||||
DefaultExposedDropdownMenu(
|
||||
modifier = Modifier.widthIn(min = minWidth),
|
||||
|
||||
@@ -2,10 +2,8 @@ package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -209,11 +207,14 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
val end = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.END)
|
||||
val fullscreen = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.FULLSCREEN)
|
||||
|
||||
val floatingTerminal = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.START)
|
||||
|
||||
fun closeAllModalsEverywhere() {
|
||||
start.closeModals()
|
||||
center.closeModals()
|
||||
end.closeModals()
|
||||
fullscreen.closeModals()
|
||||
floatingTerminal.closeModals()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
|
||||
@@ -480,12 +480,11 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
|
||||
)
|
||||
|
||||
fun UriHandler.openVerifiedSimplexUri(uri: String) {
|
||||
val URI = try { URI.create(uri) } catch (e: Exception) { null }
|
||||
if (URI != null) {
|
||||
connectIfOpenedViaUri(chatModel.remoteHostId(), URI, ChatModel)
|
||||
}
|
||||
connectIfOpenedViaUri(chatModel.remoteHostId(), uri, ChatModel)
|
||||
}
|
||||
|
||||
fun uriCreateOrNull(uri: String) = try { URI.create(uri) } catch (e: Exception) { null }
|
||||
|
||||
fun UriHandler.openUriCatching(uri: String) {
|
||||
try {
|
||||
openUri(uri)
|
||||
@@ -588,9 +587,11 @@ fun <T> KeyChangeEffect(
|
||||
var anyChange by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1) {
|
||||
if (anyChange || key1 != prevKey) {
|
||||
block(prevKey)
|
||||
val prev = prevKey
|
||||
prevKey = key1
|
||||
anyChange = true
|
||||
// Call it as the last statement because the coroutine can be cancelled earlier
|
||||
block(prev)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -610,8 +611,8 @@ fun KeyChangeEffect(
|
||||
var anyChange by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1, key2) {
|
||||
if (anyChange || key1 != initialKey || key2 != initialKey2) {
|
||||
block()
|
||||
anyChange = true
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -633,8 +634,8 @@ fun KeyChangeEffect(
|
||||
var anyChange by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1, key2, key3) {
|
||||
if (anyChange || key1 != initialKey || key2 != initialKey2 || key3 != initialKey3) {
|
||||
block()
|
||||
anyChange = true
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ enum class ConnectionLinkType {
|
||||
|
||||
suspend fun planAndConnect(
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
uri: String,
|
||||
incognito: Boolean?,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)? = null,
|
||||
@@ -29,7 +29,7 @@ suspend fun planAndConnect(
|
||||
) {
|
||||
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString())
|
||||
if (connectionPlan != null) {
|
||||
val link = strHasSingleSimplexLink(uri.toString().trim())
|
||||
val link = strHasSingleSimplexLink(uri.trim())
|
||||
val linkText = if (link?.format is Format.SimplexLink)
|
||||
"<br><br><u>${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}</u>"
|
||||
else
|
||||
@@ -323,13 +323,13 @@ suspend fun planAndConnect(
|
||||
suspend fun connectViaUri(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
uri: String,
|
||||
incognito: Boolean,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)?,
|
||||
) {
|
||||
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
||||
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri)
|
||||
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
||||
if (pcc != null) {
|
||||
withChats {
|
||||
@@ -361,7 +361,7 @@ fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType
|
||||
fun askCurrentOrIncognitoProfileAlert(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
uri: String,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
close: (() -> Unit)?,
|
||||
title: String,
|
||||
@@ -417,7 +417,7 @@ fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, co
|
||||
fun ownGroupLinkConfirmConnect(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
uri: String,
|
||||
linkText: String,
|
||||
incognito: Boolean?,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
|
||||
@@ -39,7 +39,7 @@ fun ContactConnectionInfoView(
|
||||
) {
|
||||
LaunchedEffect(connReqInvitation) {
|
||||
if (connReqInvitation != null) {
|
||||
chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false, conn = contactConnection)
|
||||
}
|
||||
}
|
||||
/** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation].
|
||||
|
||||
@@ -482,7 +482,7 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<
|
||||
withBGApi {
|
||||
planAndConnect(
|
||||
chatModel.remoteHostId(),
|
||||
URI.create(link),
|
||||
link,
|
||||
incognito = null,
|
||||
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
close = close,
|
||||
|
||||
@@ -4,18 +4,22 @@ import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
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.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -32,6 +36,7 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
@@ -43,7 +48,7 @@ enum class NewChatOption {
|
||||
fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, close: () -> Unit) {
|
||||
val selection = remember { stateGetOrPut("selection") { selection } }
|
||||
val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } }
|
||||
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) }
|
||||
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) }
|
||||
val connReqInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connReq ?: "" } }
|
||||
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
|
||||
val pastedLink = rememberSaveable { mutableStateOf("") }
|
||||
@@ -63,7 +68,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
|
||||
* Otherwise, it will be called here AFTER [AddContactLearnMore] is launched and will clear the value too soon.
|
||||
* It will be dropped automatically when connection established or when user goes away from this screen.
|
||||
**/
|
||||
if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() == 1) {
|
||||
if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() <= 1) {
|
||||
val conn = contactConnection.value
|
||||
if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
@@ -177,6 +182,15 @@ private fun CreatingLinkProgressView() {
|
||||
DefaultProgressView(stringResource(MR.strings.creating_link))
|
||||
}
|
||||
|
||||
private fun updateShownConnection(conn: PendingContactConnection) {
|
||||
chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(
|
||||
conn = conn,
|
||||
connId = conn.id,
|
||||
connReq = conn.connReqInv ?: "",
|
||||
connChatUsed = true
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RetryButton(onClick: () -> Unit) {
|
||||
Column(
|
||||
@@ -192,6 +206,242 @@ private fun RetryButton(onClick: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfilePickerOption(
|
||||
title: String,
|
||||
selected: Boolean,
|
||||
disabled: Boolean,
|
||||
onSelected: () -> Unit,
|
||||
image: @Composable () -> Unit,
|
||||
onInfo: (() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
|
||||
.clickable(enabled = !disabled, onClick = onSelected)
|
||||
.padding(horizontal = DEFAULT_PADDING, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
image()
|
||||
TextIconSpaced(false)
|
||||
Text(title, modifier = Modifier.align(Alignment.CenterVertically))
|
||||
if (onInfo != null) {
|
||||
Spacer(Modifier.padding(6.dp))
|
||||
Column(Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(
|
||||
enabled = !disabled,
|
||||
onClick = { ModalManager.start.showModal { IncognitoView() } }
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_info),
|
||||
stringResource(MR.strings.incognito),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (selected) {
|
||||
Icon(
|
||||
painterResource(
|
||||
MR.images.ic_check
|
||||
),
|
||||
title,
|
||||
Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(
|
||||
Modifier.padding(
|
||||
start = DEFAULT_PADDING_HALF,
|
||||
end = DEFAULT_PADDING_HALF,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActiveProfilePicker(
|
||||
search: MutableState<String>,
|
||||
contactConnection: PendingContactConnection?,
|
||||
close: () -> Unit,
|
||||
rhId: Long?,
|
||||
showIncognito: Boolean = true
|
||||
) {
|
||||
val switchingProfile = remember { mutableStateOf(false) }
|
||||
val incognito = remember {
|
||||
chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get()
|
||||
}
|
||||
val selectedProfile by remember { chatModel.currentUser }
|
||||
val searchTextOrPassword = rememberSaveable { search }
|
||||
// Intentionally don't use derivedStateOf in order to NOT change an order after user was selected
|
||||
val filteredProfiles = remember(searchTextOrPassword.value) {
|
||||
filteredProfiles(chatModel.users.map { it.user }.sortedBy { !it.activeUser }, searchTextOrPassword.value)
|
||||
}
|
||||
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(switchingProfile.value) {
|
||||
progressByTimeout = if (switchingProfile.value) {
|
||||
delay(500)
|
||||
switchingProfile.value
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfilePickerUserOption(user: User) {
|
||||
val selected = selectedProfile?.userId == user.userId && !incognito
|
||||
|
||||
ProfilePickerOption(
|
||||
title = user.chatViewName,
|
||||
disabled = switchingProfile.value || selected,
|
||||
selected = selected,
|
||||
onSelected = {
|
||||
switchingProfile.value = true
|
||||
withApi {
|
||||
try {
|
||||
appPreferences.incognito.set(false)
|
||||
var updatedConn: PendingContactConnection? = null;
|
||||
|
||||
if (contactConnection != null) {
|
||||
updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId)
|
||||
if (updatedConn != null) {
|
||||
withChats {
|
||||
updateContactConnection(rhId, updatedConn)
|
||||
updateShownConnection(updatedConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((contactConnection != null && updatedConn != null) || contactConnection == null) {
|
||||
controller.changeActiveUser_(
|
||||
rhId = user.remoteHostId,
|
||||
toUserId = user.userId,
|
||||
viewPwd = if (user.hidden) searchTextOrPassword.value else null
|
||||
)
|
||||
|
||||
if (chatModel.currentUser.value?.userId != user.userId) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(
|
||||
MR.strings.switching_profile_error_title),
|
||||
String.format(generalGetString(MR.strings.switching_profile_error_message), user.chatViewName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedConn != null) {
|
||||
withChats {
|
||||
updateContactConnection(user.remoteHostId, updatedConn)
|
||||
}
|
||||
}
|
||||
|
||||
close()
|
||||
} finally {
|
||||
switchingProfile.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
image = { ProfileImage(size = 42.dp, image = user.image) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoUserOption() {
|
||||
ProfilePickerOption(
|
||||
disabled = switchingProfile.value,
|
||||
title = stringResource(MR.strings.incognito),
|
||||
selected = incognito,
|
||||
onSelected = {
|
||||
if (incognito || switchingProfile.value || contactConnection == null) return@ProfilePickerOption
|
||||
|
||||
switchingProfile.value = true
|
||||
withApi {
|
||||
try {
|
||||
appPreferences.incognito.set(true)
|
||||
val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true)
|
||||
if (conn != null) {
|
||||
withChats {
|
||||
updateContactConnection(rhId, conn)
|
||||
updateShownConnection(conn)
|
||||
}
|
||||
close()
|
||||
}
|
||||
} finally {
|
||||
switchingProfile.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
image = {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Icon(
|
||||
painterResource(MR.images.ic_theater_comedy_filled),
|
||||
contentDescription = stringResource(MR.strings.incognito),
|
||||
Modifier.size(32.dp),
|
||||
tint = Indigo,
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
},
|
||||
onInfo = { ModalManager.start.showModal { IncognitoView() } },
|
||||
)
|
||||
}
|
||||
|
||||
BoxWithConstraints {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(if (progressByTimeout) 0.6f else 1f)
|
||||
) {
|
||||
LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) {
|
||||
item {
|
||||
AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING)
|
||||
}
|
||||
val activeProfile = filteredProfiles.firstOrNull { it.activeUser }
|
||||
|
||||
if (activeProfile != null) {
|
||||
val otherProfiles = filteredProfiles.filter { it.userId != activeProfile.userId }
|
||||
item {
|
||||
when {
|
||||
!showIncognito ->
|
||||
ProfilePickerUserOption(activeProfile)
|
||||
incognito -> {
|
||||
IncognitoUserOption()
|
||||
ProfilePickerUserOption(activeProfile)
|
||||
}
|
||||
else -> {
|
||||
ProfilePickerUserOption(activeProfile)
|
||||
IncognitoUserOption()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(otherProfiles) { _, p ->
|
||||
ProfilePickerUserOption(p)
|
||||
}
|
||||
} else {
|
||||
if (showIncognito) {
|
||||
item {
|
||||
IncognitoUserOption()
|
||||
}
|
||||
}
|
||||
itemsIndexed(filteredProfiles) { _, p ->
|
||||
ProfilePickerUserOption(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (progressByTimeout) {
|
||||
DefaultProgressView("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState<PendingContactConnection?>) {
|
||||
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) {
|
||||
@@ -204,23 +454,72 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
|
||||
SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() })
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
val incognito = remember { mutableStateOf(controller.appPrefs.incognito.get()) }
|
||||
IncognitoToggle(controller.appPrefs.incognito, incognito) {
|
||||
ModalManager.start.showModal { IncognitoView() }
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
val incognito by remember(chatModel.showingInvitation.value?.conn?.incognito, controller.appPrefs.incognito.get()) {
|
||||
derivedStateOf {
|
||||
chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get()
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(incognito.value) {
|
||||
withBGApi {
|
||||
val contactConn = contactConnection.value ?: return@withBGApi
|
||||
val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi
|
||||
withChats {
|
||||
contactConnection.value = conn
|
||||
updateContactConnection(rhId, conn)
|
||||
val currentUser = remember { chatModel.currentUser }.value
|
||||
|
||||
if (currentUser != null) {
|
||||
SectionView(stringResource(MR.strings.new_chat_share_profile).uppercase(), headerBottomPadding = 5.dp) {
|
||||
SectionItemView(
|
||||
padding = PaddingValues(
|
||||
top = 0.dp,
|
||||
bottom = 0.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp
|
||||
),
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
ModalView(
|
||||
{ close() },
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||
},
|
||||
content = {
|
||||
ActiveProfilePicker(
|
||||
search = search,
|
||||
close = close,
|
||||
rhId = rhId,
|
||||
contactConnection = contactConnection.value
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (incognito) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Icon(
|
||||
painterResource(MR.images.ic_theater_comedy_filled),
|
||||
contentDescription = stringResource(MR.strings.incognito),
|
||||
tint = Indigo,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
} else {
|
||||
ProfileImage(size = 42.dp, image = currentUser.image)
|
||||
}
|
||||
TextIconSpaced(false)
|
||||
Text(
|
||||
text = if (incognito) stringResource(MR.strings.incognito) else currentUser.chatViewName,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End) {
|
||||
Icon(
|
||||
painter = painterResource(MR.images.ic_arrow_forward_ios),
|
||||
contentDescription = stringResource(MR.strings.new_chat_share_profile),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
chatModel.markShowingInvitationUsed()
|
||||
if (incognito) {
|
||||
SectionTextFooter(generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared))
|
||||
}
|
||||
}
|
||||
SectionTextFooter(sharedProfileInfo(chatModel, incognito.value))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -335,6 +634,18 @@ fun LinkTextView(link: String, share: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredProfiles(users: List<User>, searchTextOrPassword: String): List<User> {
|
||||
val s = searchTextOrPassword.trim()
|
||||
val lower = s.lowercase()
|
||||
return users.filter { u ->
|
||||
if ((u.activeUser || !u.hidden) && (s == "" || u.anyNameContains(lower))) {
|
||||
true
|
||||
} else {
|
||||
correctPassword(u, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boolean {
|
||||
if (text != null && strIsSimplexLink(text)) {
|
||||
connect(rhId, text, close)
|
||||
@@ -346,7 +657,7 @@ private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boole
|
||||
private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null) {
|
||||
planAndConnect(
|
||||
rhId,
|
||||
URI.create(link),
|
||||
link,
|
||||
close = close,
|
||||
cleanup = cleanup,
|
||||
incognito = null
|
||||
@@ -366,7 +677,7 @@ private fun createInvitation(
|
||||
if (r != null) {
|
||||
withChats {
|
||||
updateContactConnection(rhId, r.second)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false, conn = r.second)
|
||||
contactConnection.value = r.second
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -53,7 +53,8 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h4,
|
||||
fontWeight = FontWeight.Medium
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
if (link != null) {
|
||||
linkButton(link)
|
||||
@@ -64,7 +65,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
) {
|
||||
Icon(painterResource(si), stringResource(sd), tint = MaterialTheme.colors.secondary)
|
||||
Text(generalGetString(sd), fontSize = 15.sp)
|
||||
@@ -648,7 +649,36 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
show = appPlatform.isDesktop
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v6.1",
|
||||
post = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = MR.images.ic_verified_user,
|
||||
titleId = MR.strings.v6_1_better_security,
|
||||
descrId = MR.strings.v6_1_better_security_descr,
|
||||
link = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = MR.images.ic_videocam,
|
||||
titleId = MR.strings.v6_1_better_calls,
|
||||
descrId = MR.strings.v6_1_better_calls_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = null,
|
||||
titleId = MR.strings.v6_1_better_user_experience,
|
||||
descrId = null,
|
||||
subfeatures = listOf(
|
||||
MR.images.ic_link to MR.strings.v6_1_switch_chat_profile_descr,
|
||||
MR.images.ic_chat to MR.strings.v6_1_customizable_message_descr,
|
||||
MR.images.ic_calendar to MR.strings.v6_1_message_dates_descr,
|
||||
MR.images.ic_forward to MR.strings.v6_1_forward_many_messages_descr,
|
||||
MR.images.ic_delete to MR.strings.v6_1_delete_many_messages_descr
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val lastVersion = versionDescriptions.last().version
|
||||
|
||||
@@ -218,7 +218,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
}
|
||||
|
||||
if (currentRemoteHost == null && developerTools) {
|
||||
if (currentRemoteHost == null) {
|
||||
SectionView(stringResource(MR.strings.network_session_mode_transport_isolation).uppercase()) {
|
||||
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
@@ -34,6 +35,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
|
||||
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
|
||||
import chat.simplex.common.views.chat.item.PreviewChatItemView
|
||||
import chat.simplex.common.views.chat.item.msgTailWidthDp
|
||||
import chat.simplex.res.MR
|
||||
import com.godaddy.android.colorpicker.ClassicColorPicker
|
||||
import com.godaddy.android.colorpicker.HsvColor
|
||||
@@ -83,6 +85,31 @@ object AppearanceScope {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageShapeSection() {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase(), contentPadding = PaddingValues()) {
|
||||
Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING + 4.dp ) ,verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(MR.strings.settings_message_shape_corner), color = colors.onBackground)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Slider(
|
||||
remember { appPreferences.chatItemRoundness.state }.value,
|
||||
valueRange = 0f..1f,
|
||||
steps = 20,
|
||||
onValueChange = {
|
||||
val diff = it % 0.05f
|
||||
appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff))
|
||||
saveThemeToDatabase(null)
|
||||
},
|
||||
colors = SliderDefaults.colors(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent,
|
||||
)
|
||||
)
|
||||
}
|
||||
SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FontScaleSection() {
|
||||
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
|
||||
@@ -168,13 +195,17 @@ object AppearanceScope {
|
||||
.padding(DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
if (withMessages) {
|
||||
val alice = remember { ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), generalGetString(MR.strings.wallpaper_preview_hello_bob)) }
|
||||
PreviewChatItemView(alice)
|
||||
PreviewChatItemView(
|
||||
ChatItem.getSampleData(2, CIDirection.DirectSnd(), Clock.System.now(), stringResource(MR.strings.wallpaper_preview_hello_alice),
|
||||
quotedItem = CIQuote(alice.chatDir, alice.id, sentAt = alice.meta.itemTs, formattedText = alice.formattedText, content = MsgContent.MCText(alice.content.text))
|
||||
)
|
||||
)
|
||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = if (chatItemTail.value) Modifier else Modifier.padding(horizontal = msgTailWidthDp)) {
|
||||
val alice = remember { ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), generalGetString(MR.strings.wallpaper_preview_hello_bob)) }
|
||||
PreviewChatItemView(alice)
|
||||
PreviewChatItemView(
|
||||
ChatItem.getSampleData(2, CIDirection.DirectSnd(), Clock.System.now(), stringResource(MR.strings.wallpaper_preview_hello_alice),
|
||||
quotedItem = CIQuote(alice.chatDir, alice.id, sentAt = alice.meta.itemTs, formattedText = alice.formattedText, content = MsgContent.MCText(alice.content.text))
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize())
|
||||
}
|
||||
@@ -606,6 +637,39 @@ object AppearanceScope {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorModeSwitcher() {
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
val themeMode = if (remember { appPrefs.currentTheme.state }.value == DefaultTheme.SYSTEM_THEME_NAME) {
|
||||
if (systemInDarkThemeCurrently) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT
|
||||
} else {
|
||||
currentTheme.base.mode
|
||||
}
|
||||
|
||||
val onLongClick = {
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
showToast(generalGetString(MR.strings.system_mode_toast))
|
||||
|
||||
saveThemeToDatabase(null)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
ThemeManager.applyTheme(if (themeMode == DefaultThemeMode.LIGHT) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName)
|
||||
saveThemeToDatabase(null)
|
||||
},
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
.onRightClick(onLongClick)
|
||||
.size(44.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(painterResource(if (themeMode == DefaultThemeMode.LIGHT) MR.images.ic_light_mode else MR.images.ic_bedtime_moon), stringResource(MR.strings.color_mode_light), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var updateBackendJob: Job = Job()
|
||||
private fun saveThemeToDatabase(themeUserDestination: Pair<Long, ThemeModeOverrides?>?) {
|
||||
val remoteHostId = chatModel.remoteHostId()
|
||||
|
||||
@@ -35,7 +35,7 @@ fun DeveloperView(
|
||||
val unchangedHints = mutableStateOf(unchangedHintPreferences())
|
||||
SectionView {
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(it, close) }) }
|
||||
ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(false, close) }) }
|
||||
ResetHintsItem(unchangedHints)
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools)
|
||||
SectionTextFooter(
|
||||
|
||||
@@ -164,9 +164,7 @@ fun NetworkAndServersView() {
|
||||
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.fullscreen.showCustomModal { close -> it(close) }}
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxy, onionHosts, sessionMode.value, true, it) } })
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||
}
|
||||
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -458,9 +456,17 @@ fun SessionModePicker(
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val values = remember {
|
||||
TransportSessionMode.values().map {
|
||||
val safeModes = TransportSessionMode.safeValues
|
||||
val modes: Array<TransportSessionMode> =
|
||||
if (appPrefs.developerTools.get()) TransportSessionMode.values()
|
||||
else if (safeModes.contains(sessionMode.value)) safeModes
|
||||
else safeModes + sessionMode.value
|
||||
modes.map {
|
||||
val userModeDescr: AnnotatedString = escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_user_description), density)
|
||||
when (it) {
|
||||
TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(MR.strings.network_session_mode_user), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_user_description), density))
|
||||
TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(MR.strings.network_session_mode_user), userModeDescr)
|
||||
TransportSessionMode.Session -> ValueTitleDesc(TransportSessionMode.Session, generalGetString(MR.strings.network_session_mode_session), userModeDescr + AnnotatedString("\n") + escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_session_description), density))
|
||||
TransportSessionMode.Server -> ValueTitleDesc(TransportSessionMode.Server, generalGetString(MR.strings.network_session_mode_server), userModeDescr + AnnotatedString("\n") + escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_server_description), density))
|
||||
TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(MR.strings.network_session_mode_entity), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_entity_description), density))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,7 @@ private fun SetDeliveryReceiptsLayout(
|
||||
skip: () -> Unit,
|
||||
userCount: Int,
|
||||
) {
|
||||
// This view located in the left panel which means it has to have a padding from right side in order
|
||||
// to see scroll bar. And this padding should be applied to upper element, not scrollable column modifier
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
Box(Modifier.padding(top = DEFAULT_PADDING, end = endPadding)) {
|
||||
Box(Modifier.padding(top = DEFAULT_PADDING)) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
||||
@@ -31,13 +31,11 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.migration.MigrateFromDeviceView
|
||||
import chat.simplex.common.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.common.views.remote.ConnectDesktopView
|
||||
import chat.simplex.common.views.remote.ConnectMobileView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) {
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
SettingsLayout(
|
||||
@@ -71,10 +69,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt
|
||||
}
|
||||
},
|
||||
withAuth = ::doWithAuth,
|
||||
drawerState = drawerState,
|
||||
)
|
||||
KeyChangeEffect(chatModel.updatingProgress.value != null) {
|
||||
drawerState.close()
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,18 +93,11 @@ fun SettingsLayout(
|
||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
|
||||
drawerState: DrawerState,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val closeSettings: () -> Unit = { scope.launch { drawerState.close() } }
|
||||
val view = LocalMultiplatformView()
|
||||
if (drawerState.isOpen) {
|
||||
BackHandler {
|
||||
closeSettings()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
hideKeyboard(view)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
hideKeyboard(view)
|
||||
}
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -118,46 +108,22 @@ fun SettingsLayout(
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.your_settings))
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_you)) {
|
||||
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
||||
if (profile != null) {
|
||||
SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
|
||||
ProfilePreview(profile, stopped = stopped)
|
||||
}
|
||||
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden, drawerState) } } }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped)
|
||||
ChatPreferencesItem(showCustomModal, stopped = stopped)
|
||||
} else if (chatModel.localUserCreated.value == false) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), {
|
||||
withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) {
|
||||
ModalManager.center.showModalCloseable { close ->
|
||||
LaunchedEffect(Unit) {
|
||||
closeSettings()
|
||||
}
|
||||
CreateProfile(chatModel, close)
|
||||
}
|
||||
}
|
||||
}, disabled = stopped)
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped)
|
||||
} else {
|
||||
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal { it, close -> ConnectDesktopView(close) }, disabled = stopped)
|
||||
}
|
||||
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
||||
SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) })
|
||||
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_chat_database)) {
|
||||
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped)
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_help)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped)
|
||||
@@ -535,7 +501,6 @@ fun PreviewSettingsLayout() {
|
||||
showCustomModal = { {} },
|
||||
showVersion = {},
|
||||
withAuth = { _, _, _ -> },
|
||||
drawerState = DrawerState(DrawerValue.Closed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
KeyChangeEffect(u.value?.remoteHostId, u.value?.userId) {
|
||||
close()
|
||||
}
|
||||
|
||||
if (user != null) {
|
||||
var profile by remember { mutableStateOf(user.profile.toProfile()) }
|
||||
UserProfileLayout(
|
||||
|
||||
@@ -36,11 +36,10 @@ import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>, drawerState: DrawerState) {
|
||||
fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>) {
|
||||
val searchTextOrPassword = rememberSaveable { search }
|
||||
val users by remember { derivedStateOf { m.users.map { it.user } } }
|
||||
val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } }
|
||||
val scope = rememberCoroutineScope()
|
||||
UserProfilesLayout(
|
||||
users = users,
|
||||
filteredUsers = filteredUsers,
|
||||
@@ -51,12 +50,6 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
addUser = {
|
||||
ModalManager.center.showModalCloseable { close ->
|
||||
CreateProfile(m, close)
|
||||
if (appPlatform.isDesktop) {
|
||||
// Hide settings to allow clicks to pass through to CreateProfile view
|
||||
DisposableEffectOnGone(always = { scope.launch { drawerState.close() } }) {
|
||||
// Show settings again to allow intercept clicks to close modals after profile creation finishes
|
||||
scope.launch(NonCancellable) { drawerState.open() } }
|
||||
}
|
||||
}
|
||||
},
|
||||
activateUser = { user ->
|
||||
@@ -303,7 +296,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
|
||||
fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
|
||||
val s = searchTextOrPassword.trim()
|
||||
val lower = s.lowercase()
|
||||
return m.users.filter { u ->
|
||||
@@ -317,7 +310,7 @@ private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User
|
||||
|
||||
private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size
|
||||
|
||||
private fun correctPassword(user: User, pwd: String): Boolean {
|
||||
fun correctPassword(user: User, pwd: String): Boolean {
|
||||
val ph = user.viewPwdHash
|
||||
return ph != null && pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
|
||||
}
|
||||
|
||||
@@ -31,7 +31,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>
|
||||
@@ -50,7 +50,7 @@
|
||||
<string name="allow_calls_only_if">السماح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك.</string>
|
||||
<string name="allow_message_reactions_only_if">اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string>
|
||||
<string name="keychain_is_storing_securely">يتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن - فهو يسمح لخدمة الإشعارات بالعمل.</string>
|
||||
<string name="empty_chat_profile_is_created">يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم ، ويفتح التطبيق كالمعتاد.</string>
|
||||
<string name="empty_chat_profile_is_created">يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم، ويفتح التطبيق كالمعتاد.</string>
|
||||
<string name="answer_call">أجب الاتصال</string>
|
||||
<string name="chat_preferences_always">دائِماً</string>
|
||||
<string name="allow_to_send_disappearing">السماح بإرسال رسائل تختفي.</string>
|
||||
@@ -141,7 +141,7 @@
|
||||
<string name="database_initialization_error_title">لا يمكن تهيئة قاعدة البيانات</string>
|
||||
<string name="attach">إرفاق</string>
|
||||
<string name="icon_descr_asked_to_receive">طلب لاستلام الصورة</string>
|
||||
<string name="app_version_name">نسخة التطبيق: v%s</string>
|
||||
<string name="app_version_name">إصدار التطبيق: v%s</string>
|
||||
<string name="auto_accept_contact">قبول تلقائي</string>
|
||||
<string name="settings_section_title_calls">المكالمات</string>
|
||||
<string name="alert_title_cant_invite_contacts">لا يمكن دعوة جهات الاتصال!</string>
|
||||
@@ -1265,8 +1265,8 @@
|
||||
<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="user_unmute">إلغاء الكتم</string>
|
||||
<string name="unmute_chat">إلغاء الكتم</string>
|
||||
<string name="user_unmute">ألغِ الكتم</string>
|
||||
<string name="unmute_chat">ألغِ الكتم</string>
|
||||
<string name="you_accepted_connection">لقد قبلت الاتصال</string>
|
||||
<string name="unhide_profile">إلغاء إخفاء ملف تعريف</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">يجب أن تكون جهة الاتصال متصلة بالإنترنت حتى يكتمل الاتصال.
|
||||
@@ -1768,9 +1768,9 @@
|
||||
<string name="snd_error_quota">تم تجاوز السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا.</string>
|
||||
<string name="snd_error_relay">خطأ في خادم الوجهة: %1$s</string>
|
||||
<string name="ci_status_other_error">خطأ: %1$s</string>
|
||||
<string name="snd_error_proxy_relay">خادم إعادة التوجيه: %1$s
|
||||
<string name="snd_error_proxy_relay">خادم التحويل: %1$s
|
||||
\nخطأ في الخادم الوجهة: %2$s</string>
|
||||
<string name="snd_error_proxy">خادم إعادة التوجيه: %1$s
|
||||
<string name="snd_error_proxy">خادم التحويل: %1$s
|
||||
\nخطأ: %2$s</string>
|
||||
<string name="message_delivery_warning_title">تحذير تسليم الرسالة</string>
|
||||
<string name="snd_error_expired">مشكلات الشبكة - انتهت صلاحية الرسالة بعد عِدة محاولات لإرسالها.</string>
|
||||
@@ -2067,4 +2067,62 @@
|
||||
<string name="reset_all_hints">صفّر كافة التلميحات</string>
|
||||
<string name="error_parsing_uri_desc">يُرجى التأكد من أن رابط SimpleX صحيح.</string>
|
||||
<string name="error_parsing_uri_title">الرابط غير صالح</string>
|
||||
<string name="n_file_errors">%1$d خطأ في الملف:
|
||||
\n%2$s</string>
|
||||
<string name="forward_files_failed_to_receive_desc">فشل تنزيل %1$d ملف/ات.</string>
|
||||
<string name="forward_files_not_accepted_desc">لم يتم تنزيل %1$d ملف/ات.</string>
|
||||
<string name="forward_files_not_accepted_receive_files">نزّل</string>
|
||||
<string name="new_chat_share_profile">شارك ملف التعريف</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">استخدم بيانات اعتماد الوكيل المختلفة لكل اتصال.</string>
|
||||
<string name="network_proxy_username">اسم المستخدم</string>
|
||||
<string name="network_proxy_auth_mode_username_password">قد يتم إرسال بيانات الاعتماد الخاصة بك غير مُعمَّاة.</string>
|
||||
<string name="network_proxy_incorrect_config_title">خطأ في حفظ الوكيل</string>
|
||||
<string name="migrate_from_device_remove_archive_question">إزالة الأرشيف؟</string>
|
||||
<string name="system_mode_toast">وضع النظام</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">سيتم إزالة أرشيف قاعدة البيانات المرفوعة نهائيًا من الخوادم.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">استخدم بيانات اعتماد الوكيل المختلفة لكل ملف تعريف.</string>
|
||||
<string name="network_proxy_random_credentials">استخدم بيانات اعتماد عشوائية</string>
|
||||
<string name="settings_section_title_chat_database">قاعدة بيانات الدردشة</string>
|
||||
<string name="forward_files_missing_desc">حُذف %1$d ملف/ات.</string>
|
||||
<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="select_chat_profile">حدد ملف تعريف الدردشة</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>
|
||||
<string name="forward_multiple">حوّل الرسائل…</string>
|
||||
<string name="forward_alert_forward_messages_without_files">تحويل الرسائل بدون ملفات؟</string>
|
||||
<string name="compose_save_messages_n">جارِ حفظ %1$s رسالة</string>
|
||||
<string name="network_proxy_incorrect_config_desc">تأكد من صحة تضبيط الوكيل.</string>
|
||||
<string name="n_other_file_errors">%1$d خطأ في ملف آخر.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">حُذفت الرسائل بعد تحديدها.</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">لا يوجد شيء لتحويله!</string>
|
||||
<string name="network_proxy_password">كلمة المرور</string>
|
||||
<string name="network_proxy_auth">استيثاق الوكيل</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">سيتم حذف الرسائل - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="icon_descr_sound_muted">الصوت مكتوم</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">حدث خطأ أثناء تهيئة WebView. تأكد من تثبيت WebView وأن بنيته المدعومة هي arm64.\nالخطأ: %s</string>
|
||||
<string name="settings_section_title_message_shape">شكل الرسالة</string>
|
||||
<string name="settings_message_shape_tail">ذيل</string>
|
||||
<string name="settings_message_shape_corner">ركن</string>
|
||||
<string name="network_session_mode_session">جلسة التطبيق</string>
|
||||
<string name="network_session_mode_server">الخادم</string>
|
||||
<string name="network_session_mode_session_description">سيتم استخدام بيانات اعتماد SOCKS الجديدة في كل مرة تبدأ فيها تشغيل التطبيق.</string>
|
||||
<string name="network_session_mode_server_description">سيتم استخدام بيانات اعتماد SOCKS الجديدة لكل خادم.</string>
|
||||
<string name="call_desktop_permission_denied_chrome">انقر فوق زر المعلومات الموجود بالقرب من حقل العنوان للسماح باستخدام الميكروفون.</string>
|
||||
<string name="call_desktop_permission_denied_safari">افتح إعدادات Safari / مواقع الويب / الميكروفون، ثم اختر السماح لـ localhost.</string>
|
||||
<string name="call_desktop_permission_denied_title">لإجراء مكالمات، اسمح باستخدام الميكروفون. أنهِ المكالمة وحاول الاتصال مرة أخرى.</string>
|
||||
<string name="v6_1_better_user_experience">تجربة مستخدم أفضل</string>
|
||||
<string name="v6_1_customizable_message_descr">شكل الرسالة قابل للتخصيص.</string>
|
||||
<string name="v6_1_better_calls_descr">تبديل الصوت والفيديو أثناء المكالمة.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">حذف أو إشراف ما يصل إلى 200 رسالة.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">حوّل ما يصل إلى 20 رسالة آن واحد.</string>
|
||||
<string name="v6_1_better_calls">مكالمات أفضل</string>
|
||||
<string name="v6_1_message_dates_descr">تواريخ أفضل للرسائل.</string>
|
||||
<string name="v6_1_better_security">أمان أفضل ✅</string>
|
||||
<string name="v6_1_better_security_descr">بروتوكولات SimpleX تمت مراجعتها بواسطة Trail of Bits.</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">تبديل ملف تعريف الدردشة لدعوات لمرة واحدة.</string>
|
||||
</resources>
|
||||
@@ -125,6 +125,7 @@
|
||||
<string name="proxy_destination_error_broker_version">Destination server version of %1$s is incompatible with forwarding server %2$s.</string>
|
||||
<string name="please_try_later">Please try later.</string>
|
||||
<string name="error_sending_message">Error sending message</string>
|
||||
<string name="error_forwarding_messages">Error forwarding messages</string>
|
||||
<string name="error_creating_message">Error creating message</string>
|
||||
<string name="error_loading_details">Error loading details</string>
|
||||
<string name="error_adding_members">Error adding member(s)</string>
|
||||
@@ -133,7 +134,9 @@
|
||||
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
|
||||
<string name="file_not_approved_title">Unknown servers!</string>
|
||||
<string name="file_not_approved_descr">Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s.</string>
|
||||
<string name="n_other_file_errors">%1$d other file error(s).</string>
|
||||
<string name="error_receiving_file">Error receiving file</string>
|
||||
<string name="n_file_errors">%1$d file error(s):\n%2$s</string>
|
||||
<string name="error_creating_address">Error creating address</string>
|
||||
<string name="contact_already_exists">Contact already exists</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to %1$s.</string>
|
||||
@@ -378,12 +381,23 @@
|
||||
<string name="no_selected_chat">No selected chat</string>
|
||||
<string name="selected_chat_items_nothing_selected">Nothing selected</string>
|
||||
<string name="selected_chat_items_selected_n">Selected %d</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Forward %1$s message(s)?</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Nothing to forward!</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Forward messages without files?</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Messages were deleted after you selected them.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d file(s) were not downloaded.</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d file(s) are still being downloaded.</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d file(s) failed to download.</string>
|
||||
<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>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Share message…</string>
|
||||
<string name="share_image">Share media…</string>
|
||||
<string name="share_file">Share file…</string>
|
||||
<string name="forward_message">Forward message…</string>
|
||||
<string name="forward_multiple">Forward messages…</string>
|
||||
<string name="cannot_share_message_alert_title">Cannot send message</string>
|
||||
<string name="cannot_share_message_alert_text">Selected chat preferences prohibit this message.</string>
|
||||
|
||||
@@ -405,6 +419,8 @@
|
||||
<string name="files_and_media_prohibited">Files and media prohibited!</string>
|
||||
<string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string>
|
||||
<string name="compose_send_direct_message_to_connect">Send direct message to connect</string>
|
||||
<string name="compose_forward_messages_n">Forwarding %1$s messages</string>
|
||||
<string name="compose_save_messages_n">Saving %1$s messages</string>
|
||||
<string name="simplex_links_not_allowed">SimpleX links not allowed</string>
|
||||
<string name="files_and_media_not_allowed">Files and media not allowed</string>
|
||||
<string name="voice_messages_not_allowed">Voice messages not allowed</string>
|
||||
@@ -666,6 +682,10 @@
|
||||
<string name="one_time_link_short">1-time link</string>
|
||||
<string name="simplex_address">SimpleX address</string>
|
||||
<string name="or_show_this_qr_code">Or show this code</string>
|
||||
<string name="new_chat_share_profile">Share profile</string>
|
||||
<string name="select_chat_profile">Select chat profile</string>
|
||||
<string name="switching_profile_error_title">Error switching profile</string>
|
||||
<string name="switching_profile_error_message">Your connection was moved to %s but an unexpected error occurred while redirecting you to the profile.</string>
|
||||
<string name="or_scan_qr_code">Or scan QR code</string>
|
||||
<string name="keep_unused_invitation_question">Keep unused invitation?</string>
|
||||
<string name="you_can_view_invitation_link_again">You can view invitation link again in connection details.</string>
|
||||
@@ -791,8 +811,12 @@
|
||||
<string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.\nPlease note: you will not be able to connect to the servers without .onion address.</string>
|
||||
<string name="network_session_mode_transport_isolation">Transport isolation</string>
|
||||
<string name="network_session_mode_user">Chat profile</string>
|
||||
<string name="network_session_mode_session">App session</string>
|
||||
<string name="network_session_mode_server">Server</string>
|
||||
<string name="network_session_mode_entity">Connection</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.]]></string>
|
||||
<string name="network_session_mode_session_description">New SOCKS credentials will be used every time you start the app.</string>
|
||||
<string name="network_session_mode_server_description">New SOCKS credentials will be used for each server.</string>
|
||||
<string name="network_session_mode_entity_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.]]></string>
|
||||
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string>
|
||||
@@ -969,6 +993,7 @@
|
||||
<string name="audio_device_wired_headphones">Headphones</string>
|
||||
<string name="audio_device_bluetooth">Bluetooth</string>
|
||||
<string name="error_initializing_web_view">Error initializing WebView. Update your system to the new version. Please contact developers.\nError: %s</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Error initializing WebView. Make sure you have WebView installed and it\'s supported architecture is arm64.\nError: %s</string>
|
||||
|
||||
<!-- SimpleXInfo -->
|
||||
<string name="next_generation_of_private_messaging">The next generation\nof private messaging</string>
|
||||
@@ -1022,6 +1047,9 @@
|
||||
<string name="call_already_ended">Call already ended!</string>
|
||||
<string name="icon_descr_video_call">video call</string>
|
||||
<string name="icon_descr_audio_call">audio call</string>
|
||||
<string name="call_desktop_permission_denied_title">To make calls, allow to use your microphone. End the call and try to call again.</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Click info button near address field to allow using microphone.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Open Safari Settings / Websites / Microphone, then choose Allow for localhost.</string>
|
||||
|
||||
<!-- Call settings -->
|
||||
<string name="settings_audio_video_calls">Audio & video calls</string>
|
||||
@@ -1055,6 +1083,7 @@
|
||||
<string name="icon_descr_audio_on">Audio on</string>
|
||||
<string name="icon_descr_speaker_off">Speaker off</string>
|
||||
<string name="icon_descr_speaker_on">Speaker on</string>
|
||||
<string name="icon_descr_sound_muted">Sound muted</string>
|
||||
<string name="icon_descr_flip_camera">Flip camera</string>
|
||||
|
||||
<!-- Call items -->
|
||||
@@ -1156,6 +1185,7 @@
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">YOU</string>
|
||||
<string name="settings_section_title_settings">SETTINGS</string>
|
||||
<string name="settings_section_title_chat_database">CHAT DATABASE</string>
|
||||
<string name="settings_section_title_help">HELP</string>
|
||||
<string name="settings_section_title_support">SUPPORT SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_app">APP</string>
|
||||
@@ -1173,6 +1203,9 @@
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_profile_images">Profile images</string>
|
||||
<string name="settings_section_title_message_shape">Message shape</string>
|
||||
<string name="settings_message_shape_corner">Corner</string>
|
||||
<string name="settings_message_shape_tail">Tail</string>
|
||||
<string name="settings_section_title_chat_theme">Chat theme</string>
|
||||
<string name="settings_section_title_user_theme">Profile theme</string>
|
||||
<string name="settings_section_title_chat_colors">Chat colors</string>
|
||||
@@ -1720,6 +1753,7 @@
|
||||
<string name="theme_remove_image">Remove image</string>
|
||||
<string name="appearance_font_size">Font size</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="system_mode_toast">System mode</string>
|
||||
|
||||
<!-- Wallpapers -->
|
||||
<string name="wallpaper_preview_hello_alice">Good afternoon!</string>
|
||||
@@ -2007,6 +2041,16 @@
|
||||
<string name="v6_0_upgrade_app_descr">Download new versions from GitHub.</string>
|
||||
<string name="v6_0_connection_servers_status">Control your network</string>
|
||||
<string name="v6_0_connection_servers_status_descr">Connection and servers status.</string>
|
||||
<string name="v6_1_better_security">Better security ✅</string>
|
||||
<string name="v6_1_better_security_descr">SimpleX protocols reviewed by Trail of Bits.</string>
|
||||
<string name="v6_1_better_calls">Better calls</string>
|
||||
<string name="v6_1_better_calls_descr">Switch audio and video during the call.</string>
|
||||
<string name="v6_1_better_user_experience">Better user experience</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">Switch chat profile for 1-time invitations.</string>
|
||||
<string name="v6_1_customizable_message_descr">Customizable message shape.</string>
|
||||
<string name="v6_1_message_dates_descr">Better message dates.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Forward up to 20 messages at once.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Delete or moderate up to 200 messages.</string>
|
||||
|
||||
<!-- CustomTimePicker -->
|
||||
<string name="custom_time_unit_seconds">seconds</string>
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
<string name="how_to_use_your_servers">Jak používat servery</string>
|
||||
<string name="your_ICE_servers">Vaše servery ICE</string>
|
||||
<string name="configure_ICE_servers">Konfigurace serverů ICE</string>
|
||||
<string name="network_settings_title">Nastavení sítě</string>
|
||||
<string name="network_settings_title">Pokročilé nastavení</string>
|
||||
<string name="network_enable_socks">Použít proxy server SOCKS\?</string>
|
||||
<string name="network_disable_socks">Použít přímé připojení k internetu\?</string>
|
||||
<string name="network_use_onion_hosts_no">Ne</string>
|
||||
@@ -434,7 +434,7 @@
|
||||
<string name="edit_image">Upravit obrázek</string>
|
||||
<string name="delete_image">Smazat obrázek</string>
|
||||
<string name="callstatus_error">chyba volání</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Servery může provozovat kdokoli.</string>
|
||||
<string name="create_your_profile">Vytvořte si svůj profil</string>
|
||||
<string name="make_private_connection">Vytvořte si soukromé připojení</string>
|
||||
<string name="encrypted_video_call">Videohovor šifrovaný e2e</string>
|
||||
@@ -667,8 +667,8 @@
|
||||
<string name="callstate_received_confirmation">obdržel potvrzení…</string>
|
||||
<string name="callstate_connecting">připojování…</string>
|
||||
<string name="privacy_redefined">Nové vymezení soukromí</string>
|
||||
<string name="first_platform_without_user_ids">1. platforma bez jakýchkoliv uživatelských identifikátorů – soukromá již od návrhu.</string>
|
||||
<string name="immune_to_spam_and_abuse">Odolná vůči spamu a zneužití</string>
|
||||
<string name="first_platform_without_user_ids">Bez uživatelských identifikátorů</string>
|
||||
<string name="immune_to_spam_and_abuse">Odolná vůči spamu</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">K ochraně soukromí, místo uživatelských ID užívaných všemi ostatními platformami, SimpleX používá identifikátory pro fronty zpráv, zvlášť pro každý z vašich kontaktů.</string>
|
||||
<string name="many_people_asked_how_can_it_deliver"><![CDATA[Mnoho lidí se ptá: <i>když SimpleX nemá žádný identifikátor uživatelů, jak může doručovat zprávy\?</i>]]></string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Vy určujete, přes které servery <b>přijímat</b> zprávy, vaše kontakty – servery, které používáte k zasílání zpráv.]]></string>
|
||||
@@ -679,8 +679,8 @@
|
||||
<string name="onboarding_notifications_mode_off">Když aplikace běží</string>
|
||||
<string name="onboarding_notifications_mode_service">Okamžité</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Nejlepší pro baterii</b>. Budete přijímat oznámení pouze když aplikace běží (žádná služba na pozadí).]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Dobré pro baterii</b>. Služba na pozadí bude kontrolovat každých 10 minut. Můžete zmeškat hovory nebo naléhavé zprávy.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Využívá více baterie</b>! Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Dobré pro baterii</b>. Apka bude kontrolovat zprávy každých 10 minut. Můžete zmeškat volání nebo naléhavé zprávy.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Využívá více baterie</b>! Apka stále běží na pozadí - oznámení se zobrazí okamžitě.]]></string>
|
||||
<string name="paste_the_link_you_received">Vložte přijatý odkaz</string>
|
||||
<string name="incoming_video_call">Příchozí videohovor</string>
|
||||
<string name="incoming_audio_call">Příchozí zvukový hovor</string>
|
||||
@@ -799,7 +799,7 @@
|
||||
<string name="rcv_group_event_member_added">pozval %1$s</string>
|
||||
<string name="rcv_group_event_member_connected">připojen</string>
|
||||
<string name="rcv_group_event_changed_member_role">změnil roli %s na %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">změnil svou roli na %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">změnil vaši roli na %s</string>
|
||||
<string name="rcv_group_event_member_deleted">odstraněn %1$s</string>
|
||||
<string name="rcv_group_event_user_deleted">odstranil vás</string>
|
||||
<string name="rcv_group_event_group_deleted">skupina odstraněna</string>
|
||||
@@ -1582,7 +1582,7 @@
|
||||
<string name="profile_update_event_member_name_changed">člen %1$s změněn na %2$s</string>
|
||||
<string name="member_info_member_blocked">blokováno</string>
|
||||
<string name="member_blocked_by_admin">Blokováno adminem</string>
|
||||
<string name="share_text_created_at">Vytvořeno v: %s</string>
|
||||
<string name="share_text_created_at">Vytvořen v: %s</string>
|
||||
<string name="message_too_large">Zpráva příliš velká</string>
|
||||
<string name="remote_host_disconnected_from"><![CDATA[Odpojeno z mobilu <b>%s</b> z důvodu: %s]]></string>
|
||||
<string name="remote_host_was_disconnected_title">Spojení zastaveno</string>
|
||||
@@ -1817,8 +1817,7 @@
|
||||
<string name="v5_8_safe_files">Bezpečné přijímání souborů</string>
|
||||
<string name="v5_8_chat_themes">Nové motivy chatu</string>
|
||||
<string name="v5_8_private_routing">Soukromé směrování zpráv 🚀</string>
|
||||
<string name="v5_8_private_routing_descr">Chraňte vaši IP adresu před relé zpráv, které jste si vybrali.
|
||||
\nPovolit v nastavení *Síť & servery*.</string>
|
||||
<string name="v5_8_private_routing_descr">Chraňte vaši IP adresu před relé zpráv vašich kontaktů.\nPovolte v nastavení *Síť & servery*.</string>
|
||||
<string name="v5_8_message_delivery">Vylepšené doručování zpráv</string>
|
||||
<string name="v5_8_persian_ui">Perské UI</string>
|
||||
<string name="remote_ctrl_connection_stopped_desc">Prosím zkontrolujte, že mobil a desktop jsou připojeny ke stejné místní síti, a že stolní firewall umožňuje připojení.
|
||||
@@ -1861,4 +1860,194 @@
|
||||
<string name="calls_prohibited_alert_title">Volání zakázáno!</string>
|
||||
<string name="cant_call_member_alert_title">Nelze zavolat člena skupiny</string>
|
||||
<string name="deleted_chats">Archivované kontakty</string>
|
||||
<string name="v6_0_your_contacts_descr">Archivujte kontakty pro pozdější chatování.</string>
|
||||
<string name="smp_proxy_error_broker_host">Adresa předávacího serveru je nekompatibilní s nastavením sítě: %1$s.</string>
|
||||
<string name="smp_proxy_error_broker_version">Verze předávacího serveru je nekompatibilní s nastavením sítě: %1$s.</string>
|
||||
<string name="proxy_destination_error_broker_host">Cílová adresa serveru %1$s je nekompatibilní s nastavením přeposílajícího serveru %2$s.</string>
|
||||
<string name="smp_proxy_error_connecting">Chyba připojení k přeposílajícímu serveru %1$s. Prosím, zkuste to později.</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Předávacímu serveru %1$s se nepodařilo připojit k cílovému serveru %2$s. Prosím, zkuste to později.</string>
|
||||
<string name="cannot_share_message_alert_text">Vybrané nastavení chatu zakazuje tuto zprávu.</string>
|
||||
<string name="smp_servers_other">Jiné SMP servery</string>
|
||||
<string name="smp_servers_configured">Nastavené SMP servery</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Probíhá</string>
|
||||
<string name="chunks_uploaded">Části nahrány</string>
|
||||
<string name="n_file_errors">%1$d chuba souboru(ů):\n%2$s</string>
|
||||
<string name="n_other_file_errors">%1$d jiná chyba souboru(ů).</string>
|
||||
<string name="error_forwarding_messages">Chyba přeposílaní zpráv</string>
|
||||
<string name="srv_error_host">Adresa serveru není kompatibilní s nastavením sítě.</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Předat %1$s zpráv(u)?</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Nic k předání!</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Předat zprávy bez souborů?</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d soubor(y) se nepodařilo stáhnout.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d soubor(y) nestažen(y).</string>
|
||||
<string name="forward_files_missing_desc">%1$d soubor(y) smazán(y).</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s zprávy nepředány</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Stáhnout</string>
|
||||
<string name="compose_forward_messages_n">Předávám %1$s zpráv</string>
|
||||
<string name="forward_multiple">Předat zprávy…</string>
|
||||
<string name="compose_save_messages_n">Uložit %1$s zpráv</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Nepoužívat autorizaci s proxy.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Chyba ukládání proxy</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Ujistěte se, že nastavení proxy je správné.</string>
|
||||
<string name="network_proxy_password">Heslo</string>
|
||||
<string name="network_proxy_auth">Proxy autentizace</string>
|
||||
<string name="app_check_for_updates_button_install">Instalovat aktualizace</string>
|
||||
<string name="app_check_for_updates_button_open">Otevřít umístění souboru</string>
|
||||
<string name="app_check_for_updates_download_started">Stahování aktualizace, nezavírejte aplikaci</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Prosím restartujte aplikaci.</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">Instalovány úspěšně</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Připomenout později</string>
|
||||
<string name="app_check_for_updates_notice_title">Zkontrolovat aktualizace</string>
|
||||
<string name="privacy_media_blur_radius_off">Vypnuto</string>
|
||||
<string name="settings_section_title_chat_database">CHAT DATABÁZE</string>
|
||||
<string name="member_info_member_disabled">vypnut</string>
|
||||
<string name="message_queue_info_server_info">info fronty serveru: %1$s\n\nposlední obdržená zpráva: %2$s</string>
|
||||
<string name="network_options_save_and_reconnect">Uložit a připojit znovu</string>
|
||||
<string name="v6_0_chat_list_media">Hrajte ze seznamu chatů.</string>
|
||||
<string name="servers_info_messages_received">Přijatých zprávy</string>
|
||||
<string name="servers_info_reconnect_servers_title">Znovu připojit servery?</string>
|
||||
<string name="completed">Kompletní</string>
|
||||
<string name="secured">Zabezpečeno</string>
|
||||
<string name="error_parsing_uri_desc">Prosím zkontrolujte, že SimpleX odkaz je správný.</string>
|
||||
<string name="error_parsing_uri_title">Chybný odkaz</string>
|
||||
<string name="proxy_destination_error_broker_version">Verze cílového serveru %1$s je nekompatibilní s nastavením přeposílajícího serveru %2$s.</string>
|
||||
<string name="message_forwarded_title">Zpráva předána</string>
|
||||
<string name="keep_conversation">Udržujte konverzaci</string>
|
||||
<string name="only_delete_conversation">Jen smazat konverzaci</string>
|
||||
<string name="confirm_delete_contact_question">Potvrdit smazání kontaktu?</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Kontakt bude smazán - nelze vrátit!</string>
|
||||
<string name="conversation_deleted">Konverzace odstraněna!</string>
|
||||
<string name="paste_link">Vložit odkaz</string>
|
||||
<string name="chat_database_exported_title">Chat databáze exportována</string>
|
||||
<string name="cant_send_message_to_member_alert_title">Členu skupiny nelze odeslat zprávu</string>
|
||||
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Požádejte váš kontakt ať povolí volání.</string>
|
||||
<string name="color_sent_quote">Odeslaných odpovědí</string>
|
||||
<string name="wallpaper_scale">Škálovat</string>
|
||||
<string name="wallpaper_scale_fit">Přizpůsobit</string>
|
||||
<string name="v6_0_privacy_blur">Rozmazání pro lepší soukromí.</string>
|
||||
<string name="v6_0_connect_faster_descr">Připojte se k vašim přátelům rychleji.</string>
|
||||
<string name="v6_0_delete_many_messages_descr">Smazat až 20 zpráv najednou.</string>
|
||||
<string name="v6_0_increase_font_size">Zvětšit velikost písma.</string>
|
||||
<string name="v6_0_connection_servers_status_descr">Stav připojení a serverů.</string>
|
||||
<string name="v6_0_connection_servers_status">Kontrolujte svou síť</string>
|
||||
<string name="servers_info_sessions_errors">Chyby</string>
|
||||
<string name="servers_info_sessions_connected">Připojen</string>
|
||||
<string name="servers_info_sessions_connecting">Připojování</string>
|
||||
<string name="servers_info_connected_servers_section_header">Připojené servery</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Dříve připojené servery</string>
|
||||
<string name="acknowledged">Potvrzeno</string>
|
||||
<string name="duplicates_label">duplikáty</string>
|
||||
<string name="deleted">Smazán</string>
|
||||
<string name="open_server_settings_button">Otevřít nastavení serveru</string>
|
||||
<string name="v6_0_new_chat_experience">Nový zážitek z chatu 🎉</string>
|
||||
<string name="v6_0_new_media_options">Nové možnosti médií</string>
|
||||
<string name="new_message">Nová zpráva</string>
|
||||
<string name="scan_paste_link">Skenovat / Vložit odkaz</string>
|
||||
<string name="no_filtered_contacts">Žádné filtrované kontakty</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Chyba inicializace WebView. Ujistěte se, že máte nainstalován WebView podporující architekturu arm64.\nChyba: %s</string>
|
||||
<string name="v6_0_private_routing_descr">Chrání vaši IP adresu a připojení.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Zprávy byly odstraněny poté, co jste je vybrali.</string>
|
||||
<string name="network_session_mode_session_description">Nové přihlašovací údaje SOCKS budou použity pokaždé, když zapnete aplikaci.</string>
|
||||
<string name="network_session_mode_server_description">Nové přihlašovací údaje SOCKS budou použity pro každý server.</string>
|
||||
<string name="servers_info_reconnect_servers_message">Znovu připojte všechny připojené servery pro vynucení doručení. Využívá další provoz.</string>
|
||||
<string name="reset_all_hints">Resetovat všechny tipy</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d soubor(y) stále stahuji.</string>
|
||||
<string name="v6_1_message_dates_descr">Lepší datování zpráv.</string>
|
||||
<string name="v6_1_better_security">Lepší zabezpečení ✅</string>
|
||||
<string name="chunks_deleted">Části odstraněny</string>
|
||||
<string name="info_view_connect_button">připojení</string>
|
||||
<string name="current_user">Aktuální profil</string>
|
||||
<string name="servers_info_reconnect_server_error">Chyba znovu připojení serveru</string>
|
||||
<string name="servers_info_reconnect_servers_error">Chyba při opětovném připojování serverů</string>
|
||||
<string name="servers_info_reconnect_server_message">Znovu připojte server pro vynucení doručení. Využívá další provoz.</string>
|
||||
<string name="servers_info_modal_error_title">Chyba</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Zprávy budou smazány - nelze vrátit!</string>
|
||||
<string name="delete_without_notification">Smazat bez upozornění</string>
|
||||
<string name="info_view_search_button">hledat</string>
|
||||
<string name="switching_profile_error_title">Chyba přepínání profilu</string>
|
||||
<string name="select_chat_profile">Vyberte chat profil</string>
|
||||
<string name="info_view_message_button">zpráva</string>
|
||||
<string name="info_view_open_button">otevřít</string>
|
||||
<string name="contact_deleted">Kontakt smazán!</string>
|
||||
<string name="member_info_member_inactive">neaktivní</string>
|
||||
<string name="servers_info_details">Detaily</string>
|
||||
<string name="servers_info_reset_stats">Resetovat všechny statistiky</string>
|
||||
<string name="please_try_later">Prosím zkuste později.</string>
|
||||
<string name="private_routing_error">Chyba soukromého směrování</string>
|
||||
<string name="network_error_broker_host_desc">Adresa serveru není kompatibilní s nastavením sítě: %1$s.</string>
|
||||
<string name="member_inactive_title">Člen neaktivní</string>
|
||||
<string name="member_inactive_desc">Zpráva může být doručena později až bude člen aktivní.</string>
|
||||
<string name="message_forwarded_desc">Zatím bez přímého spojení, zpráva je předána adminem.</string>
|
||||
<string name="cant_call_contact_connecting_wait_alert_text">Připojování ke kontaktu, počkejte nebo se podívejte později!</string>
|
||||
<string name="cant_call_contact_deleted_alert_text">Kontakt odstraněn.</string>
|
||||
<string name="deletion_errors">Chyby mazání</string>
|
||||
<string name="servers_info_detailed_statistics">Podrobné statistiky</string>
|
||||
<string name="chunks_downloaded">Části staženy</string>
|
||||
<string name="network_session_mode_server">Server</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Odesílat zprávy přímo, když je IP adresa chráněna a váš nebo cílový server nepodporuje soukromé směrování.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Odeslat zprávy přímo, když váš nebo cílový server nepodporuje soukromé směrování.</string>
|
||||
<string name="app_check_for_updates">Zkontrolovat aktualizace</string>
|
||||
<string name="app_check_for_updates_notice_disable">Vypnout</string>
|
||||
<string name="app_check_for_updates_disabled">Vypnut</string>
|
||||
<string name="app_check_for_updates_button_download">Stáhnout %s (%s)</string>
|
||||
<string name="invite_friends_short">Pozvat</string>
|
||||
<string name="create_address_button">Vytvořit</string>
|
||||
<string name="settings_message_shape_corner">Roh</string>
|
||||
<string name="settings_section_title_message_shape">Tvar zpráv</string>
|
||||
<string name="chat_database_exported_continue">Pokračovat</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Klikněte na info tlačítko blízko pole adresy, pro použití mikrofonu.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Otevřete nastavení Safari / Webové stránky / mikrofon, vyberte možnost Povolit pro localhost.</string>
|
||||
<string name="appearance_font_size">Velikost písma</string>
|
||||
<string name="v6_0_upgrade_app_descr">Stáhnout nové verze z GitHubu.</string>
|
||||
<string name="v6_0_reachable_chat_toolbar">Dosažitelný panel nástrojů chatu</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Odebrat archiv?</string>
|
||||
<string name="servers_info_files_tab">Soubory</string>
|
||||
<string name="servers_info_messages_sent">Odeslaných zpráv</string>
|
||||
<string name="servers_info_downloaded">Staženo</string>
|
||||
<string name="servers_info_reconnect_server_title">Znovu připojit server?</string>
|
||||
<string name="decryption_errors">chyba dešifrování</string>
|
||||
<string name="v6_1_better_calls">Lepší volání</string>
|
||||
<string name="v6_1_better_user_experience">Větší přívětivost</string>
|
||||
<string name="v6_1_customizable_message_descr">Přizpůsobitelný tvar zpráv.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Smazat nebo moderovat až 200 zpráv.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Předat až 20 zpráv najednou.</string>
|
||||
<string name="servers_info_missing">Žádné info, zkuste načíst znovu</string>
|
||||
<string name="servers_info">Informace o serverech</string>
|
||||
<string name="downloaded_files">Stažené soubory</string>
|
||||
<string name="download_errors">Chyby stahování</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Chyba resetování statistik</string>
|
||||
<string name="servers_info_detailed_statistics_received_messages_header">Přijaté zprávy</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">Přijato celkem</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Chyb přijmutí</string>
|
||||
<string name="servers_info_reconnect_all_servers_button">Připojte znovu všechny servery</string>
|
||||
<string name="servers_info_reset_stats_alert_confirm">Reset</string>
|
||||
<string name="servers_info_reset_stats_alert_title">Resetovat všechny statistiky?</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">Odeslané zprávy</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Odeslaných celkem</string>
|
||||
<string name="server_address">Adresa serveru</string>
|
||||
<string name="one_hand_ui">Dosažitelný panel nástrojů chatu</string>
|
||||
<string name="acknowledgement_errors">Chyba potvrzení</string>
|
||||
<string name="connections">Připojení</string>
|
||||
<string name="created">Vytvořen</string>
|
||||
<string name="expired_label">prošlý</string>
|
||||
<string name="other_label">jiné</string>
|
||||
<string name="other_errors">jiné chyby</string>
|
||||
<string name="reconnect">Znovu připojit</string>
|
||||
<string name="send_errors">Chyby odesílání</string>
|
||||
<string name="sent_directly">Odesláno přímo</string>
|
||||
<string name="sent_via_proxy">Odeslaných přes proxy</string>
|
||||
<string name="delete_members_messages__question">Odstranit %d zpráv členů?</string>
|
||||
<string name="delete_messages_mark_deleted_warning">Zprávy budou označeny pro smazání. Příjemci budou moci tyto zprávy odhalit.</string>
|
||||
<string name="select_verb">Vybrat</string>
|
||||
<string name="compose_message_placeholder">Zpráva</string>
|
||||
<string name="selected_chat_items_nothing_selected">Nic nevybráno</string>
|
||||
<string name="selected_chat_items_selected_n">Vybrány %d</string>
|
||||
<string name="privacy_media_blur_radius_medium">Střední</string>
|
||||
<string name="servers_info_subscriptions_section_header">Příjem zpráv</string>
|
||||
<string name="xftp_servers_configured">Nastavené XFTP servery</string>
|
||||
<string name="media_and_file_servers">Servery médií a souborů</string>
|
||||
<string name="message_servers">Servery zpráv</string>
|
||||
<string name="xftp_servers_other">Jiné FXTP servery</string>
|
||||
<string name="action_button_add_members">Pozvat</string>
|
||||
<string name="cant_call_member_send_message_alert_text">Pošlete zprávu pro povolení volání.</string>
|
||||
</resources>
|
||||
@@ -277,11 +277,11 @@
|
||||
<string name="accept_contact_incognito_button">Inkognito akzeptieren</string>
|
||||
<string name="reject_contact_button">Ablehnen</string>
|
||||
<!-- Clear Chat - ChatListNavLinkView.kt -->
|
||||
<string name="clear_chat_question">Chatinhalte löschen?</string>
|
||||
<string name="clear_chat_question">Chat-Inhalte entfernen?</string>
|
||||
<string name="clear_chat_warning">Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</string>
|
||||
<string name="clear_verb">Löschen</string>
|
||||
<string name="clear_chat_button">Chatinhalte löschen</string>
|
||||
<string name="clear_chat_menu_action">Chatinhalte löschen</string>
|
||||
<string name="clear_verb">Entfernen</string>
|
||||
<string name="clear_chat_button">Chat-Inhalte entfernen</string>
|
||||
<string name="clear_chat_menu_action">Chat-Inhalte entfernen</string>
|
||||
<string name="delete_contact_menu_action">Löschen</string>
|
||||
<string name="delete_group_menu_action">Löschen</string>
|
||||
<string name="mark_read">Als gelesen markieren</string>
|
||||
@@ -753,7 +753,7 @@
|
||||
<string name="skip_inviting_button">Mitgliedereinladungen überspringen</string>
|
||||
<string name="select_contacts">Kontakte auswählen</string>
|
||||
<string name="icon_descr_contact_checked">Kontakt geprüft</string>
|
||||
<string name="clear_contacts_selection_button">Löschen</string>
|
||||
<string name="clear_contacts_selection_button">Entfernen</string>
|
||||
<string name="num_contacts_selected">%d Kontakt(e) ausgewählt</string>
|
||||
<string name="no_contacts_selected">Keine Kontakte ausgewählt</string>
|
||||
<string name="invite_prohibited">Kontakt kann nicht eingeladen werden!</string>
|
||||
@@ -765,7 +765,7 @@
|
||||
<string name="button_delete_group">Gruppe löschen</string>
|
||||
<string name="delete_group_question">Gruppe löschen?</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="button_leave_group">Gruppe verlassen</string>
|
||||
<string name="button_edit_group_profile">Gruppenprofil bearbeiten</string>
|
||||
<string name="group_link">Gruppen-Link</string>
|
||||
@@ -1061,7 +1061,7 @@
|
||||
<string name="v4_6_audio_video_calls_descr">Bluetooth-Unterstützung und weitere Verbesserungen.</string>
|
||||
<string name="v4_6_group_moderation_descr">Administratoren können nun
|
||||
\n- Nachrichten von Gruppenmitgliedern löschen
|
||||
\n- Gruppenmitglieder deaktivieren („Beobachter“-Rolle)</string>
|
||||
\n- Gruppenmitglieder deaktivieren (Beobachter-Rolle)</string>
|
||||
<string name="v4_6_group_welcome_message">Gruppen-Begrüßungsmeldung</string>
|
||||
<string name="v4_6_reduced_battery_usage">Weiter reduzierter Batterieverbrauch</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string>
|
||||
@@ -1397,7 +1397,7 @@
|
||||
<string name="v5_2_message_delivery_receipts_descr">Wir haben das zweite Häkchen vermisst! ✅</string>
|
||||
<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 im Chat deaktiviert sind.</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>
|
||||
@@ -1630,7 +1630,7 @@
|
||||
<string name="tap_to_scan">Zum Scannen tippen</string>
|
||||
<string name="keep_invitation_link">Behalten</string>
|
||||
<string name="tap_to_paste_link">Zum Link einfügen tippen</string>
|
||||
<string name="search_or_paste_simplex_link">Suchen oder fügen Sie den SimpleX-Link ein</string>
|
||||
<string name="search_or_paste_simplex_link">Suchen oder SimpleX-Link einfügen</string>
|
||||
<string name="chat_is_stopped_you_should_transfer_database">Der Chat wurde gestoppt. Wenn diese Datenbank bereits auf einem anderen Gerät von Ihnen verwendet wurde, sollten Sie diese dorthin zurück übertragen, bevor Sie den Chat starten.</string>
|
||||
<string name="start_chat_question">Chat starten?</string>
|
||||
<string name="show_internal_errors">Interne Fehler anzeigen</string>
|
||||
@@ -1673,7 +1673,7 @@
|
||||
<string name="v5_5_private_notes_descr">Mit verschlüsselten Dateien und Medien.</string>
|
||||
<string name="v5_5_private_notes">Private Notizen</string>
|
||||
<string name="clear_note_folder_warning">Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="clear_note_folder_question">Private Notizen löschen?</string>
|
||||
<string name="clear_note_folder_question">Private Notizen entfernen?</string>
|
||||
<string name="rcv_group_event_member_blocked">%s wurde blockiert</string>
|
||||
<string name="rcv_group_event_member_unblocked">%s wurde freigegeben</string>
|
||||
<string name="snd_group_event_member_blocked">Sie haben %s blockiert</string>
|
||||
@@ -1990,7 +1990,7 @@
|
||||
<string name="downloaded_files">Heruntergeladene Dateien</string>
|
||||
<string name="download_errors">Fehler beim Herunterladen</string>
|
||||
<string name="duplicates_label">Duplikate</string>
|
||||
<string name="expired_label">abgelaufen</string>
|
||||
<string name="expired_label">Abgelaufen</string>
|
||||
<string name="open_server_settings_button">Server-Einstellungen öffnen</string>
|
||||
<string name="other_errors">Andere Fehler</string>
|
||||
<string name="proxied">Proxy</string>
|
||||
@@ -2081,16 +2081,16 @@
|
||||
<string name="info_view_connect_button">Verbinden</string>
|
||||
<string name="info_view_message_button">Nachricht</string>
|
||||
<string name="info_view_open_button">Öffnen</string>
|
||||
<string name="conversation_deleted">Unterhaltung gelöscht!</string>
|
||||
<string name="only_delete_conversation">Nur die Unterhaltung löschen</string>
|
||||
<string name="conversation_deleted">Chat-Inhalte gelöscht!</string>
|
||||
<string name="only_delete_conversation">Nur die Chat-Inhalte löschen</string>
|
||||
<string name="info_view_search_button">Suchen</string>
|
||||
<string name="info_view_video_button">Video</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">Sie können in der Chatliste weiterhin die Unterhaltung mit %1$s einsehen.</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">Sie können in der Chat-Liste weiterhin die Unterhaltung mit %1$s einsehen.</string>
|
||||
<string name="paste_link">Link einfügen</string>
|
||||
<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">Erreichbare Chat-Symbolleiste</string>
|
||||
<string name="one_hand_ui">Chat-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>
|
||||
@@ -2106,11 +2106,11 @@
|
||||
<string name="delete_contact_cannot_undo_warning">Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="delete_without_notification">Ohne Benachrichtigung löschen</string>
|
||||
<string name="action_button_add_members">Einladen</string>
|
||||
<string name="keep_conversation">Unterhaltung behalten</string>
|
||||
<string name="keep_conversation">Chat-Inhalte beibehalten</string>
|
||||
<string name="cant_call_member_send_message_alert_text">Nachricht senden, um Anrufe zu aktivieren.</string>
|
||||
<string name="you_can_still_send_messages_to_contact">Sie können aus den archivierten Kontakten heraus Nachrichten an %1$s versenden.</string>
|
||||
<string name="toolbar_settings">Einstellungen</string>
|
||||
<string name="moderate_messages_will_be_deleted_warning">Die Nachrichten werden für alle Mitglieder gelöscht.</string>
|
||||
<string name="moderate_messages_will_be_deleted_warning">Die Nachrichten werden für alle Gruppenmitglieder gelöscht.</string>
|
||||
<string name="moderate_messages_will_be_marked_warning">Die Nachrichten werden für alle Mitglieder als moderiert markiert.</string>
|
||||
<string name="delete_members_messages__question">%d Nachrichten der Mitglieder löschen?</string>
|
||||
<string name="compose_message_placeholder">Nachricht</string>
|
||||
@@ -2120,7 +2120,7 @@
|
||||
<string name="select_verb">Auswählen</string>
|
||||
<string name="invite_friends_short">Einladen</string>
|
||||
<string name="network_option_tcp_connection">TCP-Verbindung</string>
|
||||
<string name="v6_0_increase_font_size">Schriftgröße erhöhen.</string>
|
||||
<string name="v6_0_increase_font_size">Schriftgröße anpassen.</string>
|
||||
<string name="v6_0_new_chat_experience">Neue Chat-Erfahrung 🎉</string>
|
||||
<string name="v6_0_upgrade_app">Die App automatisch aktualisieren</string>
|
||||
<string name="v6_0_connection_servers_status_descr">Verbindungs- und Server-Status.</string>
|
||||
@@ -2136,8 +2136,8 @@
|
||||
<string name="v6_0_your_contacts_descr">Kontakte für spätere Chats archivieren.</string>
|
||||
<string name="v6_0_private_routing_descr">Ihre IP-Adresse und Verbindungen werden geschützt.</string>
|
||||
<string name="v6_0_delete_many_messages_descr">Löschen Sie bis zu 20 Nachrichten auf einmal.</string>
|
||||
<string name="v6_0_reachable_chat_toolbar">Erreichbare Chat-Symbolleiste</string>
|
||||
<string name="v6_0_reachable_chat_toolbar_descr">Die App mit einer Hand nutzen.</string>
|
||||
<string name="v6_0_reachable_chat_toolbar">Chat-Symbolleiste unten</string>
|
||||
<string name="v6_0_reachable_chat_toolbar_descr">Die App mit einer Hand bedienen.</string>
|
||||
<string name="v6_0_connect_faster_descr">Schneller mit Ihren Freunden verbinden.</string>
|
||||
<string name="chat_database_exported_title">Chat-Datenbank wurde exportiert</string>
|
||||
<string name="chat_database_exported_continue">Weiter</string>
|
||||
@@ -2151,4 +2151,62 @@
|
||||
<string name="new_message">Neue Nachricht</string>
|
||||
<string name="error_parsing_uri_desc">Bitte überprüfen Sie, ob der SimpleX-Link korrekt ist.</string>
|
||||
<string name="error_parsing_uri_title">Ungültiger Link</string>
|
||||
<string name="settings_section_title_chat_database">CHAT-DATENBANK</string>
|
||||
<string name="switching_profile_error_title">Fehler beim Wechseln des Profils</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Die Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="new_chat_share_profile">Profil teilen</string>
|
||||
<string name="system_mode_toast">System-Modus</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Das hochgeladene Datenbank-Archiv wird dauerhaft von den Servern entfernt.</string>
|
||||
<string name="select_chat_profile">Chat-Profil auswählen</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Archiv entfernen?</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Ihre Anmeldeinformationen können unverschlüsselt versendet werden.</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Verwenden Sie keine Anmeldeinformationen mit einem Proxy.</string>
|
||||
<string name="switching_profile_error_message">Ihre Verbindung wurde auf %s verschoben, aber während der Weiterleitung auf das Profil trat ein unerwarteter Fehler auf.</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Stellen Sie sicher, dass die Proxy-Konfiguration richtig ist.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Fehler beim Speichern des Proxys</string>
|
||||
<string name="network_proxy_password">Passwort</string>
|
||||
<string name="network_proxy_auth">Proxy-Authentifizierung</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Verwenden Sie für jede Verbindung unterschiedliche Proxy-Anmeldeinformationen.</string>
|
||||
<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="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>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten.</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Es gibt nichts zum Weiterleiten!</string>
|
||||
<string name="n_other_file_errors">%1$d andere(r) Datei-Fehler.</string>
|
||||
<string name="forward_alert_title_messages_to_forward">%1$s Nachricht(en) weiterleiten?</string>
|
||||
<string name="forward_files_missing_desc">%1$d Datei(en) wurde(n) gelöscht.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d Datei(en) wurde(n) nicht heruntergeladen.</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Nachrichten ohne Dateien weiterleiten?</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s Nachrichten wurden nicht weitergeleitet</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Herunterladen</string>
|
||||
<string name="compose_forward_messages_n">Es wird/werden %1$s Nachricht(en) weitergeleitet</string>
|
||||
<string name="forward_multiple">Nachrichten werden weitergeleitet…</string>
|
||||
<string name="compose_save_messages_n">Es wird/werden %1$s Nachricht(en) gesichert</string>
|
||||
<string name="icon_descr_sound_muted">Ton stummgeschaltet</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Fehler bei der Initialisierung von WebView. Stellen Sie sicher, dass Sie WebView installiert haben, und es die ARM64-Architektur unterstützt.\nFehler: %s</string>
|
||||
<string name="settings_section_title_message_shape">Form der Nachricht</string>
|
||||
<string name="settings_message_shape_tail">Sprechblase</string>
|
||||
<string name="settings_message_shape_corner">Abrundung Ecken</string>
|
||||
<string name="network_session_mode_session">App-Sitzung</string>
|
||||
<string name="network_session_mode_server_description">Für jeden Server werden neue SOCKS-Anmeldeinformationen genutzt</string>
|
||||
<string name="network_session_mode_server">Server</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Klicken Sie auf die Info-Schaltfläche neben dem Adressfeld, um die Verwendung des Mikrofons zu erlauben.</string>
|
||||
<string name="call_desktop_permission_denied_title">Um Anrufe durchzuführen, erlauben Sie die Nutzung Ihres Mikrofons. Beenden Sie den Anruf und versuchen Sie es erneut.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Öffnen Sie die Safari-Einstellungen / Webseiten / Mikrofon und wählen Sie dann \"Für Localhost erlauben\".</string>
|
||||
<string name="v6_1_better_calls">Verbesserte Anrufe</string>
|
||||
<string name="v6_1_customizable_message_descr">Anpassbares Format des Nachrichtenfelds</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Bis zu 200 Nachrichten löschen oder moderieren</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Bis zu 20 Nachrichten auf einmal weiterleiten</string>
|
||||
<string name="v6_1_better_security_descr">Die SimpleX-Protokolle wurden von Trail of Bits überprüft.</string>
|
||||
<string name="v6_1_better_calls_descr">Während des Anrufs zwischen Audio und Video wechseln</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">Das Chat-Profil für Einmal-Einladungen wechseln</string>
|
||||
<string name="network_session_mode_session_description">Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt</string>
|
||||
<string name="v6_1_better_security">Verbesserte Sicherheit ✅</string>
|
||||
<string name="v6_1_message_dates_descr">Verbesserte Nachrichten-Datumsinformation</string>
|
||||
<string name="v6_1_better_user_experience">Verbesserte Nutzer-Erfahrung</string>
|
||||
</resources>
|
||||
@@ -227,4 +227,20 @@
|
||||
<string name="deleted_description">διαγράφτηκε</string>
|
||||
<string name="moderated_item_description">με συντονιστή %s</string>
|
||||
<string name="blocked_item_description">φραγμένος</string>
|
||||
<string name="learn_more_about_address">Σχετικά με τη διεύθυνση SimpleX</string>
|
||||
<string name="conn_event_ratchet_sync_started">συμφωνία κρυπτογράφησης…</string>
|
||||
<string name="chat_theme_apply_to_all_modes">Όλες οι χρωματικές λειτουργίες</string>
|
||||
<string name="abort_switch_receiving_address_desc">Η αλλαγή διεύθυνσης θα ακυρωθεί. Θα χρησιμοποιηθεί η παλιά διεύθυνση παραλαβής.</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Ενεργές συνδέσεις</string>
|
||||
<string name="network_settings_title">Προχωρημένες ρυθμίσεις</string>
|
||||
<string name="color_primary_variant">Πρόσθετη προφορά</string>
|
||||
<string name="add_contact_tab">Προσθήκη επαφής</string>
|
||||
<string name="abort_switch_receiving_address">Διακοπή αλλαγής διεύθυνσης</string>
|
||||
<string name="wallpaper_advanced_settings">Προχωρημένες ρυθμίσεις</string>
|
||||
<string name="v5_6_safer_groups_descr">Οι διαχειριστές μπορούν να αποκλείσουν ένα μέλος για όλους.</string>
|
||||
<string name="acknowledged">Αναγνωρισμένο</string>
|
||||
<string name="above_then_preposition_continuation">παραπάνω, λοιπόν:</string>
|
||||
<string name="add_address_to_your_profile">Προσθέστε τη διεύθυνση στο προφίλ σας, έτσι ώστε οι επαφές σας να μπορούν να τη μοιραστούν με άλλα άτομα. Το ενημέρωμένο προφίλ θα σταλεί στις επαφές σας.</string>
|
||||
<string name="feature_roles_admins">διαχειριστές</string>
|
||||
<string name="acknowledgement_errors">Λάθη αναγνώρισης</string>
|
||||
</resources>
|
||||
@@ -83,7 +83,7 @@
|
||||
<string name="database_passphrase_will_be_updated">La contraseña de cifrado de la base de datos será actualizada.</string>
|
||||
<string name="info_row_database_id">ID base de datos</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Los mensajes directos entre miembros del grupo no están permitidos.</string>
|
||||
<string name="passphrase_is_different">La contraseña de la base de datos es distinta a la almacenada en Keystore.</string>
|
||||
<string name="passphrase_is_different">La contraseña de la base de datos es diferente a la almacenada en Keystore.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">La base de datos será cifrada y la contraseña se guardará en Keystore.</string>
|
||||
<string name="delete_contact_question">¿Eliminar contacto\?</string>
|
||||
<string name="delete_message__question">¿Eliminar mensaje\?</string>
|
||||
@@ -366,7 +366,7 @@
|
||||
<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>
|
||||
<string name="failed_to_parse_chats_title">No se pudieron cargar los chats</string>
|
||||
<string name="failed_to_parse_chats_title">Fallo en la carga de chats</string>
|
||||
<string name="simplex_link_mode_full">Enlace completo</string>
|
||||
<string name="error_deleting_contact">Error al eliminar contacto</string>
|
||||
<string name="error_joining_group">Error al unirte al grupo</string>
|
||||
@@ -562,7 +562,7 @@
|
||||
<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="users_delete_data_only">Sólo datos del perfil</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>
|
||||
<string name="marked_deleted_description">marcado eliminado</string>
|
||||
@@ -579,7 +579,7 @@
|
||||
<string name="make_private_connection">Establecer una conexión privada</string>
|
||||
<string name="network_error_desc">Comprueba tu conexión de red con %1$s e inténtalo de nuevo.</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">El remitente puede haber eliminado la solicitud de conexión.</string>
|
||||
<string name="error_smp_test_certificate">Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta</string>
|
||||
<string name="error_smp_test_certificate">Posiblemente la huella del certificado en la dirección del servidor es incorrecta</string>
|
||||
<string name="reply_verb">Responder</string>
|
||||
<string name="save_passphrase_in_keychain">Guardar contraseña en Keystore</string>
|
||||
<string name="database_restore_error">Error al restaurar base de datos</string>
|
||||
@@ -668,7 +668,7 @@
|
||||
<string name="receiving_via">Recibiendo vía</string>
|
||||
<string name="network_option_protocol_timeout">Timeout protocolo</string>
|
||||
<string name="network_option_seconds_label">seg</string>
|
||||
<string name="users_delete_with_connections">Datos del perfil y conexiones</string>
|
||||
<string name="users_delete_with_connections">Eliminar perfil y conexiones</string>
|
||||
<string name="prohibit_sending_disappearing_messages">No se permiten mensajes temporales.</string>
|
||||
<string name="only_you_can_send_voice">Sólo tú puedes enviar mensajes de voz.</string>
|
||||
<string name="only_your_contact_can_send_voice">Sólo tu contacto puede enviar mensajes de voz.</string>
|
||||
@@ -820,7 +820,7 @@
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Intentando conectar con el servidor para recibir mensajes de este contacto.</string>
|
||||
<string name="unknown_message_format">formato de mensaje desconocido</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Intentando conectar con el servidor para recibir mensajes de este contacto (error: %1$s).</string>
|
||||
<string name="error_smp_test_failed_at_step">Prueba fallida en el paso %s.</string>
|
||||
<string name="error_smp_test_failed_at_step">Prueba no superada en el paso %s.</string>
|
||||
<string name="tap_to_start_new_chat">Pulsa para iniciar chat nuevo</string>
|
||||
<string name="share_message">Compartir mensaje…</string>
|
||||
<string name="share_image">Compartir medios…</string>
|
||||
@@ -830,10 +830,10 @@
|
||||
<string name="chat_help_tap_button">Pulsa el botón</string>
|
||||
<string name="to_start_a_new_chat_help_header">Para iniciar un chat nuevo</string>
|
||||
<string name="switch_receiving_address">Cambiar servidor de recepción</string>
|
||||
<string name="group_is_decentralized">Completamente descentralizado y sólo visible para los miembros.</string>
|
||||
<string name="group_is_decentralized">Totalmente descentralizado. Visible sólo para los miembros.</string>
|
||||
<string name="to_connect_via_link_title">Para conectarte mediante enlace</string>
|
||||
<string name="smp_servers_test_failed">¡Error en prueba del servidor!</string>
|
||||
<string name="smp_servers_test_some_failed">Algunos servidores no superaron la prueba:</string>
|
||||
<string name="smp_servers_test_failed">¡Prueba no superada!</string>
|
||||
<string name="smp_servers_test_some_failed">Algunos servidores no han superado la prueba:</string>
|
||||
<string name="smp_servers_use_server">Usar servidor</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Usar para conexiones nuevas</string>
|
||||
<string name="theme_system">Sistema</string>
|
||||
@@ -949,7 +949,7 @@
|
||||
<string name="your_chat_profiles">Mis perfiles</string>
|
||||
<string name="your_simplex_contact_address">Mi dirección SimpleX</string>
|
||||
<string name="smp_servers_your_server">Tu servidor</string>
|
||||
<string name="smp_servers_your_server_address">Dirección de tu servidor</string>
|
||||
<string name="smp_servers_your_server_address">Dirección del servidor</string>
|
||||
<string name="your_current_profile">Tu perfil actual</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil.</string>
|
||||
<string name="language_system">Sistema</string>
|
||||
@@ -971,7 +971,7 @@
|
||||
<string name="button_welcome_message">Mensaje de bienvenida</string>
|
||||
<string name="group_welcome_title">Mensaje de bienvenida</string>
|
||||
<string name="make_profile_private">¡Hacer perfil privado!</string>
|
||||
<string name="dont_show_again">No mostrar de nuevo</string>
|
||||
<string name="dont_show_again">No volver a mostrar</string>
|
||||
<string name="muted_when_inactive">¡Silenciado cuando está inactivo!</string>
|
||||
<string name="v4_6_group_moderation">Moderación de grupos</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Perfiles ocultos</string>
|
||||
@@ -1347,7 +1347,7 @@
|
||||
<string name="connect_via_link_incognito">Conectar en incógnito</string>
|
||||
<string name="turn_off_battery_optimization_button">Permitir</string>
|
||||
<string name="turn_off_system_restriction_button">Abrir configuración</string>
|
||||
<string name="connect__a_new_random_profile_will_be_shared">Se compartirá un perfil nuevo aleatorio.</string>
|
||||
<string name="connect__a_new_random_profile_will_be_shared">Compartirás un perfil nuevo aleatorio.</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">Pega el enlace recibido para conectar con tu contacto…</string>
|
||||
<string name="connect__your_profile_will_be_shared">El perfil %1$s será compartido.</string>
|
||||
<string name="disable_notifications_button">Desactivar notificaciones</string>
|
||||
@@ -1383,7 +1383,7 @@
|
||||
<string name="v5_3_new_desktop_app">Nueva aplicación para ordenador!</string>
|
||||
<string name="v5_3_new_interface_languages">6 idiomas nuevos para el interfaz</string>
|
||||
<string name="v5_3_encrypt_local_files_descr">Cifrado de los nuevos archivos locales (excepto vídeos).</string>
|
||||
<string name="compose_send_direct_message_to_connect">Enviar mensaje directo para conectar</string>
|
||||
<string name="compose_send_direct_message_to_connect">Envía un mensaje para conectar</string>
|
||||
<string name="v5_3_discover_join_groups">Descubre y únete a grupos</string>
|
||||
<string name="v5_3_simpler_incognito_mode">Modo incógnito simplificado</string>
|
||||
<string name="v5_3_new_interface_languages_descr">Árabe, Búlgaro, Finlandés, Hebreo, Tailandés y Ucraniano - gracias a los usuarios y Weblate.</string>
|
||||
@@ -1450,7 +1450,7 @@
|
||||
<string name="bad_desktop_address">Dirección ordenador incorrecta</string>
|
||||
<string name="devices">Dispositivo</string>
|
||||
<string name="non_content_uri_alert_title">Ruta archivo no valida.</string>
|
||||
<string name="disconnect_desktop_question">¿Desconectar ordenador?</string>
|
||||
<string name="disconnect_desktop_question">¿Desconectar del ordenador?</string>
|
||||
<string name="block_member_desc">¡Los mensajes nuevos de %s estarán ocultos!</string>
|
||||
<string name="desktop_app_version_is_incompatible">La versión de aplicación del ordenador %s no es compatible con esta aplicación.</string>
|
||||
<string name="blocked_item_description">bloqueado</string>
|
||||
@@ -1627,9 +1627,9 @@
|
||||
<string name="past_member_vName">Miembro pasado %1$s</string>
|
||||
<string name="profile_update_event_member_name_changed">el miembro %1$s ha cambiado a %2$s</string>
|
||||
<string name="profile_update_event_removed_address">dirección de contacto eliminada</string>
|
||||
<string name="profile_update_event_removed_picture">imagen de perfil eliminada</string>
|
||||
<string name="profile_update_event_removed_picture">ha eliminado la imagen del perfil</string>
|
||||
<string name="profile_update_event_set_new_address">nueva dirección de contacto</string>
|
||||
<string name="profile_update_event_set_new_picture">nueva imagen de perfil</string>
|
||||
<string name="profile_update_event_set_new_picture">tiene nueva imagen del perfil</string>
|
||||
<string name="call_service_notification_audio_call">Llamada</string>
|
||||
<string name="call_service_notification_end_call">Llamada finalizada</string>
|
||||
<string name="call_service_notification_video_call">Videollamada</string>
|
||||
@@ -1686,7 +1686,7 @@
|
||||
<string name="migrate_from_device_finalize_migration">Finalizar migración</string>
|
||||
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Atención</b>: el archivo será eliminado.]]></string>
|
||||
<string name="migrate_from_device_check_connection_and_try_again">Comprueba tu conexión a internet y vuelve a intentarlo</string>
|
||||
<string name="migrate_from_device_confirm_you_remember_passphrase">Para migrar confirma que recuerdas la frase de contraseña de la base de datos.</string>
|
||||
<string name="migrate_from_device_confirm_you_remember_passphrase">Para migrar la base de datos confirma que recuerdas la frase de contraseña.</string>
|
||||
<string name="migrate_from_device_error_verifying_passphrase">Error al verificar la frase de contraseña:</string>
|
||||
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Recuerda</b>: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad.]]></string>
|
||||
<string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[En el nuevo dispositivo selecciona <i>Migrar desde otro dispositivo</i> y escanea el código QR.]]></string>
|
||||
@@ -2070,4 +2070,62 @@
|
||||
<string name="new_message">Nuevo mensaje</string>
|
||||
<string name="error_parsing_uri_title">Enlace no válido</string>
|
||||
<string name="error_parsing_uri_desc">Por favor, comprueba que el enlace SimpleX es correcto.</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d archivo(s) se está(n) descargando todavía.</string>
|
||||
<string name="n_other_file_errors">%1$d otro(s) error(es) de archivo.</string>
|
||||
<string name="settings_section_title_chat_database">BASE DE DATOS</string>
|
||||
<string name="error_forwarding_messages">Error en reenvío de mensajes</string>
|
||||
<string name="forward_alert_title_messages_to_forward">¿Reenviar %1$s mensaje(s)?</string>
|
||||
<string name="forward_multiple">Reenviar mensajes…</string>
|
||||
<string name="system_mode_toast">Modo de sistema</string>
|
||||
<string name="forward_alert_forward_messages_without_files">¿Reenviar mensajes sin los archivos?</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Asegúrate de que la configuración del proxy es correcta.</string>
|
||||
<string name="forward_files_missing_desc">%1$d archivo(s) ha(n) sido eliminado(s).</string>
|
||||
<string name="n_file_errors">%1$d error(es) de archivo
|
||||
\n%2$s</string>
|
||||
<string name="forward_files_failed_to_receive_desc">La descarga ha fallado para %1$d archivo(s).</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d archivo(s) no se ha(n) descargado.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s mensajes no enviados</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Descargar</string>
|
||||
<string name="compose_forward_messages_n">Reenviando %1$s mensajes</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Los mensajes han sido borrados después de seleccionarlos.</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">¡Nada para reenviar!</string>
|
||||
<string name="compose_save_messages_n">Guardando %1$s mensajes</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">No uses credenciales con proxy.</string>
|
||||
<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_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>
|
||||
<string name="migrate_from_device_remove_archive_question">¿Eliminar archivo?</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">El archivo de bases de datos subido será eliminado permanentemente de los servidores.</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Los mensajes serán eliminados. ¡No podrá deshacerse!</string>
|
||||
<string name="switching_profile_error_title">Error al cambiar perfil</string>
|
||||
<string name="select_chat_profile">Selecciona perfil de chat</string>
|
||||
<string name="new_chat_share_profile">Comparte perfil</string>
|
||||
<string name="switching_profile_error_message">Tu conexión ha sido trasladada a %s pero ha ocurrido un error inesperado al redirigirte al perfil.</string>
|
||||
<string name="icon_descr_sound_muted">Sonido silenciado</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Error al iniciar WebView. Asegúrate de tener WebView instalado y que sea compatible con la arquitectura amr64.\nError: %s</string>
|
||||
<string name="settings_section_title_message_shape">Forma del mensaje</string>
|
||||
<string name="settings_message_shape_corner">Esquinas</string>
|
||||
<string name="settings_message_shape_tail">Cola</string>
|
||||
<string name="network_session_mode_session_description">Se usarán credenciales SOCKS nuevas cada vez que inicies la aplicación.</string>
|
||||
<string name="network_session_mode_session">Sesión de aplicación</string>
|
||||
<string name="call_desktop_permission_denied_safari">Abre la configuración de Safari / Sitios Web / Micrófono y a continuación selecciona Permitir para localhost.</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Pulsa el botón info del campo dirección para permitir el uso del micrófono.</string>
|
||||
<string name="call_desktop_permission_denied_title">Para hacer llamadas, permite el uso del micrófono. Cuelga e intenta llamar de nuevo.</string>
|
||||
<string name="network_session_mode_server_description">Se usarán credenciales SOCKS nuevas por cada servidor.</string>
|
||||
<string name="network_session_mode_server">Servidor</string>
|
||||
<string name="v6_1_better_calls">Llamadas mejoradas</string>
|
||||
<string name="v6_1_message_dates_descr">Sistema de fechas mejorado.</string>
|
||||
<string name="v6_1_better_user_experience">Experiencia de usuario mejorada</string>
|
||||
<string name="v6_1_customizable_message_descr">Forma personalizable de los mensajes.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Desplazamiento de hasta 20 mensajes.</string>
|
||||
<string name="v6_1_better_security_descr">Protocolos de SimpleX auditados por Trail of Bits.</string>
|
||||
<string name="v6_1_better_calls_descr">Intercambia audio y video durante la llamada.</string>
|
||||
<string name="v6_1_better_security">Seguridad mejorada ✅</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Borra o modera hasta 200 mensajes a la vez.</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">Cambia el perfil de chat para invitaciones de un solo uso.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M497-481q28-30.5 43.25-70.5T555.5-634q0-42.5-15-82.5T497-787q57.5 8 95.5 51.75t38 101.25q0 57.5-38 101.25T497-481Zm199 308q10-17 15.25-36t5.25-39.09v-38.27q0-34.45-15-65.54-15-31.1-40-55.1 48.23 17.4 89.36 44.95Q792-334.5 792-286.5v38.5q0 30.94-22.03 52.97Q747.94-173 717-173h-21Zm96-348.5h-41.5q-15.5 0-26.5-11T713-559q0-15.5 11-26.5t26.5-11H792V-638q0-15.5 11-26.5t26.5-11q15.5 0 26.5 11t11 26.5v41.5h41.5q15.5 0 26.5 11t11 26.5q0 15.5-11 26.5t-26.5 11H867v41.5q0 15.5-11 26.5t-26.5 11q-15.5 0-26.5-11T792-480v-41.5ZM325.5-479q-64.5 0-109.75-45.25T170.5-634q0-64.5 45.25-109.75T325.5-789q64.5 0 109.75 45.25T480.5-634q0 64.5-45.25 109.75T325.5-479Zm-311 231v-31.03q0-32.97 16.75-60.22t45.27-41.76Q137.5-411 199.75-426.25 262-441.5 325.5-441.5t125.75 15.25Q513.5-411 574.48-381.01q28.52 14.51 45.27 41.76Q636.5-312 636.5-279.03V-248q0 30.94-22.03 52.97Q592.44-173 561.5-173h-472q-30.94 0-52.97-22.03Q14.5-217.06 14.5-248Zm311-306q33 0 56.5-23.5t23.5-56.5q0-33-23.5-56.5T325.5-714q-33 0-56.5 23.5T245.5-634q0 33 23.5 56.5t56.5 23.5Zm-236 306h472v-31q0-11.19-5.5-20.34-5.5-9.16-15-14.16-53.5-26.5-107.17-39.75-53.68-13.25-108.33-13.25-55 0-108.5 13.25T110-313.5q-9.5 5-15 14.16-5.5 9.15-5.5 20.34v31Zm236-386Zm0 386Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M483.82-81.5q-83.12 0-156.43-31.82-73.32-31.81-127.85-86.25Q145-254 113.25-327.25 81.5-400.5 81.5-484q0-131.24 77-236.62t202.35-146.46q16.05-4.92 29.85 4.83t11.8 25.75q-6.5 88.5 24 170.4t94 145.35Q583.5-458 665.75-427t170.75 24q15-1.5 24.75 12.25t5.25 29.25q-40 125.5-145.35 202.75Q615.81-81.5 483.82-81.5ZM484-139q100.95 0 183.22-57.5Q749.5-254 800-343q-90.29-7.95-173.55-41.22Q543.2-417.5 479.5-481q-63.96-62.86-96.73-145.68Q350-709.5 342.5-798.5q-89 48.5-146.25 131.03Q139-584.95 139-484q0 144.38 100.31 244.69T484-139Zm-5-342Z"/></svg>
|
||||
|
After Width: | Height: | Size: 636 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M182-85q-22.97 0-40.23-17.27-17.27-17.26-17.27-40.23v-616q0-22.97 17.27-40.23Q159.03-816 182-816h67.5v-60H312v60h336v-60h62.5v60H778q22.97 0 40.23 17.27 17.27 17.26 17.27 40.23v616q0 22.97-17.27 40.23Q800.97-85 778-85H182Zm0-57.5h596V-569H182v426.5Zm0-484h596v-132H182v132Zm0 0v-132 132Zm298.05 226q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm-160 0q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm320 0q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm-160 160q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm-160 0q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm320 0q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg>
|
||||
|
After Width: | Height: | Size: 569 B |
@@ -2,16 +2,16 @@
|
||||
<resources>
|
||||
<string name="group_info_section_title_num_members">%1$s ANGGOTA</string>
|
||||
<string name="address_section_title">Alamat</string>
|
||||
<string name="moderated_items_description">%1$d pesan dihapus admin %2$s</string>
|
||||
<string name="moderated_items_description">%1$d pesan dimoderasi oleh %2$s</string>
|
||||
<string name="send_disappearing_message_1_minute">1 menit</string>
|
||||
<string name="send_disappearing_message_30_seconds">30 detik</string>
|
||||
<string name="send_disappearing_message_5_minutes">5 menit</string>
|
||||
<string name="clear_chat_warning">Semua pesan akan dihapus - ini tidak bisa dikembalikan! Pesan akan HANYA dihapus untukmu.</string>
|
||||
<string name="accept_connection_request__question">Terima permintaan relasi?</string>
|
||||
<string name="accept_connection_request__question">Terima permintaan koneksi?</string>
|
||||
<string name="learn_more_about_address">Tentang alamat SimpleX</string>
|
||||
<string name="add_contact_tab">Tambah kontak</string>
|
||||
<string name="about_simplex">Tentang SimpleX</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d gagal mendekripsi pesan.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d pesan gagal terdekripsi.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Tolong laporkan hal ini ke pengembang.</string>
|
||||
<string name="chat_item_ttl_month">1 bulan</string>
|
||||
<string name="chat_item_ttl_week">1 minggu</string>
|
||||
@@ -19,7 +19,7 @@
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Anda sudah bergabung dalam grup <b>%1$s</b>.]]></string>
|
||||
<string name="call_service_notification_audio_call">Panggilan suara</string>
|
||||
<string name="abort_switch_receiving_address_confirm">Batal</string>
|
||||
<string name="allow_voice_messages_question">izinkan pesan suara?</string>
|
||||
<string name="allow_voice_messages_question">Izinkan pesan suara?</string>
|
||||
<string name="accept_contact_button">Terima</string>
|
||||
<string name="app_version_title">Versi aplikasi</string>
|
||||
<string name="app_version_name">Versi aplikasi: v%s</string>
|
||||
@@ -30,13 +30,13 @@
|
||||
<string name="users_add">Tambah profil</string>
|
||||
<string name="color_primary">Corak</string>
|
||||
<string name="color_secondary_variant">Tambahan sekunder</string>
|
||||
<string name="theme_destination_app_theme">Motif aplikasi</string>
|
||||
<string name="theme_destination_app_theme">Tema aplikasi</string>
|
||||
<string name="chat_preferences_always">selalu</string>
|
||||
<string name="allow_to_send_voice">diizinkan mengirim pesan suara.</string>
|
||||
<string name="allow_to_send_voice">Izinkan mengirim pesan suara.</string>
|
||||
<string name="feature_roles_all_members">semua anggota</string>
|
||||
<string name="v4_6_audio_video_calls">Panggilan suara dan video</string>
|
||||
<string name="v5_2_more_things">Hal-hal lain</string>
|
||||
<string name="connect_plan_already_connecting">Sudah menghubungi!</string>
|
||||
<string name="v5_2_more_things">Beberapa hal lainnya</string>
|
||||
<string name="connect_plan_already_connecting">Sudah terhubungkan!</string>
|
||||
<string name="connect_plan_already_joining_the_group">Sudah bergabung dengan grup!</string>
|
||||
<string name="contact_wants_to_connect_via_call">%1$s ingin menghubungimu lewat</string>
|
||||
<string name="abort_switch_receiving_address_question">Batalkan penggantian alamat?</string>
|
||||
@@ -54,7 +54,7 @@
|
||||
<string name="bold_text">tebal</string>
|
||||
<string name="callstatus_calling">menelepon…</string>
|
||||
<string name="audio_device_bluetooth">Bluetooth</string>
|
||||
<string name="icon_descr_call_ended">Panggilan ditutup</string>
|
||||
<string name="icon_descr_call_ended">Panggilan diakhiri</string>
|
||||
<string name="change_verb">Ubah</string>
|
||||
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Peringatan</b>: arsip akan dihapus.]]></string>
|
||||
<string name="create_group_button_to_create_new_group"><![CDATA[<b>Buat grup</b>: untuk membuat grup baru.]]></string>
|
||||
@@ -112,4 +112,155 @@
|
||||
<string name="app_was_crashed">Tampilan macet</string>
|
||||
<string name="non_content_uri_alert_text">Anda membagikan lokasi file yang tidak valid. Laporkan masalah ini ke pengembang aplikasi.</string>
|
||||
<string name="server_error">error</string>
|
||||
<string name="one_time_link_short">Tuatan 1 kali pakai</string>
|
||||
<string name="integrity_msg_skipped">%1$d pesan yang terlewati</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d pesan yang dilewati</string>
|
||||
<string name="remote_host_error_bad_version"><![CDATA[Versi perangkat <b>%s</b> tidak didukung. Harap pastikan kamu menggunakan versi yang sama pada kedua perangkat.]]></string>
|
||||
<string name="remote_host_error_busy"><![CDATA[Perangkat <b>%s</b> sedang sibuk]]></string>
|
||||
<string name="remote_host_error_disconnected"><![CDATA[Perangkat <b>%s</b> tidak terhubung]]></string>
|
||||
<string name="remote_host_error_inactive"><![CDATA[Perangkat <b>%s</b> tidak aktif]]></string>
|
||||
<string name="remote_host_error_missing"><![CDATA[Perangkat <b>%s</b> tidak di temukan]]></string>
|
||||
<string name="accept_contact_incognito_button">Terima penyamaran</string>
|
||||
<string name="callstatus_accepted">panggilan diterima</string>
|
||||
<string name="v5_3_new_interface_languages">6 bahasa antarmuka baru</string>
|
||||
<string name="remote_host_was_disconnected_toast"><![CDATA[Perangkat <b>%s</b> tidak terhubung]]></string>
|
||||
<string name="la_no_app_password">Tidak ada kode sandi aplikasi</string>
|
||||
<string name="message_forwarded_desc">Belum ada koneksi langsung, pesan diteruskan oleh admin.</string>
|
||||
<string name="no_filtered_chats">Tidak ada obrolan yang difilter</string>
|
||||
<string name="selected_chat_items_nothing_selected">Tidak ada yang dipilih</string>
|
||||
<string name="images_limit_desc">Hanya 10 gambar dapat dikirim pada saat bersamaan</string>
|
||||
<string name="notifications">Notifikasi</string>
|
||||
<string name="only_delete_conversation">Hanya menghapus percakapan</string>
|
||||
<string name="v5_7_network_descr">Koneksi jaringan yang lebih handal.</string>
|
||||
<string name="v6_0_new_chat_experience">Pengalaman obrolan yang baru 🎉</string>
|
||||
<string name="v6_0_new_media_options">Pilihan media baru</string>
|
||||
<string name="allow_calls_question">Izinkan panggilan?</string>
|
||||
<string name="new_message">Pesan baru</string>
|
||||
<string name="only_you_can_send_disappearing">Hanya kamu yang dapat mengirim pesan menghilang.</string>
|
||||
<string name="first_platform_without_user_ids">Tidak ada pengidentifikasi pengguna.</string>
|
||||
<string name="only_group_owners_can_change_prefs">Hanya pemilik grup yang dapat mengubah preferensi grup.</string>
|
||||
<string name="rcv_group_and_other_events">dan %d peristiwa lainnya</string>
|
||||
<string name="new_member_role">Peran baru anggota</string>
|
||||
<string name="no_contacts_selected">Tidak ada kontak di pilih</string>
|
||||
<string name="no_contacts_to_add">Tidak ada kontak untuk ditambahkan</string>
|
||||
<string name="item_info_no_text">Tidak ada teks</string>
|
||||
<string name="block_member_desc">Semua pesan baru dari %s akan disembunyikan!</string>
|
||||
<string name="message_queue_info_none">tidak ada</string>
|
||||
<string name="network_status">Status jaringan</string>
|
||||
<string name="users_delete_all_chats_deleted">Seluruh obrolan dan pesan akan dihapus - ini tidak bisa dibatalkan!</string>
|
||||
<string name="no_connected_mobile">Tidak ada perangkat terkoneksi</string>
|
||||
<string name="system_restricted_background_in_call_title">Tidak ada panggilan latar</string>
|
||||
<string name="settings_notification_preview_title">Pratinjau notifikasi</string>
|
||||
<string name="settings_notifications_mode_title">Layanan notifikasi</string>
|
||||
<string name="notifications_mode_service">Selalu aktif</string>
|
||||
<string name="notification_new_contact_request">Permintaan kontak baru</string>
|
||||
<string name="notification_preview_new_message">Pesan baru</string>
|
||||
<string name="message_delivery_error_desc">Kemungkinan besar kontak ini telah menghapus koneksi dengan kamu.</string>
|
||||
<string name="snd_error_expired">Masalah jaringan - pesan kadaluwarsa setelah beberapa kali mencoba mengirim.</string>
|
||||
<string name="no_selected_chat">Tidak ada obrolan dipilih</string>
|
||||
<string name="only_owners_can_enable_files_and_media">Hanya pemilik grup yang dapat mengaktifkan file dan media.</string>
|
||||
<string name="only_stored_on_members_devices">(hanya disimpan oleh anggota grup)</string>
|
||||
<string name="icon_descr_more_button">Lagi</string>
|
||||
<string name="one_time_link">Tautan undangan satu kali</string>
|
||||
<string name="new_chat">Obrolan baru</string>
|
||||
<string name="network_and_servers">Jaringan & server</string>
|
||||
<string name="network_settings_title">Pengaturan tingkat lanjut</string>
|
||||
<string name="network_use_onion_hosts_required_desc">Host Onion akan diperlukan untuk koneksi.
|
||||
\nHarap diperhatikan: Anda tidak akan dapat terhubung ke server tanpa alamat .onion.</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Host Onion tidak akan digunakan.</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Selalu gunakan perutean pribadi.</string>
|
||||
<string name="shutdown_alert_desc">Pemberitahuan akan berhenti bekerja sampai kamu meluncurkan ulang aplikasi</string>
|
||||
<string name="all_your_contacts_will_remain_connected_update_sent">Semua kontak kamu akan tetap terhubung. Pembaruan profil akan dikirim ke kontak kamu.</string>
|
||||
<string name="status_no_e2e_encryption">Tidak ada enkripsi ujung-ujung</string>
|
||||
<string name="new_passcode">Kode sandi baru</string>
|
||||
<string name="la_mode_off">Mati</string>
|
||||
<string name="all_app_data_will_be_cleared">Seluruh data aplikasi dihapus.</string>
|
||||
<string name="new_database_archive">Arsip database baru</string>
|
||||
<string name="old_database_archive">Arsip database lama</string>
|
||||
<string name="chat_item_ttl_none">tidak pernah</string>
|
||||
<string name="no_received_app_files">Tidak ada file yang diterima atau dikirim</string>
|
||||
<string name="conn_event_ratchet_sync_started">Menyetujui enkripsi…</string>
|
||||
<string name="muted_when_inactive">Bisukan ketika tidak aktif!</string>
|
||||
<string name="chat_theme_apply_to_all_modes">Seluruh mode warna</string>
|
||||
<string name="chat_preferences_off">mati</string>`
|
||||
<string name="chat_preferences_no">tidak</string>
|
||||
<string name="chat_preferences_on">on</string>
|
||||
<string name="feature_off">mati</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Izinkan kontak kamu menghapus pesan terkirim secara permanen. (24 jam)</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Izinkan penghapusan pesan yang tidak dapat diubah hanya jika kontak kamu mengizinkannya. (24 jam)</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Izinkan kontak kamu mengirim pesan suara.</string>
|
||||
<string name="only_you_can_delete_messages">Hanya kamu yang dapat menghapus pesan secara permanen (kontak kamu dapat menandainya untuk dihapus). (24 jam)</string>
|
||||
<string name="only_your_contact_can_delete">Hanya kontak kamu yang dapat menghapus pesan secara permanen (kamu dapat menandainya untuk dihapus). (24 jam)</string>
|
||||
<string name="only_you_can_add_message_reactions">Hanya kamu yang dapat menambahkan reaksi pesan.</string>
|
||||
<string name="only_you_can_make_calls">Hanya kamu yang dapat melakukan panggilan.</string>
|
||||
<string name="only_your_contact_can_add_message_reactions">Hanya kontak kamu yang dapat menambahkan reaksi pesan.</string>
|
||||
<string name="allow_direct_messages">Izinkan pengiriman pesan langsung ke anggota.</string>
|
||||
<string name="allow_to_send_disappearing">Izinkan untuk mengirim pesan menghilang.</string>
|
||||
<string name="feature_offered_item_with_param">ditawarkan %s: %2s</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Beberapa profil obrolan</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Lebih banyak peningkatan akan segera hadir!</string>
|
||||
<string name="v4_6_group_moderation_descr">Tidak ada admin yang dapat:
|
||||
\n- menghapus pesan anggota.
|
||||
\n- menonaktifkan anggota (peran “pengamat”)</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">Lebih banyak peningkatan akan segera hadir!</string>
|
||||
<string name="v5_2_more_things_descr">- pengiriman pesan yang lebih stabil.
|
||||
\n- group yang sedikit lebih baik.
|
||||
\n- dan lainnya!</string>
|
||||
<string name="v5_3_new_desktop_app">Aplikasi desktop baru!</string>
|
||||
<string name="v5_7_network">Manajemen jaringan</string>
|
||||
<string name="custom_time_unit_months">bulan</string>
|
||||
<string name="migrate_from_device_all_data_will_be_uploaded">Semua kontak, percakapan, dan file kamu akan dienkripsi dengan aman dan diunggah dalam beberapa bagian ke relay XFTP yang telah dikonfigurasi.</string>
|
||||
<string name="network_type_no_network_connection">Tidak ada koneksi jaringan</string>
|
||||
<string name="mute_chat">Bisu</string>
|
||||
<string name="settings_section_title_network_connection">Koneksi jaringan</string>
|
||||
<string name="network_smp_proxy_mode_never">TIdak pernah</string>
|
||||
<string name="privacy_media_blur_radius_off">Mati</string>
|
||||
<string name="videos_limit_desc">Hanya 10 video dapat dikirim pada saat bersamaan</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Hanya perangkat klien yang menyimpan profil pengguna, kontak, grup, dan pesan yang dikirim dengan <b>enkripsi ujung-ke-ujung 2 lapis</b>.]]></string>
|
||||
<string name="only_group_owners_can_enable_voice">Hanya pemilik grup yang dapat mengaktifkan pesan suara.</string>
|
||||
<string name="clear_note_folder_warning">Seluruh pesan akan dihapus - ini tidak bisa dibatalkan!</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Izinkan turun versi</string>
|
||||
<string name="no_info_on_delivery">Tidak ada informasi pengiriman</string>
|
||||
<string name="allow_verb">Boleh</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">Tidak ada rincian</string>
|
||||
<string name="add_contact">Tautan undangan satu kali</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Host Onion akan di pakai jika tersedia.</string>
|
||||
<string name="always_use_relay">Selalu gunakan relay</string>
|
||||
<string name="self_destruct_new_display_name">Nama tampilan baru:</string>
|
||||
<string name="new_passphrase">Frasa sandi baru</string>
|
||||
<string name="notifications_will_be_hidden">Pemberitahuan akan dikirim hanya sampai saat aplikasi berhenti!</string>
|
||||
<string name="wallpaper_advanced_settings">Pengaturan tingkat lanjut</string>
|
||||
<string name="allow_message_reactions_only_if">Izinkan reaksi pesan hanya jika kontak Anda mengizinkannya.</string>
|
||||
<string name="allow_voice_messages_only_if">Izinkan pesan suara hanya jika kontak kamu mengizinkannya.</string>
|
||||
<string name="allow_your_contacts_adding_message_reactions">Izinkan kontak kamu menambahkan reaksi pesan.</string>
|
||||
<string name="allow_your_contacts_to_call">Izinkan kontak kamu untuk menghubungimu.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Izinkan kontak kamu untuk mengirim pesan menghilang.</string>
|
||||
<string name="allow_calls_only_if">Izinkan panggilan hanya jika kontak kamu mengizinkannya.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Izinkan pesan menghilang hanya jika kontak kamu mengizinkannya.</string>
|
||||
<string name="allow_message_reactions">Izinkan reaksi pesan.</string>
|
||||
<string name="allow_to_delete_messages">Izinkan untuk menghapus pesan terkirim secara permanen. (24 jam)</string>
|
||||
<string name="user_mute">Bisu</string>
|
||||
<string name="no_history">Tidak ada riwayat</string>
|
||||
<string name="v5_8_chat_themes">Tema obrolan yang baru</string>
|
||||
<string name="all_users">Semua profil</string>
|
||||
<string name="allow_to_send_simplex_links">Izinkan untuk mengirim tautan SimpleX.</string>
|
||||
<string name="v5_1_self_destruct_passcode_descr">Semua data akan terhapus jika ini dimasukkan.</string>
|
||||
<string name="not_compatible">Tidak kompatibel!</string>
|
||||
<string name="new_mobile_device">Perangkat seluler baru</string>
|
||||
<string name="only_one_device_can_work_at_the_same_time">Hanya satu perangkat yang dapat bekerja pada saat bersamaan</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Tidak</string>
|
||||
<string name="no_filtered_contacts">Tidak ada kontak yang di filter</string>
|
||||
<string name="group_member_role_observer">pengamat</string>
|
||||
<string name="snd_conn_event_ratchet_sync_started">menyetujui enkripsi untuk %s…</string>
|
||||
<string name="all_group_members_will_remain_connected">Semua anggota grup akan tetap terhubung.</string>
|
||||
<string name="servers_info_missing">Tidak ada info, coba muat ulang</string>
|
||||
<string name="network_use_onion_hosts_no">Tidak</string>
|
||||
<string name="new_in_version">Baru di %s</string>
|
||||
<string name="feature_offered_item">ditawarkan %s</string>
|
||||
<string name="only_you_can_send_voice">Hanya kamu yang dapat mengirim pesan suara.</string>
|
||||
<string name="only_your_contact_can_make_calls">Hanya kontak kamu yang dapat melakukan panggilan.</string>
|
||||
<string name="allow_to_send_files">Izinkan untuk mengirim file dan media.</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Semua kontak kamu akan tetap terhubung.</string>
|
||||
<string name="forward_files_missing_desc">%1$d berkas telah dihapus.</string>
|
||||
</resources>
|
||||
@@ -413,7 +413,7 @@
|
||||
<string name="icon_descr_group_inactive">Gruppo inattivo</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">indirizzo cambiato per te</string>
|
||||
<string name="rcv_group_event_changed_member_role">ha cambiato il ruolo di %s in %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">cambiato il tuo ruolo in %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">ha cambiato il tuo ruolo in %s</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">cambio indirizzo…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">cambio indirizzo per %s…</string>
|
||||
<string name="rcv_group_event_member_connected">si è connesso/a</string>
|
||||
@@ -2069,4 +2069,63 @@
|
||||
<string name="new_message">Nuovo messaggio</string>
|
||||
<string name="error_parsing_uri_title">Link non valido</string>
|
||||
<string name="error_parsing_uri_desc">Controlla che il link SimpleX sia corretto.</string>
|
||||
<string name="switching_profile_error_title">Errore nel cambio di profilo</string>
|
||||
<string name="select_chat_profile">Seleziona il profilo di chat</string>
|
||||
<string name="new_chat_share_profile">Condividi il profilo</string>
|
||||
<string name="settings_section_title_chat_database">DATABASE DELLA CHAT</string>
|
||||
<string name="system_mode_toast">Modalità di sistema</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Rimuovere l\'archivio?</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">I messaggi verranno eliminati. Non è reversibile!</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">L\'archivio del database caricato verrà rimosso definitivamente dai server.</string>
|
||||
<string name="switching_profile_error_message">La tua connessione è stata spostata a %s, ma si è verificato un errore imprevisto durante il reindirizzamento al profilo.</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Non usare credenziali con proxy.</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Assicurati che la configurazione del proxy sia corretta.</string>
|
||||
<string name="network_proxy_auth">Autenticazione del proxy</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Usa diverse credenziali del proxy per ogni connessione.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Usa diverse credenziali del proxy per ogni profilo.</string>
|
||||
<string name="network_proxy_random_credentials">Usa credenziali casuali</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Le credenziali potrebbero essere inviate in chiaro.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Errore di salvataggio del proxy</string>
|
||||
<string name="network_proxy_password">Password</string>
|
||||
<string name="network_proxy_username">Nome utente</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d file è/sono ancora in scaricamento.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s messaggi non inoltrati</string>
|
||||
<string name="n_other_file_errors">%1$d altro/i errore/i di file.</string>
|
||||
<string name="error_forwarding_messages">Errore nell\'inoltro dei messaggi</string>
|
||||
<string name="n_file_errors">%1$d errore/i di file:
|
||||
\n%2$s</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Inoltrare %1$s messaggio/i?</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Inoltrare i messaggi senza file?</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">I messaggi sono stati eliminati dopo che li hai selezionati.</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Niente da inoltrare!</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d file ha/hanno fallito lo scaricamento.</string>
|
||||
<string name="forward_files_missing_desc">%1$d file è/sono stato/i eliminato/i.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d file non è/sono stato/i scaricato/i.</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Scarica</string>
|
||||
<string name="compose_forward_messages_n">Inoltro di %1$s messaggi</string>
|
||||
<string name="forward_multiple">Inoltra messaggi…</string>
|
||||
<string name="compose_save_messages_n">Salvataggio di %1$s messaggi</string>
|
||||
<string name="icon_descr_sound_muted">Audio silenziato</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Errore di inizializzazione di WebView. Assicurati di avere WebView installato e che la sua architettura supportata sia arm64.
|
||||
\nErrore: %s</string>
|
||||
<string name="settings_message_shape_corner">Angolo</string>
|
||||
<string name="settings_section_title_message_shape">Forma del messaggio</string>
|
||||
<string name="settings_message_shape_tail">Coda</string>
|
||||
<string name="network_session_mode_session">Sessione dell\'app</string>
|
||||
<string name="network_session_mode_session_description">Le nuove credenziali SOCKS verranno usate ogni volta che avvii l\'app.</string>
|
||||
<string name="network_session_mode_server_description">Le nuove credenziali SOCKS verranno usate per ogni server.</string>
|
||||
<string name="network_session_mode_server">Server</string>
|
||||
<string name="call_desktop_permission_denied_safari">Apri le impostazioni di Safari / Siti web / Microfono, quindi scegli Consenti per localhost.</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Clicca il pulsante info vicino al campo indirizzo per consentire l\'uso del microfono.</string>
|
||||
<string name="call_desktop_permission_denied_title">Per effettuare chiamate, consenti di usare il microfono. Termina la chiamata e cerca di richiamare.</string>
|
||||
<string name="v6_1_better_calls">Chiamate migliorate</string>
|
||||
<string name="v6_1_message_dates_descr">Date dei messaggi migliorate.</string>
|
||||
<string name="v6_1_better_security">Sicurezza migliorata ✅</string>
|
||||
<string name="v6_1_better_user_experience">Esperienza utente migliorata</string>
|
||||
<string name="v6_1_customizable_message_descr">Forma dei messaggi personalizzabile.</string>
|
||||
<string name="v6_1_better_security_descr">Protocolli di SimpleX esaminati da Trail of Bits.</string>
|
||||
<string name="v6_1_better_calls_descr">Cambia tra audio e video durante la chiamata.</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">Cambia profilo di chat per inviti una tantum.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Elimina o modera fino a 200 messaggi.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Inoltra fino a 20 messaggi alla volta.</string>
|
||||
</resources>
|
||||
@@ -312,7 +312,7 @@
|
||||
<string name="how_to_use_your_servers">自分のサーバの使い方</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICEサーバ (1行に1サーバ)</string>
|
||||
<string name="network_and_servers">ネットワークとサーバ</string>
|
||||
<string name="network_settings_title">ネットワーク設定</string>
|
||||
<string name="network_settings_title">高度な設定</string>
|
||||
<string name="delete_address">アドレスを削除</string>
|
||||
<string name="exit_without_saving">保存せずに閉じる</string>
|
||||
<string name="display_name_cannot_contain_whitespace">表示の名前には空白が使用できません。</string>
|
||||
@@ -535,7 +535,7 @@
|
||||
<string name="colored_text">色付き</string>
|
||||
<string name="callstate_received_answer">応答</string>
|
||||
<string name="decentralized">分散型</string>
|
||||
<string name="immune_to_spam_and_abuse">スパムや悪質送信を完全防止</string>
|
||||
<string name="immune_to_spam_and_abuse">スパム耐性</string>
|
||||
<string name="onboarding_notifications_mode_service">即時</string>
|
||||
<string name="onboarding_notifications_mode_periodic">定期的</string>
|
||||
<string name="call_already_ended">通話は既に終了してます!</string>
|
||||
@@ -1837,4 +1837,58 @@
|
||||
<string name="smp_servers_configured">SMPサーバーの構成</string>
|
||||
<string name="servers_info_sessions_connected">接続中</string>
|
||||
<string name="xftp_servers_configured">XFTPサーバーの構成</string>
|
||||
<string name="one_hand_ui_card_title">チャトリスト切り替え</string>
|
||||
<string name="contact_list_header_title">連絡先</string>
|
||||
<string name="message_servers">メッセージサーバ</string>
|
||||
<string name="media_and_file_servers">メディア&ファイルサーバ</string>
|
||||
<string name="one_hand_ui">チャットツールバーを近づける</string>
|
||||
<string name="invite_friends_short">招待</string>
|
||||
<string name="create_address_button">作成</string>
|
||||
<string name="compose_message_placeholder">メッセージ</string>
|
||||
<string name="v6_0_reachable_chat_toolbar">チャットツールバーを近づける</string>
|
||||
<string name="scan_paste_link">QRスキャン / リンクの貼り付け</string>
|
||||
<string name="v6_0_reachable_chat_toolbar_descr">片手でアプリを利用できます</string>
|
||||
<string name="action_button_add_members">招待</string>
|
||||
<string name="paste_link">リンクの貼り付け</string>
|
||||
<string name="app_check_for_updates_notice_disable">無効</string>
|
||||
<string name="current_user">現在のプロフィール</string>
|
||||
<string name="all_users">全てのプロフィール</string>
|
||||
<string name="info_view_call_button">通話</string>
|
||||
<string name="confirm_delete_contact_question">連絡先の削除を確認しますか?</string>
|
||||
<string name="info_view_connect_button">接続</string>
|
||||
<string name="delete_contact_cannot_undo_warning">連絡先が削除されます - この操作は取り消せません!</string>
|
||||
<string name="switching_profile_error_title">プロフィールの切り替えエラー</string>
|
||||
<string name="privacy_media_blur_radius">メディアのぼかし</string>
|
||||
<string name="settings_section_title_chat_database">チャットデータベース</string>
|
||||
<string name="chat_database_exported_continue">続ける</string>
|
||||
<string name="contact_deleted">連絡先の削除完了!</string>
|
||||
<string name="servers_info_details">詳細</string>
|
||||
<string name="member_info_member_inactive">非アクティブ</string>
|
||||
<string name="app_check_for_updates_disabled">無効</string>
|
||||
<string name="network_proxy_incorrect_config_title">プロキシの保存エラー</string>
|
||||
<string name="allow_calls_question">通話を許可しますか?</string>
|
||||
<string name="cant_call_contact_deleted_alert_text">連絡先が削除されました。</string>
|
||||
<string name="member_info_member_disabled">無効</string>
|
||||
<string name="v6_0_delete_many_messages_descr">一度に最大20件のメッセージを削除できます。</string>
|
||||
<string name="servers_info_connected_servers_section_header">サーバに接続中</string>
|
||||
<string name="servers_info_modal_error_title">エラー</string>
|
||||
<string name="servers_info_reconnect_server_error">サーバーへの再接続エラー</string>
|
||||
<string name="servers_info_sessions_errors">エラー</string>
|
||||
<string name="servers_info_files_tab">ファイル</string>
|
||||
<string name="decryption_errors">復号化エラー</string>
|
||||
<string name="deletion_errors">削除エラー</string>
|
||||
<string name="duplicates_label">重複</string>
|
||||
<string name="expired_label">期限切れ</string>
|
||||
<string name="servers_info_detailed_statistics">統計の詳細</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">プロキシで認証情報を使用しないでください。</string>
|
||||
<string name="servers_info_reconnect_servers_error">サーバーへの再接続エラー</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">統計のリセットエラー</string>
|
||||
<string name="cannot_share_message_alert_title">メッセージを送信することができません</string>
|
||||
<string name="cant_call_contact_alert_title">連絡先と通話することができません</string>
|
||||
<string name="servers_info_sessions_connecting">接続待ち</string>
|
||||
<string name="privacy_media_blur_radius_off">無し</string>
|
||||
<string name="privacy_media_blur_radius_soft">控え目</string>
|
||||
<string name="network_options_save_and_reconnect">保存して再接続</string>
|
||||
<string name="privacy_media_blur_radius_strong">強め</string>
|
||||
<string name="privacy_media_blur_radius_medium">普通</string>
|
||||
</resources>
|
||||
@@ -72,7 +72,7 @@
|
||||
<string name="about_simplex">Over SimpleX</string>
|
||||
<string name="about_simplex_chat">Over SimpleX Chat</string>
|
||||
<string name="above_then_preposition_continuation">hier boven, dan:</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle gesprekken en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle chats en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="clear_chat_warning">Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Sta verdwijnende berichten alleen toe als uw contact dit toestaat.</string>
|
||||
<string name="allow_voice_messages_only_if">Sta spraak berichten alleen toe als uw contact ze toestaat.</string>
|
||||
@@ -90,7 +90,7 @@
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="app_version_title">App versie</string>
|
||||
<string name="app_version_name">App versie: v%s</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[Er wordt een aparte TCP-verbinding (en SOCKS-referentie) gebruikt <b> voor elk chat profiel dat je in de app hebt </b>.]]></string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[Er wordt een aparte TCP-verbinding (en SOCKS-referentie) gebruikt <b> voor elk chatprofiel dat je in de app hebt </b>.]]></string>
|
||||
<string name="audio_call_no_encryption">audio oproep (niet e2e versleuteld)</string>
|
||||
<string name="notifications_mode_service_desc">Achtergrondservice is altijd actief, meldingen worden weergegeven zodra de berichten beschikbaar zijn.</string>
|
||||
<string name="icon_descr_call_ended">Oproep beëindigd</string>
|
||||
@@ -120,9 +120,9 @@
|
||||
<string name="chat_archive_header">Gesprek archief</string>
|
||||
<string name="change_database_passphrase_question">Wachtwoord database wijzigen\?</string>
|
||||
<string name="chat_is_stopped_indication">Chat is gestopt</string>
|
||||
<string name="chat_preferences">Gesprek voorkeuren</string>
|
||||
<string name="network_session_mode_user">Chat profiel</string>
|
||||
<string name="settings_section_title_chats">GESPREKKEN</string>
|
||||
<string name="chat_preferences">Chat voorkeuren</string>
|
||||
<string name="network_session_mode_user">Chatprofiel</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
<string name="chat_with_developers">Praat met de ontwikkelaars</string>
|
||||
<string name="smp_servers_check_address">Controleer het server adres en probeer het opnieuw.</string>
|
||||
<string name="choose_file">Bestand</string>
|
||||
@@ -180,7 +180,7 @@
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">"De database wordt versleuteld en het wachtwoord wordt opgeslagen in de Keychain."</string>
|
||||
<string name="database_passphrase_will_be_updated">Het wachtwoord voor database versleuteling wordt bijgewerkt.</string>
|
||||
<string name="database_error">Database fout</string>
|
||||
<string name="database_passphrase_is_required">Database wachtwoord is vereist om je gesprekken te openen.</string>
|
||||
<string name="database_passphrase_is_required">Database wachtwoord is vereist om je chats te openen.</string>
|
||||
<string name="contact_already_exists">Contact bestaat al</string>
|
||||
<string name="icon_descr_call_connecting">Oproep verbinden</string>
|
||||
<string name="button_create_group_link">Maak link</string>
|
||||
@@ -233,7 +233,7 @@
|
||||
<string name="delete_chat_archive_question">Chat archief verwijderen\?</string>
|
||||
<string name="delete_archive">Archief verwijderen</string>
|
||||
<string name="delete_contact_question">Verwijder contact\?</string>
|
||||
<string name="delete_chat_profile_question">Chat profiel verwijderen\?</string>
|
||||
<string name="delete_chat_profile_question">Chatprofiel verwijderen?</string>
|
||||
<string name="full_deletion">Verwijderen voor iedereen</string>
|
||||
<string name="delete_link">Link verwijderen</string>
|
||||
<string name="conn_level_desc_direct">direct</string>
|
||||
@@ -250,7 +250,7 @@
|
||||
<string name="delete_message__question">Verwijder bericht\?</string>
|
||||
<string name="delete_messages">Verwijder berichten</string>
|
||||
<string name="smp_server_test_delete_queue">Wachtrij verwijderen</string>
|
||||
<string name="delete_files_and_media_for_all_users">Verwijder bestanden voor alle chat profielen</string>
|
||||
<string name="delete_files_and_media_for_all_users">Verwijder bestanden voor alle chatprofielen</string>
|
||||
<string name="for_me_only">Verwijder voor mij</string>
|
||||
<string name="button_delete_group">Groep verwijderen</string>
|
||||
<string name="delete_link_question">Link verwijderen\?</string>
|
||||
@@ -284,7 +284,7 @@
|
||||
<string name="ttl_mth">%dmth</string>
|
||||
<string name="ttl_hours">%d uren</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="users_delete_question">Chat profiel verwijderen\?</string>
|
||||
<string name="users_delete_question">Chatprofiel verwijderen?</string>
|
||||
<string name="users_delete_profile_for">Chat profiel verwijderen voor</string>
|
||||
<string name="deleted_description">verwijderd</string>
|
||||
<string name="simplex_link_mode_description">Beschrijving</string>
|
||||
@@ -347,7 +347,7 @@
|
||||
<string name="group_members_can_delete">Groepsleden kunnen verzonden berichten onomkeerbaar verwijderen. (24 uur)</string>
|
||||
<string name="group_members_can_send_dms">Groepsleden kunnen directe berichten sturen</string>
|
||||
<string name="group_members_can_send_voice">Groepsleden kunnen spraak berichten verzenden.</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per chat profiel (standaard) of per verbinding (BETA).</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per chatprofiel (standaard) of per verbinding (BETA).</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Verschillende namen, avatars en transportisolatie.</string>
|
||||
<string name="v4_4_french_interface">Franse interface</string>
|
||||
<string name="error_saving_group_profile">Fout bij opslaan van groep profiel</string>
|
||||
@@ -392,7 +392,7 @@
|
||||
<string name="error_saving_smp_servers">Fout bij opslaan van SMP servers</string>
|
||||
<string name="error_setting_network_config">Fout bij updaten van netwerk configuratie</string>
|
||||
<string name="failed_to_parse_chat_title">Kan het gesprek niet laden</string>
|
||||
<string name="failed_to_parse_chats_title">Kan de gesprekken niet laden</string>
|
||||
<string name="failed_to_parse_chats_title">Kan de chats niet laden</string>
|
||||
<string name="simplex_link_mode_full">Volledige link</string>
|
||||
<string name="integrity_msg_duplicate">dubbel bericht</string>
|
||||
<string name="invalid_connection_link">Ongeldige verbinding link</string>
|
||||
@@ -456,10 +456,10 @@
|
||||
<string name="leave_group_question">Groep verlaten\?</string>
|
||||
<string name="new_member_role">Nieuwe leden rol</string>
|
||||
<string name="no_contacts_to_add">Geen contacten om toe te voegen</string>
|
||||
<string name="incognito_info_allows">Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chat profiel.</string>
|
||||
<string name="incognito_info_allows">Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel.</string>
|
||||
<string name="theme_light">Licht</string>
|
||||
<string name="chat_preferences_no">nee</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Meerdere chat profielen</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Meerdere chatprofielen</string>
|
||||
<string name="v4_5_italian_interface">Italiaanse interface</string>
|
||||
<string name="v4_5_message_draft">Concept bericht</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string>
|
||||
@@ -590,7 +590,7 @@
|
||||
<string name="only_your_contact_can_send_voice">Alleen uw contact kan spraak berichten verzenden.</string>
|
||||
<string name="prohibit_message_deletion">Verbied het onomkeerbaar verwijderen van berichten.</string>
|
||||
<string name="feature_offered_item">voorgesteld %s</string>
|
||||
<string name="store_passphrase_securely_without_recover">Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de gesprekken.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chats.</string>
|
||||
<string name="store_passphrase_securely">Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u deze kwijtraakt.</string>
|
||||
<string name="open_chat">Chat openen</string>
|
||||
<string name="restore_database_alert_desc">Voer het vorige wachtwoord in na het herstellen van de database back-up. Deze actie kan niet ongedaan gemaakt worden.</string>
|
||||
@@ -620,12 +620,12 @@
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[U bepaalt via welke server(s) je de berichten <b>ontvangt</b>, uw contacten de servers die u gebruikt om ze berichten te sturen.]]></string>
|
||||
<string name="icon_descr_video_on">Video aan</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string>
|
||||
<string name="messages_section_description">Deze instelling is van toepassing op berichten in uw huidige chat profiel</string>
|
||||
<string name="messages_section_description">Deze instelling is van toepassing op berichten in uw huidige chatprofiel</string>
|
||||
<string name="save_archive">Bewaar archief</string>
|
||||
<string name="rcv_group_event_updated_group_profile">bijgewerkt groep profiel</string>
|
||||
<string name="group_member_status_removed">verwijderd</string>
|
||||
<string name="group_main_profile_sent">Uw chat profiel wordt verzonden naar de groepsleden</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Je hebt al een chat profiel met dezelfde weergave naam. Kies een andere naam.</string>
|
||||
<string name="group_main_profile_sent">Uw chatprofiel wordt verzonden naar de groepsleden</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Je hebt al een chatprofiel met dezelfde weergave naam. Kies een andere naam.</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">U bent al verbonden met %1$s.</string>
|
||||
<string name="error_smp_test_failed_at_step">Test mislukt bij stap %s.</string>
|
||||
<string name="smp_server_test_secure_queue">Veilige wachtrij</string>
|
||||
@@ -650,8 +650,8 @@
|
||||
<string name="this_text_is_available_in_settings">Deze tekst is beschikbaar in instellingen</string>
|
||||
<string name="welcome">Welkom!</string>
|
||||
<string name="group_preview_you_are_invited">je bent uitgenodigd voor de groep</string>
|
||||
<string name="you_have_no_chats">Je hebt geen gesprekken</string>
|
||||
<string name="your_chats">Gesprekken</string>
|
||||
<string name="you_have_no_chats">Je hebt geen chats</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="share_file">Deel bestand…</string>
|
||||
<string name="share_image">Afbeelding delen…</string>
|
||||
<string name="icon_descr_waiting_for_image">Wachten op afbeelding</string>
|
||||
@@ -680,7 +680,7 @@
|
||||
<string name="icon_descr_address">SimpleX Adres</string>
|
||||
<string name="show_QR_code">Toon QR-code</string>
|
||||
<string name="image_descr_simplex_logo">SimpleX-Logo</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Je chat profiel wordt verzonden naar uw contact</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Je chatprofiel wordt verzonden naar uw contact</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">U wordt verbonden wanneer uw verbindingsverzoek wordt geaccepteerd, even geduld a.u.b. of controleer later!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Je wordt verbonden wanneer het apparaat van je contact online is, even geduld a.u.b. of controleer het later!</string>
|
||||
@@ -696,7 +696,7 @@
|
||||
<string name="send_us_an_email">Stuur ons een e-mail</string>
|
||||
<string name="chat_lock">SimpleX Vergrendelen</string>
|
||||
<string name="smp_servers">SMP servers</string>
|
||||
<string name="smp_servers_save">Bewaar servers</string>
|
||||
<string name="smp_servers_save">Servers opslaan</string>
|
||||
<string name="smp_servers_test_failed">Servertest mislukt!</string>
|
||||
<string name="smp_servers_test_some_failed">Sommige servers hebben de test niet doorstaan:</string>
|
||||
<string name="smp_servers_test_servers">Servers testen</string>
|
||||
@@ -747,7 +747,7 @@
|
||||
<string name="run_chat_section">CHAT UITVOEREN</string>
|
||||
<string name="your_chat_database">Uw chat database</string>
|
||||
<string name="set_password_to_export">Wachtwoord instellen om te exporteren</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Start de app opnieuw om een nieuw chat profiel aan te maken.</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Start de app opnieuw om een nieuw chatprofiel aan te maken.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">U mag ALLEEN de meest recente versie van uw chat-database op één apparaat gebruiken, anders ontvangt u mogelijk geen berichten meer van sommige contacten.</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Start de app opnieuw om de geïmporteerde chat database te gebruiken.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Stop de chat om database acties mogelijk te maken.</string>
|
||||
@@ -791,7 +791,7 @@
|
||||
<string name="group_invitation_tap_to_join_incognito">Tik hier om incognito lid te worden</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Je bent lid geworden van deze groep. Verbinding maken met uitnodigend lid.</string>
|
||||
<string name="you_sent_group_invitation">Je hebt een groep uitnodiging verzonden</string>
|
||||
<string name="snd_group_event_user_left">jij bent vertrokken</string>
|
||||
<string name="snd_group_event_user_left">je bent vertrokken</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">je bent van adres veranderd</string>
|
||||
<string name="select_contacts">Selecteer contacten</string>
|
||||
<string name="skip_inviting_button">Sla het uitnodigen van leden over</string>
|
||||
@@ -842,7 +842,7 @@
|
||||
<string name="use_camera_button">Camera</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Gebruik voor nieuwe verbindingen</string>
|
||||
<string name="star_on_github">Star on GitHub</string>
|
||||
<string name="smp_servers_per_user">De servers voor nieuwe verbindingen van je huidige chat profiel</string>
|
||||
<string name="smp_servers_per_user">De servers voor nieuwe verbindingen van je huidige chatprofiel</string>
|
||||
<string name="your_SMP_servers">Uw SMP servers</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Opgeslagen WebRTC ICE servers worden verwijderd.</string>
|
||||
<string name="your_ICE_servers">Uw ICE servers</string>
|
||||
@@ -865,7 +865,7 @@
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Uw contacten kunnen volledige verwijdering van berichten toestaan.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen.</string>
|
||||
<string name="wrong_passphrase">Verkeerd wachtwoord voor de database</string>
|
||||
<string name="save_passphrase_and_open_chat">Bewaar het wachtwoord en open je gesprekken</string>
|
||||
<string name="save_passphrase_and_open_chat">Bewaar het wachtwoord en open je chats</string>
|
||||
<string name="database_backup_can_be_restored">De poging om het wachtwoord van de database te wijzigen is niet voltooid.</string>
|
||||
<string name="restore_database">Database back-up terugzetten</string>
|
||||
<string name="restore_database_alert_title">Database back-up terugzetten\?</string>
|
||||
@@ -950,7 +950,7 @@
|
||||
<string name="initial_member_role">Initiële rol</string>
|
||||
<string name="group_member_role_observer">Waarnemer</string>
|
||||
<string name="observer_cant_send_message_title">Je kunt geen berichten versturen!</string>
|
||||
<string name="you_are_observer">jij bent waarnemer</string>
|
||||
<string name="you_are_observer">je bent waarnemer</string>
|
||||
<string name="language_system">Systeem</string>
|
||||
<string name="v4_6_audio_video_calls">Audio en video oproepen</string>
|
||||
<string name="confirm_password">Bevestig wachtwoord</string>
|
||||
@@ -963,7 +963,7 @@
|
||||
<string name="error_updating_user_privacy">Fout bij updaten van gebruikers privacy</string>
|
||||
<string name="v4_6_reduced_battery_usage">Verder verminderd batterij verbruik</string>
|
||||
<string name="v4_6_group_welcome_message">Groep welkom bericht</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Verborgen chat profielen</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Verborgen chatprofielen</string>
|
||||
<string name="hide_profile">Profiel verbergen</string>
|
||||
<string name="user_hide">Verbergen</string>
|
||||
<string name="hidden_profile_password">Verborgen profiel wachtwoord</string>
|
||||
@@ -974,7 +974,7 @@
|
||||
<string name="v4_6_group_moderation_descr">Nu kunnen beheerders:
|
||||
\n- berichten van leden verwijderen.
|
||||
\n- schakel leden uit ("waarnemer" rol)</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Bescherm je chat profielen met een wachtwoord!</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Bescherm je chatprofielen met een wachtwoord!</string>
|
||||
<string name="password_to_show">Wachtwoord om weer te geven</string>
|
||||
<string name="save_and_update_group_profile">Groep profiel opslaan en bijwerken</string>
|
||||
<string name="smp_save_servers_question">Servers opslaan\?</string>
|
||||
@@ -988,7 +988,7 @@
|
||||
<string name="user_unhide">zichtbaar maken</string>
|
||||
<string name="user_unmute">Dempen opheffen</string>
|
||||
<string name="group_welcome_title">Welkom bericht</string>
|
||||
<string name="to_reveal_profile_enter_password">Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chat profielen.</string>
|
||||
<string name="to_reveal_profile_enter_password">Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chatprofielen.</string>
|
||||
<string name="button_welcome_message">Welkom bericht</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn.</string>
|
||||
<string name="database_downgrade">Database downgraden</string>
|
||||
@@ -1011,9 +1011,9 @@
|
||||
<string name="settings_section_title_experimenta">EXPERIMENTEEL</string>
|
||||
<string name="delete_profile">Verwijder profiel</string>
|
||||
<string name="profile_password">Profiel wachtwoord</string>
|
||||
<string name="unhide_chat_profile">Chat profiel zichtbaar maken</string>
|
||||
<string name="unhide_chat_profile">Chatprofiel zichtbaar maken</string>
|
||||
<string name="unhide_profile">Profiel zichtbaar maken</string>
|
||||
<string name="delete_chat_profile">Chat profiel verwijderen\?</string>
|
||||
<string name="delete_chat_profile">Chatprofiel verwijderen?</string>
|
||||
<string name="icon_descr_video_asked_to_receive">Gevraagd om de video te ontvangen</string>
|
||||
<string name="videos_limit_desc">Er kunnen slechts 10 video\'s tegelijk worden verzonden</string>
|
||||
<string name="videos_limit_title">Te veel video\'s!</string>
|
||||
@@ -1107,7 +1107,7 @@
|
||||
<string name="v5_0_polish_interface">Poolse interface</string>
|
||||
<string name="v5_0_polish_interface_descr">Dank aan de gebruikers – draag bij via Weblate!</string>
|
||||
<string name="v5_0_large_files_support">Video\'s en bestanden tot 1 GB</string>
|
||||
<string name="auth_open_chat_profiles">Chat profielen openen</string>
|
||||
<string name="auth_open_chat_profiles">Open chatprofielen</string>
|
||||
<string name="learn_more_about_address">Over SimpleX adres</string>
|
||||
<string name="learn_more">Kom meer te weten</string>
|
||||
<string name="scan_qr_to_connect_to_contact">Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken.</string>
|
||||
@@ -1249,7 +1249,7 @@
|
||||
<string name="abort_switch_receiving_address_desc">Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt.</string>
|
||||
<string name="abort_switch_receiving_address">Annuleer het wijzigen van het adres</string>
|
||||
<string name="abort_switch_receiving_address_confirm">Afbreken</string>
|
||||
<string name="no_filtered_chats">Geen gefilterde gesprekken</string>
|
||||
<string name="no_filtered_chats">Geen gefilterde chats</string>
|
||||
<string name="only_owners_can_enable_files_and_media">Alleen groep eigenaren kunnen bestanden en media inschakelen.</string>
|
||||
<string name="files_are_prohibited_in_group">Bestanden en media zijn verboden in deze groep.</string>
|
||||
<string name="favorite_chat">Favoriet</string>
|
||||
@@ -1303,7 +1303,7 @@
|
||||
<string name="send_receipts">Ontvangst bevestiging verzenden</string>
|
||||
<string name="v5_2_message_delivery_receipts_descr">De tweede vink die we gemist hebben! ✅</string>
|
||||
<string name="v5_2_favourites_filter_descr">Filter ongelezen en favoriete chats.</string>
|
||||
<string name="v5_2_favourites_filter">Vind gesprekken sneller</string>
|
||||
<string name="v5_2_favourites_filter">Vind chats sneller</string>
|
||||
<string name="v5_2_fix_encryption_descr">Repareer versleuteling na het herstellen van back-ups.</string>
|
||||
<string name="v5_2_fix_encryption">Behoud uw verbindingen</string>
|
||||
<string name="v5_2_disappear_one_message">Eén bericht laten verdwijnen</string>
|
||||
@@ -1538,7 +1538,7 @@
|
||||
<string name="recent_history">Zichtbare geschiedenis</string>
|
||||
<string name="la_app_passcode">App toegangscode</string>
|
||||
<string name="new_chat">Nieuw gesprek</string>
|
||||
<string name="loading_chats">Gesprekken laden…</string>
|
||||
<string name="loading_chats">Chats laden…</string>
|
||||
<string name="creating_link">Link maken…</string>
|
||||
<string name="or_scan_qr_code">Of scan de QR-code</string>
|
||||
<string name="invalid_qr_code">Ongeldige QR-code</string>
|
||||
@@ -1888,7 +1888,7 @@
|
||||
<string name="servers_info_private_data_disclaimer">Beginnend vanaf %s.
|
||||
\nAlle gegevens zijn privé op uw apparaat.</string>
|
||||
<string name="servers_info_connected_servers_section_header">Verbonden servers</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">in behandeling</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">In behandeling</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Eerder verbonden servers</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Proxied servers</string>
|
||||
<string name="servers_info_subscriptions_total">Totaal</string>
|
||||
@@ -1929,7 +1929,7 @@
|
||||
<string name="downloaded_files">Gedownloade bestanden</string>
|
||||
<string name="secured">Beveiligd</string>
|
||||
<string name="size">Maat</string>
|
||||
<string name="subscribed">Ingeschreven</string>
|
||||
<string name="subscribed">Subscribed</string>
|
||||
<string name="uploaded_files">Geüploade bestanden</string>
|
||||
<string name="upload_errors">Upload fouten</string>
|
||||
<string name="download_errors">Downloadfouten</string>
|
||||
@@ -1938,7 +1938,7 @@
|
||||
<string name="all_users">Alle profielen</string>
|
||||
<string name="attempts_label">pogingen</string>
|
||||
<string name="chunks_deleted">Stukken verwijderd</string>
|
||||
<string name="completed">voltooid</string>
|
||||
<string name="completed">Voltooid</string>
|
||||
<string name="servers_info_sessions_connected">Verbonden</string>
|
||||
<string name="servers_info_sessions_connecting">Verbinden</string>
|
||||
<string name="servers_info_reconnect_server_error">Fout bij opnieuw verbinding maken met de server</string>
|
||||
@@ -1977,8 +1977,8 @@
|
||||
<string name="app_check_for_updates">Controleer op updates</string>
|
||||
<string name="app_check_for_updates_notice_title">Controleer op updates</string>
|
||||
<string name="app_check_for_updates_notice_disable">Uitschakelen</string>
|
||||
<string name="subscription_errors">Inschrijving fouten</string>
|
||||
<string name="subscription_results_ignored">Inschrijvingen genegeerd</string>
|
||||
<string name="subscription_errors">Subscription fouten</string>
|
||||
<string name="subscription_results_ignored">Subscriptions genegeerd</string>
|
||||
<string name="app_check_for_updates_stable">Stabiel</string>
|
||||
<string name="app_check_for_updates_update_available">Update beschikbaar: %s</string>
|
||||
<string name="app_check_for_updates_canceled">Downloaden van update geannuleerd</string>
|
||||
@@ -2057,7 +2057,7 @@
|
||||
<string name="one_hand_ui_change_instruction">U kunt dit wijzigen in de instellingen onder uiterlijk</string>
|
||||
<string name="create_address_button">Creëren</string>
|
||||
<string name="v6_0_privacy_blur">Vervagen voor betere privacy.</string>
|
||||
<string name="v6_0_chat_list_media">Afspelen via de gesprekken lijst.</string>
|
||||
<string name="v6_0_chat_list_media">Afspelen via de chatlijst.</string>
|
||||
<string name="v6_0_upgrade_app_descr">Download nieuwe versies van GitHub.</string>
|
||||
<string name="v6_0_increase_font_size">Vergroot het lettertype.</string>
|
||||
<string name="v6_0_upgrade_app">App automatisch upgraden</string>
|
||||
@@ -2067,4 +2067,62 @@
|
||||
<string name="new_message">Nieuw bericht</string>
|
||||
<string name="error_parsing_uri_title">Ongeldige link</string>
|
||||
<string name="error_parsing_uri_desc">Controleer of de SimpleX-link correct is.</string>
|
||||
<string name="switching_profile_error_title">Fout bij wisselen van profiel</string>
|
||||
<string name="select_chat_profile">Selecteer chatprofiel</string>
|
||||
<string name="new_chat_share_profile">Profiel delen</string>
|
||||
<string name="switching_profile_error_message">Uw verbinding is verplaatst naar %s, maar er is een onverwachte fout opgetreden tijdens het omleiden naar het profiel.</string>
|
||||
<string name="settings_section_title_chat_database">CHAT DATABASE</string>
|
||||
<string name="system_mode_toast">Systeemmodus</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Archief verwijderen?</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Het geüploade databasearchief wordt permanent van de servers verwijderd.</string>
|
||||
<string name="n_file_errors">%1$d bestandsfout(en):
|
||||
\n%2$s</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s berichten niet doorgestuurd</string>
|
||||
<string name="forward_alert_title_messages_to_forward">%1$s bericht(en) doorsturen?</string>
|
||||
<string name="network_proxy_auth">Proxy-authenticatie</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Berichten zijn verwijderd nadat u ze had geselecteerd.</string>
|
||||
<string name="network_proxy_username">Gebruikersnaam</string>
|
||||
<string name="n_other_file_errors">%1$d overige bestandsfout(en).</string>
|
||||
<string name="error_forwarding_messages">Fout bij het doorsturen van berichten</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Niets om door te sturen!</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d bestand(en) worden nog gedownload.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d bestand(en) zijn niet gedownload.</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Berichten doorsturen zonder bestanden?</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d bestand(en) konden niet worden gedownload.</string>
|
||||
<string name="forward_files_missing_desc">%1$d bestand(en) zijn verwijderd.</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Download</string>
|
||||
<string name="compose_forward_messages_n">%1$s berichten doorsturen</string>
|
||||
<string name="forward_multiple">Berichten doorsturen…</string>
|
||||
<string name="compose_save_messages_n">%1$s berichten opslaan</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Gebruik geen inloggegevens met proxy.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Gebruik voor elke verbinding verschillende proxy-inloggegevens.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Gebruik voor elk profiel verschillende proxy-inloggegevens.</string>
|
||||
<string name="network_proxy_random_credentials">Gebruik willekeurige inloggegevens</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Uw inloggegevens worden mogelijk niet-versleuteld verzonden.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Fout bij opslaan proxy</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Zorg ervoor dat de proxyconfiguratie correct is.</string>
|
||||
<string name="network_proxy_password">Wachtwoord</string>
|
||||
<string name="icon_descr_sound_muted">Geluid gedempt</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Fout bij initialiseren van WebView. Zorg ervoor dat WebView geïnstalleerd is en de ondersteunde architectuur is arm64.\nFout: %s</string>
|
||||
<string name="settings_message_shape_corner">Hoek</string>
|
||||
<string name="settings_section_title_message_shape">Berichtvorm</string>
|
||||
<string name="network_session_mode_session">Appsessie</string>
|
||||
<string name="network_session_mode_session_description">Elke keer dat u de app start, worden er nieuwe SOCKS-inloggegevens gebruikt.</string>
|
||||
<string name="network_session_mode_server_description">Voor elke server worden nieuwe SOCKS-inloggegevens gebruikt.</string>
|
||||
<string name="network_session_mode_server">Server</string>
|
||||
<string name="settings_message_shape_tail">Staart</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Klik op de infoknop naast het adresveld om het gebruik van de microfoon toe te staan.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Open Safari Instellingen / Websites / Microfoon en kies Toestaan voor localhost.</string>
|
||||
<string name="call_desktop_permission_denied_title">Als u wilt bellen, geeft u toestemming om uw microfoon te gebruiken. Beëindig het gesprek en probeer opnieuw te bellen.</string>
|
||||
<string name="v6_1_better_security">Betere beveiliging ✅</string>
|
||||
<string name="v6_1_better_security_descr">SimpleX-protocollen beoordeeld door Trail of Bits.</string>
|
||||
<string name="v6_1_message_dates_descr">Betere datums voor berichten.</string>
|
||||
<string name="v6_1_better_user_experience">Betere gebruikerservaring</string>
|
||||
<string name="v6_1_customizable_message_descr">Aanpasbare berichtvorm.</string>
|
||||
<string name="v6_1_better_calls_descr">Wisselen tussen audio en video tijdens het gesprek.</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">Wijzig chatprofiel voor eenmalige uitnodigingen.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Maximaal 200 berichten verwijderen of modereren.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Stuur maximaal 20 berichten tegelijk door.</string>
|
||||
<string name="v6_1_better_calls">Betere gesprekken</string>
|
||||
</resources>
|
||||
@@ -2069,4 +2069,61 @@
|
||||
<string name="v6_0_connect_faster_descr">Szybciej łącz się ze znajomymi.</string>
|
||||
<string name="v6_0_connection_servers_status">Kontroluj swoją sieć</string>
|
||||
<string name="v6_0_delete_many_messages_descr">Usuń do 20 wiadomości na raz.</string>
|
||||
<string name="error_forwarding_messages">Błąd przekazywania wiadomości</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d plik(ów/i) nie zostały pobrane.</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Czy przekazać %1$s wiadomoś(ć/ci)?</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Przekazać wiadomości bez plików?</string>
|
||||
<string name="forward_files_missing_desc">%1$d plik(ów/i) zostały usunięte.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s wiadomoś(ć/ci) nie przekazane</string>
|
||||
<string name="compose_forward_messages_n">Przekazywanie %1$s wiadomoś(ć/ci)</string>
|
||||
<string name="forward_multiple">Przekazywanie wiadomości…</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Nie używaj danych logowania do proxy.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Błąd zapisywania ustawień proxy</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Sprawdź czy konfiguracja serwera proxy jest poprawna.</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d plik(ów/i) dalej są pobierane.</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d plik(ów/i) nie udało się pobrać.</string>
|
||||
<string name="switching_profile_error_title">Błąd zmiany profilu</string>
|
||||
<string name="settings_section_title_chat_database">BAZA CZATU</string>
|
||||
<string name="n_file_errors">%1$d błędów plików:\n%2$s</string>
|
||||
<string name="n_other_file_errors">%1$d innych błędów plików.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Wiadomości zostały usunięte po wybraniu ich.</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Nic do przekazania!</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Pobierz</string>
|
||||
<string name="compose_save_messages_n">Zapisywanie %1$s wiadomości</string>
|
||||
<string name="network_proxy_auth">Uwierzytelnianie proxy</string>
|
||||
<string name="network_proxy_password">Hasło</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Błąd inicjalizacji WebView. Upewnij się, że WebView jest zainstalowany, a jego obsługiwana architektura to arm64.\nBłąd: %s</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Wiadomości zostaną usunięte - nie można tego cofnąć!</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Usunąć archiwum?</string>
|
||||
<string name="settings_message_shape_corner">Róg</string>
|
||||
<string name="settings_section_title_message_shape">Kształt wiadomości</string>
|
||||
<string name="network_session_mode_session">Sesja aplikacji</string>
|
||||
<string name="network_session_mode_session_description">Nowe poświadczenia SOCKS będą używane przy każdym uruchomieniu aplikacji.</string>
|
||||
<string name="network_session_mode_server_description">Dla każdego serwera zostaną użyte nowe poświadczenia SOCKS.</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Kliknij przycisk informacji przy polu adresu, aby zezwolić na korzystanie z mikrofonu.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Otwórz Safari Ustawienia / Strony internetowe / Mikrofon, a następnie wybierz opcję Zezwalaj dla localhost.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Użyj różnych poświadczeń proxy dla każdego połączenia.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Użyj różnych poświadczeń proxy dla każdego profilu.</string>
|
||||
<string name="network_proxy_random_credentials">Użyj losowych poświadczeń</string>
|
||||
<string name="network_proxy_username">Nazwa użytkownika</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Twoje poświadczenia mogą zostać wysłane niezaszyfrowane.</string>
|
||||
<string name="icon_descr_sound_muted">Dźwięk wyciszony</string>
|
||||
<string name="select_chat_profile">Wybierz profil czatu</string>
|
||||
<string name="new_chat_share_profile">Udostępnij profil</string>
|
||||
<string name="switching_profile_error_message">Twoje połączenie zostało przeniesione do %s, ale podczas przekierowania do profilu wystąpił nieoczekiwany błąd.</string>
|
||||
<string name="system_mode_toast">Tryb systemu</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Przesłane archiwum bazy danych zostanie trwale usunięte z serwerów.</string>
|
||||
<string name="network_session_mode_server">Serwer</string>
|
||||
<string name="settings_message_shape_tail">Ogon</string>
|
||||
<string name="call_desktop_permission_denied_title">Aby wykonywać połączenia, zezwól na korzystanie z mikrofonu. Zakończ połączenie i spróbuj zadzwonić ponownie.</string>
|
||||
<string name="v6_1_better_security">Lepsze bezpieczeństwo ✅</string>
|
||||
<string name="v6_1_message_dates_descr">Lepsze daty wiadomości.</string>
|
||||
<string name="v6_1_customizable_message_descr">Możliwość dostosowania kształtu wiadomości.</string>
|
||||
<string name="v6_1_better_calls">Lepsze połączenia</string>
|
||||
<string name="v6_1_better_user_experience">Lepsze doświadczenie użytkownika</string>
|
||||
<string name="v6_1_better_security_descr">Protokoły SimpleX sprawdzone przez Trail of Bits.</string>
|
||||
<string name="v6_1_better_calls_descr">Przełączanie audio i wideo podczas połączenia.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Usuń lub moderuj do 200 wiadomości.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Przekazywanie do 20 wiadomości jednocześnie.</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">Przełącz profil czatu dla zaproszeń jednorazowych.</string>
|
||||
</resources>
|
||||
@@ -74,7 +74,7 @@
|
||||
<string name="smp_servers_check_address">Verifique o endereço do servidor e tente novamente.</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada perfil de bate-papo que você tiver no aplicativo</b>.]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Melhor para bateria</b>. Você receberá notificações apenas quando o aplicativo estiver em execução (SEM o serviço em segundo plano).]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consome mais bateria</b>! O serviço em segundo plano está sempre em execução - as notificações são exibidas assim que as mensagens estiverem disponíveis.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consome mais bateria</b>! O aplicativo em segundo plano está sempre em execução - as notificações são exibidas instantaneamente.]]></string>
|
||||
<string name="settings_section_title_chats">BATE-PAPOS</string>
|
||||
<string name="settings_section_title_icon">ÍCONE DO APLICATIVO</string>
|
||||
<string name="chat_database_section">BANCO DE DADOS DE BATE-PAPO</string>
|
||||
@@ -91,7 +91,7 @@
|
||||
<string name="notifications_mode_service_desc">O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis.</string>
|
||||
<string name="network_session_mode_entity_description">Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada contato e membro do grupo</b>.
|
||||
\n<b>Atenção</b>: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bom para bateria</b>. O serviço em segundo plano procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bom para bateria</b>. O aplicativo procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]></string>
|
||||
<string name="callstatus_ended">chamda encerrada %1$s</string>
|
||||
<string name="chat_with_developers">Converse com os desenvolvedores</string>
|
||||
<string name="create_group_link">Criar link de grupo</string>
|
||||
@@ -256,12 +256,12 @@
|
||||
<string name="settings_section_title_device">DISPOSITIVO</string>
|
||||
<string name="settings_developer_tools">Ferramentas de desenvolvedor</string>
|
||||
<string name="group_member_status_introduced">conectando (introduzido)</string>
|
||||
<string name="color_primary">Realçe</string>
|
||||
<string name="color_primary">Tonalidade</string>
|
||||
<string name="error_removing_member">Erro ao remover membro</string>
|
||||
<string name="error_changing_role">Erro ao alterar cargo</string>
|
||||
<string name="conn_level_desc_direct">direto</string>
|
||||
<string name="server_error">erro</string>
|
||||
<string name="failed_to_parse_chat_title">Falha ao carregar o bate-papo</string>
|
||||
<string name="failed_to_parse_chat_title">Falha ao carregar a conversa</string>
|
||||
<string name="error_setting_network_config">Erro ao atualizar a configuração de conexão</string>
|
||||
<string name="error_sending_message">Erro ao enviar mensagem</string>
|
||||
<string name="error_adding_members">Erro ao adicionar membro(s)</string>
|
||||
@@ -317,8 +317,8 @@
|
||||
<string name="icon_descr_expand_role">Expandir seleção de cargo</string>
|
||||
<string name="error_saving_group_profile">Erro ao salvar o perfil do grupo</string>
|
||||
<string name="direct_messages">Mensagens diretas</string>
|
||||
<string name="feature_enabled">habilitado</string>
|
||||
<string name="feature_enabled_for_contact">habilitado para contato</string>
|
||||
<string name="feature_enabled">ativado</string>
|
||||
<string name="feature_enabled_for_contact">ativado para contato</string>
|
||||
<string name="feature_enabled_for_you">ativado para você</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
@@ -379,7 +379,7 @@
|
||||
<string name="group_full_name_field">Nome completo do grupo:</string>
|
||||
<string name="v4_2_group_links">Links de grupo</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Privacidade e segurança aprimoradas</string>
|
||||
<string name="failed_to_parse_chats_title">Falha ao carregar o bate-papo</string>
|
||||
<string name="failed_to_parse_chats_title">Falha ao carregar as conversas</string>
|
||||
<string name="file_with_path">Arquivo: %s</string>
|
||||
<string name="file_saved">Arquivo salvo</string>
|
||||
<string name="group_members_can_send_voice">Os membros do grupo podem enviar mensagens de voz.</string>
|
||||
@@ -403,7 +403,7 @@
|
||||
<string name="incorrect_code">Código de segurança incorreto!</string>
|
||||
<string name="install_simplex_chat_for_terminal">Instale o SimpleX para terminal</string>
|
||||
<string name="how_it_works">Como funciona</string>
|
||||
<string name="immune_to_spam_and_abuse">Imune a spam e abuso</string>
|
||||
<string name="immune_to_spam_and_abuse">Imune a spam</string>
|
||||
<string name="icon_descr_flip_camera">Vire a câmera</string>
|
||||
<string name="icon_descr_hang_up">Desligar</string>
|
||||
<string name="settings_section_title_incognito">Modo anônimo</string>
|
||||
@@ -551,7 +551,7 @@
|
||||
<string name="invalid_message_format">formato de mensagem inválido</string>
|
||||
<string name="live">AO VIVO</string>
|
||||
<string name="moderated_item_description">moderado por %s</string>
|
||||
<string name="invalid_chat">chat inválido</string>
|
||||
<string name="invalid_chat">conversa inválida</string>
|
||||
<string name="simplex_link_mode_browser_warning">Abrir o link no navegador pode reduzir a privacidade e a segurança da conexão. Links SimpleX não confiáveis ficarão vermelhos.</string>
|
||||
<string name="invalid_connection_link">Link de conexão inválido</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Certifique-se de que os endereços do servidor SMP estejam no formato correto, separados por linhas e não estejam duplicados.</string>
|
||||
@@ -562,7 +562,7 @@
|
||||
<string name="notifications_mode_off">Executa quando o aplicativo está aberto</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">enviado</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">o envio falhou</string>
|
||||
<string name="your_chats">Bate-papos</string>
|
||||
<string name="your_chats">Conversas</string>
|
||||
<string name="paste_button">Colar</string>
|
||||
<string name="one_time_link">Link de convite de uso único</string>
|
||||
<string name="chat_with_the_founder">Enviar perguntas e idéias</string>
|
||||
@@ -764,7 +764,7 @@
|
||||
<string name="privacy_redefined">Privacidade redefinida</string>
|
||||
<string name="onboarding_notifications_mode_title">Notificações privadas</string>
|
||||
<string name="make_private_connection">Fazer uma conexão privada</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Pessoas podem se conectar com você somente via links compartilhados.</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Você decide quem pode se conectar.</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Pode acontecer quando:
|
||||
\n1. As mensagens expiraram no remetente após 2 dias ou no servidor após 30 dias.
|
||||
\n2. A descriptografia da mensagem falhou porque você ou seu contato usou o backup do banco de dados antigo.
|
||||
@@ -785,7 +785,7 @@
|
||||
<string name="icon_descr_record_voice_message">Gravar mensagem de voz</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Certifique-se de que os endereços do servidor WebRTC ICE estão em formato correto, separados por linha e não estejam duplicados.</string>
|
||||
<string name="network_and_servers">Conexão e servidores</string>
|
||||
<string name="network_settings_title">Configurações de conexão</string>
|
||||
<string name="network_settings_title">Configurações avançadas</string>
|
||||
<string name="no_details">sem detalhes</string>
|
||||
<string name="add_contact">Link de convite de uso único</string>
|
||||
<string name="smp_servers_your_server_address">Seu endereço de servidor</string>
|
||||
@@ -803,7 +803,7 @@
|
||||
<string name="network_use_onion_hosts_no">Não</string>
|
||||
<string name="network_disable_socks">Usar conexão direta com a internet\?</string>
|
||||
<string name="hide_dev_options">Ocultar:</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Seu perfil é guardado no seu diapositivo e é compartilhado somente com seus contatos. Servidores SimpleX não podem ver seu perfil.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Seu perfil é guardado no seu dispositivo e é compartilhado somente com seus contatos. Servidores SimpleX não podem ver seu perfil.</string>
|
||||
<string name="save_preferences_question">Salvar preferências\?</string>
|
||||
<string name="save_and_notify_group_members">Salvar e notificar membros do grupo</string>
|
||||
<string name="error_saving_user_password">Erro ao salvar a senha do usuário</string>
|
||||
@@ -857,7 +857,8 @@
|
||||
<string name="to_start_a_new_chat_help_header">Para começar um novo bate-papo</string>
|
||||
<string name="la_notice_turn_on">Ligar</string>
|
||||
<string name="welcome">Bem-vindo(a)!</string>
|
||||
<string name="next_generation_of_private_messaging">A próxima geração de mensageiros privados</string>
|
||||
<string name="next_generation_of_private_messaging">A próxima geração
|
||||
\nde mensageiros privados</string>
|
||||
<string name="settings_section_title_socks">PROXY SOCKS</string>
|
||||
<string name="database_backup_can_be_restored">A tentativa de alterar a senha do banco de dados não foi concluída.</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Pare o bate-papo para exportar, importar ou excluir o banco de dados do chat. Você não poderá receber e enviar mensagens enquanto o chat estiver interrompido.</string>
|
||||
@@ -883,7 +884,7 @@
|
||||
<string name="notifications_mode_periodic">Inicia periodicamente</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">envio não autorizado</string>
|
||||
<string name="tap_to_start_new_chat">Toque para iniciar um novo bate-papo</string>
|
||||
<string name="you_have_no_chats">Você não tem bate-papos</string>
|
||||
<string name="you_have_no_chats">Você não tem conversas</string>
|
||||
<string name="callstate_waiting_for_answer">aguardando resposta…</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Seu banco de dados de bate-papo atual será EXCLUÍDO e SUBSTITUÍDO pelo importado.
|
||||
\nEsta ação não pode ser desfeita - seu perfil, contatos, mensagens e arquivos serão perdidos de forma irreversível.</string>
|
||||
@@ -902,12 +903,12 @@
|
||||
<string name="update_database">Atualizar</string>
|
||||
<string name="periodic_notifications_desc">O app busca novas mensagens periodicamente – ele usa alguns por cento da bateria por dia. O aplicativo não usa notificações por push – os dados do seu dispositivo não são enviados para os servidores.</string>
|
||||
<string name="enter_passphrase_notification_desc">Para receber notificações, por favor, digite a senha do banco de dados</string>
|
||||
<string name="simplex_service_notification_title">Serviço SimpleX</string>
|
||||
<string name="simplex_service_notification_title">Serviço de Chat SimpleX</string>
|
||||
<string name="settings_notification_preview_mode_title">Mostrar prévia</string>
|
||||
<string name="notification_preview_mode_message_desc">Mostrar contato e mensagem</string>
|
||||
<string name="notification_preview_mode_contact_desc">Mostrar somente contato</string>
|
||||
<string name="share_verb">Compartilhar</string>
|
||||
<string name="auth_stop_chat">Parar bate-papo</string>
|
||||
<string name="auth_stop_chat">Parar conversa</string>
|
||||
<string name="auth_unlock">Desbloquear</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">A mensagem será excluída para todos os membros.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">A mensagem será marcada como moderada para todos os membros.</string>
|
||||
@@ -925,7 +926,7 @@
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="network_session_mode_transport_isolation">Isolamento de transporte</string>
|
||||
<string name="you_control_your_chat">Você controla sua conversa!</string>
|
||||
<string name="first_platform_without_user_ids">A 1ª plataforma sem nenhum identificador de usuário – privada por design.</string>
|
||||
<string name="first_platform_without_user_ids">Sem identificadores de usuário.</string>
|
||||
<string name="icon_descr_speaker_on">Alto-falante ligado</string>
|
||||
<string name="icon_descr_video_off">Vídeo desativado</string>
|
||||
<string name="icon_descr_speaker_off">Alto-falante desligado</string>
|
||||
@@ -976,7 +977,7 @@
|
||||
<string name="callstate_starting">iniciando…</string>
|
||||
<string name="callstate_waiting_for_confirmation">aguardando confirmação…</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Não armazenamos nenhum dos seus contatos ou mensagens (uma vez entregues) nos servidores.</string>
|
||||
<string name="alert_title_skipped_messages">Mensagens omitidas</string>
|
||||
<string name="alert_title_skipped_messages">Mensagens ignoradas</string>
|
||||
<string name="tap_to_activate_profile">Toque para ativar o perfil.</string>
|
||||
<string name="unhide_chat_profile">Mostrar perfil de chat</string>
|
||||
<string name="unhide_profile">Mostrar perfil</string>
|
||||
@@ -994,7 +995,7 @@
|
||||
<string name="image_descr_link_preview">imagem de pré-visualização do link</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">Para proteger suas informações, ative o bloqueio SimpleX.
|
||||
\nVocê será solicitado a completar a autenticação antes que este recurso seja ativado.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protocolo de código aberto – qualquer um pode hospedar os servidores.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Qualquer um pode hospedar os servidores.</string>
|
||||
<string name="theme_light">Claro</string>
|
||||
<string name="chat_preferences_contact_allows">O contato permite</string>
|
||||
<string name="chat_preferences_on">ativado</string>
|
||||
@@ -1109,7 +1110,7 @@
|
||||
<string name="revoke_file__confirm">Revogar</string>
|
||||
<string name="learn_more_about_address">Sobre o endereço SimpleX</string>
|
||||
<string name="color_secondary_variant">Secundária adicional</string>
|
||||
<string name="color_primary_variant">Realçe adicional</string>
|
||||
<string name="color_primary_variant">Tonalidade adicional</string>
|
||||
<string name="one_time_link_short">Link de uso único</string>
|
||||
<string name="add_address_to_your_profile">Adicione o endereço ao seu perfil, para que seus contatos possam compartilhá-lo com outras pessoas. A atualização do perfil será enviada aos seus contatos.</string>
|
||||
<string name="create_address_and_let_people_connect">Crie um endereço para permitir que as pessoas se conectem com você.</string>
|
||||
@@ -1139,7 +1140,7 @@
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">Você não perderá seus contatos se, posteriormente, excluir seu endereço.</string>
|
||||
<string name="simplex_address">Endereço SimpleX</string>
|
||||
<string name="you_can_accept_or_reject_connection">Quando as pessoas solicitam uma conexão, você pode aceitá-la ou rejeitá-la.</string>
|
||||
<string name="theme_colors_section_title">CORES DO TEMA</string>
|
||||
<string name="theme_colors_section_title">CORES DA INTERFACE</string>
|
||||
<string name="share_with_contacts">compartilhar com os contatos</string>
|
||||
<string name="profile_update_will_be_sent_to_contacts">A atualização do perfil será enviada aos seus contatos.</string>
|
||||
<string name="save_settings_question">Salvar configurações\?</string>
|
||||
@@ -1238,7 +1239,7 @@
|
||||
<string name="change_self_destruct_passcode">Alterar senha de auto-destruição</string>
|
||||
<string name="if_you_enter_self_destruct_code">Se você digitar sua senha de auto-destruição ao abrir o aplicativo:</string>
|
||||
<string name="item_info_no_text">sem texto</string>
|
||||
<string name="non_fatal_errors_occured_during_import">Alguns erros não-fatais ocurreram durante importação - pode ver o console de Chat para mais detalhes.</string>
|
||||
<string name="non_fatal_errors_occured_during_import">Alguns erros não fatais ocorreram durante importação:</string>
|
||||
<string name="search_verb">Pesquisar</string>
|
||||
<string name="la_mode_off">Desativado</string>
|
||||
<string name="files_and_media">Arquivos e mídia</string>
|
||||
@@ -1281,7 +1282,7 @@
|
||||
<string name="settings_shutdown">Desligar</string>
|
||||
<string name="fix_connection">Corrigir conexão</string>
|
||||
<string name="fix_connection_question">Corrigir conexão\?</string>
|
||||
<string name="no_filtered_chats">Sem bate-papo filtrados</string>
|
||||
<string name="no_filtered_chats">Sem conversas filtradas</string>
|
||||
<string name="sync_connection_force_confirm">Renegociar</string>
|
||||
<string name="unfavorite_chat">Desfavoritar</string>
|
||||
<string name="sync_connection_force_question">Renegociar a criptografia\?</string>
|
||||
@@ -1335,7 +1336,7 @@
|
||||
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Enviar recibos de entrega serão habilitados para todos os contatos em todos os perfis visíveis.</string>
|
||||
<string name="rcv_group_event_2_members_connected">%s e %s conectados</string>
|
||||
<string name="connect_via_member_address_alert_title">Conectar diretamente\?</string>
|
||||
<string name="no_selected_chat">Nenhum bate-papo selecionado</string>
|
||||
<string name="no_selected_chat">Nenhuma conversa selecionada</string>
|
||||
<string name="privacy_message_draft">Rascunho de mensagem</string>
|
||||
<string name="send_receipts_disabled">desativado</string>
|
||||
<string name="system_restricted_background_desc">SimpleX não pode ser executado em segundo plano. Você receberá as notificações somente quando o aplicativo estiver em execução.</string>
|
||||
@@ -1562,7 +1563,7 @@
|
||||
<string name="encryption_renegotiation_error">Erro de renegociação de criptografia</string>
|
||||
<string name="unable_to_open_browser_title">Erro ao abrir o navegador</string>
|
||||
<string name="error_sending_message_contact_invitation">Erro ao enviar o convite</string>
|
||||
<string name="loading_chats">Carregando bate-papos…</string>
|
||||
<string name="loading_chats">Carregando conversas…</string>
|
||||
<string name="remote_host_was_disconnected_toast"><![CDATA[Dispositivo Móvel <b>%s</b> foi desconectado]]></string>
|
||||
<string name="remote_host_error_disconnected"><![CDATA[Dispositivo Móvel <b>%s</b> foi desconectado]]></string>
|
||||
<string name="only_one_device_can_work_at_the_same_time">Apenas um dispositivo pode funcionar ao mesmo tempo</string>
|
||||
@@ -1659,4 +1660,448 @@
|
||||
<string name="xftp_servers_configured">Servidores XFTP configurados</string>
|
||||
<string name="migrate_from_device_check_connection_and_try_again">Verifique sua conexão de internet e tente novamente</string>
|
||||
<string name="app_check_for_updates">Verificar atualizações</string>
|
||||
<string name="color_mode">Modo de cor</string>
|
||||
<string name="migrate_from_device_delete_database_from_device">Apagar banco de dados desse dispositivo</string>
|
||||
<string name="proxy_destination_error_broker_host">O endereço do servidor de destino de %1$s é incompatível com o as configurações %2$s do servidor de encaminhamento.</string>
|
||||
<string name="snd_error_quota">Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente.</string>
|
||||
<string name="snd_error_relay">Erro do servidor de destino: %1$s</string>
|
||||
<string name="member_info_member_inactive">Inativo</string>
|
||||
<string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Escolha<i>Migre de outro dispositivo</i>no novo dispositivo e escaneie o QR code.]]></string>
|
||||
<string name="migrate_from_device_confirm_you_remember_passphrase">Confirme se você se lembra da senha do banco de dados para migrá-lo.</string>
|
||||
<string name="privacy_media_blur_radius">Borrar conteúdo</string>
|
||||
<string name="v6_0_privacy_blur">Borrar para melhor privacidade.</string>
|
||||
<string name="confirm_delete_contact_question">Confirmar exclusão do contato?</string>
|
||||
<string name="info_view_connect_button">conectar</string>
|
||||
<string name="delete_without_notification">Apagar sem notificar</string>
|
||||
<string name="color_primary_variant2">Tonalidade adicional 2</string>
|
||||
<string name="migrate_to_device_confirm_network_settings">Confirmar configurações de rede</string>
|
||||
<string name="chat_theme_apply_to_all_modes">Todos os modos de cor</string>
|
||||
<string name="chat_theme_apply_to_dark_mode">Modo escuro</string>
|
||||
<string name="cannot_share_message_alert_title">Não é possível enviar mensagem</string>
|
||||
<string name="settings_section_title_chat_colors">Cores do chat</string>
|
||||
<string name="create_address_button">Criar</string>
|
||||
<string name="migrate_from_device_confirm_upload">Confirmar upload</string>
|
||||
<string name="feature_roles_admins">administradores</string>
|
||||
<string name="cant_call_contact_deleted_alert_text">O contato foi apagado.</string>
|
||||
<string name="allow_calls_question">Permitir chamadas?</string>
|
||||
<string name="cant_call_contact_alert_title">Não é possível chamar o contato</string>
|
||||
<string name="cant_call_contact_connecting_wait_alert_text">Conectando ao contato, por favor aguarde ou volte depois!</string>
|
||||
<string name="calls_prohibited_alert_title">Chamadas proibidas!</string>
|
||||
<string name="cant_call_member_alert_title">Não é possível chamar membro do grupo</string>
|
||||
<string name="cant_send_message_to_member_alert_title">Não é possível mandar mensagem para o membro do grupo</string>
|
||||
<string name="v6_0_connection_servers_status">Controle sua rede</string>
|
||||
<string name="v6_0_delete_many_messages_descr">Apague até 20 mensagens por vez.</string>
|
||||
<string name="v6_0_your_contacts_descr">Arquivar contatos para conversar depois.</string>
|
||||
<string name="v6_0_connect_faster_descr">Conecte aos seus amigos mais rapidamente.</string>
|
||||
<string name="color_mode_dark">Escuro</string>
|
||||
<string name="v5_8_safe_files_descr">Confirmar arquivos de servidores desconhecidos.</string>
|
||||
<string name="copy_error">Copiar erro</string>
|
||||
<string name="migrate_to_device_chat_migrated">Conversa migrada!</string>
|
||||
<string name="info_view_call_button">chamar</string>
|
||||
<string name="delete_contact_cannot_undo_warning">O contato será apagado - essa ação não pode ser desfeita!</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="app_check_for_updates_download_completed_title">A atualização do aplicativo foi baixada</string>
|
||||
<string name="settings_section_title_chat_theme">Tema da conversa</string>
|
||||
<string name="info_row_debug_delivery">Entrega de depuração</string>
|
||||
<string name="dark_mode_colors">Cores do modo escuro</string>
|
||||
<string name="migrate_from_device_creating_archive_link">Criando link de arquivo</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Permitir downgrade</string>
|
||||
<string name="all_users">Todos os usuários</string>
|
||||
<string name="servers_info_sessions_connected">Conectado</string>
|
||||
<string name="servers_info_sessions_connecting">Conectando</string>
|
||||
<string name="current_user">Perfil atual</string>
|
||||
<string name="servers_info_connected_servers_section_header">Servidores conectados</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Conexões ativas</string>
|
||||
<string name="attempts_label">tentativas</string>
|
||||
<string name="acknowledged">Reconhecido</string>
|
||||
<string name="acknowledgement_errors">Erros conhecidos</string>
|
||||
<string name="connections">Conexões</string>
|
||||
<string name="created">Criado</string>
|
||||
<string name="decryption_errors">erros de decriptação</string>
|
||||
<string name="deleted">Apagado</string>
|
||||
<string name="deletion_errors">Erros de exclusão</string>
|
||||
<string name="chunks_deleted">Pedaços excluídos</string>
|
||||
<string name="chunks_downloaded">Pedaços baixados</string>
|
||||
<string name="chunks_uploaded">Pedaços carregados</string>
|
||||
<string name="delete_members_messages__question">Apagar %d mensagens dos membros?</string>
|
||||
<string name="contact_deleted">Contato apagado!</string>
|
||||
<string name="conversation_deleted">Conversa apagada!</string>
|
||||
<string name="deleted_chats">Contatos arquivados</string>
|
||||
<string name="chat_database_exported_title">Banco de dados da conversa exportado</string>
|
||||
<string name="chat_database_exported_continue">Continuar</string>
|
||||
<string name="v6_0_connection_servers_status_descr">Conexão e status dos servidores.</string>
|
||||
<string name="migrate_to_device_repeat_download">Repetir download</string>
|
||||
<string name="network_option_rcv_concurrency">Recebendo simultaneidade</string>
|
||||
<string name="chat_theme_reset_to_user_theme">Redefinir para o tema do usuário</string>
|
||||
<string name="v5_8_persian_ui">IU Persa</string>
|
||||
<string name="message_delivery_warning_title">Aviso de entrega de mensagem</string>
|
||||
<string name="ci_status_other_error">Erro: %1$s</string>
|
||||
<string name="snd_error_auth">Chave incorreta ou conexão desconhecida - provavelmente esta conexão foi excluída.</string>
|
||||
<string name="e2ee_info_no_pq_short">Essa conversa é protegida por criptografia ponta a ponta</string>
|
||||
<string name="migrate_from_device_repeat_upload">Repetir upload</string>
|
||||
<string name="smp_proxy_error_connecting">Erro de conexão ao servidor de encaminhamento %1$s. Por favor tente mais tarde.</string>
|
||||
<string name="smp_proxy_error_broker_version">A versão do servidor de encaminhamento é incompatível com as configurações de rede: %1$s.</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">O servidor de encaminhamento %1$s falhou ao se conectar ao servidor de destino %2$s. Por favor tente mais tarde.</string>
|
||||
<string name="smp_proxy_error_broker_host">O endereço do servidor de encaminhamento é incompatível com as configurações de rede: %1$s.</string>
|
||||
<string name="proxy_destination_error_broker_version">A versão do servidor de destino de %1$s é incompatível com o servidor de encaminhamento %2$s.</string>
|
||||
<string name="snd_error_expired">Problemas de rede - a mensagem expirou após muitas tentativas de envio.</string>
|
||||
<string name="snd_error_proxy">Servidor de encaminhamento: %1$s
|
||||
\nErro: %2$s</string>
|
||||
<string name="recipients_can_not_see_who_message_from">Destinatário(s) não podem ver de onde essa mensagem veio.</string>
|
||||
<string name="member_inactive_desc">A mensagem poderá ser entregue mais tarde se o membro se tornar ativo.</string>
|
||||
<string name="smp_servers_other">Outros servidores SMP</string>
|
||||
<string name="private_routing_explanation">Para proteger seu endereço IP, roteamento privado usa seus servidores SMP para entregar mensagens.</string>
|
||||
<string name="app_check_for_updates_button_skip">Pular essa versão</string>
|
||||
<string name="app_check_for_updates_button_download">Baixar %s (%s)</string>
|
||||
<string name="app_check_for_updates_canceled">Download da atualização cancelado</string>
|
||||
<string name="chat_list_always_visible">Mostrar lista de conversas em nova janela</string>
|
||||
<string name="appearance_font_size">Tamanho da fonte</string>
|
||||
<string name="color_wallpaper_background">Fundo do papel de parede</string>
|
||||
<string name="color_wallpaper_tint">Tonalidade do papel de parede</string>
|
||||
<string name="theme_remove_image">Remover imagem</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="chat_theme_set_default_theme">Definir tema padrão</string>
|
||||
<string name="prohibit_sending_simplex_links">Proibido enviar links SimpleX</string>
|
||||
<string name="migrate_from_device_error_uploading_archive">Erro ao carregar o arquivo</string>
|
||||
<string name="migrate_from_device_upload_failed">Falha ao carregar</string>
|
||||
<string name="migrate_from_device_uploading_archive">Carregando arquivo</string>
|
||||
<string name="servers_info_detailed_statistics">Estatísticas detalhadas</string>
|
||||
<string name="file_error_relay">Erro no servidor de arquivo: %1$s</string>
|
||||
<string name="saved_from_chat_item_info_title">Salvo de</string>
|
||||
<string name="download_file">Baixar</string>
|
||||
<string name="forward_chat_item">Encaminhar</string>
|
||||
<string name="share_text_file_status">Status de arquivo: %s</string>
|
||||
<string name="invite_friends_short">Convidar</string>
|
||||
<string name="v6_0_increase_font_size">Aumentar tamanho da fonte.</string>
|
||||
<string name="v6_0_upgrade_app">Aprimorar aplicativo automaticamente</string>
|
||||
<string name="servers_info_reset_stats_alert_title">Redefinir todas as estatísticas?</string>
|
||||
<string name="servers_info_reset_stats_alert_message">As estatísticas dos servidores serão redefinidas - isso não poderá ser desfeito!</string>
|
||||
<string name="error_parsing_uri_title">Link inválido</string>
|
||||
<string name="error_parsing_uri_desc">Por favor cheque se o link SimpleX está correto</string>
|
||||
<string name="e2ee_info_pq"><![CDATA[Mensagens, arquivo e chamadas são protegidas por <b>criptografia quantum resistant e2e</b> com perfeito sigilo direto, repúdio e recuperação de vazamento.]]></string>
|
||||
<string name="e2ee_info_pq_short">Essa conversa é protegida por criptografia quantum resistant ponta a ponta</string>
|
||||
<string name="please_try_later">Por favor tente mais tarde.</string>
|
||||
<string name="selected_chat_items_selected_n">Selecionado %d</string>
|
||||
<string name="simplex_links_not_allowed">SimpleX links não permitidos</string>
|
||||
<string name="files_and_media_not_allowed">Arquivos e mídia não permitidos</string>
|
||||
<string name="compose_message_placeholder">Mensagem</string>
|
||||
<string name="temporary_file_error">Erro de arquivo temporário</string>
|
||||
<string name="info_view_message_button">mensagem</string>
|
||||
<string name="info_view_search_button">pesquisar</string>
|
||||
<string name="info_view_video_button">vídeo</string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Roteamento privado</string>
|
||||
<string name="network_smp_proxy_mode_never_description">NÃO use roteamento privado.</string>
|
||||
<string name="permissions_open_settings">Abrir configurações</string>
|
||||
<string name="audio_device_earpiece">Fone de ouvido</string>
|
||||
<string name="error_initializing_web_view">Erro ao iniciar o WebView. Atualize seu sistema para a nova versão. Por favor contate os desenvolvedores.
|
||||
\nErro: %s</string>
|
||||
<string name="privacy_media_blur_radius_off">Desativado</string>
|
||||
<string name="privacy_media_blur_radius_strong">Forte</string>
|
||||
<string name="v5_6_quantum_resistant_encryption_descr">Ativar em conversas diretas (BETA)!</string>
|
||||
<string name="migrate_to_device_finalize_migration">Finalizar migração em outro dispositivo.</string>
|
||||
<string name="color_mode_light">Claro</string>
|
||||
<string name="v5_7_forward_descr">A origem da mensagem permanece privada.</string>
|
||||
<string name="migrate_to_device_title">Migrar aqui</string>
|
||||
<string name="migrate_to_device_migrating">Migrando</string>
|
||||
<string name="v6_0_new_chat_experience">Nova experiência de conversa 🎉</string>
|
||||
<string name="v6_0_new_media_options">Novas opções de mídia</string>
|
||||
<string name="or_paste_archive_link">Ou cole o link do arquivo</string>
|
||||
<string name="v6_0_chat_list_media">Reproduzir da lista de conversa.</string>
|
||||
<string name="servers_info_reset_stats">Redefinir todas as estatísticas</string>
|
||||
<string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Aviso: iniciar conversa em múltiplos dispositivos não é suportado e pode causar falhas na entrega de mensagens</string>
|
||||
<string name="network_type_ethernet">Internet cabeada</string>
|
||||
<string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Você <b>não deve</b> usar a mesma base de dados em dois dispositivos.]]></string>
|
||||
<string name="group_members_can_send_simplex_links">Membros do grupo podem enviar link SimpleX</string>
|
||||
<string name="migrate_to_device_importing_archive">Importando arquivo</string>
|
||||
<string name="chat_theme_apply_to_light_mode">Modo claro</string>
|
||||
<string name="feature_enabled_for">Ativado para</string>
|
||||
<string name="v5_7_call_sounds_descr">Ao conectar em chamadas de áudio de vídeo.</string>
|
||||
<string name="migrate_from_device_title">Migrar dispositivo</string>
|
||||
<string name="migrate_from_device_error_saving_settings">Erro ao salvar configurações</string>
|
||||
<string name="migrate_from_device_exported_file_doesnt_exist">Arquivo exportado não existe</string>
|
||||
<string name="migrate_from_device_bytes_uploaded">%s carregados</string>
|
||||
<string name="migrate_from_device_chat_should_be_stopped">Para continuar, a conversa precisa ser interrompida.</string>
|
||||
<string name="network_type_other">Outro</string>
|
||||
<string name="network_type_no_network_connection">Sem conexão de rede</string>
|
||||
<string name="cannot_share_message_alert_text">As preferências de conversa selecionadas proíbem essa mensagem.</string>
|
||||
<string name="file_error">Erro de arquivo</string>
|
||||
<string name="reset_single_color">Redefinir cor</string>
|
||||
<string name="v5_7_call_sounds">Sons de chamada</string>
|
||||
<string name="v5_7_shape_profile_images">Formato das imagens de perfil</string>
|
||||
<string name="v5_7_new_interface_languages">IU Lituana</string>
|
||||
<string name="v5_8_private_routing">Roteamento de mensagem privada 🚀</string>
|
||||
<string name="v5_8_chat_themes">Novos temas de conversa</string>
|
||||
<string name="v5_8_message_delivery_descr">Com uso de bateria reduzida.</string>
|
||||
<string name="migrate_to_device_download_failed">Falha no download</string>
|
||||
<string name="migrate_to_device_import_failed">Falha na importação</string>
|
||||
<string name="migrate_to_device_downloading_archive">Baixando arquivo</string>
|
||||
<string name="migrate_to_device_repeat_import">Repetir importação</string>
|
||||
<string name="migrate_to_device_try_again">Você pode tentar novamente.</string>
|
||||
<string name="migrate_from_device_error_exporting_archive">Erro ao exportar banco de dados de conversa</string>
|
||||
<string name="migrate_from_device_error_verifying_passphrase">Erro ao verificar a palavra-chave:</string>
|
||||
<string name="saved_description">salvo</string>
|
||||
<string name="saved_from_description">salvo de %s</string>
|
||||
<string name="forwarded_from_chat_item_info_title">Encaminhado de</string>
|
||||
<string name="voice_messages_not_allowed">Mensagens de voz não permitidas</string>
|
||||
<string name="new_message">Nova mensagem</string>
|
||||
<string name="paste_link">Colar link</string>
|
||||
<string name="simplex_links">Links SimpleX</string>
|
||||
<string name="v5_7_forward">Encaminhar e salvar mensagens</string>
|
||||
<string name="privacy_media_blur_radius_soft">Suave</string>
|
||||
<string name="privacy_media_blur_radius_medium">Médio</string>
|
||||
<string name="you_need_to_allow_calls">Você precisa permitir seu contato ligue para poder ligar para ele.</string>
|
||||
<string name="chat_theme_reset_to_app_theme">Redefinir para o tema do aplicativo</string>
|
||||
<string name="color_received_quote">Resposta recebida</string>
|
||||
<string name="wallpaper_preview_hello_alice">Boa tarde!</string>
|
||||
<string name="wallpaper_preview_hello_bob">Bom dia!</string>
|
||||
<string name="v6_0_upgrade_app_descr">Baixe novas versões no GitHub.</string>
|
||||
<string name="forwarded_description">encaminhado</string>
|
||||
<string name="migrate_to_device_file_delete_or_link_invalid">O arquivo foi deletado ou o link está inválido</string>
|
||||
<string name="auth_open_migration_to_another_device">Abrir tela de migração</string>
|
||||
<string name="v5_6_safer_groups">Grupos seguros</string>
|
||||
<string name="migrate_from_device_database_init">Preparando upload</string>
|
||||
<string name="settings_section_title_network_connection">Conexão de rede</string>
|
||||
<string name="file_not_approved_title">Servidores desconhecidos!</string>
|
||||
<string name="file_not_approved_descr">Sem Tor ou VPN, seu endereço de IP ficará visível para esses relays XFTP
|
||||
\n%1$s.</string>
|
||||
<string name="error_showing_desktop_notification">Erro ao exibir notificação, contate os desenvolvedores.</string>
|
||||
<string name="saved_chat_item_info_tab">Salvo</string>
|
||||
<string name="forwarded_chat_item_info_tab">Encaminhado</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Servidores desconhecidos</string>
|
||||
<string name="network_smp_proxy_mode_unprotected">Desprotegido</string>
|
||||
<string name="network_smp_proxy_mode_never">Nunca</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Modo de roteamento de mensagens</string>
|
||||
<string name="permissions_required">Conceder permissões</string>
|
||||
<string name="audio_device_speaker">Alto falante</string>
|
||||
<string name="audio_device_wired_headphones">Headphones</string>
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Sem Tor ou VPN, seu endereço de IP ficará visível para servidores de arquivo.</string>
|
||||
<string name="settings_section_title_files">ARQUIVOS</string>
|
||||
<string name="settings_section_title_profile_images">Fotos de perfil</string>
|
||||
<string name="settings_section_title_private_message_routing">ROTEAMENTO DE MENSAGEM PRIVADA</string>
|
||||
<string name="conn_event_disabled_pq">criptografia padrão ponta a ponta</string>
|
||||
<string name="feature_roles_owners">proprietários</string>
|
||||
<string name="migrate_from_device_to_another_device">Migrar para outro dispositivo</string>
|
||||
<string name="migrate_from_device_verify_passphrase">Verificar palavra-passe</string>
|
||||
<string name="network_type_network_wifi">WiFi</string>
|
||||
<string name="one_hand_ui_card_title">Alternar lista de conversa:</string>
|
||||
<string name="one_hand_ui_change_instruction">Você pode mudar isso em configurações de Aparência.</string>
|
||||
<string name="member_info_member_disabled">desativado</string>
|
||||
<string name="message_queue_info_none">nenhum</string>
|
||||
<string name="message_queue_info_server_info">informações da fila do servidor: %1$s
|
||||
\n
|
||||
\núltima mensagem recebida: %2$s</string>
|
||||
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Por favor peça para seu contato ativar as chamadas.</string>
|
||||
<string name="cant_call_member_send_message_alert_text">Enviar mensagem para ativar chamadas.</string>
|
||||
<string name="network_options_save_and_reconnect">Salvar e reconectar</string>
|
||||
<string name="network_option_tcp_connection">Conexão TCP</string>
|
||||
<string name="v6_0_reachable_chat_toolbar">Barra de ferramentas de conversa acessível</string>
|
||||
<string name="v6_0_private_routing_descr">Isso protege seu endereço de IP e conexões.</string>
|
||||
<string name="v6_0_reachable_chat_toolbar_descr">Use o aplicativo com uma mão.</string>
|
||||
<string name="uploaded_files">Arquivos carregados</string>
|
||||
<string name="servers_info_downloaded">Baixado</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Erro ao redefinir estatísticas</string>
|
||||
<string name="servers_info_reset_stats_alert_confirm">Redefinir</string>
|
||||
<string name="info_row_file_status">Status de arquivo</string>
|
||||
<string name="message_queue_info">Informações da fila de mensagens</string>
|
||||
<string name="color_mode_system">Sistema</string>
|
||||
<string name="wallpaper_scale_repeat">Repetir</string>
|
||||
<string name="wallpaper_scale">Escala</string>
|
||||
<string name="wallpaper_scale_fill">Preencher</string>
|
||||
<string name="wallpaper_scale_fit">Ajustar</string>
|
||||
<string name="simplex_links_are_prohibited_in_group">Links SimpleX são proibidos neste grupo.</string>
|
||||
<string name="v5_6_app_data_migration_descr">Migrar para outro dispositivo via QR code.</string>
|
||||
<string name="v5_6_picture_in_picture_calls">Chamadas picture-in-picture</string>
|
||||
<string name="v5_6_picture_in_picture_calls_descr">Use o aplicativo enquanto está em chamada.</string>
|
||||
<string name="v5_7_quantum_resistant_encryption_descr">Será ativado em conversas diretas!</string>
|
||||
<string name="v5_8_safe_files">Receber arquivos de forma segura</string>
|
||||
<string name="v5_7_network">Gerenciamento de rede</string>
|
||||
<string name="v5_8_chat_themes_descr">Faça suas conversas terem uma aparência diferente!</string>
|
||||
<string name="v5_7_network_descr">Conexão de rede mais confiável.</string>
|
||||
<string name="migrate_to_device_database_init">Preparando download</string>
|
||||
<string name="migrate_to_device_bytes_downloaded">%s baixados</string>
|
||||
<string name="paste_archive_link">Colar link de arquivo</string>
|
||||
<string name="migrate_to_device_enter_passphrase">Insira a palavra-chave</string>
|
||||
<string name="migrate_to_device_error_downloading_archive">Erro ao baixar o arquivo</string>
|
||||
<string name="migrate_from_device_or_share_this_file_link">Ou de forma segura compartilhe esse link de arquivo</string>
|
||||
<string name="migrate_from_device_verify_database_passphrase">Verifique a palavra-passe do banco de dados</string>
|
||||
<string name="e2ee_info_no_pq"><![CDATA[Mensagens, arquivo e chamadas são protegidas por <b>criptografia ponta-a-ponta</b> com perfeito sigilo direto, repúdio e recuperação de vazamento.]]></string>
|
||||
<string name="file_error_no_file">Arquivo não encontrado - provavelmente o arquivo foi excluído ou cancelado.</string>
|
||||
<string name="file_error_auth">Chave incorreta ou arquivo de pedaço de endereço - provavelmente o arquivo foi excluído.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Mande mensagens diretamente quando o seu endereço de IP está protegido e o servidor de destino não suporta roteamento privado.</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Use roteamento privado em servidores desconhecidos.</string>
|
||||
<string name="scan_paste_link">Escanear / Colar link</string>
|
||||
<string name="migrate_to_device_downloading_details">Baixando detalhes de link</string>
|
||||
<string name="migrate_from_device_finalize_migration">Finalizar migração</string>
|
||||
<string name="v5_6_quantum_resistant_encryption">Criptografia Quantum resistant</string>
|
||||
<string name="migrate_from_device_migration_complete">Migração concluída</string>
|
||||
<string name="v5_8_private_routing_descr">Proteja seu endereço de IP dos retransmissores de mensagem escolhidos por seus contatos.
|
||||
\nAtive nas configurações *Redes e servidores* .</string>
|
||||
<string name="migrate_from_device_start_chat">Iniciar conversa</string>
|
||||
<string name="toolbar_settings">Configurações</string>
|
||||
<string name="info_view_open_button">abrir</string>
|
||||
<string name="keep_conversation">Manter conversa</string>
|
||||
<string name="only_delete_conversation">Apenas excluir conversa</string>
|
||||
<string name="xftp_servers_other">Outros servidores XFTP</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Use roteamento privado em servidores desconhecidos quando o endereço de IP não está protegido.</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Sim</string>
|
||||
<string name="app_check_for_updates_button_install">Instalar atualização</string>
|
||||
<string name="app_check_for_updates_update_available">Atualização disponível: %s</string>
|
||||
<string name="app_check_for_updates_button_open">Abrir local do arquivo</string>
|
||||
<string name="app_check_for_updates_notice_disable">Desativar</string>
|
||||
<string name="permissions_record_audio">Microfone</string>
|
||||
<string name="permissions_grant_in_settings">Conceder nas configurações</string>
|
||||
<string name="permissions_find_in_settings_and_grant">Encontre essa permissão nas configurações do Android e conceda-a manualmente.</string>
|
||||
<string name="app_will_ask_to_confirm_unknown_file_servers">O aplicativo irá perguntar para confirmar os downloads de servidores de arquivo desconhecidos (exceto .onion ou quando o proxy SOCKS estiver habilitado).</string>
|
||||
<string name="settings_section_title_user_theme">Tema de perfil</string>
|
||||
<string name="set_passphrase">Defina uma palavra-chave</string>
|
||||
<string name="conn_event_enabled_pq">criptografia quantum resistant e2e</string>
|
||||
<string name="action_button_add_members">Convidar</string>
|
||||
<string name="info_row_message_status">Status da mensagem</string>
|
||||
<string name="v5_8_message_delivery">Entrega de mensagens aprimorada</string>
|
||||
<string name="v5_7_shape_profile_images_descr">Quadrado, circulo, ou qualquer coisa entre eles.</string>
|
||||
<string name="remote_ctrl_connection_stopped_desc">Por favor verifique se o celular e o computador estão conectados na mesma rede local e o firewall do computador permite a conexão.
|
||||
\nPor favor compartilhe qualquer outro problema com os desenvolvedores.</string>
|
||||
<string name="remote_ctrl_connection_stopped_identity_desc">Esse link foi usado em outros dispositivo móvel, por favor crie um novo link no computador.</string>
|
||||
<string name="migrate_from_device_error_deleting_database">Erro ao excluir banco de dados</string>
|
||||
<string name="migrate_to_device_confirm_network_settings_footer">Por favor confirme que as configurações de rede estão corretas para este dispositivo.</string>
|
||||
<string name="migrate_from_device_stopping_chat">Parando conversa</string>
|
||||
<string name="migrate_from_device_try_again">Você pode tentar novamente.</string>
|
||||
<string name="network_error_broker_version_desc">A versão do servidor é incompatível com seu aplicativo: %1$s.</string>
|
||||
<string name="private_routing_error">Erro de roteamento privado</string>
|
||||
<string name="network_error_broker_host_desc">O endereço do servidor é incompatível com as configurações de rede: %1$s.</string>
|
||||
<string name="snd_error_proxy_relay">Servidor de encaminhamento: %1$s
|
||||
\nErro no servidor de destino: %2$s</string>
|
||||
<string name="srv_error_host">Endereço do servidor é incompatível com as configurações de rede.</string>
|
||||
<string name="srv_error_version">A versão do servidor é incompatível com as configurações de rede.</string>
|
||||
<string name="forward_message">Encaminhar mensagem…</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Quando IP oculto</string>
|
||||
<string name="protect_ip_address">Proteger endereço IP</string>
|
||||
<string name="color_sent_quote">Enviar resposta</string>
|
||||
<string name="servers_info_files_tab">Arquivos</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Não</string>
|
||||
<string name="app_check_for_updates_download_started">Baixando atualização do aplicativo, não feche o aplicativo</string>
|
||||
<string name="permissions_grant">Conceder permissão para fazer chamadas</string>
|
||||
<string name="invalid_file_link">Link inválido</string>
|
||||
<string name="servers_info_details">Detalhes</string>
|
||||
<string name="servers_info_sessions_errors">Erros</string>
|
||||
<string name="servers_info_messages_received">Mensagens recebidas</string>
|
||||
<string name="servers_info_messages_sent">Mensagens enviadas</string>
|
||||
<string name="servers_info_missing">Sem informação, tente recarregar</string>
|
||||
<string name="servers_info">Informação dos servidores</string>
|
||||
<string name="servers_info_target">Mostrando informação para</string>
|
||||
<string name="servers_info_statistics_section_header">Estatísticas</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Sessões de transporte</string>
|
||||
<string name="servers_info_subscriptions_section_header">Recepção de mensagem</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Pendente</string>
|
||||
<string name="servers_info_private_data_disclaimer">Começando de %s.
|
||||
\nTodos os dados são privados do seu dispositivo.</string>
|
||||
<string name="servers_info_subscriptions_total">Total</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Servidores proxiados</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Servidores conectados anteriormente</string>
|
||||
<string name="servers_info_reconnect_servers_message">Reconecte todos os servidores conectados para forçar entrega de mensagem. Isso usa tráfego adicional.</string>
|
||||
<string name="servers_info_reconnect_server_title">Reconectar servidor?</string>
|
||||
<string name="servers_info_reconnect_servers_title">Reconectar servidores?</string>
|
||||
<string name="servers_info_reconnect_server_message">Reconectar servidor para forçar entrega de mensagem. Isso usa tráfego adicional.</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">Você não está conectado nesses servidores. Roteamento privado é usado para entregar mensagens para eles.</string>
|
||||
<string name="servers_info_modal_error_title">Erro</string>
|
||||
<string name="servers_info_reconnect_server_error">Erro ao reconectar servidor</string>
|
||||
<string name="servers_info_reconnect_servers_error">Erro ao reconectar servidores</string>
|
||||
<string name="servers_info_reconnect_all_servers_button">Reconectar todos os servidores</string>
|
||||
<string name="reconnect">Reconectar</string>
|
||||
<string name="sent_directly">Enviar diretamente</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">Enviar mensagens</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Enviar total</string>
|
||||
<string name="sent_via_proxy">Enviar via proxy</string>
|
||||
<string name="smp_server">Servidor SMP</string>
|
||||
<string name="servers_info_detailed_statistics_received_messages_header">Mensagens recebidas</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">Total recebido</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Receber erros</string>
|
||||
<string name="servers_info_starting_from">Começando de %s.</string>
|
||||
<string name="xftp_server">Servidor XFTP</string>
|
||||
<string name="secured">Seguro</string>
|
||||
<string name="send_errors">Enviar erros</string>
|
||||
<string name="subscribed">Inscrito</string>
|
||||
<string name="duplicates_label">duplicatas</string>
|
||||
<string name="expired_label">expirada</string>
|
||||
<string name="other_label">outro</string>
|
||||
<string name="subscription_errors">Erros de inscrição</string>
|
||||
<string name="other_errors">outros erros</string>
|
||||
<string name="proxied">Proxied</string>
|
||||
<string name="subscription_results_ignored">Inscrições ignoradas</string>
|
||||
<string name="download_errors">Erros de download</string>
|
||||
<string name="downloaded_files">Arquivos baixados</string>
|
||||
<string name="server_address">Endereço do servidor</string>
|
||||
<string name="size">Tamanho</string>
|
||||
<string name="upload_errors">Erros de upload</string>
|
||||
<string name="open_server_settings_button">Abrir configurações de servidor</string>
|
||||
<string name="select_verb">Selecione</string>
|
||||
<string name="moderate_messages_will_be_deleted_warning">As mensagens serão excluídas para todos os membros.</string>
|
||||
<string name="moderate_messages_will_be_marked_warning">As mensagens serão marcadas como moderadas para todos os membros.</string>
|
||||
<string name="message_forwarded_title">Mensagem encaminhada</string>
|
||||
<string name="message_forwarded_desc">Ainda não há conexão direta, a mensagem é encaminhada pelo administrador.</string>
|
||||
<string name="member_inactive_title">Membro inativo</string>
|
||||
<string name="selected_chat_items_nothing_selected">Nada selecionado</string>
|
||||
<string name="delete_messages_mark_deleted_warning">Mensagens serão marcadas para exclusão. O(s) destinatário(s) poderá(ão) revelar essas mensagens.</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">Você ainda pode ver a conversa com %1$s na lista de conversas.</string>
|
||||
<string name="no_filtered_contacts">Nenhum contato filtrado</string>
|
||||
<string name="contact_list_header_title">Seus contatos</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">NÃO envie mensagens diretamente, mesmo que o seu servidor ou o servidor de destino não suporte roteamento privado.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Mande mensagens diretamente quando o seu servidor ou o servidor de destino não suporta roteamento privado.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Retorno de roteamento de mensagens</string>
|
||||
<string name="private_routing_show_message_status">Mostrar status da mensagem</string>
|
||||
<string name="migrate_from_another_device">Migrar de outro dispositivo</string>
|
||||
<string name="share_text_message_status">Status da mensagem: %s</string>
|
||||
<string name="servers_info_uploaded">Carregado</string>
|
||||
<string name="message_servers">Servidores de mensagem</string>
|
||||
<string name="media_and_file_servers">Servidores de mídia e arquivo</string>
|
||||
<string name="subscription_percentage">Mostrar porcentagem</string>
|
||||
<string name="network_socks_proxy">Proxy SOCKS</string>
|
||||
<string name="chat_database_exported_save">Você pode salvar o arquivo exportado.</string>
|
||||
<string name="chat_database_exported_migrate">Você pode migrar o banco de dados exportado.</string>
|
||||
<string name="chat_database_exported_not_all_files">Alguns arquivos não foram exportados</string>
|
||||
<string name="you_can_still_send_messages_to_contact">Você pode enviar mensagens para %1$s de Contatos arquivados.</string>
|
||||
<string name="reset_all_hints">Redefinir todas as dicas</string>
|
||||
<string name="app_check_for_updates_disabled">Desativado</string>
|
||||
<string name="app_check_for_updates_stable">Estável</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">Instalado com sucesso</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Por favor reinicie o aplicativo.</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Me lembre mais tarde</string>
|
||||
<string name="app_check_for_updates_notice_desc">Para ser notificado sobre os novos lançamentos, habilite a checagem periódica de versões Estáveis e Beta.</string>
|
||||
<string name="one_hand_ui">Barra de ferramentas de conversa acessível</string>
|
||||
<string name="forward_files_failed_to_receive_desc">Falha no baixar de %1$d arquivo(s).</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s mensagens não encaminhadas.</string>
|
||||
<string name="settings_section_title_chat_database">DADOS DO BATE-PAPO</string>
|
||||
<string name="network_proxy_random_credentials">Utilize credenciais aleatórias</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">O arquivo de banco de dados enviado será removido permanentemente dos servidores.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Use credenciais diferentes de proxy para cada conexão.</string>
|
||||
<string name="switching_profile_error_message">Sua conexão foi movida para %s, mas um erro inesperado ocorreu ao redirecioná-lo para o seu perfil.</string>
|
||||
<string name="n_file_errors">%1$d erro(s) de arquivo(s):
|
||||
\n%2$s</string>
|
||||
<string name="n_other_file_errors">%1$d outro erro de arquivo.</string>
|
||||
<string name="error_forwarding_messages">Erro ao encaminhar mensagens.</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Encaminhar %1$s mensagens?</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Encaminhar mensagens sem arquivos?</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">As mensagens foram excluidas após vocês selecioná-las.</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Nada para encaminhar!</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d arquivo(s) ainda estão sendo baixados.</string>
|
||||
<string name="forward_files_missing_desc">%1$d arquivos foram excluidos.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d arquivos não foram baixados.</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Baixar</string>
|
||||
<string name="forward_multiple">Emcaminhar mensagens…</string>
|
||||
<string name="compose_forward_messages_n">Encaminhando %1$s mensagens.</string>
|
||||
<string name="compose_save_messages_n">Salvando %1$s mensagens</string>
|
||||
<string name="network_proxy_auth">Autenticação de proxy</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Não utilize credenciais com proxy.</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Certifique-se de que configuração do proxy está correta.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Use diferentes credenciais de proxy para cada perfil.</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Suas credenciais podem ser enviadas sem criptografia.</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Remover arquivo?</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">As mensagens serão excluídas - isso não pode ser desfeito!</string>
|
||||
<string name="switching_profile_error_title">Erro ao alternar perfil</string>
|
||||
<string name="select_chat_profile">Selecionar perfil de bate-papo</string>
|
||||
<string name="new_chat_share_profile">Compartilhar perfil</string>
|
||||
<string name="system_mode_toast">Modo sistema</string>
|
||||
<string name="network_proxy_incorrect_config_title">Erro ao salvar proxy</string>
|
||||
<string name="network_proxy_password">Senha</string>
|
||||
<string name="network_proxy_username">Nome de usuário</string>
|
||||
</resources>
|
||||
@@ -49,7 +49,7 @@
|
||||
<string name="accept_feature">Aceitar</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Permitir que seus contatos enviem mensagens que desaparecem.</string>
|
||||
<string name="accept_feature_set_1_day">Definir 1 dia</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Permitir a exclusão irreversível de mensagens apenas se o seu contato permitir.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Permitir a exclusão irreversível de mensagens apenas se o seu contato permitir. (24 horas)</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Permitir que seus contatos eliminem de forma irreversível mensagens enviadas.</string>
|
||||
<string name="allow_voice_messages_only_if">Permitir mensagens de voz apenas se o contato permitir.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Permitir que seus contatos enviem mensagens de voz.</string>
|
||||
@@ -130,7 +130,7 @@
|
||||
<string name="network_enable_socks_info">Aceder aos servidores via proxy SOCKS no porto %d\? O proxy tem de iniciar antes de ativar esta opção.</string>
|
||||
<string name="smp_servers_add_to_another_device">Adicionar a outro dispositivo</string>
|
||||
<string name="group_member_role_admin">administrador</string>
|
||||
<string name="allow_to_delete_messages">Permitir apagar irreversivelmente as mensagens enviadas.</string>
|
||||
<string name="allow_to_delete_messages">Permitir apagar irreversivelmente as mensagens enviadas. (24 horas)</string>
|
||||
<string name="delete_address__question">Eliminar endereço\?</string>
|
||||
<string name="delete_after">Eliminar após</string>
|
||||
<string name="delete_verb">Eliminar</string>
|
||||
@@ -565,7 +565,7 @@
|
||||
<string name="v4_3_voice_messages_desc">Máximo de 40 segundos, recebido instantaneamente.</string>
|
||||
<string name="icon_descr_more_button">Mais</string>
|
||||
<string name="network_and_servers">Rede e servidores</string>
|
||||
<string name="network_settings_title">Definições de rede</string>
|
||||
<string name="network_settings_title">Configurações avançadas</string>
|
||||
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Você pode iniciar a conversa através das Definições da aplicação / Base de Dados ou reiniciando a aplicação.</string>
|
||||
<string name="update_network_settings_confirmation">Atualizar</string>
|
||||
@@ -949,4 +949,34 @@
|
||||
<string name="servers_info_details">Detalhes</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">Você pode partilhar o seu endereço com os seus contactos para permitir que se conectem com %s.</string>
|
||||
<string name="you_can_share_your_address">Você pode partilhar o seu endereço como uma ligação ou código QR - qualquer pessoa pode conectar-se a si.</string>
|
||||
<string name="turn_off_battery_optimization_button">Permitir</string>
|
||||
<string name="block_member_desc">Todas as novas mensagens de %s serão ocultadas!</string>
|
||||
<string name="only_you_can_make_calls">Somente você pode fazer ligações.</string>
|
||||
<string name="chat_theme_apply_to_all_modes">Todos os modos de cores</string>
|
||||
<string name="feature_roles_admins">administradores</string>
|
||||
<string name="snd_conn_event_ratchet_sync_started">"aceitando criptografia para %s…"</string>
|
||||
<string name="add_contact_tab">Adicionar contato</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Permitir downgrade</string>
|
||||
<string name="clear_note_folder_warning">Todas as mensagens serão deletadas - isso não poderá ser desfeito!</string>
|
||||
<string name="v5_2_more_things">Mais algumas coisas</string>
|
||||
<string name="allow_to_send_files">Permitir envio de arquivos e mídias.</string>
|
||||
<string name="v5_6_safer_groups_descr">Administradores podem bloquear um membro para todos.</string>
|
||||
<string name="conn_event_ratchet_sync_started">Aceitando criptografia</string>
|
||||
<string name="wallpaper_advanced_settings">Configurações avançadas</string>
|
||||
<string name="feature_roles_all_members">todos os membros</string>
|
||||
<string name="acknowledgement_errors">Erros de reconhecimento</string>
|
||||
<string name="abort_switch_receiving_address_desc">Mudança de endereço será cancelada. Antigo endereço de recebimento será usado.</string>
|
||||
<string name="allow_calls_question">Permitir ligações?</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Conexões ativas</string>
|
||||
<string name="migrate_from_device_all_data_will_be_uploaded">Todos os seus contatos, conversas e arquivos serão encriptados e enviados em chunks para relays XFTP configurados.</string>
|
||||
<string name="n_other_file_errors">%1$d erro(s) de outro arquivo.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s mensagens não encaminhadas</string>
|
||||
<string name="network_smp_proxy_mode_always">Sempre</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Sempre use uma rota privata.</string>
|
||||
<string name="allow_to_send_simplex_links">Permitir o envio de links SimpleX.</string>
|
||||
<string name="moderated_items_description">%1$d mensagens moderadas por %2$s</string>
|
||||
<string name="rcv_group_and_other_events">e %d outros</string>
|
||||
<string name="all_users">Todos os perfis</string>
|
||||
<string name="connect_plan_already_connecting">Já conectando!</string>
|
||||
<string name="connect_plan_already_joining_the_group">Já entrando no grupo!</string>
|
||||
</resources>
|
||||
@@ -674,4 +674,57 @@
|
||||
<string name="migrate_from_device_delete_database_from_device">Șterge baza de date de pe acest dispozitiv</string>
|
||||
<string name="delete_chat_profile">Șterge profil de conversație</string>
|
||||
<string name="delete_files_and_media_for_all_users">Șterge fișiere pentru toate profilurile de conversație</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Oricine poate găzdui servere.</string>
|
||||
<string name="deleted_chats">Contacte arhivate</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="calls_prohibited_alert_title">Apeluri interzise!</string>
|
||||
<string name="cant_call_member_alert_title">Nu se poate apela membrul grupului</string>
|
||||
<string name="chat_database_exported_title">Bază de date conversație exportată</string>
|
||||
<string name="app_check_for_updates">Verifică pentru actualizări</string>
|
||||
<string name="profile_update_event_contact_name_changed">Contactul %1$s a schimbat la %2$s</string>
|
||||
<string name="cant_call_contact_deleted_alert_text">Contactul este șters.</string>
|
||||
<string name="v6_0_connection_servers_status">Controlează-ți rețeaua</string>
|
||||
<string name="database_encryption_will_be_updated">Fraza de acces pentru criptarea bazei de date va fi actualizată și stocată în Keystore.</string>
|
||||
<string name="database_passphrase">Fraza de acces a bazei de date</string>
|
||||
<string name="xftp_servers_configured">Servere XFTP configurate</string>
|
||||
<string name="set_password_to_export_desc">Baza de date este criptată folosind o expresie de acces aleatorie. Schimbați-o înainte de a exporta.</string>
|
||||
<string name="info_view_call_button">apel</string>
|
||||
<string name="info_view_connect_button">conectare</string>
|
||||
<string name="contact_deleted">Contact șters!</string>
|
||||
<string name="conversation_deleted">Conversație ștearsă!</string>
|
||||
<string name="app_check_for_updates_download_completed_title">Actualizarea aplicației este descărcată</string>
|
||||
<string name="app_check_for_updates_notice_title">Verifică pentru actualizări</string>
|
||||
<string name="create_address_button">Creează</string>
|
||||
<string name="privacy_media_blur_radius">Estompează media</string>
|
||||
<string name="settings_section_title_chat_database">BAZĂ DE DATE CHAT</string>
|
||||
<string name="v6_0_connect_faster_descr">Conectează-te cu prietenii mai ușor.</string>
|
||||
<string name="attempts_label">încercări</string>
|
||||
<string name="completed">Completat</string>
|
||||
<string name="developer_options">ID-urile bazei de date și opțiunea de izolare a transportului.</string>
|
||||
<string name="info_row_database_id">ID bază de date</string>
|
||||
<string name="share_text_database_id">ID bază de date: %d</string>
|
||||
<string name="connections">Conexiuni</string>
|
||||
<string name="created">Creat</string>
|
||||
<string name="v6_0_privacy_blur">Estompează pentru intimitate mai bună.</string>
|
||||
<string name="v6_0_connection_servers_status_descr">Stare conexiune și servere</string>
|
||||
<string name="database_error">Eroare bază de date</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">Contactele pot marca mesajele pentru ștergere; tu le vei putea vedea.</string>
|
||||
<string name="database_downgrade">Downgrade al bazei de date</string>
|
||||
<string name="database_migration_in_progress">Migrarea bazei de date este în proces.
|
||||
\nPoate dura câteva minute.</string>
|
||||
<string name="database_passphrase_and_export">Fraza de acces a bazei de date și export</string>
|
||||
<string name="all_users">Toate profilurile</string>
|
||||
<string name="current_user">Profil actual</string>
|
||||
<string name="confirm_delete_contact_question">Confirmi ștergerea contactului?</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Contactul va fi șters - acest lucru nu poate fi anulat!</string>
|
||||
<string name="smp_servers_configured">Servere SMP configurate</string>
|
||||
<string name="servers_info_sessions_connected">Conectat</string>
|
||||
<string name="correct_name_to">Corectează numele la %s?</string>
|
||||
<string name="chat_database_exported_continue">Continuă</string>
|
||||
<string name="encrypted_with_random_passphrase">Baza de date este criptată folosind o expresie de acces aleatorie, o poți schimba</string>
|
||||
<string name="cant_send_message_to_member_alert_title">Nu se pot trimite mesaje membrului grupului</string>
|
||||
<string name="servers_info_sessions_connecting">Se conectează</string>
|
||||
<string name="servers_info_connected_servers_section_header">Servere conectate</string>
|
||||
<string name="cant_call_contact_alert_title">Nu se poate apela contactul</string>
|
||||
<string name="cant_call_contact_connecting_wait_alert_text">Se conectează la contact, te rog așteaptă sau verifică mai târziu!</string>
|
||||
</resources>
|
||||
@@ -445,7 +445,7 @@
|
||||
<string name="this_string_is_not_a_connection_link">Эта строка не является ссылкой-приглашением!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Вы также можете соединиться, открыв ссылку там, где Вы её получили. Если ссылка откроется в браузере, нажмите кнопку <b>Открыть в приложении</b>.]]></string>
|
||||
<!-- CICallStatus -->
|
||||
<string name="callstatus_calling">входящий звонок…</string>
|
||||
<string name="callstatus_calling">звонок…</string>
|
||||
<string name="callstatus_missed">пропущенный звонок</string>
|
||||
<string name="callstatus_rejected">отклоненный звонок</string>
|
||||
<string name="callstatus_accepted">принятый звонок</string>
|
||||
@@ -2150,4 +2150,63 @@
|
||||
<string name="v6_0_chat_list_media">Открыть из списка чатов.</string>
|
||||
<string name="reset_all_hints">Сбросить все подсказки.</string>
|
||||
<string name="one_hand_ui_change_instruction">Вы можете изменить это в настройках Интерфейса.</string>
|
||||
<string name="compose_forward_messages_n">Пересылка %1$s сообщений</string>
|
||||
<string name="compose_save_messages_n">Сохранение %1$s сообщений</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Убедитесь, что конфигурация прокси правильная.</string>
|
||||
<string name="network_proxy_auth">Аутентификация прокси</string>
|
||||
<string name="network_proxy_random_credentials">Использовать случайные учетные данные</string>
|
||||
<string name="system_mode_toast">Режим системы</string>
|
||||
<string name="error_forwarding_messages">Ошибка пересылки сообщений</string>
|
||||
<string name="n_file_errors">%1$d ошибок файлов:\n%2$s</string>
|
||||
<string name="n_other_file_errors">%1$d других ошибок файлов.</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Переслать %1$s сообщение(й)?</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Переслать сообщения без файлов?</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Сообщения были удалены после того, как вы их выбрали.</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Нет сообщений, которые можно переслать!</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d файл(ов) загружаются.</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d файл(ов) не удалось загрузить.</string>
|
||||
<string name="forward_files_missing_desc">%1$d файлов было удалено.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d файлов не было загружено.</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Загрузить</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s сообщений не переслано</string>
|
||||
<string name="forward_multiple">Переслать сообщения…</string>
|
||||
<string name="error_parsing_uri_desc">Проверьте правильность ссылки SimpleX.</string>
|
||||
<string name="error_parsing_uri_title">Неверная ссылка</string>
|
||||
<string name="settings_section_title_chat_database">БАЗА ДАННЫХ</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Ошибка инициализации WebView. Убедитесь, что у вас установлен WebView и его поддерживаемая архитектура – arm64.\nОшибка: %s</string>
|
||||
<string name="icon_descr_sound_muted">Звук отключен</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Сообщения будут удалены — это нельзя отменить!</string>
|
||||
<string name="switching_profile_error_title">Ошибка переключения профиля</string>
|
||||
<string name="select_chat_profile">Выберите профиль чата</string>
|
||||
<string name="new_chat_share_profile">Поделиться профилем</string>
|
||||
<string name="switching_profile_error_message">Соединение было перемещено на %s, но при смене профиля произошла неожиданная ошибка.</string>
|
||||
<string name="settings_message_shape_corner">Угол</string>
|
||||
<string name="network_session_mode_session">Сессия приложения</string>
|
||||
<string name="network_session_mode_session_description">Новые учетные данные SOCKS будут использоваться при каждом запуске приложения.</string>
|
||||
<string name="network_session_mode_server_description">Новые учетные данные SOCKS будут использоваться для каждого сервера.</string>
|
||||
<string name="network_session_mode_server">Сервер</string>
|
||||
<string name="settings_section_title_message_shape">Форма сообщений</string>
|
||||
<string name="settings_message_shape_tail">Хвост</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Нажмите кнопку информации рядом с адресной строкой, чтобы разрешить микрофон.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Откройте Настройки Safari / Веб-сайты / Микрофон, затем выберите Разрешить для localhost.</string>
|
||||
<string name="v6_1_better_calls">Улучшенные звонки</string>
|
||||
<string name="v6_1_message_dates_descr">Улучшенные даты сообщений.</string>
|
||||
<string name="v6_1_better_security">Улучшенная безопасность ✅</string>
|
||||
<string name="v6_1_better_user_experience">Улучшенный интерфейс</string>
|
||||
<string name="v6_1_customizable_message_descr">Настраиваемая форма сообщений.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Удаляйте или модерируйте до 200 сообщений.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Пересылайте до 20 сообщений за раз.</string>
|
||||
<string name="v6_1_better_calls_descr">Переключайте звук и видео во время звонка.</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">Переключайте профиль чата для одноразовых приглашений.</string>
|
||||
<string name="v6_1_better_security_descr">Аудит SimpleX протоколов от Trail of Bits.</string>
|
||||
<string name="call_desktop_permission_denied_title">Чтобы совершать звонки, разрешите использовать микрофон. Завершите вызов и попробуйте позвонить снова.</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Не использовать учетные данные с прокси.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Ошибка сохранения прокси</string>
|
||||
<string name="network_proxy_password">Пароль</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Использовать разные учетные данные прокси для каждого соединения.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Использовать разные учетные данные прокси для каждого профиля.</string>
|
||||
<string name="network_proxy_username">Имя пользователя</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Ваши учетные данные могут быть отправлены в незашифрованном виде.</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Удалить архив?</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Загруженный архив базы данных будет навсегда удален с серверов.</string>
|
||||
</resources>
|
||||
@@ -22,7 +22,7 @@
|
||||
<string name="appearance_settings">Görünüm</string>
|
||||
<string name="your_settings">Ayarlarınız</string>
|
||||
<string name="mute_chat">Sessize al</string>
|
||||
<string name="unmute_chat">Sessizden çıkar</string>
|
||||
<string name="unmute_chat">Susturmayı kaldır</string>
|
||||
<string name="abort_switch_receiving_address_confirm">İptal</string>
|
||||
<string name="abort_switch_receiving_address_question">Adres değişikliğini iptal et\?</string>
|
||||
<string name="send_disappearing_message_30_seconds">30 saniye</string>
|
||||
@@ -94,7 +94,7 @@
|
||||
<string name="save_preferences_question">Tercihleri kaydet\?</string>
|
||||
<string name="save_profile_password">Profil parolasını kaydet</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Profil sadece konuştuğun kişilerle paylaşılır.</string>
|
||||
<string name="next_generation_of_private_messaging">Gizli iletişimin gelecek kuşağı</string>
|
||||
<string name="next_generation_of_private_messaging">Gizli iletişimin\ngelecek kuşağı</string>
|
||||
<string name="icon_descr_audio_off">Ses kapalı</string>
|
||||
<string name="authentication_cancelled">Doğrulama iptal edildi</string>
|
||||
<string name="settings_restart_app">Yeniden başlat</string>
|
||||
@@ -592,7 +592,7 @@
|
||||
<string name="hidden_profile_password">Gizli profil parolası</string>
|
||||
<string name="how_to_use_markdown">Markdown nasıl kullanılır</string>
|
||||
<string name="how_it_works">Nasıl çalışıyor</string>
|
||||
<string name="immune_to_spam_and_abuse">Kötüye kullanmaya ve istenmeyen mesajlara duyarlı</string>
|
||||
<string name="immune_to_spam_and_abuse">Spamdan etkilenmez</string>
|
||||
<string name="icon_descr_hang_up">Çağırıyı bitir.</string>
|
||||
<string name="icon_descr_flip_camera">Kameranın karşı yüzüne geç</string>
|
||||
<string name="import_database_question">Konuşma veri tabanı içe aktarılsın mı?</string>
|
||||
@@ -757,7 +757,7 @@
|
||||
<string name="callstatus_connecting">aramaya bağlanılıyor…</string>
|
||||
<string name="delivery_receipts_are_disabled">Gönderildi bilgisi kapalı!</string>
|
||||
<string name="v5_2_more_things">Birkaç şey daha</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Daha fazla pil kullanır</b>! Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Daha fazla pil kullanır</b>! Uygulama her zaman arka planda çalışır - bildirimler anında gösterilir.]]></string>
|
||||
<string name="in_developing_title">Çok yakında!</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kişi ve tüm mesajlar silinecektir - bu geri alınamaz!</string>
|
||||
<string name="alert_title_contact_connection_pending">Kişi henüz bağlanmadı!</string>
|
||||
@@ -825,7 +825,7 @@
|
||||
<string name="mark_read">Okundu olarak işaretle</string>
|
||||
<string name="mark_unread">Okunmadı olarak işaretle</string>
|
||||
<string name="chat_console">Sohbet konsolu</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Pil için iyi</b>. Arka plan hizmeti mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Pil ömrü için iyi</b>. Uygulama mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.]]></string>
|
||||
<string name="v4_4_verify_connection_security_desc">Güvenlik kodlarını kişilerinizle karşılaştırın.</string>
|
||||
<string name="notifications_mode_service_desc">Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Pil için en iyisi</b>. Sadece uygulama çalışırken bildirim alırsınız (arka plan hizmeti YOK).]]></string>
|
||||
@@ -914,7 +914,7 @@
|
||||
<string name="ok">TAMAM</string>
|
||||
<string name="icon_descr_more_button">Daha fazla</string>
|
||||
<string name="one_time_link">Tek seferlik davet bağlantısı</string>
|
||||
<string name="network_settings_title">Ağ ayarları</string>
|
||||
<string name="network_settings_title">Gelişmiş ayarlar</string>
|
||||
<string name="shutdown_alert_desc">Siz uygulamayı yeniden başlatana kadar bildirimler çalışmayacaktır</string>
|
||||
<string name="la_mode_off">Kapalı</string>
|
||||
<string name="self_destruct_new_display_name">Yeni görünen ad:</string>
|
||||
@@ -989,7 +989,7 @@
|
||||
<string name="v4_5_private_filenames_descr">Zaman dilimi, görsel/ses korumak için UTC kullan.</string>
|
||||
<string name="v4_5_private_filenames">Özel dosya adları</string>
|
||||
<string name="to_start_a_new_chat_help_header">Yeni bir sohbet başlatmak için</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">İnsanlar size sadece paylaştığınız bağlantılar üzerinden ulaşabilir.</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Kimin bağlanabileceğine siz karar verirsiniz.</string>
|
||||
<string name="privacy_redefined">Gizlilik yeniden tanımlanıyor</string>
|
||||
<string name="read_more_in_github">GitHub repomuzda daha fazlasını okuyun.</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Periyodik</string>
|
||||
@@ -1109,7 +1109,7 @@
|
||||
<string name="notification_preview_mode_message_desc">Kişiyi ve mesajı göster</string>
|
||||
<string name="v5_4_better_groups">Daha iyi gruplar</string>
|
||||
<string name="video_decoding_exception_desc">Videonun kodu çözülemiyor. Lütfen farklı bir video deneyin veya geliştiricilerle iletişime geçin.</string>
|
||||
<string name="non_fatal_errors_occured_during_import">İçe aktarma sırasında bir takım hatlar oluştu - daha fazla detay için sohbet konsoluna bakabilirsiniz.</string>
|
||||
<string name="non_fatal_errors_occured_during_import">İçe aktarma sırasında bazı önemli olmayan hatalar oluştu:</string>
|
||||
<string name="show_developer_options">Geliştirici seçeneklerini göster</string>
|
||||
<string name="rcv_group_event_1_member_connected">%s bağlandı</string>
|
||||
<string name="network_disable_socks_info">Onaylarsanız, mesajlaşma sunucuları IP adresinizi ve sağlayıcınızı - hangi sunuculara bağlandığınızı - görebilecektir.</string>
|
||||
@@ -1188,7 +1188,7 @@
|
||||
<string name="la_lock_mode">SimpleX Kilit modu</string>
|
||||
<string name="share_file">Dosya paylaş…</string>
|
||||
<string name="periodic_notifications_desc">Uygulama yeni mesajları periyodik olarak alır - günde pilin yüzde birkaçını kullanır. Uygulama anlık bildirimleri kullanmaz - cihazınızdan gelen veriler sunuculara gönderilmez.</string>
|
||||
<string name="first_platform_without_user_ids">Herhangi bir kullanıcı tanımlayıcısı olmayan ilk platform - tasarım gereği gizli.</string>
|
||||
<string name="first_platform_without_user_ids">Herhangi bir kullanıcı tanımlayıcısı yok.</string>
|
||||
<string name="icon_descr_speaker_off">Hoparlör kapalı</string>
|
||||
<string name="sync_connection_force_desc">Şifreleme çalışıyor ve yeni bir şifreleme anlaşması gerekli değil. Yoksa bağlantı hataları ortaya çıkabilir!</string>
|
||||
<string name="show_call_on_lock_screen">Göster</string>
|
||||
@@ -1777,7 +1777,7 @@
|
||||
<string name="srv_error_version">Sunucu sürümü ağ ayarlarıyla uyumlu değil.</string>
|
||||
<string name="snd_error_auth">Yanlış anahtar veya bilinmeyen bağlantı - büyük olasılıkla bu bağlantı silinmiştir.</string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Gizli yönlendirme</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Bilinmeyen yönlendiriciler</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Bilinmeyen sunucular</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Her zaman gizli yönlendirmeyi kullan.</string>
|
||||
<string name="network_smp_proxy_mode_never_description">Gizli yönlendirmeyi KULLANMA.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Mesaj yönlendirme modu</string>
|
||||
@@ -1854,4 +1854,274 @@
|
||||
<string name="message_queue_info_server_info">sunucu kuyruk bilgisi: %1$s
|
||||
\n
|
||||
\nson alınan msj: %2$s</string>
|
||||
<string name="smp_proxy_error_connecting">Yönlendirme sunucusuna (%1$s) bağlantı sırasında hata oluştu. Lütfen daha sonra tekrar deneyin.</string>
|
||||
<string name="file_error_relay">Dosya sunucusu hatası. %1$s</string>
|
||||
<string name="scan_paste_link">Tara / Bağlantı yapıştır</string>
|
||||
<string name="app_check_for_updates">Güncellemeleri kontrol et</string>
|
||||
<string name="app_check_for_updates_disabled">Devre dışı</string>
|
||||
<string name="app_check_for_updates_download_completed_title">Uygulama Güncellemesi indirildi</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Daha sonra hatırlat</string>
|
||||
<string name="app_check_for_updates_notice_title">Güncellemeleri kontrol et</string>
|
||||
<string name="servers_info_reconnect_server_message">Mesajı göndermeye zorlamak için sunucuya yeniden bağlan. Bu ekstra internet kullanır.</string>
|
||||
<string name="servers_info_modal_error_title">Hata</string>
|
||||
<string name="error_forwarding_messages">Mesajların yönlendirilmesi sırasında hata oluştu.</string>
|
||||
<string name="file_error_no_file">Dosya bulunamadı - muhtemelen dosya silindi veya göderim iptal edildi.</string>
|
||||
<string name="forward_alert_title_messages_to_forward">%1$s Mesaj yönlendirilsin mi ?</string>
|
||||
<string name="selected_chat_items_nothing_selected">Hiçbir şey seçilmedi.</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Yönlendirilecek bir şey yok!</string>
|
||||
<string name="selected_chat_items_selected_n">%d seçildi.</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Mesajlar dosyalar olmadan iletilsin mi ?</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Mesajlar siz seçtikten sonra silindi.</string>
|
||||
<string name="cannot_share_message_alert_title">Mesaj gönderilemedi</string>
|
||||
<string name="forward_files_not_accepted_receive_files">İndir</string>
|
||||
<string name="forward_multiple">Mesajları ilet..</string>
|
||||
<string name="compose_forward_messages_n">%1$s mesaj iletiliyor</string>
|
||||
<string name="compose_save_messages_n">%1$s Mesaj kayıt ediliyor.</string>
|
||||
<string name="info_view_connect_button">bağlan</string>
|
||||
<string name="info_view_call_button">Ara</string>
|
||||
<string name="info_view_message_button">mesaj</string>
|
||||
<string name="only_delete_conversation">Sadece sohbeti sil</string>
|
||||
<string name="info_view_open_button">açık</string>
|
||||
<string name="info_view_search_button">ara</string>
|
||||
<string name="switching_profile_error_title">Profil değiştirme sırasında hata oluştu.</string>
|
||||
<string name="select_chat_profile">Sohbet profili seç</string>
|
||||
<string name="xftp_servers_configured">XFTP sunucuları yapılandırıldı</string>
|
||||
<string name="media_and_file_servers">Medya ve dosya sunucuları</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Kimlik bilgilerini proxy ile kullanmayın.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Proxy kayıt edilirken hata oluştu.</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Proxy konfigürasyonunun doğru olduğundan emin olun.</string>
|
||||
<string name="network_proxy_password">Şifre</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="app_check_for_updates_download_started">Uygulama güncellemesi indiriliyor, uygulamayı kapatmayın</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">Kurulum başarılı</string>
|
||||
<string name="app_check_for_updates_button_install">Güncellemeyi Kur</string>
|
||||
<string name="app_check_for_updates_button_open">Dosya konumunu aç</string>
|
||||
<string name="app_check_for_updates_notice_disable">Devre dışı bırak</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Lütfen uygulamayı yeniden başlatın.</string>
|
||||
<string name="invite_friends_short">Davet</string>
|
||||
<string name="create_address_button">Yarat</string>
|
||||
<string name="privacy_media_blur_radius_off">Kapalı</string>
|
||||
<string name="settings_section_title_chat_database">Mesajlaşma Veritabanı</string>
|
||||
<string name="chat_database_exported_continue">Devam et</string>
|
||||
<string name="share_text_message_status">Mesaj durumu: %s</string>
|
||||
<string name="appearance_font_size">Yazı tipi boyutu</string>
|
||||
<string name="v6_0_new_chat_experience">Yeni bir sohbet deneyimi 🎉</string>
|
||||
<string name="v6_0_connection_servers_status">Ağınızı kontrol edin</string>
|
||||
<string name="v6_0_connection_servers_status_descr">Bağlantı ve sunucuların durumu</string>
|
||||
<string name="v6_0_increase_font_size">Yazı boyutunu arttır</string>
|
||||
<string name="remote_ctrl_connection_stopped_desc">Lütfen telefonun ve bilgisayarın aynı lokal ağa bağlı olduğundan ve bilgisayar güvenlik duvarının bağlantıya izin verdiğinden emin olun. Lütfen diğer herhangi bir sorunu geliştiricilerle paylaşın.</string>
|
||||
<string name="servers_info_reset_stats_alert_title">Tüm istatistikler sıfırlansın mı ?</string>
|
||||
<string name="servers_info_reconnect_server_error">Hata, sunucuya yeniden bağlanılıyor</string>
|
||||
<string name="servers_info_reconnect_all_servers_button">Tüm sunuculara yeniden bağlan</string>
|
||||
<string name="duplicates_label">Kopyalar</string>
|
||||
<string name="chunks_uploaded">Parçalar yüklendi</string>
|
||||
<string name="downloaded_files">Dosyalar İndirildi</string>
|
||||
<string name="download_errors">İndirme hataları</string>
|
||||
<string name="info_row_message_status">Mesaj durumu</string>
|
||||
<string name="error_parsing_uri_title">Geçersiz link</string>
|
||||
<string name="error_parsing_uri_desc">Lütfen SimpleX bağlantısının doğru olup olmadığını kontrol edin.</string>
|
||||
<string name="proxy_destination_error_broker_host">Varış sunucusu ardesi (%1$s) yönlendirme sunucusu (%2$s) ile uyumsuz.</string>
|
||||
<string name="proxy_destination_error_broker_version">Varış sunucusu sürümü (%1$s) yönlendirme sunucusu (%2$s) ile uyumsuz.</string>
|
||||
<string name="message_forwarded_title">Mesaj iletildi</string>
|
||||
<string name="member_inactive_desc">Kullanıcı aktif olursa mesaj iletilebilir.</string>
|
||||
<string name="compose_message_placeholder">Mesaj</string>
|
||||
<string name="deleted_chats">Arşivli kişiler</string>
|
||||
<string name="no_filtered_contacts">Filtrelenmiş kişiler yok</string>
|
||||
<string name="paste_link">Bağlantıyı yapıştır</string>
|
||||
<string name="action_button_add_members">Davet et</string>
|
||||
<string name="member_info_member_disabled">Devre dışı</string>
|
||||
<string name="member_info_member_inactive">inaktif</string>
|
||||
<string name="allow_calls_question">Aramalara izin verilsin mi ?</string>
|
||||
<string name="calls_prohibited_alert_title">Aramalara izin verilmiyor</string>
|
||||
<string name="cant_call_contact_deleted_alert_text">Kişi silindi.</string>
|
||||
<string name="cant_send_message_to_member_alert_title">Grup üyesine mesaj gönderilemiyor</string>
|
||||
<string name="cant_call_member_send_message_alert_text">Çağrıları aktif etmek için mesaj gönder.</string>
|
||||
<string name="v6_0_your_contacts_descr">Daha sonra görüşmek için kişileri arşivleyin</string>
|
||||
<string name="v6_0_private_routing_descr">IP adresinizi ve bağlantılarınızı korur.</string>
|
||||
<string name="v6_0_new_media_options">Yeni medya seçenekleri</string>
|
||||
<string name="v6_0_privacy_blur">Daha iyi gizlilik için bulanıklaştır.</string>
|
||||
<string name="v6_0_connect_faster_descr">Arkadaşlarınıza daha hızlı bağlanın</string>
|
||||
<string name="v6_0_delete_many_messages_descr">Tek seferde en fazla 20 mesaj silin.</string>
|
||||
<string name="v6_0_chat_list_media">Sohbet listesinden oynat.</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Arşiv kaldırılsın mı ?</string>
|
||||
<string name="servers_info_sessions_connected">Bağlandı</string>
|
||||
<string name="current_user">Aktif profil</string>
|
||||
<string name="servers_info_files_tab">Dosyalar</string>
|
||||
<string name="servers_info_missing">Bilgi yok, yenilemeyi deneyin</string>
|
||||
<string name="servers_info_sessions_connecting">Bağlanıyor</string>
|
||||
<string name="servers_info_sessions_errors">Hatalar</string>
|
||||
<string name="servers_info_messages_received">Mesajlar alındı</string>
|
||||
<string name="servers_info_messages_sent">Mesajlar gönderildi</string>
|
||||
<string name="servers_info_reconnect_servers_message">Mesaj iletimine zorlamak için tüm sunuculara yeniden bağlan. Bu ekstra internet kullanılır.</string>
|
||||
<string name="servers_info_reconnect_server_title">Sunucuya yeniden bağlansın mı ?</string>
|
||||
<string name="servers_info_reconnect_servers_title">Sunuculara yeniden bağlanılsın mı ?</string>
|
||||
<string name="servers_info_reconnect_servers_error">Hata sunuculara yeniden bağlanılıyor</string>
|
||||
<string name="servers_info_reset_stats_alert_confirm">Sıfırla</string>
|
||||
<string name="connections">Bağlantılar</string>
|
||||
<string name="created">Yaratıldı</string>
|
||||
<string name="decryption_errors">Şifre çözme hataları</string>
|
||||
<string name="other_errors">diğer hatalar</string>
|
||||
<string name="completed">Tamamlandı</string>
|
||||
<string name="deleted">Silindi</string>
|
||||
<string name="deletion_errors">Silme hatası</string>
|
||||
<string name="open_server_settings_button">Sunucu ayarlarını aç</string>
|
||||
<string name="share_text_file_status">Dosya durumu: %s</string>
|
||||
<string name="new_message">Yeni mesaj</string>
|
||||
<string name="smp_servers_other">Diğer SMP sunucuları</string>
|
||||
<string name="reconnect">Yeniden bağlan</string>
|
||||
<string name="sent_directly">Direkt gönderildi.</string>
|
||||
<string name="all_users">Tüm Profiller</string>
|
||||
<string name="cant_call_member_alert_title">Grup üyesi aranamıyor</string>
|
||||
<string name="chat_database_exported_title">Veritabanı dışa aktarıldı</string>
|
||||
<string name="chunks_deleted">Parçalar silindi</string>
|
||||
<string name="chunks_downloaded">Parçalar indirildi</string>
|
||||
<string name="servers_info_connected_servers_section_header">Bağlı sunucular</string>
|
||||
<string name="cant_call_contact_connecting_wait_alert_text">Kişiye bağlanılıyor, lütfen bekleyin ya da daha sonra kontrol edin.</string>
|
||||
<string name="copy_error">Kopyalama hatası</string>
|
||||
<string name="delete_without_notification">Bildirim göndermeden sil</string>
|
||||
<string name="v6_0_upgrade_app_descr">Yeni versiyonları GitHub\'dan indirin</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Hata istatistikler sıfırlanıyor</string>
|
||||
<string name="smp_proxy_error_broker_host">Yönlendirme sunucu adresi (%1$s) ağ ayarlarıyla uyumsuz.</string>
|
||||
<string name="info_row_file_status">Dosya durumu</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Yönlendirme sunucusu (%1$s) varış sunucusuna (%2$s) bağlanamadı. Lütfen daha sonra tekrar deneyin.</string>
|
||||
<string name="smp_proxy_error_broker_version">Yönlendirme suncusu sürümü ağ ayarlarıyla uyumsuz: %1$s</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Bekliyor</string>
|
||||
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Lütfen kişinizden çağrılara izin vermesini isteyin.</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Önceden bağlanılmış sunucular</string>
|
||||
<string name="cant_call_contact_alert_title">Kişi aranamıyor</string>
|
||||
<string name="network_options_save_and_reconnect">Kayıt et ve yeniden bağlan</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Mesajlar silinecek - bu geri alınamaz!</string>
|
||||
<string name="delete_messages_mark_deleted_warning">Mesajlar silinmek üzere işaretlendi. Alıcı (lar) bu mesajları görebilecek.</string>
|
||||
<string name="delete_members_messages__question">Üylerin %d mesajı silinsin mi ?</string>
|
||||
<string name="member_inactive_title">Üye inaktif</string>
|
||||
<string name="message_forwarded_desc">Henüz direkt bağlantı yok mesaj admin tarafından yönlendirildi.</string>
|
||||
<string name="message_servers">Mesaj sunucuları</string>
|
||||
<string name="smp_servers_configured">SMP sunucları yapılandırıldı</string>
|
||||
<string name="xftp_servers_other">Diğer XFTP sunucuları</string>
|
||||
<string name="privacy_media_blur_radius">Medyayı bulanıklaştır.</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Kişiler silinecek - bu geri alınamaz !</string>
|
||||
<string name="keep_conversation">Sohbeti sakla.</string>
|
||||
<string name="confirm_delete_contact_question">Kişiyi silmek istediğinizden emin misiniz ?</string>
|
||||
<string name="conversation_deleted">Sohbet silindi!</string>
|
||||
<string name="contact_deleted">Kişiler silindi!</string>
|
||||
<string name="please_try_later">Lütfen sonra tekrar deneyin</string>
|
||||
<string name="private_routing_error">Gizli yönlendirme hatası</string>
|
||||
<string name="file_error">Dosya hatası</string>
|
||||
<string name="servers_info_details">Detaylar</string>
|
||||
<string name="expired_label">Süresi dolmuş</string>
|
||||
<string name="send_errors">Gönderme hataları</string>
|
||||
<string name="attempts_label">denemeler</string>
|
||||
<string name="servers_info_detailed_statistics">Detaylı istatistikler</string>
|
||||
<string name="servers_info_downloaded">İndirildi</string>
|
||||
<string name="other_label">diğer</string>
|
||||
<string name="servers_info_detailed_statistics_received_messages_header">Alınan mesajlar</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">Toplam alınan</string>
|
||||
<string name="select_verb">Seç</string>
|
||||
<string name="cannot_share_message_alert_text">Seçilen sohbet tercihleri bu mesajı yasakladı.</string>
|
||||
<string name="servers_info_reset_stats">Tüm istatistikleri sıfırla</string>
|
||||
<string name="reset_all_hints">Tüm ip uçlarını sıfırla</string>
|
||||
<string name="n_file_errors">%1$d dosya hata(ları)\n%2$s</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d dosya(lar) hala indiriliyor.</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d dosyası (ları) indirilemedi.</string>
|
||||
<string name="forward_files_missing_desc">%1$d dosyası (ları) silindi.</string>
|
||||
<string name="n_other_file_errors">%1$d diğer dosya hatası(ları).</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d dosyası (ları) indirilemedi.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s mesajları iletilemedi</string>
|
||||
<string name="privacy_media_blur_radius_medium">Orta</string>
|
||||
<string name="servers_info_subscriptions_section_header">Mesaj alındısı</string>
|
||||
<string name="acknowledged">Onaylandı</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">WebView başlatılırken hata oluştu. WebView\'in yüklü olduğundan ve desteklenen mimarisinin arm64 olduğundan emin olun.\nHata: %s</string>
|
||||
<string name="network_session_mode_session_description">Uygulamayı her başlattığınızda yeni SOCKS kimlik bilgileri kullanılacaktır.</string>
|
||||
<string name="network_session_mode_server_description">Her sunucu için yeni SOCKS kimlik bilgileri kullanılacaktır.</string>
|
||||
<string name="app_check_for_updates_button_download">İndir %s (%s)</string>
|
||||
<string name="settings_section_title_message_shape">Mesaj şekli</string>
|
||||
<string name="subscription_percentage">Yüzdeyi göster</string>
|
||||
<string name="app_check_for_updates_canceled">Güncelleme indirme işlemi iptal edildi</string>
|
||||
<string name="subscription_errors">Abone olurken hata</string>
|
||||
<string name="moderate_messages_will_be_deleted_warning">Mesajlar tüm üyeler için silinecektir.</string>
|
||||
<string name="moderate_messages_will_be_marked_warning">Mesajlar tüm üyeler için moderasyonlu olarak işaretlenecektir.</string>
|
||||
<string name="file_error_auth">Yanlış anahtar veya bilinmeyen dosya yığın adresi - büyük olasılıkla dosya silinmiştir.</string>
|
||||
<string name="toolbar_settings">Ayarlar</string>
|
||||
<string name="new_chat_share_profile">Profil paylaş</string>
|
||||
<string name="switching_profile_error_message">Bağlantınız %s\'ye taşındı ancak sizi profile yönlendirirken beklenmedik bir hata oluştu.</string>
|
||||
<string name="network_proxy_auth">Proxy kimlik doğrulaması</string>
|
||||
<string name="network_proxy_random_credentials">Rastgele kimlik bilgileri kullan</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Her bağlantı için farklı proxy kimlik bilgileri kullan.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Her profil için farklı proxy kimlik bilgileri kullan.</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Kimlik bilgileriniz şifrelenmeden gönderilebilir.</string>
|
||||
<string name="network_proxy_username">Kullanıcı Adı</string>
|
||||
<string name="app_check_for_updates_button_skip">Bu sürümü atlayın</string>
|
||||
<string name="app_check_for_updates_notice_desc">Yeni sürümlerden haberdar olmak için Kararlı veya Beta sürümleri için periyodik kontrolü açın.</string>
|
||||
<string name="privacy_media_blur_radius_soft">Yumuşak</string>
|
||||
<string name="chat_database_exported_not_all_files">Bazı dosya(lar) dışa aktarılmadı</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="v6_0_upgrade_app">Uygulamayı otomatik olarak yükselt</string>
|
||||
<string name="uploaded_files">Yüklenen dosyalar</string>
|
||||
<string name="size">Boyut</string>
|
||||
<string name="subscribed">Abone olundu</string>
|
||||
<string name="subscription_results_ignored">Abonelikler göz ardı edildi</string>
|
||||
<string name="contact_list_header_title">Bağlantılarınız</string>
|
||||
<string name="icon_descr_sound_muted">Ses kapatıldı</string>
|
||||
<string name="servers_info_uploaded">Yüklendi</string>
|
||||
<string name="servers_info_reset_stats_alert_message">Sunucu istatistikleri sıfırlanacaktır - bu geri alınamaz!</string>
|
||||
<string name="one_hand_ui">Erişilebilir sohbet araç çubuğu</string>
|
||||
<string name="one_hand_ui_change_instruction">Görünüm ayarlarından değiştirebilirsiniz.</string>
|
||||
<string name="one_hand_ui_card_title">Sohbet listesini değiştir:</string>
|
||||
<string name="system_mode_toast">Sistem modu</string>
|
||||
<string name="v6_0_reachable_chat_toolbar">Erişilebilir sohbet araç çubuğu</string>
|
||||
<string name="servers_info_target">İçin bilgi gösteriliyor</string>
|
||||
<string name="servers_info_statistics_section_header">İstatistikler</string>
|
||||
<string name="servers_info_private_data_disclaimer">%s\'den başlayarak.\nTüm veriler cihazınıza özeldir.</string>
|
||||
<string name="sent_via_proxy">Bir proxy aracılığıyla gönderildi</string>
|
||||
<string name="server_address">Sunucu adresi</string>
|
||||
<string name="upload_errors">Yükleme hataları</string>
|
||||
<string name="network_option_tcp_connection">TCP bağlantısı</string>
|
||||
<string name="network_socks_proxy">SOCKS proxy</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Proxy sunucuları</string>
|
||||
<string name="temporary_file_error">Geçici dosya hatası</string>
|
||||
<string name="info_view_video_button">Video</string>
|
||||
<string name="you_can_still_send_messages_to_contact">Arşivlenen kişilerden %1$s\'e mesaj gönderebilirsiniz.</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">Sohbetler listesinde %1$s ile yapılan konuşmayı hala görüntüleyebilirsiniz.</string>
|
||||
<string name="xftp_server">XFTP sunucusu</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Alım sırasında hata</string>
|
||||
<string name="smp_server">SMP sunucusu</string>
|
||||
<string name="network_error_broker_host_desc">Sunucu adresi ağ ayarlarıyla uyumsuz: %1$s.</string>
|
||||
<string name="network_error_broker_version_desc">Sunucu sürümü uygulamanızla uyumlu değil: %1$s.</string>
|
||||
<string name="you_need_to_allow_calls">Kendiniz arayabilmeniz için önce irtibat kişinizin sizi aramasına izin vermelisiniz.</string>
|
||||
<string name="servers_info_starting_from">%s\'den başlayarak.</string>
|
||||
<string name="acknowledgement_errors">Onay hataları</string>
|
||||
<string name="secured">Güvenli</string>
|
||||
<string name="network_session_mode_session">Uygulama oturumu</string>
|
||||
<string name="network_session_mode_server">Sunucu</string>
|
||||
<string name="app_check_for_updates_stable">Stabil</string>
|
||||
<string name="app_check_for_updates_update_available">Güncelleme mevcut: %s</string>
|
||||
<string name="remote_ctrl_connection_stopped_identity_desc">Bu bağlantı başka bir mobil cihazda kullanıldı, lütfen masaüstünde yeni bir bağlantı oluşturun.</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Mikrofon kullanımına izin vermek için adres alanının yanındaki bilgi düğmesine tıklayın.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Safari Ayarları / Web Siteleri / Mikrofon\'u açın, ardından localhost için İzin Ver\'i seçin.</string>
|
||||
<string name="call_desktop_permission_denied_title">Arama yapmak için mikrofonunuzu kullanmanıza izin verin. Aramayı sonlandırın ve tekrar aramayı deneyin.</string>
|
||||
<string name="settings_message_shape_tail">Konuşma balonu</string>
|
||||
<string name="privacy_media_blur_radius_strong">Güçlü</string>
|
||||
<string name="v6_0_reachable_chat_toolbar_descr">Uygulamayı tek elle kullan.</string>
|
||||
<string name="servers_info">Sunucu bilgileri</string>
|
||||
<string name="servers_info_subscriptions_total">Toplam</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">Bu sunuculara bağlı değilsiniz. Mesajları onlara iletmek için özel yönlendirme kullanılır.</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Aktif bağlantılar</string>
|
||||
<string name="settings_message_shape_corner">Köşeleri yuvarlama</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Yüklenen veritabanı arşivi sunuculardan kalıcı olarak kaldırılacaktır.</string>
|
||||
<string name="proxied">Proxyli</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Taşıma oturumları</string>
|
||||
<string name="chat_database_exported_migrate">Dışa aktarılan veritabanını taşıyabilirsiniz.</string>
|
||||
<string name="chat_database_exported_save">Dışa aktarılan arşivi kaydedebilirsiniz.</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Gönderilen tüm mesajların toplamı</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">Gönderilen mesajlar</string>
|
||||
<string name="v6_1_message_dates_descr">Daha iyi mesaj tarihleri.</string>
|
||||
<string name="v6_1_customizable_message_descr">Özelleştirilebilir mesaj şekli.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Aynı anda en fazla 20 mesaj iletin.</string>
|
||||
<string name="v6_1_better_calls_descr">Görüşme sırasında ses ve görüntüyü değiştirin.</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">Sohbet profilini 1 kerelik davetler için değiştirin.</string>
|
||||
<string name="v6_1_better_calls">Daha iyi aramalar</string>
|
||||
<string name="v6_1_better_user_experience">Daha iyi kullanıcı deneyimi</string>
|
||||
<string name="v6_1_delete_many_messages_descr">200\'e kadar mesajı silin veya düzenleyin.</string>
|
||||
<string name="v6_1_better_security">Daha iyi güvenlik ✅</string>
|
||||
<string name="v6_1_better_security_descr">SimpleX protokolleri Trail of Bits tarafından incelenmiştir.</string>
|
||||
</resources>
|
||||
@@ -776,7 +776,7 @@
|
||||
<string name="v4_5_italian_interface_descr">Дякуємо користувачам – приєднуйтеся через Weblate!</string>
|
||||
<string name="v4_6_group_moderation_descr">Тепер адміністратори можуть:
|
||||
\n- видаляти повідомлення учасників.
|
||||
\n- вимикати учасників (роль спостерігач)</string>
|
||||
\n- вимикати учасників (роль спостерігача).</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Встановіть повідомлення, яке показується новим учасникам!</string>
|
||||
<string name="v4_6_reduced_battery_usage">Додатково зменшено використання батареї</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">Більше поліпшень незабаром!</string>
|
||||
@@ -2067,4 +2067,52 @@
|
||||
<string name="reset_all_hints">Скинути всі підказки</string>
|
||||
<string name="app_check_for_updates_update_available">Доступно оновлення: %s</string>
|
||||
<string name="app_check_for_updates_canceled">Завантаження оновлення скасовано</string>
|
||||
<string name="settings_section_title_chat_database">БАЗА ДАНИХ ЧАТУ</string>
|
||||
<string name="select_chat_profile">Вибрати профіль чату</string>
|
||||
<string name="switching_profile_error_title">Помилка при зміні профілю</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Повідомлення будуть видалені — це не можна скасувати!</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Видалити архів?</string>
|
||||
<string name="new_chat_share_profile">Поділитися профілем</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Завантажений архів бази даних буде остаточно видалено з серверів.</string>
|
||||
<string name="switching_profile_error_message">Ваше з\'єднання було перенесено на %s, але виникла несподівана помилка під час перенаправлення на профіль.</string>
|
||||
<string name="system_mode_toast">Режим системи</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Не використовуйте облікові дані з проксі.</string>
|
||||
<string name="network_proxy_auth">Аутентифікація проксі</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Використовуйте різні облікові дані проксі для кожного з\'єднання.</string>
|
||||
<string name="network_proxy_random_credentials">Використовувати випадкові облікові дані</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Ваші облікові дані можуть бути надіслані в незашифрованому вигляді.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Помилка під час збереження проксі</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Переконайтеся, що конфігурація проксі правильна.</string>
|
||||
<string name="network_proxy_password">Пароль</string>
|
||||
<string name="network_proxy_username">Ім\'я користувача</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Використовуйте різні облікові дані проксі для кожного профілю.</string>
|
||||
<string name="n_other_file_errors">%1$d інша(і) помилка(и) файлу.</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d файл(и) не вдалося завантажити.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s повідомлень не було переслано.</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Завантажити</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Переслати %1$s повідомлень?</string>
|
||||
<string name="forward_multiple">Пересилаю повідомлення…</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Переслати повідомлення без файлів?</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Немає нічого для пересилання!</string>
|
||||
<string name="compose_save_messages_n">Зберігаю %1$s повідомлень.</string>
|
||||
<string name="n_file_errors">%1$d помилка(и) файлу:
|
||||
\n%2$s</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d файл(и) не були завантажені.</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d файл(и) ще завантажуються.</string>
|
||||
<string name="forward_files_missing_desc">%1$d файл(и) були видалені.</string>
|
||||
<string name="compose_forward_messages_n">Пересилаю %1$s повідомлень</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Повідомлення були видалені після того, як ви їх вибрали.</string>
|
||||
<string name="error_forwarding_messages">Помилка при пересиланні повідомлень</string>
|
||||
<string name="icon_descr_sound_muted">Звук вимкнено</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Помилка ініціалізації WebView. Переконайтеся, що WebView встановлено, і його підтримувана архітектура — arm64. \nПомилка: %s</string>
|
||||
<string name="settings_message_shape_tail">Хвіст</string>
|
||||
<string name="settings_message_shape_corner">Куточок</string>
|
||||
<string name="settings_section_title_message_shape">Форма повідомлення</string>
|
||||
<string name="network_session_mode_session">Сесія додатку</string>
|
||||
<string name="network_session_mode_session_description">Нові облікові дані SOCKS будуть використовуватись щоразу, коли ви запускаєте додаток.</string>
|
||||
<string name="network_session_mode_server_description">Нові облікові дані SOCKS будуть використовуватись для кожного сервера.</string>
|
||||
<string name="network_session_mode_server">Сервер</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Натисніть кнопку інформації поруч із полем адреси, щоб дозволити використання мікрофона.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Відкрийте Налаштування Safari / Сайти / Мікрофон, а потім виберіть \"Дозволити для localhost\".</string>
|
||||
<string name="call_desktop_permission_denied_title">Щоб здійснювати дзвінки, дозволіть використовувати ваш мікрофон. Завершіть дзвінок і спробуйте зателефонувати знову.</string>
|
||||
</resources>
|
||||
@@ -724,4 +724,184 @@
|
||||
<string name="servers_info_reconnect_servers_error">Lỗi kết nối lại máy chủ</string>
|
||||
<string name="servers_info_sessions_errors">Lỗi</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Lỗi khôi phục thống kê</string>
|
||||
<string name="error_updating_link_for_group">Lỗi cập nhật liên kết nhóm</string>
|
||||
<string name="error_showing_message">lỗi hiển thị tin nhắn</string>
|
||||
<string name="error_showing_content">lỗi hiển thị nội dung</string>
|
||||
<string name="error_saving_ICE_servers">Lỗi lưu máy chủ ICE</string>
|
||||
<string name="error_saving_xftp_servers">Lỗi lưu máy chủ XFTP</string>
|
||||
<string name="error_saving_smp_servers">Lỗi lưu máy chủ SMP</string>
|
||||
<string name="error_sending_message">Lỗi gửi tin nhắn</string>
|
||||
<string name="error_starting_chat">Lỗi khởi động ứng dụng</string>
|
||||
<string name="error_stopping_chat">Lỗi dừng ứng dụng</string>
|
||||
<string name="error_showing_desktop_notification">Lỗi hiển thị thông báo, liên hệ với nhà phát triển.</string>
|
||||
<string name="error_saving_user_password">Lỗi lưu mật khẩu người dùng</string>
|
||||
<string name="migrate_from_device_error_saving_settings">Lỗi lưu cài đặt</string>
|
||||
<string name="error_sending_message_contact_invitation">Lỗi gửi lời mời</string>
|
||||
<string name="failed_to_active_user_title">Lỗi chuyển đổi hồ sơ!</string>
|
||||
<string name="error_synchronizing_connection">Lỗi đồng bộ kết nối</string>
|
||||
<string name="error_setting_address">Lỗi cài đặt địa chỉ</string>
|
||||
<string name="v5_2_disappear_one_message_descr">Dù đã tắt trong cuộc trò chuyện.</string>
|
||||
<string name="error_setting_network_config">Lỗi cập nhật cấu hình mạng</string>
|
||||
<string name="error_updating_user_privacy">Lỗi cập nhật quyền riêng tư người dùng</string>
|
||||
<string name="icon_descr_expand_role">Mở rộng chọn quyền hạn</string>
|
||||
<string name="settings_section_title_experimenta">THỬ NGHIỆM</string>
|
||||
<string name="expand_verb">Mở rộng</string>
|
||||
<string name="exit_without_saving">Thoát mà không lưu</string>
|
||||
<string name="expired_label">đã hết hạn</string>
|
||||
<string name="migrate_from_device_error_verifying_passphrase">Lỗi xác thực mật khẩu:</string>
|
||||
<string name="possible_slow_function_desc">Quá trình thực hiện chức năng mất quá nhiều thời gian: %1$dgiây: %2$s</string>
|
||||
<string name="settings_experimental_features">Tính năng thử nghiệm</string>
|
||||
<string name="export_database">Xuất cơ sở dữ liệu</string>
|
||||
<string name="migrate_from_device_error_uploading_archive">Lỗi tải lên kho lưu trữ</string>
|
||||
<string name="migrate_from_device_exported_file_doesnt_exist">Tập tin đã xuất không tồn tại</string>
|
||||
<string name="settings_section_title_files">TẬP TIN</string>
|
||||
<string name="failed_to_parse_chats_title">Không thể tải tin nhắn</string>
|
||||
<string name="file_error_no_file">Không tìm thấy tập tin - có thể tập tin đã bị xóa và hủy bỏ.</string>
|
||||
<string name="file_error">Lỗi tập tin</string>
|
||||
<string name="export_theme">Xuất chủ đề</string>
|
||||
<string name="failed_to_parse_chat_title">Không thể tải tin nhắn</string>
|
||||
<string name="choose_file">Tập tin</string>
|
||||
<string name="v5_0_large_files_support_descr">Nhanh chóng và không cần phải đợi người gửi hoạt động!</string>
|
||||
<string name="file_not_found">Không tìm thấy tập tin</string>
|
||||
<string name="file_with_path">Tập tin: %s</string>
|
||||
<string name="v5_4_better_groups_descr">Tham gia nhanh chóng hơn và xử lý tin nhắn ổn định hơn.</string>
|
||||
<string name="servers_info_files_tab">Tập tin</string>
|
||||
<string name="favorite_chat">Yêu thích</string>
|
||||
<string name="icon_descr_file">Tập tin</string>
|
||||
<string name="file_error_relay">Lỗi máy chủ tệp: %1$s</string>
|
||||
<string name="info_row_file_status">Trạng thái tệp</string>
|
||||
<string name="files_and_media_not_allowed">Tệp và phương tiện truyền thông không được cho phép</string>
|
||||
<string name="files_are_prohibited_in_group">Tệp và phương tiện truyền thông bị cấm trong nhóm này.</string>
|
||||
<string name="revoke_file__message">Tệp sẽ bị xóa khỏi máy chủ.</string>
|
||||
<string name="files_and_media_section">Tệp & phương tiện truyền thông</string>
|
||||
<string name="files_and_media_prohibited">Tệp và phương tiện truyền thông bị cấm!</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">Tệp sẽ được nhận khi liên hệ của bạn hoàn tất quá trình tải lên.</string>
|
||||
<string name="files_and_media">Tệp và phương tiện truyền thông</string>
|
||||
<string name="migrate_to_device_file_delete_or_link_invalid">Tệp đã bị xóa hoặc liên kết không hợp lệ</string>
|
||||
<string name="file_saved">Tệp đã được lưu</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Tệp sẽ được nhận khi liên hệ của bạn hoạt động, vui lòng chờ hoặc kiểm tra lại sau!</string>
|
||||
<string name="share_text_file_status">Trạng thái tệp: %s</string>
|
||||
<string name="wallpaper_scale_fill">Lấp đầy</string>
|
||||
<string name="settings_section_title_chat_database">CƠ SỞ DỮ LIỆU SIMPLEX CHAT</string>
|
||||
<string name="switching_profile_error_title">Lỗi chuyển đổi hồ sơ</string>
|
||||
<string name="v5_2_favourites_filter_descr">Lọc các cuộc hội thoại chưa đọc và các cuộc hội thoại yêu thích.</string>
|
||||
<string name="v5_1_message_reactions_descr">Cuối cùng, chúng ta đã có chúng! 🚀</string>
|
||||
<string name="migrate_to_device_finalize_migration">Hoàn tất quá trình di chuyển ở thiết bị khác.</string>
|
||||
<string name="migrate_from_device_finalize_migration">Hoàn tất quá trình di chuyển</string>
|
||||
<string name="v5_2_favourites_filter">Tìm các cuộc trò chuyện nhanh hơn</string>
|
||||
<string name="permissions_find_in_settings_and_grant">Tìm kiếm quyền này trong phần cài đặt Android và cấp quyền theo cách thủ công.</string>
|
||||
<string name="fix_connection_confirm">Sửa</string>
|
||||
<string name="wallpaper_scale_fit">Kích thước phù hợp</string>
|
||||
<string name="forward_chat_item">Chuyển tiếp</string>
|
||||
<string name="forwarded_from_chat_item_info_title">Đã được chuyển tiếp từ</string>
|
||||
<string name="forwarded_description">đã được chuyển tiếp</string>
|
||||
<string name="snd_error_proxy_relay">Máy chủ chuyển tiếp: %1$s
|
||||
\nLỗi máy chủ đích: %2$s</string>
|
||||
<string name="snd_error_proxy">Máy chủ chuyển tiếp: %1$s
|
||||
\nLỗi: %2$s</string>
|
||||
<string name="v5_7_forward">Chuyển tiếp và lưu tin nhắn</string>
|
||||
<string name="fix_connection_not_supported_by_group_member">Chức năng sửa không hỗ trợ bởi thành viên nhóm</string>
|
||||
<string name="for_everybody">Cho tất cả mọi người</string>
|
||||
<string name="forwarded_chat_item_info_tab">Đã được chuyển tiếp</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Không sử dụng thông tin đăng nhập với proxy.</string>
|
||||
<string name="fix_connection">Sửa kết nối</string>
|
||||
<string name="fix_connection_question">Sửa kết nối?</string>
|
||||
<string name="fix_connection_not_supported_by_contact">Chức năng sửa không hỗ trợ bởi liên hệ</string>
|
||||
<string name="v5_2_fix_encryption_descr">Sửa mã hóa sau khi hồi phục dữ liệu dự phòng.</string>
|
||||
<string name="network_proxy_incorrect_config_title">Lỗi lưu proxy</string>
|
||||
<string name="icon_descr_flip_camera">Đổi máy ảnh</string>
|
||||
<string name="appearance_font_size">Kích thước font</string>
|
||||
<string name="n_other_file_errors">%1$d lỗi tệp khác.</string>
|
||||
<string name="error_forwarding_messages">Lỗi chuyển tiếp tin nhắn</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d tệp tải không thành công.</string>
|
||||
<string name="forward_files_missing_desc">%1$d tệp đã bị xóa.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d tệp đã không được tải xuống.</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Tải xuống</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Chuyển tiếp %1$s tin nhắn?</string>
|
||||
<string name="forward_multiple">Chuyển tiếp tin nhắn…</string>
|
||||
<string name="n_file_errors">%1$d lỗi tệp:
|
||||
\n%2$s</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d tệp đang được tải xuống.</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s tin nhắn không được chuyển tiếp</string>
|
||||
<string name="compose_forward_messages_n">Đang chuyển tiếp %1$s tin nhắn</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Máy chủ chuyển tiếp %1$s không thể kết nối tới máy chủ đích %2$s. Vui lòng thử lại sau.</string>
|
||||
<string name="smp_proxy_error_broker_host">Địa chỉ máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s.</string>
|
||||
<string name="smp_proxy_error_broker_version">Phiên bản máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s.</string>
|
||||
<string name="section_title_for_console">CHO CONSOLE</string>
|
||||
<string name="forward_message">Chuyển tiếp tin nhắn…</string>
|
||||
<string name="v4_6_reduced_battery_usage">Giảm thiểu sử dụng pin hơn nữa</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Chuyển tiếp tin nhắn mà không có tệp?</string>
|
||||
<string name="wallpaper_preview_hello_alice">Chào buổi chiều!</string>
|
||||
<string name="v4_4_french_interface">Giao diện tiếng Pháp</string>
|
||||
<string name="found_desktop">Đã tìm thấy máy tính</string>
|
||||
<string name="simplex_link_mode_full">Liên kết đầy đủ</string>
|
||||
<string name="from_gallery_button">Từ Thư viện</string>
|
||||
<string name="full_name__field">Tên đầy đủ:</string>
|
||||
<string name="group_is_decentralized">Tuyệt đối phi tập trung - chỉ hiển thị cho thành viên.</string>
|
||||
<string name="wallpaper_preview_hello_bob">Chào buổi sáng!</string>
|
||||
<string name="group_member_status_group_deleted">nhóm đã bị xóa</string>
|
||||
<string name="group_members_can_add_message_reactions">Các thành viên nhóm có thể thả cảm xúc tin nhắn.</string>
|
||||
<string name="group_members_can_delete">Các thành viên nhóm có thể xóa theo cách không thể hồi phục các tin nhắn đã gửi. (24 giờ)</string>
|
||||
<string name="group_members_can_send_voice">Các thành viên trong nhóm có thể gửi tin nhắn thoại.</string>
|
||||
<string name="permissions_required">Cấp quyền</string>
|
||||
<string name="group_link">Liên kết nhóm</string>
|
||||
<string name="info_row_group">Nhóm</string>
|
||||
<string name="group_full_name_field">Tên đầy đủ nhóm:</string>
|
||||
<string name="group_members_can_send_disappearing">Các thành viên trong nhóm có thể gửi tin nhắn tự xóa.</string>
|
||||
<string name="group_members_can_send_files">Các thành viên trong nhóm có thể gửi tệp và phương tiện truyền thông.</string>
|
||||
<string name="group_members_can_send_simplex_links">Các thành viên trong nhóm có thể gửi liên kết SimpleX.</string>
|
||||
<string name="icon_descr_group_inactive">Nhóm không hoạt động</string>
|
||||
<string name="connect_plan_group_already_exists">Nhóm đã tồn tại rồi!</string>
|
||||
<string name="group_members_can_send_dms">Các thành viên trong nhóm có thể gửi tin nhắn trực tiếp.</string>
|
||||
<string name="v4_2_group_links">Liên kết nhóm</string>
|
||||
<string name="group_invitation_expired">Lời mời nhóm đã hết hạn</string>
|
||||
<string name="alert_message_group_invitation_expired">Lời mới nhóm không còn có hiệu lực, nó đã bị xóa bởi người gửi.</string>
|
||||
<string name="permissions_grant">Cho phép thực hiện cuộc gọi</string>
|
||||
<string name="permissions_grant_in_settings">Cấp quyền trong cài đặt</string>
|
||||
<string name="hide_notification">Ẩn</string>
|
||||
<string name="icon_descr_hang_up">Ngắt kết nối</string>
|
||||
<string name="notification_preview_mode_hidden">Ẩn</string>
|
||||
<string name="hidden_profile_password">Mật khẩu hồ sơ ẩn</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Hồ sơ nhóm được lưu trữ tại thiết bị của thành viên, không lưu trữ trên máy chủ.</string>
|
||||
<string name="snd_group_event_group_profile_updated">hồ sơ nhóm đã được cập nhật</string>
|
||||
<string name="icon_descr_help">trợ giúp</string>
|
||||
<string name="hide_dev_options">Ẩn:</string>
|
||||
<string name="email_invite_body">Xin chào!
|
||||
\nKết nối với tôi qua SimpleX Chat: %s</string>
|
||||
<string name="hide_profile">Ẩn hồ sơ</string>
|
||||
<string name="settings_section_title_help">TRỢ GIÚP</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Nhóm sẽ bị xóa cho tất cả các thành viên - điều này không thể hoàn tác!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Nhóm sẽ bị xóa cho bạn - điều này không thể hoàn tác!</string>
|
||||
<string name="group_preferences">Tùy chọn nhóm</string>
|
||||
<string name="recent_history_is_not_sent_to_new_members">Lịch sử không được gửi đến các thành viên mới.</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Ẩn màn hình ứng dụng trong danh sách các ứng dụng gần đây.</string>
|
||||
<string name="v4_6_group_moderation">Quản trị nhóm</string>
|
||||
<string name="v4_6_group_welcome_message">Lời chào nhóm</string>
|
||||
<string name="audio_device_wired_headphones">Tai nghe</string>
|
||||
<string name="notification_display_mode_hidden_desc">Ẩn liên hệ và tin nhắn</string>
|
||||
<string name="hide_verb">Ẩn</string>
|
||||
<string name="user_hide">Ẩn</string>
|
||||
<string name="edit_history">Lịch sử</string>
|
||||
<string name="alert_title_no_group">Không tìm thấy nhóm!</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Hồ sơ trò chuyện ẩn</string>
|
||||
<string name="how_to_use_simplex_chat">Cách sử dụng</string>
|
||||
<string name="how_it_works">Cách thức hoạt động</string>
|
||||
<string name="how_simplex_works">Cách thức SimpleX hoạt động</string>
|
||||
<string name="how_to">Cách làm</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Lỗi khởi động WebView. Hãy đảm bảo bạn đã cài đặt WebView và kiến trúc hỗ trợ của nó là arm64.\nLỗi: %s</string>
|
||||
<string name="custom_time_unit_hours">giờ</string>
|
||||
<string name="host_verb">Lưu trữ</string>
|
||||
<string name="network_session_mode_session">Phiên làm việc trên ứng dụng</string>
|
||||
<string name="how_to_use_markdown">Cách sử dụng markdown</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Nhấn nút thông tin gần trường địa chỉ để cho phép sử dụng microphone.</string>
|
||||
<string name="v6_1_better_calls">Trải nghiệm cuộc gọi tốt hơn</string>
|
||||
<string name="v6_1_better_security">Bảo mật hơn ✅</string>
|
||||
<string name="v6_1_better_user_experience">Trải nghiệm người dùng tuyệt vời hơn</string>
|
||||
<string name="v6_1_customizable_message_descr">Hình dạng tin nhắn có thể tùy chỉnh được.</string>
|
||||
<string name="v6_1_message_dates_descr">Mô tả thời gian tin nhắn tốt hơn.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Xóa hay kiểm duyệt tối đa 200 tin nhắn.</string>
|
||||
<string name="settings_message_shape_corner">Góc</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Chuyển tiếp tối đa 20 tin nhắn cùng một lúc.</string>
|
||||
<string name="how_to_use_your_servers">Cách sử dụng máy chủ của bạn</string>
|
||||
<string name="v5_5_new_interface_languages">Giao diện Hungary và Thổ Nhĩ Kỳ</string>
|
||||
</resources>
|
||||
@@ -502,7 +502,7 @@
|
||||
<string name="show_call_on_lock_screen">显示</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">您只能在一台设备上使用最新版本的聊天数据库,否则您可能会停止接收来自某些联系人的消息。</string>
|
||||
<string name="new_passphrase">新密码……</string>
|
||||
<string name="member_role_will_be_changed_with_notification">该角色将更改为“%s”。群组中每个人都会收到通知。</string>
|
||||
<string name="member_role_will_be_changed_with_notification">该角色将更改为 %s。群组中每个人都会收到通知。</string>
|
||||
<string name="chat_lock">SimpleX 锁定</string>
|
||||
<string name="periodic_notifications">定期通知</string>
|
||||
<string name="notifications_mode_periodic">定期启动</string>
|
||||
@@ -733,7 +733,7 @@
|
||||
<string name="image_decoding_exception_desc">图像无法解码。 请尝试不同的图像或联系开发者。</string>
|
||||
<string name="theme">主题</string>
|
||||
<string name="delete_files_and_media_desc">此操作无法撤消——所有接收和发送的文件和媒体都将被删除。 低分辨率图片将保留。</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">角色将更改为“%s”。 该成员将收到新的邀请。</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">角色将更改为%s。 该成员将收到新的邀请。</string>
|
||||
<string name="enable_automatic_deletion_message">此操作无法撤消——早于所选的发送和接收的消息将被删除。 这可能需要几分钟时间。</string>
|
||||
<string name="this_QR_code_is_not_a_link">此二维码不是链接!</string>
|
||||
<string name="switch_receiving_address_desc">接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。</string>
|
||||
@@ -946,7 +946,7 @@
|
||||
<string name="moderate_message_will_be_deleted_warning">将为所有成员删除该消息。</string>
|
||||
<string name="moderate_message_will_be_marked_warning">该消息将对所有成员标记为已被管理员移除。</string>
|
||||
<string name="delete_member_message__question">删除成员消息?</string>
|
||||
<string name="group_member_role_observer">观察者</string>
|
||||
<string name="group_member_role_observer">观察员</string>
|
||||
<string name="you_are_observer">您是观察者</string>
|
||||
<string name="error_updating_link_for_group">更新群组链接错误</string>
|
||||
<string name="observer_cant_send_message_title">您无法发送消息!</string>
|
||||
@@ -968,7 +968,7 @@
|
||||
<string name="v4_6_reduced_battery_usage_descr">更多改进即将推出!</string>
|
||||
<string name="v4_6_group_moderation_descr">现在管理员可以:
|
||||
\n- 删除成员的消息。
|
||||
\n- 禁用成员(“观察员”角色)</string>
|
||||
\n- 禁用成员(观察员角色)</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">使用密码保护您的聊天资料!</string>
|
||||
<string name="confirm_password">确认密码</string>
|
||||
<string name="error_updating_user_privacy">更新用户隐私错误</string>
|
||||
@@ -2068,4 +2068,62 @@
|
||||
<string name="new_message">新消息</string>
|
||||
<string name="error_parsing_uri_desc">请检查 Simple X 链接是否正确。</string>
|
||||
<string name="error_parsing_uri_title">无效链接</string>
|
||||
<string name="settings_section_title_chat_database">聊天数据库</string>
|
||||
<string name="system_mode_toast">系统模式</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">上传的数据库存档将永久性从服务器被删除。</string>
|
||||
<string name="network_proxy_incorrect_config_desc">确保代理配置正确</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">消息将被删除 - 此操作无法撤销!</string>
|
||||
<string name="switching_profile_error_message">你的连接被移动到 %s,但在将你重定向到配置文件时发生了意料之外的错误。</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">代理不使用身份验证凭据</string>
|
||||
<string name="switching_profile_error_title">切换配置文件出错</string>
|
||||
<string name="network_proxy_auth">代理身份验证</string>
|
||||
<string name="migrate_from_device_remove_archive_question">删除存档?</string>
|
||||
<string name="select_chat_profile">选择聊天配置文件</string>
|
||||
<string name="network_proxy_incorrect_config_title">保存代理出错</string>
|
||||
<string name="network_proxy_password">密码</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">每个连接使用不同的代理身份验证凭据。</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">每个配置文件使用不同的代理身份验证。</string>
|
||||
<string name="network_proxy_auth_mode_username_password">你的凭据可能以未经加密的方式被发送。</string>
|
||||
<string name="network_proxy_random_credentials">使用随机凭据</string>
|
||||
<string name="network_proxy_username">用户名</string>
|
||||
<string name="new_chat_share_profile">分享配置文件</string>
|
||||
<string name="error_forwarding_messages">转发消息出错</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">在你选中消息后这些消息被删除。</string>
|
||||
<string name="n_file_errors">%1$d 个文件错误:
|
||||
\n%2$s</string>
|
||||
<string name="n_other_file_errors">其他 %1$d 个文件错误。</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d 个文件未被下载。</string>
|
||||
<string name="forward_alert_title_messages_to_forward">转发 %1$s 条消息?</string>
|
||||
<string name="forward_alert_forward_messages_without_files">仅转发消息不转发文件?</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">没什么可转发的!</string>
|
||||
<string name="forward_files_in_progress_desc">仍有 %1$d 个文件在下载中。</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d 个文件下载失败。</string>
|
||||
<string name="forward_files_missing_desc">%1$d 个文件被删除了。</string>
|
||||
<string name="forward_files_not_accepted_receive_files">下载</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s 条消息未被转发</string>
|
||||
<string name="forward_multiple">转发消息…</string>
|
||||
<string name="compose_forward_messages_n">转发 %1$s 条消息</string>
|
||||
<string name="compose_save_messages_n">保存 %1$s 条消息</string>
|
||||
<string name="icon_descr_sound_muted">已静音</string>
|
||||
<string name="settings_section_title_message_shape">管理形状</string>
|
||||
<string name="settings_message_shape_corner">拐角</string>
|
||||
<string name="settings_message_shape_tail">尾部</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">初始化 WebView 出错。确保你安装了 WebView 且其支持的架构为 arm64。\n错误:%s</string>
|
||||
<string name="network_session_mode_session">应用会话</string>
|
||||
<string name="network_session_mode_session_description">每次启动应用都会使用新的 SOCKS5 凭据。</string>
|
||||
<string name="network_session_mode_server">服务器</string>
|
||||
<string name="call_desktop_permission_denied_safari">打开 Safari 设置/网站/麦克风,接着在 localhost 选择“允许”。</string>
|
||||
<string name="call_desktop_permission_denied_title">要进行通话,请允许使用设备麦克风。结束通话并尝试再次呼叫。</string>
|
||||
<string name="call_desktop_permission_denied_chrome">单击地址附近的\"信息\"按钮允许使用麦克风。</string>
|
||||
<string name="network_session_mode_server_description">每个服务器都会使用新的 SOCKS5 凭据。</string>
|
||||
<string name="v6_1_message_dates_descr">更好的消息日期。</string>
|
||||
<string name="v6_1_better_security">更佳的安全性✅</string>
|
||||
<string name="v6_1_better_user_experience">更佳的使用体验</string>
|
||||
<string name="v6_1_customizable_message_descr">可自定义消息形状。</string>
|
||||
<string name="v6_1_forward_many_messages_descr">一次性转发最多20条消息。</string>
|
||||
<string name="v6_1_better_security_descr">Trail of Bits 审核了 SimpleX 协议。</string>
|
||||
<string name="v6_1_better_calls_descr">通话期间切换音频和视频。</string>
|
||||
<string name="v6_1_switch_chat_profile_descr">对一次性邀请切换聊天配置文件。</string>
|
||||
<string name="v6_1_better_calls">更佳的通话</string>
|
||||
<string name="v6_1_delete_many_messages_descr">允许自行删除或管理员移除最多200条消息。</string>
|
||||
</resources>
|
||||
@@ -6,6 +6,15 @@
|
||||
<script src="../lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-screen-video-stream"
|
||||
class="inline"
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
onclick="javascript:toggleRemoteScreenVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
class="inline"
|
||||
@@ -14,6 +23,7 @@
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
onclick="javascript:toggleRemoteVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-video-stream"
|
||||
class="inline"
|
||||
@@ -22,6 +32,15 @@
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-screen-video-stream"
|
||||
class="inline"
|
||||
muted
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
></video>
|
||||
</body>
|
||||
<footer>
|
||||
<script src="../call.js"></script>
|
||||
|
||||
@@ -12,6 +12,35 @@ 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;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
@@ -23,6 +52,17 @@ body {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
max-width: 30%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 30%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -30,6 +70,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -37,6 +84,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
<script src="/lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-screen-video-stream"
|
||||
class="inline"
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
onclick="javascript:toggleRemoteScreenVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
class="inline"
|
||||
@@ -14,6 +23,7 @@
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
onclick="javascript:toggleRemoteVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-video-stream"
|
||||
class="inline"
|
||||
@@ -21,28 +31,45 @@
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-screen-video-stream"
|
||||
class="inline"
|
||||
muted
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
></video>
|
||||
|
||||
<div id="progress"></div>
|
||||
<div id="info-block">
|
||||
<p id="state"></p>
|
||||
<p id="description"></p>
|
||||
<b id="media-sources" style="color: #fff"></b>
|
||||
</div>
|
||||
<div id="audio-call-icon">
|
||||
<img src="/desktop/images/ic_phone_in_talk.svg" />
|
||||
</div>
|
||||
|
||||
<div id="permission-denied">
|
||||
<p id="permission-denied-title"></p>
|
||||
<p id="permission-denied-desc"></p>
|
||||
</div>
|
||||
|
||||
<p id="manage-call">
|
||||
<button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()">
|
||||
<button id="toggle-screen" onclick="javascript:toggleScreenManually()">
|
||||
<img src="/desktop/images/ic_screen_share.svg" />
|
||||
</button>
|
||||
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
|
||||
<button id="toggle-mic" onclick="javascript:toggleMicManually()">
|
||||
<img src="/desktop/images/ic_mic.svg" />
|
||||
</button>
|
||||
<button id="end-call" onclick="javascript:endCallManually()">
|
||||
<button id="end-call" style="background: red" onclick="javascript:endCallManually()">
|
||||
<img src="/desktop/images/ic_call_end_filled.svg" />
|
||||
</button>
|
||||
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
|
||||
<button id="toggle-speaker" onclick="javascript:toggleSpeakerManually()">
|
||||
<img src="/desktop/images/ic_volume_up.svg" />
|
||||
</button>
|
||||
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
|
||||
<button id="toggle-camera" onclick="javascript:toggleCameraManually()">
|
||||
<img src="/desktop/images/ic_videocam_off.svg" />
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="red" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 435 B After Width: | Height: | Size: 437 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 -960 960 960" width="44" fill="white"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg>
|
||||
|
After Width: | Height: | Size: 582 B |
@@ -3,6 +3,7 @@ body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#remote-video-stream.inline {
|
||||
@@ -12,7 +13,37 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed {
|
||||
position: absolute;
|
||||
max-width: 20%;
|
||||
max-height: 20%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed-pip {
|
||||
position: absolute;
|
||||
max-width: 30%;
|
||||
max-height: 30%;
|
||||
object-fit: cover;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.inline {
|
||||
background-color: black;
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
@@ -23,6 +54,17 @@ body {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 33%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -30,6 +72,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -37,6 +86,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
@@ -57,19 +113,32 @@ body {
|
||||
#manage-call {
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
top: 90%;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-column-gap: 30px;
|
||||
grid-column-gap: 38px;
|
||||
}
|
||||
|
||||
#manage-call button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-color: inherit;
|
||||
background-color: #ffffff33;
|
||||
border-radius: 50%;
|
||||
padding: 13px;
|
||||
width: 61px;
|
||||
height: 61px;
|
||||
}
|
||||
|
||||
#manage-call img {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
#manage-call button .video {
|
||||
background: #00000033;
|
||||
}
|
||||
|
||||
#progress {
|
||||
@@ -89,6 +158,23 @@ body {
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
#permission-denied {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#permission-denied-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#permission-denied-desc {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
@@ -110,10 +196,8 @@ body {
|
||||
#info-block {
|
||||
position: absolute;
|
||||
color: white;
|
||||
line-height: 10px;
|
||||
opacity: 0.8;
|
||||
width: 200px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#info-block.audio {
|
||||
|
||||
@@ -9,6 +9,7 @@ socket.addEventListener("open", (_event) => {
|
||||
sendMessageToNative = (msg) => {
|
||||
console.log("Message to server");
|
||||
socket.send(JSON.stringify(msg));
|
||||
reactOnMessageToServer(msg);
|
||||
};
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
@@ -27,71 +28,166 @@ socket.addEventListener("close", (_event) => {
|
||||
function endCallManually() {
|
||||
sendMessageToNative({ resp: { type: "end" } });
|
||||
}
|
||||
function toggleAudioManually() {
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) {
|
||||
document.getElementById("toggle-audio").innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Audio)
|
||||
? '<img src="/desktop/images/ic_mic.svg" />'
|
||||
: '<img src="/desktop/images/ic_mic_off.svg" />';
|
||||
}
|
||||
function toggleMicManually() {
|
||||
const enable = activeCall ? !activeCall.localMediaSources.mic : !inactiveCallMediaSources.mic;
|
||||
const apiCall = {
|
||||
command: { type: "media", source: CallMediaSource.Mic, enable: enable },
|
||||
};
|
||||
processCommand(apiCall);
|
||||
}
|
||||
function toggleSpeakerManually() {
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.remoteStream) {
|
||||
document.getElementById("toggle-speaker").innerHTML = toggleMedia(activeCall.remoteStream, CallMediaType.Audio)
|
||||
? '<img src="/desktop/images/ic_volume_up.svg" />'
|
||||
: '<img src="/desktop/images/ic_volume_down.svg" />';
|
||||
if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.remoteStream) && activeCall.peerMediaSources.mic) {
|
||||
enableSpeakerIcon(togglePeerMedia(activeCall.remoteStream, CallMediaType.Audio), !activeCall.peerMediaSources.mic);
|
||||
}
|
||||
}
|
||||
function toggleVideoManually() {
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) {
|
||||
let res;
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled) {
|
||||
activeCall.cameraEnabled = !activeCall.cameraEnabled;
|
||||
res = activeCall.cameraEnabled;
|
||||
}
|
||||
else {
|
||||
res = toggleMedia(activeCall.localStream, CallMediaType.Video);
|
||||
}
|
||||
document.getElementById("toggle-video").innerHTML = res
|
||||
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
|
||||
: '<img src="/desktop/images/ic_videocam_off.svg" />';
|
||||
}
|
||||
function toggleCameraManually() {
|
||||
const enable = activeCall ? !activeCall.localMediaSources.camera : !inactiveCallMediaSources.camera;
|
||||
const apiCall = {
|
||||
command: { type: "media", source: CallMediaSource.Camera, enable: enable },
|
||||
};
|
||||
processCommand(apiCall);
|
||||
}
|
||||
async function toggleScreenManually() {
|
||||
const was = activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled;
|
||||
await toggleScreenShare();
|
||||
if (was != (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled)) {
|
||||
document.getElementById("toggle-screen").innerHTML = (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled)
|
||||
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
|
||||
: '<img src="/desktop/images/ic_screen_share.svg" />';
|
||||
}
|
||||
// override function in call.ts to adapt UI to enabled media sources
|
||||
localOrPeerMediaSourcesChanged = (call) => {
|
||||
enableMicIcon(call.localMediaSources.mic);
|
||||
enableCameraIcon(call.localMediaSources.camera);
|
||||
enableScreenIcon(call.localMediaSources.screenVideo);
|
||||
const className = localMedia(call) == CallMediaType.Video || peerMedia(call) == CallMediaType.Video ? CallMediaType.Video : CallMediaType.Audio;
|
||||
document.getElementById("info-block").className = className;
|
||||
if (call.connection.iceConnectionState == "connected") {
|
||||
document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none";
|
||||
}
|
||||
// document.getElementById("media-sources")!.innerText = mediaSourcesStatus(call)
|
||||
document.getElementById("manage-call").className = localMedia(call) == CallMediaType.Video ? CallMediaType.Video : "";
|
||||
};
|
||||
// override function in call.ts to adapt UI to enabled media sources
|
||||
inactiveCallMediaSourcesChanged = (inactiveCallMediaSources) => {
|
||||
const mic = inactiveCallMediaSources.mic;
|
||||
const camera = inactiveCallMediaSources.camera;
|
||||
const screenVideo = inactiveCallMediaSources.screenVideo;
|
||||
enableMicIcon(mic);
|
||||
enableCameraIcon(camera);
|
||||
enableScreenIcon(screenVideo);
|
||||
const className = camera ? CallMediaType.Video : CallMediaType.Audio;
|
||||
document.getElementById("info-block").className = className;
|
||||
// document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources)
|
||||
};
|
||||
failedToGetPermissions = (title, description) => {
|
||||
document.getElementById("info-block").style.visibility = "hidden";
|
||||
document.getElementById("progress").style.visibility = "hidden";
|
||||
document.getElementById("permission-denied-title").innerText = title;
|
||||
document.getElementById("permission-denied-desc").innerText = description;
|
||||
document.getElementById("toggle-mic").style.visibility = "hidden";
|
||||
document.getElementById("toggle-camera").style.visibility = "hidden";
|
||||
document.getElementById("toggle-screen").style.visibility = "hidden";
|
||||
document.getElementById("toggle-speaker").style.visibility = "hidden";
|
||||
};
|
||||
function enableMicIcon(enabled) {
|
||||
document.getElementById("toggle-mic").innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_mic.svg" />'
|
||||
: '<img src="/desktop/images/ic_mic_off.svg" />';
|
||||
}
|
||||
function enableCameraIcon(enabled) {
|
||||
document.getElementById("toggle-camera").innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
|
||||
: '<img src="/desktop/images/ic_videocam_off.svg" />';
|
||||
}
|
||||
function enableScreenIcon(enabled) {
|
||||
document.getElementById("toggle-screen").innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
|
||||
: '<img src="/desktop/images/ic_screen_share.svg" />';
|
||||
}
|
||||
function enableSpeakerIcon(enabled, muted) {
|
||||
document.getElementById("toggle-speaker").innerHTML = muted
|
||||
? '<img src="/desktop/images/ic_volume_off.svg" />'
|
||||
: enabled
|
||||
? '<img src="/desktop/images/ic_volume_up.svg" />'
|
||||
: '<img src="/desktop/images/ic_volume_down.svg" />';
|
||||
document.getElementById("toggle-speaker").style.opacity = muted ? "0.7" : "1";
|
||||
}
|
||||
function mediaSourcesStatus(call) {
|
||||
let status = "local";
|
||||
if (call.localMediaSources.mic)
|
||||
status += " mic";
|
||||
if (call.localMediaSources.camera)
|
||||
status += " cam";
|
||||
if (call.localMediaSources.screenAudio)
|
||||
status += " scrA";
|
||||
if (call.localMediaSources.screenVideo)
|
||||
status += " scrV";
|
||||
status += " | peer";
|
||||
if (call.peerMediaSources.mic)
|
||||
status += " mic";
|
||||
if (call.peerMediaSources.camera)
|
||||
status += " cam";
|
||||
if (call.peerMediaSources.screenAudio)
|
||||
status += " scrA";
|
||||
if (call.peerMediaSources.screenVideo)
|
||||
status += " scrV";
|
||||
return status;
|
||||
}
|
||||
function inactiveCallMediaSourcesStatus(inactiveCallMediaSources) {
|
||||
let status = "local";
|
||||
const mic = inactiveCallMediaSources.mic;
|
||||
const camera = inactiveCallMediaSources.camera;
|
||||
const screenAudio = inactiveCallMediaSources.screenAudio;
|
||||
const screenVideo = inactiveCallMediaSources.screenVideo;
|
||||
if (mic)
|
||||
status += " mic";
|
||||
if (camera)
|
||||
status += " cam";
|
||||
if (screenAudio)
|
||||
status += " scrA";
|
||||
if (screenVideo)
|
||||
status += " scrV";
|
||||
return status;
|
||||
}
|
||||
function reactOnMessageFromServer(msg) {
|
||||
var _a;
|
||||
switch ((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) {
|
||||
var _a, _b, _c;
|
||||
// screen is not allowed to be enabled before connection estabilished
|
||||
if (((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) == "capabilities" || ((_b = msg.command) === null || _b === void 0 ? void 0 : _b.type) == "offer") {
|
||||
document.getElementById("toggle-screen").style.opacity = "0.7";
|
||||
}
|
||||
else if (activeCall) {
|
||||
document.getElementById("toggle-screen").style.opacity = "1";
|
||||
}
|
||||
switch ((_c = msg.command) === null || _c === void 0 ? void 0 : _c.type) {
|
||||
case "capabilities":
|
||||
document.getElementById("info-block").className = msg.command.media;
|
||||
break;
|
||||
case "offer":
|
||||
case "start":
|
||||
document.getElementById("toggle-audio").style.display = "inline-block";
|
||||
document.getElementById("toggle-speaker").style.display = "inline-block";
|
||||
if (msg.command.media == CallMediaType.Video) {
|
||||
document.getElementById("toggle-video").style.display = "inline-block";
|
||||
document.getElementById("toggle-screen").style.display = "inline-block";
|
||||
}
|
||||
document.getElementById("info-block").className = msg.command.media;
|
||||
document.getElementById("toggle-mic").style.display = "inline-block";
|
||||
document.getElementById("toggle-speaker").style.display = "inline-block";
|
||||
document.getElementById("toggle-camera").style.display = "inline-block";
|
||||
document.getElementById("toggle-screen").style.display = "inline-block";
|
||||
enableSpeakerIcon(true, true);
|
||||
break;
|
||||
case "description":
|
||||
updateCallInfoView(msg.command.state, msg.command.description);
|
||||
if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection.connectionState) == "connected") {
|
||||
document.getElementById("progress").style.display = "none";
|
||||
if (document.getElementById("info-block").className == CallMediaType.Audio) {
|
||||
document.getElementById("audio-call-icon").style.display = "block";
|
||||
}
|
||||
document.getElementById("audio-call-icon").style.display =
|
||||
document.getElementById("info-block").className == CallMediaType.Audio ? "block" : "none";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
function reactOnMessageToServer(msg) {
|
||||
var _a;
|
||||
if (!activeCall)
|
||||
return;
|
||||
switch ((_a = msg.resp) === null || _a === void 0 ? void 0 : _a.type) {
|
||||
case "peerMedia":
|
||||
const className = localMedia(activeCall) == CallMediaType.Video || peerMedia(activeCall) == CallMediaType.Video ? "video" : "audio";
|
||||
document.getElementById("info-block").className = className;
|
||||
document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none";
|
||||
enableSpeakerIcon(activeCall.remoteStream.getAudioTracks().every((elem) => elem.enabled), !activeCall.peerMediaSources.mic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function updateCallInfoView(state, description) {
|
||||
document.getElementById("state").innerText = state;
|
||||
document.getElementById("description").innerText = description;
|
||||
|
||||
@@ -1 +1 @@
|
||||
var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",t={},i={compressToBase64:function(o){if(null==o)return"";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else{if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString);
|
||||
var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});
|
||||
@@ -199,7 +199,13 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
|
||||
768.dp)
|
||||
Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) {
|
||||
SimpleXTheme {
|
||||
TerminalView(ChatModel) { hiddenUntilRestart = true }
|
||||
TerminalView(true) { hiddenUntilRestart = true }
|
||||
ModalManager.floatingTerminal.showInView()
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
ModalManager.floatingTerminal.closeModals()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,14 @@ actual fun ActiveCallView() {
|
||||
val callRh = call.remoteHostId
|
||||
when (val r = apiMsg.resp) {
|
||||
is WCallResponse.Capabilities -> withBGApi {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
val callType = CallType(call.initialCallType, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
|
||||
CallSoundsPlayer.startConnectingCallSound(scope)
|
||||
activeCallWaitDeliveryReceipt(scope)
|
||||
}
|
||||
is WCallResponse.Offer -> withBGApi {
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Answer -> withBGApi {
|
||||
@@ -65,6 +65,15 @@ actual fun ActiveCallView() {
|
||||
is WCallResponse.Connected -> {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
|
||||
}
|
||||
is WCallResponse.PeerMedia -> {
|
||||
val sources = call.peerMediaSources
|
||||
chatModel.activeCall.value = when (r.source) {
|
||||
CallMediaSource.Mic -> call.copy(peerMediaSources = sources.copy(mic = r.enabled))
|
||||
CallMediaSource.Camera -> call.copy(peerMediaSources = sources.copy(camera = r.enabled))
|
||||
CallMediaSource.ScreenAudio -> call.copy(peerMediaSources = sources.copy(screenAudio = r.enabled))
|
||||
CallMediaSource.ScreenVideo -> call.copy(peerMediaSources = sources.copy(screenVideo = r.enabled))
|
||||
}
|
||||
}
|
||||
is WCallResponse.End -> {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
@@ -77,15 +86,18 @@ actual fun ActiveCallView() {
|
||||
is WCallCommand.Answer ->
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
|
||||
is WCallCommand.Media -> {
|
||||
when (cmd.media) {
|
||||
CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable)
|
||||
CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable)
|
||||
val sources = call.localMediaSources
|
||||
when (cmd.source) {
|
||||
CallMediaSource.Mic -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(mic = cmd.enable))
|
||||
CallMediaSource.Camera -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(camera = cmd.enable))
|
||||
CallMediaSource.ScreenAudio -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
|
||||
CallMediaSource.ScreenVideo -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
|
||||
}
|
||||
}
|
||||
is WCallCommand.Camera -> {
|
||||
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
|
||||
if (!call.audioEnabled) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
|
||||
if (!call.localMediaSources.mic) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
|
||||
}
|
||||
}
|
||||
is WCallCommand.End ->
|
||||
@@ -93,6 +105,14 @@ actual fun ActiveCallView() {
|
||||
else -> {}
|
||||
}
|
||||
is WCallResponse.Error -> {
|
||||
when (apiMsg.command) {
|
||||
is WCallCommand.Capabilities -> chatModel.callCommand.add(WCallCommand.Permission(
|
||||
title = generalGetString(MR.strings.call_desktop_permission_denied_title),
|
||||
chrome = generalGetString(MR.strings.call_desktop_permission_denied_chrome),
|
||||
safari = generalGetString(MR.strings.call_desktop_permission_denied_safari)
|
||||
))
|
||||
else -> {}
|
||||
}
|
||||
Log.e(TAG, "ActiveCallView: command error ${r.message}")
|
||||
}
|
||||
}
|
||||
@@ -204,7 +224,7 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit): NanoWSD {
|
||||
return when {
|
||||
session.headers["upgrade"] == "websocket" -> super.handle(session)
|
||||
session.uri.contains("/simplex/call/") -> resourcesToResponse("/desktop/call.html")
|
||||
else -> resourcesToResponse(URI.create(session.uri).path)
|
||||
else -> resourcesToResponse(uriCreateOrNull(session.uri)?.path ?: return newFixedLengthResponse("Error parsing URL"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@Composable
|
||||
actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CompositionLocalProvider(
|
||||
LocalIndication provides NoIndication
|
||||
) {
|
||||
@@ -35,7 +34,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 71.dp, bottom = 92.dp)
|
||||
.padding(end = 15.dp, bottom = 92.dp)
|
||||
.size(67.dp)
|
||||
.combinedClickable(onClick = {
|
||||
val chat = chatModel.getChat(call.contact.id)
|
||||
@@ -56,7 +55,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
if (media == CallMediaType.Video) {
|
||||
if (call.hasVideo) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam_filled),
|
||||
stringResource(MR.strings.icon_descr_video_call),
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.User
|
||||
import chat.simplex.common.model.UserInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
actual fun UserPickerUsersSection(
|
||||
users: List<UserInfo>,
|
||||
stopped: Boolean,
|
||||
onUserClicked: (user: User) -> Unit,
|
||||
) {
|
||||
if (users.isNotEmpty()) {
|
||||
val userRows = users.chunked(5)
|
||||
val rowsToDisplay = if (userRows.size > 2) 2 else userRows.size
|
||||
val horizontalPadding = DEFAULT_PADDING_HALF + 8.dp
|
||||
|
||||
Column(Modifier
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.height((55.dp + 16.sp.toDp()) * rowsToDisplay + (if (rowsToDisplay > 1) DEFAULT_PADDING else 0.dp))
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING)
|
||||
) {
|
||||
val spaceBetween = (((DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) - (horizontalPadding)) - (65.dp * 5)) / 5
|
||||
|
||||
userRows.forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(spaceBetween),
|
||||
) {
|
||||
row.forEach { u ->
|
||||
Column(modifier = Modifier
|
||||
.clickable (
|
||||
onClick = { onUserClicked(u.user) },
|
||||
enabled = !stopped
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val user = u.user
|
||||
Box {
|
||||
ProfileImage(size = 55.dp, image = user.profile.image, color = MaterialTheme.colors.secondaryVariant)
|
||||
|
||||
if (u.unreadCount > 0 && !user.activeUser) {
|
||||
unreadBadge(u.unreadCount, user.showNtfs, true)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
user.displayName,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = if (user.activeUser) FontWeight.Bold else FontWeight.Normal,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.width(65.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<AnimatedViewState>, content: @Composable () -> Unit) {
|
||||
AnimatedVisibility(
|
||||
visible = pickerState.value.isVisible(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING }),
|
||||
contentAlignment = Alignment.TopStart
|
||||
) {
|
||||
ColumnWithScrollBar(modifier) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,9 @@ fun AppearanceScope.AppearanceLayout(
|
||||
SectionDividerSpaced()
|
||||
ThemesSection(systemDarkTheme)
|
||||
|
||||
SectionDividerSpaced()
|
||||
MessageShapeSection()
|
||||
|
||||
SectionDividerSpaced()
|
||||
ProfileImageSection()
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.0.5
|
||||
android.version_code=241
|
||||
android.version_name=6.1
|
||||
android.version_code=247
|
||||
|
||||
desktop.version_name=6.0.5
|
||||
desktop.version_code=68
|
||||
desktop.version_name=6.1
|
||||
desktop.version_code=73
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||