android, desktop: rework UX of creating new connection (#3529)
* android, desktop: rework UX of creating new connection * different place for clipboard listener * changes * changes * changes * button, strings * focus * code optimization and search icon * incognito link * comment * paddings * optimization * icon size and space in search * appbar background color * secondary color for icons in search bar * lighter tool bar and avatars * darker avatars in toolbar * background for selected item and divider * replacing connection view with actual chat view * clear * close unneeded view * filter icon background * filter doesn't hide current chat with empty search field * fixes for review * clearing focus on hiding keyboard * fix invalid qr code message * rename * color * buttons and text visibility when chat is not running yet * loading chats label --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
parent
2bacc00a06
commit
9c061508a4
@ -1,6 +1,9 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import chat.simplex.common.platform.Log
|
||||
import android.app.UiModeManager
|
||||
import android.os.*
|
||||
import androidx.lifecycle.*
|
||||
@ -95,6 +98,13 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
}
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
isAppOnForeground = true
|
||||
/**
|
||||
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
|
||||
* access to clipboard because the app considered in background.
|
||||
* This will ensure that the app will get the event on resume
|
||||
* */
|
||||
val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
chatModel.clipboardHasText.value = service.hasPrimaryClip()
|
||||
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
|
||||
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
||||
}
|
||||
|
@ -26,3 +26,5 @@ actual fun Modifier.desktopOnExternalDrag(
|
||||
): Modifier = this
|
||||
|
||||
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this
|
||||
|
||||
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this
|
||||
|
@ -17,7 +17,8 @@ actual fun ChatListNavLinkLayout(
|
||||
dropdownMenuItems: (@Composable () -> Unit)?,
|
||||
showMenu: MutableState<Boolean>,
|
||||
stopped: Boolean,
|
||||
selectedChat: State<Boolean>
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!stopped) modifier = modifier
|
||||
|
@ -1,5 +1,7 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.*
|
||||
import android.graphics.Typeface
|
||||
@ -12,6 +14,7 @@ import android.text.SpannedString
|
||||
import android.text.style.*
|
||||
import android.util.Base64
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.*
|
||||
@ -24,7 +27,6 @@ import androidx.core.text.HtmlCompat
|
||||
import chat.simplex.common.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
@ -55,6 +57,19 @@ fun keepScreenOn(on: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun SetupClipboardListener() {
|
||||
DisposableEffect(Unit) {
|
||||
val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val listener = { chatModel.clipboardHasText.value = service.hasPrimaryClip() }
|
||||
chatModel.clipboardHasText.value = service.hasPrimaryClip()
|
||||
service.addPrimaryClipChangedListener(listener)
|
||||
onDispose {
|
||||
service.removePrimaryClipChangedListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString {
|
||||
return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density)
|
||||
}
|
||||
|
@ -1,68 +0,0 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.RemoteHostInfo
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
actual fun ConnectViaLinkView(m: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
// TODO this should close if remote host changes in model
|
||||
val selection = remember {
|
||||
mutableStateOf(
|
||||
runCatching { ConnectViaLinkTab.valueOf(m.controller.appPrefs.connectViaLinkTab.get()!!) }.getOrDefault(ConnectViaLinkTab.SCAN)
|
||||
)
|
||||
}
|
||||
val tabTitles = ConnectViaLinkTab.values().map {
|
||||
when (it) {
|
||||
ConnectViaLinkTab.SCAN -> stringResource(MR.strings.scan_QR_code)
|
||||
ConnectViaLinkTab.PASTE -> stringResource(MR.strings.paste_the_link_you_received)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
when (selection.value) {
|
||||
ConnectViaLinkTab.SCAN -> {
|
||||
ScanToConnectView(m, rh, close)
|
||||
}
|
||||
ConnectViaLinkTab.PASTE -> {
|
||||
PasteToConnectView(m, rh, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
TabRow(
|
||||
selectedTabIndex = selection.value.ordinal,
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
) {
|
||||
tabTitles.forEachIndexed { index, it ->
|
||||
Tab(
|
||||
selected = selection.value.ordinal == index,
|
||||
onClick = {
|
||||
selection.value = ConnectViaLinkTab.values()[index]
|
||||
m.controller.appPrefs.connectViaLinkTab.set(selection.value .name)
|
||||
},
|
||||
text = { Text(it, fontSize = 13.sp) },
|
||||
icon = {
|
||||
Icon(
|
||||
if (ConnectViaLinkTab.SCAN.ordinal == index) painterResource(MR.images.ic_qr_code) else painterResource(MR.images.ic_article),
|
||||
it
|
||||
)
|
||||
},
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
unselectedContentColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,16 +2,20 @@ package chat.simplex.common.views.newchat
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import boofcv.abst.fiducial.QrCodeDetector
|
||||
@ -20,18 +24,23 @@ import boofcv.android.ConvertCameraImage
|
||||
import boofcv.factory.fiducial.FactoryFiducial
|
||||
import boofcv.struct.image.GrayU8
|
||||
import chat.simplex.common.platform.TAG
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import java.util.concurrent.*
|
||||
|
||||
// Adapted from learntodroid - https://gist.github.com/learntodroid/8f839be0b29d0378f843af70607bd7f5
|
||||
|
||||
@Composable
|
||||
actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
actual fun QRCodeScanner(
|
||||
showQRCodeScanner: MutableState<Boolean>,
|
||||
padding: PaddingValues,
|
||||
onBarcode: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var preview by remember { mutableStateOf<Preview?>(null) }
|
||||
@ -48,57 +57,102 @@ actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = { AndroidViewContext ->
|
||||
PreviewView(AndroidViewContext).apply {
|
||||
this.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clipToBounds()
|
||||
) { previewView ->
|
||||
val cameraSelector: CameraSelector = CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||
.build()
|
||||
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
cameraProviderFuture?.addListener({
|
||||
preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
val detector: QrCodeDetector<GrayU8> = FactoryFiducial.qrcode(null, GrayU8::class.java)
|
||||
fun getQR(imageProxy: ImageProxy) {
|
||||
val currentTimeStamp = System.currentTimeMillis()
|
||||
if (currentTimeStamp - lastAnalyzedTimeStamp >= TimeUnit.SECONDS.toMillis(1)) {
|
||||
detector.process(imageProxyToGrayU8(imageProxy))
|
||||
val found = detector.detections
|
||||
val qr = found.firstOrNull()
|
||||
if (qr != null) {
|
||||
if (qr.message != contactLink) {
|
||||
// Make sure link is new and not a repeat
|
||||
contactLink = qr.message
|
||||
onBarcode(contactLink)
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
val modifier = Modifier
|
||||
.padding(padding)
|
||||
.clipToBounds()
|
||||
.widthIn(max = 400.dp)
|
||||
.aspectRatio(1f)
|
||||
val showScanner = remember { showQRCodeScanner }
|
||||
if (showScanner.value && cameraPermissionState.hasPermission) {
|
||||
AndroidView(
|
||||
factory = { AndroidViewContext ->
|
||||
PreviewView(AndroidViewContext).apply {
|
||||
this.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
) { previewView ->
|
||||
val cameraSelector: CameraSelector = CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||
.build()
|
||||
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
cameraProviderFuture?.addListener({
|
||||
preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
val detector: QrCodeDetector<GrayU8> = FactoryFiducial.qrcode(null, GrayU8::class.java)
|
||||
fun getQR(imageProxy: ImageProxy) {
|
||||
val currentTimeStamp = System.currentTimeMillis()
|
||||
if (currentTimeStamp - lastAnalyzedTimeStamp >= TimeUnit.SECONDS.toMillis(1)) {
|
||||
detector.process(imageProxyToGrayU8(imageProxy))
|
||||
val found = detector.detections
|
||||
val qr = found.firstOrNull()
|
||||
if (qr != null) {
|
||||
if (qr.message != contactLink) {
|
||||
// Make sure link is new and not a repeat
|
||||
contactLink = qr.message
|
||||
onBarcode(contactLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
imageProxy.close()
|
||||
}
|
||||
|
||||
val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> getQR(proxy) }
|
||||
val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.setImageQueueDepth(1)
|
||||
.build()
|
||||
.also { it.setAnalyzer(cameraExecutor, imageAnalyzer) }
|
||||
try {
|
||||
cameraProviderFuture?.get()?.unbindAll()
|
||||
cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "CameraPreview: ${e.localizedMessage}")
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
} else {
|
||||
val buttonColors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.9f),
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
disabledBackgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.9f),
|
||||
disabledContentColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
when {
|
||||
!cameraPermissionState.hasPermission && !cameraPermissionState.permissionRequested && showScanner.value -> {
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
!cameraPermissionState.hasPermission -> {
|
||||
Button({ withBGApi { cameraPermissionState.launchPermissionRequest() } }, modifier = modifier, colors = buttonColors) {
|
||||
Icon(painterResource(MR.images.ic_camera_enhance), null)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||
Text(stringResource(MR.strings.enable_camera_access))
|
||||
}
|
||||
}
|
||||
cameraPermissionState.hasPermission -> {
|
||||
Button({ showQRCodeScanner.value = true }, modifier = modifier, colors = buttonColors) {
|
||||
Icon(painterResource(MR.images.ic_qr_code), null)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||
Text(stringResource(MR.strings.tap_to_scan))
|
||||
}
|
||||
}
|
||||
!LocalContext.current.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) -> {
|
||||
Button({ }, enabled = false, modifier = modifier, colors = buttonColors) {
|
||||
Text(stringResource(MR.strings.camera_not_available))
|
||||
}
|
||||
}
|
||||
imageProxy.close()
|
||||
}
|
||||
val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> getQR(proxy) }
|
||||
val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.setImageQueueDepth(1)
|
||||
.build()
|
||||
.also { it.setAnalyzer(cameraExecutor, imageAnalyzer) }
|
||||
try {
|
||||
cameraProviderFuture?.get()?.unbindAll()
|
||||
cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "CameraPreview: ${e.localizedMessage}")
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.RemoteHostInfo
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
actual fun ScanToConnectView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ConnectContactLayout(
|
||||
chatModel = chatModel,
|
||||
rh = rh,
|
||||
incognitoPref = chatModel.controller.appPrefs.incognito,
|
||||
close = close
|
||||
)
|
||||
}
|
@ -107,7 +107,7 @@ fun MainScreen() {
|
||||
val localUserCreated = chatModel.localUserCreated.value
|
||||
var showInitializationView by remember { mutableStateOf(false) }
|
||||
when {
|
||||
chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView()
|
||||
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
||||
showChatDatabaseError -> {
|
||||
chatModel.chatDbStatus.value?.let {
|
||||
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
|
||||
@ -125,6 +125,7 @@ fun MainScreen() {
|
||||
}
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) }
|
||||
SetupClipboardListener()
|
||||
if (appPlatform.isAndroid) {
|
||||
AndroidScreen(settingsState)
|
||||
} else {
|
||||
@ -342,22 +343,6 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InitializationView() {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
Text(stringResource(MR.strings.opening_database))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwitchingUsersView() {
|
||||
if (remember { chatModel.switchingUsersAndHosts }.value) {
|
||||
|
@ -97,8 +97,8 @@ object ChatModel {
|
||||
val showCallView = mutableStateOf(false)
|
||||
val switchingCall = mutableStateOf(false)
|
||||
|
||||
// currently showing QR code
|
||||
val connReqInv = mutableStateOf(null as String?)
|
||||
// currently showing invitation
|
||||
val showingInvitation = mutableStateOf(null as ShowingInvitation?)
|
||||
|
||||
var draft = mutableStateOf(null as ComposeState?)
|
||||
var draftChatId = mutableStateOf(null as String?)
|
||||
@ -109,6 +109,8 @@ object ChatModel {
|
||||
val filesToDelete = mutableSetOf<File>()
|
||||
val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) }
|
||||
|
||||
val clipboardHasText = mutableStateOf(false)
|
||||
|
||||
var updatingChatsMutex: Mutex = Mutex()
|
||||
|
||||
val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null
|
||||
@ -561,15 +563,30 @@ object ChatModel {
|
||||
chats.add(index = 0, chat)
|
||||
}
|
||||
|
||||
fun dismissConnReqView(id: String) {
|
||||
if (connReqInv.value == null) return
|
||||
val info = getChat(id)?.chatInfo as? ChatInfo.ContactConnection ?: return
|
||||
if (info.contactConnection.connReqInv == connReqInv.value) {
|
||||
connReqInv.value = null
|
||||
ModalManager.center.closeModals()
|
||||
fun replaceConnReqView(id: String, withId: String) {
|
||||
if (id == showingInvitation.value?.connId) {
|
||||
showingInvitation.value = null
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatId.value = withId
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissConnReqView(id: String) {
|
||||
if (id == showingInvitation.value?.connId) {
|
||||
showingInvitation.value = null
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatId.value = null
|
||||
// Close NewChatView
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
fun markShowingInvitationUsed() {
|
||||
showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true)
|
||||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
chats.removeAll { it.id == id && it.remoteHostId == rhId }
|
||||
}
|
||||
@ -630,6 +647,12 @@ object ChatModel {
|
||||
fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true
|
||||
}
|
||||
|
||||
data class ShowingInvitation(
|
||||
val connId: String,
|
||||
val connReq: String,
|
||||
val connChatUsed: Boolean
|
||||
)
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
Direct("@"),
|
||||
Group("#"),
|
||||
@ -2664,6 +2687,8 @@ sealed class Format {
|
||||
is Phone -> linkStyle
|
||||
}
|
||||
|
||||
val isSimplexLink = this is SimplexLink
|
||||
|
||||
companion object {
|
||||
val linkStyle @Composable get() = SpanStyle(color = MaterialTheme.colors.primary, textDecoration = TextDecoration.Underline)
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.newchat.ConnectViaLinkTab
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import com.charleskorn.kaml.Yaml
|
||||
@ -135,7 +134,6 @@ class AppPreferences {
|
||||
val networkTCPKeepIntvl = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_INTVL, KeepAliveOpts.defaults.keepIntvl)
|
||||
val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt)
|
||||
val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false)
|
||||
val connectViaLinkTab = mkStrPreference(SHARED_PREFS_CONNECT_VIA_LINK_TAB, ConnectViaLinkTab.SCAN.name)
|
||||
val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false)
|
||||
val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true)
|
||||
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
|
||||
@ -292,7 +290,6 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl"
|
||||
private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt"
|
||||
private const val SHARED_PREFS_INCOGNITO = "Incognito"
|
||||
private const val SHARED_PREFS_CONNECT_VIA_LINK_TAB = "ConnectViaLinkTab"
|
||||
private const val SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN = "LiveMessageAlertShown"
|
||||
private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice"
|
||||
private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert"
|
||||
@ -890,19 +887,19 @@ object ChatController {
|
||||
|
||||
|
||||
|
||||
suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair<String, PendingContactConnection>? {
|
||||
suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair<Pair<String, PendingContactConnection>?, (() -> Unit)?> {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "apiAddContact: no current user")
|
||||
return null
|
||||
return null to null
|
||||
}
|
||||
val r = sendCmd(rh, CC.APIAddContact(userId, incognito))
|
||||
return when (r) {
|
||||
is CR.Invitation -> r.connReqInvitation to r.connection
|
||||
is CR.Invitation -> (r.connReqInvitation to r.connection) to null
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r)
|
||||
return null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) }
|
||||
}
|
||||
null
|
||||
null to null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -981,6 +978,13 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteChat(chat: Chat, notify: Boolean? = null) {
|
||||
val cInfo = chat.chatInfo
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) {
|
||||
chatModel.removeChat(chat.remoteHostId, cInfo.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify))
|
||||
when {
|
||||
@ -1568,7 +1572,7 @@ object ChatController {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.dismissConnReqView(conn.id)
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
@ -1582,7 +1586,7 @@ object ChatController {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.dismissConnReqView(conn.id)
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
@ -1717,7 +1721,7 @@ object ChatController {
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
val conn = r.hostContact?.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.dismissConnReqView(conn.id)
|
||||
chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
@ -1727,7 +1731,7 @@ object ChatController {
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
val hostConn = r.hostMember.activeConn
|
||||
if (hostConn != null) {
|
||||
chatModel.dismissConnReqView(hostConn.id)
|
||||
chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}")
|
||||
chatModel.removeChat(rhId, hostConn.id)
|
||||
}
|
||||
}
|
||||
|
@ -73,11 +73,14 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
}
|
||||
} else {
|
||||
val savedOnboardingStage = appPreferences.onboardingStage.get()
|
||||
appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||
val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||
OnboardingStage.Step3_CreateSimpleXAddress
|
||||
} else {
|
||||
savedOnboardingStage
|
||||
})
|
||||
}
|
||||
if (appPreferences.onboardingStage.get() != newStage) {
|
||||
appPreferences.onboardingStage.set(newStage)
|
||||
}
|
||||
if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
|
||||
chatModel.setDeliveryReceipts.value = true
|
||||
}
|
||||
|
@ -23,3 +23,5 @@ expect fun Modifier.desktopOnExternalDrag(
|
||||
): Modifier
|
||||
|
||||
expect fun Modifier.onRightClick(action: () -> Unit): Modifier
|
||||
|
||||
expect fun Modifier.desktopPointerHoverIconHand(): Modifier
|
||||
|
@ -355,7 +355,7 @@ fun ChatInfoLayout(
|
||||
|
||||
if (contact.contactLink != null) {
|
||||
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
|
||||
SimpleXLinkQRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
|
||||
SimpleXLinkQRCode(contact.contactLink)
|
||||
val clipboard = LocalClipboardManager.current
|
||||
ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) }
|
||||
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName))
|
||||
|
@ -33,6 +33,7 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.AudioPlayer
|
||||
import chat.simplex.common.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
@ -114,343 +115,371 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
ChatLayout(
|
||||
chat,
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (
|
||||
chat.chatInfo is ChatInfo.Direct
|
||||
&& !chat.chatInfo.contact.ready
|
||||
&& chat.chatInfo.contact.active
|
||||
&& !chat.chatInfo.contact.nextSendGrpInv
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.contact_connection_pending),
|
||||
Modifier.padding(top = 4.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
ComposeView(
|
||||
chatModel, chat, composeState, attachmentOption,
|
||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||
)
|
||||
}
|
||||
},
|
||||
attachmentOption,
|
||||
attachmentBottomSheetState,
|
||||
chatModel.chatItems,
|
||||
searchText,
|
||||
useLinkPreviews = useLinkPreviews,
|
||||
linkMode = chatModel.simplexLinkMode.value,
|
||||
back = {
|
||||
hideKeyboard(view)
|
||||
AudioPlayer.stop()
|
||||
chatModel.chatId.value = null
|
||||
chatModel.groupMembers.clear()
|
||||
},
|
||||
info = {
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
ModalManager.end.closeModals()
|
||||
return@ChatLayout
|
||||
}
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
// The idea is to preload information before showing a modal because large groups can take time to load all members
|
||||
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
|
||||
var preloadedCode: String? = null
|
||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
val chat = remember { activeChat }.value
|
||||
if (chat?.chatInfo is ChatInfo.Direct) {
|
||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct, is ChatInfo.Group -> {
|
||||
ChatLayout(
|
||||
chat,
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (
|
||||
chat.chatInfo is ChatInfo.Direct
|
||||
&& !chat.chatInfo.contact.ready
|
||||
&& chat.chatInfo.contact.active
|
||||
&& !chat.chatInfo.contact.nextSendGrpInv
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.contact_connection_pending),
|
||||
Modifier.padding(top = 4.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
ComposeView(
|
||||
chatModel, chat, composeState, attachmentOption,
|
||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||
)
|
||||
}
|
||||
},
|
||||
attachmentOption,
|
||||
attachmentBottomSheetState,
|
||||
chatModel.chatItems,
|
||||
searchText,
|
||||
useLinkPreviews = useLinkPreviews,
|
||||
linkMode = chatModel.simplexLinkMode.value,
|
||||
back = {
|
||||
hideKeyboard(view)
|
||||
AudioPlayer.stop()
|
||||
chatModel.chatId.value = null
|
||||
chatModel.groupMembers.clear()
|
||||
},
|
||||
info = {
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
ModalManager.end.closeModals()
|
||||
return@ChatLayout
|
||||
}
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
// The idea is to preload information before showing a modal because large groups can take time to load all members
|
||||
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
|
||||
var preloadedCode: String? = null
|
||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
}
|
||||
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
loadPrevMessages = {
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||
val firstId = chatModel.chatItems.firstOrNull()?.id
|
||||
if (c != null && firstId != null) {
|
||||
withApi {
|
||||
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
|
||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
||||
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
withApi {
|
||||
val cInfo = chat.chatInfo
|
||||
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
val deletedChatItem: ChatItem?
|
||||
val toChatItem: ChatItem?
|
||||
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
val r = chatModel.controller.apiDeleteMemberChatItem(
|
||||
chatRh,
|
||||
groupId = groupInfo.groupId,
|
||||
groupMemberId = groupMember.groupMemberId,
|
||||
itemId = itemId
|
||||
)
|
||||
deletedChatItem = r?.first
|
||||
toChatItem = r?.second
|
||||
} else {
|
||||
val r = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = itemId,
|
||||
mode = mode
|
||||
)
|
||||
deletedChatItem = r?.deletedChatItem?.chatItem
|
||||
toChatItem = r?.toChatItem?.chatItem
|
||||
}
|
||||
if (toChatItem == null && deletedChatItem != null) {
|
||||
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
} else if (toChatItem != null) {
|
||||
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessages = { itemIds ->
|
||||
if (itemIds.isNotEmpty()) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
||||
for (itemId in itemIds) {
|
||||
val di = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||
)?.deletedChatItem?.chatItem
|
||||
if (di != null) {
|
||||
deletedItems.add(di)
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
val chat = remember { activeChat }.value
|
||||
if (chat?.chatInfo is ChatInfo.Direct) {
|
||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (di in deletedItems) {
|
||||
chatModel.removeChatItem(chatRh, chatInfo, di)
|
||||
},
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
receiveFile = { fileId, encrypted ->
|
||||
withApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) }
|
||||
},
|
||||
cancelFile = { fileId ->
|
||||
withApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
|
||||
},
|
||||
joinGroup = { groupId, onComplete ->
|
||||
withApi {
|
||||
chatModel.controller.apiJoinGroup(chatRh, groupId)
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
startCall = out@ { media ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withApi { chatModel.callManager.endCall(call) }
|
||||
},
|
||||
acceptCall = { contact ->
|
||||
hideKeyboard(view)
|
||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
},
|
||||
acceptFeature = { contact, feature, param ->
|
||||
withApi {
|
||||
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
|
||||
}
|
||||
},
|
||||
openDirectChat = { contactId ->
|
||||
withApi {
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
if (r != null) {
|
||||
val contactStats = r.first
|
||||
if (contactStats != null)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateMemberStats = { groupInfo, member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
val memStats = r.second
|
||||
if (memStats != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
},
|
||||
loadPrevMessages = {
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||
val firstId = chatModel.chatItems.firstOrNull()?.id
|
||||
if (c != null && firstId != null) {
|
||||
withApi {
|
||||
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
|
||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
||||
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
syncContactConnection = { contact ->
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
syncMemberConnection = { groupInfo, member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
}
|
||||
}
|
||||
},
|
||||
findModelChat = { chatId ->
|
||||
chatModel.getChat(chatId)
|
||||
},
|
||||
findModelMember = { memberId ->
|
||||
chatModel.groupMembers.find { it.id == memberId }
|
||||
},
|
||||
setReaction = { cInfo, cItem, add, reaction ->
|
||||
withApi {
|
||||
val updatedCI = chatModel.controller.apiChatItemReaction(
|
||||
rh = chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = cItem.id,
|
||||
add = add,
|
||||
reaction = reaction
|
||||
)
|
||||
if (updatedCI != null) {
|
||||
chatModel.updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
}
|
||||
},
|
||||
showItemDetails = { cInfo, cItem ->
|
||||
withApi {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
withApi {
|
||||
val cInfo = chat.chatInfo
|
||||
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
val deletedChatItem: ChatItem?
|
||||
val toChatItem: ChatItem?
|
||||
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
val r = chatModel.controller.apiDeleteMemberChatItem(
|
||||
chatRh,
|
||||
groupId = groupInfo.groupId,
|
||||
groupMemberId = groupMember.groupMemberId,
|
||||
itemId = itemId
|
||||
)
|
||||
deletedChatItem = r?.first
|
||||
toChatItem = r?.second
|
||||
} else {
|
||||
val r = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = itemId,
|
||||
mode = mode
|
||||
)
|
||||
deletedChatItem = r?.deletedChatItem?.chatItem
|
||||
toChatItem = r?.toChatItem?.chatItem
|
||||
}
|
||||
if (toChatItem == null && deletedChatItem != null) {
|
||||
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
} else if (toChatItem != null) {
|
||||
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
}
|
||||
}
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModal(endButtons = { ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
} }) {
|
||||
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
},
|
||||
deleteMessages = { itemIds ->
|
||||
if (itemIds.isNotEmpty()) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
||||
for (itemId in itemIds) {
|
||||
val di = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||
)?.deletedChatItem?.chatItem
|
||||
if (di != null) {
|
||||
deletedItems.add(di)
|
||||
}
|
||||
}
|
||||
for (di in deletedItems) {
|
||||
chatModel.removeChatItem(chatRh, chatInfo, di)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
},
|
||||
receiveFile = { fileId, encrypted ->
|
||||
withApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) }
|
||||
},
|
||||
cancelFile = { fileId ->
|
||||
withApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
|
||||
},
|
||||
joinGroup = { groupId, onComplete ->
|
||||
withApi {
|
||||
chatModel.controller.apiJoinGroup(chatRh, groupId)
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
startCall = out@{ media ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withApi { chatModel.callManager.endCall(call) }
|
||||
},
|
||||
acceptCall = { contact ->
|
||||
hideKeyboard(view)
|
||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
},
|
||||
acceptFeature = { contact, feature, param ->
|
||||
withApi {
|
||||
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
|
||||
}
|
||||
},
|
||||
openDirectChat = { contactId ->
|
||||
withApi {
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
if (r != null) {
|
||||
val contactStats = r.first
|
||||
if (contactStats != null)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateMemberStats = { groupInfo, member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
val memStats = r.second
|
||||
if (memStats != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
syncContactConnection = { contact ->
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
syncMemberConnection = { groupInfo, member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
}
|
||||
}
|
||||
},
|
||||
findModelChat = { chatId ->
|
||||
chatModel.getChat(chatId)
|
||||
},
|
||||
findModelMember = { memberId ->
|
||||
chatModel.groupMembers.find { it.id == memberId }
|
||||
},
|
||||
setReaction = { cInfo, cItem, add, reaction ->
|
||||
withApi {
|
||||
val updatedCI = chatModel.controller.apiChatItemReaction(
|
||||
rh = chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = cItem.id,
|
||||
add = add,
|
||||
reaction = reaction
|
||||
)
|
||||
if (updatedCI != null) {
|
||||
chatModel.updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
}
|
||||
},
|
||||
showItemDetails = { cInfo, cItem ->
|
||||
withApi {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
}
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModal(endButtons = {
|
||||
ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
}
|
||||
}) {
|
||||
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
},
|
||||
openGroupLink = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
markRead = { range, unreadCountAfter ->
|
||||
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
withBGApi {
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
range
|
||||
)
|
||||
}
|
||||
},
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = { value ->
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
|
||||
withApi {
|
||||
apiFindMessages(c, chatModel, value)
|
||||
searchText.value = value
|
||||
}
|
||||
},
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactConnection -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
ModalView(close, showClose = appPlatform.isAndroid, content = {
|
||||
ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
|
||||
})
|
||||
LaunchedEffect(chat.id) {
|
||||
onComposed(chat.id)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
||||
}
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
},
|
||||
openGroupLink = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
||||
}
|
||||
is ChatInfo.InvalidJSON -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
ModalView(close, showClose = appPlatform.isAndroid, content = {
|
||||
InvalidJSONView(chat.chatInfo.json)
|
||||
})
|
||||
LaunchedEffect(chat.id) {
|
||||
onComposed(chat.id)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
}
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
},
|
||||
markRead = { range, unreadCountAfter ->
|
||||
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
withBGApi {
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
range
|
||||
)
|
||||
}
|
||||
},
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = { value ->
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
|
||||
withApi {
|
||||
apiFindMessages(c, chatModel, value)
|
||||
searchText.value = value
|
||||
}
|
||||
},
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -733,7 +762,7 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) {
|
||||
fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
@ -13,29 +13,20 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
@Composable
|
||||
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
Modifier.fillMaxSize()
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.scan_code), withPadding = false)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
verifyCode(text) {
|
||||
if (it) {
|
||||
close()
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.incorrect_code)
|
||||
)
|
||||
}
|
||||
AppBarTitle(stringResource(MR.strings.scan_code))
|
||||
QRCodeScanner { text ->
|
||||
verifyCode(text) {
|
||||
if (it) {
|
||||
close()
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.incorrect_code)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(stringResource(MR.strings.scan_code_from_contacts_app))
|
||||
Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
|
@ -74,9 +74,7 @@ private fun VerifyCodeLayout(
|
||||
}
|
||||
}
|
||||
|
||||
SectionView {
|
||||
QRCode(connectionCode, Modifier.aspectRatio(1f))
|
||||
}
|
||||
QRCode(connectionCode, padding = PaddingValues(vertical = DEFAULT_PADDING_HALF))
|
||||
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(Modifier.weight(2f))
|
||||
|
@ -153,7 +153,7 @@ fun GroupLinkLayout(
|
||||
}
|
||||
initialLaunch = false
|
||||
}
|
||||
SimpleXLinkQRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING))
|
||||
SimpleXLinkQRCode(groupLink)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
@ -289,7 +289,7 @@ fun GroupMemberInfoLayout(
|
||||
|
||||
if (member.contactLink != null) {
|
||||
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
|
||||
SimpleXLinkQRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
|
||||
SimpleXLinkQRCode(member.contactLink)
|
||||
val clipboard = LocalClipboardManager.current
|
||||
ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) }
|
||||
if (contactId != null) {
|
||||
@ -506,7 +506,7 @@ fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
|
||||
try {
|
||||
val uri = URI(connReqUri)
|
||||
withApi {
|
||||
planAndConnect(chatModel, rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
|
||||
planAndConnect(rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
@ -50,8 +50,8 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
|
||||
)
|
||||
Text(stringResource(MR.strings.above_then_preposition_continuation))
|
||||
}
|
||||
Text(annotatedStringResource(MR.strings.add_new_contact_to_create_one_time_QR_code), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(MR.strings.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(MR.strings.add_contact_button_to_create_link_or_connect_via_link), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(MR.strings.create_group_button_to_create_new_group), lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
Column(
|
||||
|
@ -24,17 +24,15 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.group.deleteGroupDialog
|
||||
import chat.simplex.common.views.chat.group.leaveGroupDialog
|
||||
import chat.simplex.common.views.chat.item.InvalidJSONView
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
|
||||
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
||||
@ -45,7 +43,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
showMenu.value = false
|
||||
delay(500L)
|
||||
}
|
||||
val selectedChat = remember(chat.id) { derivedStateOf { chat.id == ChatModel.chatId.value } }
|
||||
val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } }
|
||||
val showChatPreviews = chatModel.showChatPreviews.value
|
||||
val inProgress = remember { mutableStateOf(false) }
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
@ -75,7 +73,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
},
|
||||
showMenu,
|
||||
stopped,
|
||||
selectedChat
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
@ -93,7 +92,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
},
|
||||
showMenu,
|
||||
stopped,
|
||||
selectedChat
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
)
|
||||
is ChatInfo.ContactRequest ->
|
||||
ChatListNavLinkLayout(
|
||||
@ -110,7 +110,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
},
|
||||
showMenu,
|
||||
stopped,
|
||||
selectedChat
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
)
|
||||
is ChatInfo.ContactConnection ->
|
||||
ChatListNavLinkLayout(
|
||||
@ -120,11 +121,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
}
|
||||
},
|
||||
click = {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
|
||||
ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
|
||||
}
|
||||
chatModel.chatId.value = chat.id
|
||||
},
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
|
||||
@ -133,7 +130,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
},
|
||||
showMenu,
|
||||
stopped,
|
||||
selectedChat
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
)
|
||||
is ChatInfo.InvalidJSON ->
|
||||
ChatListNavLinkLayout(
|
||||
@ -143,13 +141,13 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
}
|
||||
},
|
||||
click = {
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModal(true) { InvalidJSONView(chat.chatInfo.json) }
|
||||
chatModel.chatId.value = chat.id
|
||||
},
|
||||
dropdownMenuItems = null,
|
||||
showMenu,
|
||||
stopped,
|
||||
selectedChat
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -476,10 +474,7 @@ fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactConnectionAlert(rhId, chatInfo.contactConnection, chatModel) {
|
||||
if (chatModel.chatId.value == null) {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
chatModel.dismissConnReqView(chatInfo.contactConnection.id)
|
||||
}
|
||||
showMenu.value = false
|
||||
},
|
||||
@ -804,7 +799,8 @@ expect fun ChatListNavLinkLayout(
|
||||
dropdownMenuItems: (@Composable () -> Unit)?,
|
||||
showMenu: MutableState<Boolean>,
|
||||
stopped: Boolean,
|
||||
selectedChat: State<Boolean>
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
)
|
||||
|
||||
@Preview/*(
|
||||
@ -846,7 +842,8 @@ fun PreviewChatListNavLinkDirect() {
|
||||
dropdownMenuItems = null,
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
stopped = false,
|
||||
selectedChat = remember { mutableStateOf(false) }
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -890,7 +887,8 @@ fun PreviewChatListNavLinkGroup() {
|
||||
dropdownMenuItems = null,
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
stopped = false,
|
||||
selectedChat = remember { mutableStateOf(false) }
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -911,7 +909,8 @@ fun PreviewChatListNavLinkContactRequest() {
|
||||
dropdownMenuItems = null,
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
stopped = false,
|
||||
selectedChat = remember { mutableStateOf(false) }
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -10,11 +10,15 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.SettingsViewState
|
||||
import chat.simplex.common.model.*
|
||||
@ -29,6 +33,7 @@ import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
@ -60,10 +65,10 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(searchText, scaffoldState.drawerState, userPickerState, stopped)} },
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||
@ -73,7 +78,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||
floatingActionButton = {
|
||||
if (searchInList.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
@ -101,19 +106,20 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (chatModel.chats.isNotEmpty()) {
|
||||
ChatList(chatModel, search = searchInList)
|
||||
ChatList(chatModel, searchText = searchText)
|
||||
} else if (!chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||
if (!stopped && !newChatSheetState.collectAsState().value.isVisible() && chatModel.chatRunning.value == true) {
|
||||
OnboardingButtons(showNewChatSheet)
|
||||
}
|
||||
Text(stringResource(MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
|
||||
Text(stringResource(
|
||||
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (searchInList.isEmpty()) {
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
DesktopActiveCallOverlayLayout(newChatSheetState)
|
||||
// TODO disable this button and sheet for the duration of the switch
|
||||
tryOrShowError("NewChatSheet", error = {}) {
|
||||
@ -168,20 +174,8 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
|
||||
if (showSearch) {
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
}
|
||||
private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
if (chatModel.chats.size > 0) {
|
||||
barButtons.add {
|
||||
IconButton({ showSearch = true }) {
|
||||
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stopped) {
|
||||
barButtons.add {
|
||||
IconButton(onClick = {
|
||||
@ -201,9 +195,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
val scope = rememberCoroutineScope()
|
||||
DefaultTopAppBar(
|
||||
navigationButton = {
|
||||
if (showSearch) {
|
||||
NavigationButtonBack(hideSearchOnBack)
|
||||
} else if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||
} else {
|
||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||
@ -227,13 +219,18 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (chatModel.chats.size > 0) {
|
||||
ToggleFilterButton()
|
||||
val enabled = remember { derivedStateOf { searchInList.value.text.isEmpty() } }
|
||||
if (enabled.value) {
|
||||
ToggleFilterEnabledButton()
|
||||
} else {
|
||||
ToggleFilterDisabledButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onTitleClick = null,
|
||||
showSearch = showSearch,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
showSearch = false,
|
||||
onSearchValueChanged = {},
|
||||
buttons = barButtons
|
||||
)
|
||||
Divider(Modifier.padding(top = AppBarHeight))
|
||||
@ -246,7 +243,8 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U
|
||||
Box {
|
||||
ProfileImage(
|
||||
image = image,
|
||||
size = 37.dp
|
||||
size = 37.dp,
|
||||
color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
)
|
||||
if (!allRead) {
|
||||
unreadBadge()
|
||||
@ -281,7 +279,7 @@ private fun BoxScope.unreadBadge(text: String? = "") {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleFilterButton() {
|
||||
private fun ToggleFilterEnabledButton() {
|
||||
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
|
||||
IconButton(onClick = { pref.set(!pref.get()) }) {
|
||||
Icon(
|
||||
@ -290,7 +288,7 @@ private fun ToggleFilterButton() {
|
||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.padding(3.dp)
|
||||
.background(color = if (pref.state.value) MaterialTheme.colors.primary else MaterialTheme.colors.background, shape = RoundedCornerShape(50))
|
||||
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.border(width = 1.dp, color = MaterialTheme.colors.primary, shape = RoundedCornerShape(50))
|
||||
.padding(3.dp)
|
||||
.size(16.dp)
|
||||
@ -298,6 +296,22 @@ private fun ToggleFilterButton() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleFilterDisabledButton() {
|
||||
IconButton({}, enabled = false) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_filter_list),
|
||||
null,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.padding(3.dp)
|
||||
.border(width = 1.dp, color = MaterialTheme.colors.secondary, shape = RoundedCornerShape(50))
|
||||
.padding(3.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
||||
|
||||
@ -307,11 +321,115 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
chatModel.appOpenUrl.value = rhId to uri
|
||||
} else {
|
||||
withApi {
|
||||
planAndConnect(chatModel, rhId, uri, incognito = null, close = null)
|
||||
planAndConnect(rhId, uri, incognito = null, close = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), tint = MaterialTheme.colors.secondary)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||
alwaysVisible = true,
|
||||
searchText = searchText,
|
||||
enabled = !remember { searchShowingSimplexLink }.value,
|
||||
trailingContent = null,
|
||||
) {
|
||||
searchText.value = searchText.value.copy(it)
|
||||
}
|
||||
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
||||
if (hasText.value) {
|
||||
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
KeyChangeEffect(chatModel.currentRemoteHost.value) {
|
||||
hideSearchOnBack()
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val clipboardHasText = remember(focused) { chatModel.clipboardHasText }.value
|
||||
if (clipboardHasText) {
|
||||
IconButton(
|
||||
onClick = { searchText.value = searchText.value.copy(clipboard.getText()?.text ?: return@IconButton) },
|
||||
Modifier.size(30.dp).desktopPointerHoverIconHand()
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_article), null, tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
IconButton(
|
||||
onClick = {
|
||||
val fixedRhId = chatModel.currentRemoteHost.value
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close ->
|
||||
NewChatView(fixedRhId, selection = NewChatOption.CONNECT, showQRCodeScanner = true, close = close)
|
||||
}
|
||||
},
|
||||
Modifier.size(30.dp).desktopPointerHoverIconHand()
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_qr_code), null, tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardState = getKeyboardState()
|
||||
LaunchedEffect(keyboardState.value) {
|
||||
if (keyboardState.value == KeyboardState.Closed && focused) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { searchText.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val link = strHasSingleSimplexLink(it.trim())
|
||||
if (link != null) {
|
||||
// if SimpleX link is pasted, show connection dialogue
|
||||
hideKeyboard(view)
|
||||
if (link.format is Format.SimplexLink) {
|
||||
val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts)
|
||||
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
|
||||
}
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
|
||||
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, cleanup: (() -> Unit)?) {
|
||||
withBGApi {
|
||||
planAndConnect(
|
||||
chatModel.remoteHostId(),
|
||||
URI.create(link),
|
||||
incognito = null,
|
||||
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
close = null,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorSettingsView() {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
@ -322,7 +440,7 @@ private fun ErrorSettingsView() {
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
private fun ChatList(chatModel: ChatModel, search: String) {
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
@ -332,13 +450,35 @@ private fun ChatList(chatModel: ChatModel, search: String) {
|
||||
// In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side
|
||||
// which is related to [derivedStateOf]. Using safe alternative instead
|
||||
// val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } }
|
||||
val chats = filteredChats(showUnreadAndFavorites, search, allChats.toList())
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.toList())
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
Modifier.fillMaxWidth(),
|
||||
listState
|
||||
) {
|
||||
items(chats) { chat ->
|
||||
ChatListNavLinkView(chat, chatModel)
|
||||
stickyHeader {
|
||||
Column(
|
||||
Modifier
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
IntOffset(0, y)
|
||||
}
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
itemsIndexed(chats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
} }
|
||||
ChatListNavLinkView(chat, nextChatSelected)
|
||||
}
|
||||
}
|
||||
if (chats.isEmpty() && !chatModel.chats.isEmpty()) {
|
||||
@ -348,28 +488,39 @@ private fun ChatList(chatModel: ChatModel, search: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredChats(showUnreadAndFavorites: Boolean, searchText: String, chats: List<Chat>): List<Chat> {
|
||||
val s = searchText.trim().lowercase()
|
||||
return if (s.isEmpty() && !showUnreadAndFavorites)
|
||||
chats
|
||||
else {
|
||||
chats.filter { chat ->
|
||||
when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.Direct -> if (s.isEmpty()) {
|
||||
filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
private fun filteredChats(
|
||||
showUnreadAndFavorites: Boolean,
|
||||
searchShowingSimplexLink: State<Boolean>,
|
||||
searchChatFilteredBySimplexLink: State<String?>,
|
||||
searchText: String,
|
||||
chats: List<Chat>
|
||||
): List<Chat> {
|
||||
val linkChatId = searchChatFilteredBySimplexLink.value
|
||||
return if (linkChatId != null) {
|
||||
chats.filter { it.id == linkChatId }
|
||||
} else {
|
||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||
if (s.isEmpty() && !showUnreadAndFavorites)
|
||||
chats
|
||||
else {
|
||||
chats.filter { chat ->
|
||||
when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.Direct -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
}
|
||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
||||
} else {
|
||||
viewNameContains(cInfo, s)
|
||||
}
|
||||
is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s)
|
||||
is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value)
|
||||
is ChatInfo.InvalidJSON -> chat.id == chatModel.chatId.value
|
||||
}
|
||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||
(filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited)
|
||||
} else {
|
||||
viewNameContains(cInfo, s)
|
||||
}
|
||||
is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s)
|
||||
is ChatInfo.ContactConnection -> s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s)
|
||||
is ChatInfo.InvalidJSON -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,28 @@ class AlertManager {
|
||||
|
||||
fun showAlertDialogButtonsColumn(
|
||||
title: String,
|
||||
text: AnnotatedString? = null,
|
||||
text: String? = null,
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
hostDevice: Pair<Long?, String>? = null,
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
|
||||
title = alertTitle(title),
|
||||
buttons = {
|
||||
AlertContent(text, hostDevice, extraPadding = true) {
|
||||
buttons()
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertDialogButtonsColumn(
|
||||
title: String,
|
||||
text: AnnotatedString,
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
hostDevice: Pair<Long?, String>? = null,
|
||||
buttons: @Composable () -> Unit,
|
||||
|
@ -0,0 +1,27 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
|
||||
@Composable
|
||||
fun DefaultProgressView(description: String?) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
if (description != null) {
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ fun DefaultTopAppBar(
|
||||
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = false, onValueChange = onSearchValueChanged)
|
||||
}
|
||||
},
|
||||
backgroundColor = if (isInDarkTheme()) ToolbarDark else ToolbarLight,
|
||||
backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f),
|
||||
navigationIcon = navigationButton,
|
||||
buttons = if (!showSearch) buttons else emptyList(),
|
||||
centered = !showSearch,
|
||||
|
@ -38,6 +38,12 @@ enum class ModalPlacement {
|
||||
START, CENTER, END, FULLSCREEN
|
||||
}
|
||||
|
||||
class ModalData {
|
||||
private val state = mutableMapOf<String, MutableState<Any>>()
|
||||
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
|
||||
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
|
||||
}
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private val modalViews = arrayListOf<Pair<Boolean, (@Composable (close: () -> Unit) -> Unit)>>()
|
||||
private val modalCount = mutableStateOf(0)
|
||||
@ -45,15 +51,17 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private var oldViewChanging = AtomicBoolean(false)
|
||||
private var passcodeView: MutableState<(@Composable (close: () -> Unit) -> Unit)?> = mutableStateOf(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
val data = ModalData()
|
||||
showCustomModal { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = content)
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content() })
|
||||
}
|
||||
}
|
||||
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable (close: () -> Unit) -> Unit) {
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
val data = ModalData()
|
||||
showCustomModal { close ->
|
||||
ModalView(close, showClose = showClose, content = { content(close) })
|
||||
ModalView(close, showClose = showClose, content = { data.content(close) })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,8 @@ fun SearchTextField(
|
||||
alwaysVisible: Boolean,
|
||||
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) },
|
||||
placeholder: String = stringResource(MR.strings.search_verb),
|
||||
enabled: Boolean = true,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
onValueChange: (String) -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@ -54,12 +56,12 @@ fun SearchTextField(
|
||||
}
|
||||
}
|
||||
|
||||
val enabled = true
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Unspecified,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
focusedIndicatorColor = Color.Unspecified,
|
||||
unfocusedIndicatorColor = Color.Unspecified,
|
||||
disabledIndicatorColor = Color.Unspecified,
|
||||
)
|
||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@ -77,6 +79,7 @@ fun SearchTextField(
|
||||
searchText.value = it
|
||||
onValueChange(it.text)
|
||||
},
|
||||
enabled = rememberUpdatedState(enabled).value,
|
||||
cursorBrush = SolidColor(colors.cursorColor(false).value),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
@ -105,7 +108,7 @@ fun SearchTextField(
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
|
||||
}
|
||||
}} else null,
|
||||
}} else trailingContent,
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
|
@ -60,6 +60,9 @@ fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedStr
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun SetupClipboardListener()
|
||||
|
||||
// maximum image file size to be auto-accepted
|
||||
const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB
|
||||
const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
|
||||
|
@ -4,14 +4,16 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.views.helpers.AppBarTitle
|
||||
import chat.simplex.common.views.helpers.KeyChangeEffect
|
||||
import chat.simplex.common.views.onboarding.ReadableText
|
||||
import chat.simplex.common.views.onboarding.ReadableTextWithLink
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun AddContactLearnMore() {
|
||||
fun AddContactLearnMore(close: () -> Unit) {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
@ -20,4 +22,7 @@ fun AddContactLearnMore() {
|
||||
ReadableText(MR.strings.if_you_cant_meet_in_person)
|
||||
ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/readme.html#connect-to-friends")
|
||||
}
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
@ -1,186 +0,0 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.shareText
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun AddContactView(
|
||||
chatModel: ChatModel,
|
||||
rh: RemoteHostInfo?,
|
||||
connReqInvitation: String,
|
||||
contactConnection: MutableState<PendingContactConnection?>
|
||||
) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
AddContactLayout(
|
||||
rh = rh,
|
||||
chatModel = chatModel,
|
||||
incognitoPref = chatModel.controller.appPrefs.incognito,
|
||||
connReq = connReqInvitation,
|
||||
contactConnection = contactConnection,
|
||||
learnMore = {
|
||||
ModalManager.center.showModal {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
AddContactLearnMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddContactLayout(
|
||||
chatModel: ChatModel,
|
||||
rh: RemoteHostInfo?,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
connReq: String,
|
||||
contactConnection: MutableState<PendingContactConnection?>,
|
||||
learnMore: () -> Unit
|
||||
) {
|
||||
val incognito = remember { mutableStateOf(incognitoPref.get()) }
|
||||
|
||||
LaunchedEffect(incognito.value) {
|
||||
withApi {
|
||||
val contactConnVal = contactConnection.value
|
||||
if (contactConnVal != null) {
|
||||
chatModel.controller.apiSetConnectionIncognito(rh?.remoteHostId, contactConnVal.pccConnId, incognito.value)?.let {
|
||||
contactConnection.value = it
|
||||
chatModel.updateContactConnection(rh?.remoteHostId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.add_contact), hostDevice(rh?.remoteHostId))
|
||||
|
||||
SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) {
|
||||
if (connReq.isNotEmpty()) {
|
||||
SimpleXLinkQRCode(
|
||||
connReq, Modifier
|
||||
.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
|
||||
.aspectRatio(1f)
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
|
||||
IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } }
|
||||
ShareLinkButton(connReq)
|
||||
OneTimeLinkLearnMoreButton(learnMore)
|
||||
}
|
||||
SectionTextFooter(sharedProfileInfo(chatModel, incognito.value))
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareLinkButton(connReqInvitation: String) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_share),
|
||||
stringResource(MR.strings.share_invitation_link),
|
||||
click = { clipboard.shareText(simplexChatLink(connReqInvitation)) },
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OneTimeLinkLearnMoreButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_info),
|
||||
stringResource(MR.strings.learn_more),
|
||||
onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoToggle(
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
incognito: MutableState<Boolean>,
|
||||
onClickInfo: () -> Unit
|
||||
) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = if (incognito.value) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy),
|
||||
text = null,
|
||||
click = onClickInfo,
|
||||
iconColor = if (incognito.value) Indigo else MaterialTheme.colors.secondary,
|
||||
extraPadding = false
|
||||
) {
|
||||
SharedPreferenceToggleWithIcon(
|
||||
stringResource(MR.strings.incognito),
|
||||
painterResource(MR.images.ic_info),
|
||||
stopped = false,
|
||||
onClickInfo = onClickInfo,
|
||||
preference = incognitoPref,
|
||||
preferenceState = incognito
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun sharedProfileInfo(
|
||||
chatModel: ChatModel,
|
||||
incognito: Boolean
|
||||
): String {
|
||||
val name = chatModel.currentUser.value?.displayName ?: ""
|
||||
return if (incognito) {
|
||||
generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared)
|
||||
} else {
|
||||
String.format(generalGetString(MR.strings.connect__your_profile_will_be_shared), name)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)*/
|
||||
@Composable
|
||||
fun PreviewAddContactView() {
|
||||
SimpleXTheme {
|
||||
AddContactLayout(
|
||||
rh = null,
|
||||
chatModel = ChatModel,
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||
contactConnection = mutableStateOf(PendingContactConnection.getSampleData()),
|
||||
learnMore = {},
|
||||
)
|
||||
}
|
||||
}
|
@ -1,57 +1,51 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.IncognitoView
|
||||
import chat.simplex.res.MR
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
expect fun ScanToConnectView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit)
|
||||
|
||||
enum class ConnectionLinkType {
|
||||
INVITATION, CONTACT, GROUP
|
||||
}
|
||||
|
||||
suspend fun planAndConnect(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
incognito: Boolean?,
|
||||
close: (() -> Unit)?
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)? = null,
|
||||
filterKnownContact: ((Contact) -> Unit)? = null,
|
||||
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
|
||||
) {
|
||||
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString())
|
||||
if (connectionPlan != null) {
|
||||
val link = strHasSingleSimplexLink(uri.toString().trim())
|
||||
val linkText = if (link?.format is Format.SimplexLink)
|
||||
"<br><br><u>${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}</u>"
|
||||
else
|
||||
""
|
||||
when (connectionPlan) {
|
||||
is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) {
|
||||
InvitationLinkPlan.Ok -> {
|
||||
Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito")
|
||||
if (incognito != null) {
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_via_invitation_link),
|
||||
text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
|
||||
connectDestructive = false
|
||||
text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText,
|
||||
connectDestructive = false,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -60,9 +54,11 @@ suspend fun planAndConnect(
|
||||
if (incognito != null) {
|
||||
AlertManager.privacySensitive.showAlertDialog(
|
||||
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
|
||||
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link),
|
||||
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText,
|
||||
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
|
||||
onDismiss = cleanup,
|
||||
onDismissRequest = cleanup,
|
||||
destructive = true,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
@ -70,8 +66,9 @@ suspend fun planAndConnect(
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
|
||||
text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link)),
|
||||
connectDestructive = true
|
||||
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText,
|
||||
connectDestructive = true,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -79,42 +76,54 @@ suspend fun planAndConnect(
|
||||
Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito")
|
||||
val contact = connectionPlan.invitationLinkPlan.contact_
|
||||
if (contact != null) {
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName),
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
if (filterKnownContact != null) {
|
||||
filterKnownContact(contact)
|
||||
} else {
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
}
|
||||
} else {
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.connect_plan_already_connecting),
|
||||
generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link),
|
||||
generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + linkText,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
}
|
||||
}
|
||||
is InvitationLinkPlan.Known -> {
|
||||
Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito")
|
||||
val contact = connectionPlan.invitationLinkPlan.contact
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName),
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
if (filterKnownContact != null) {
|
||||
filterKnownContact(contact)
|
||||
} else {
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
is ConnectionPlan.ContactAddress -> when (connectionPlan.contactAddressPlan) {
|
||||
ContactAddressPlan.Ok -> {
|
||||
Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito")
|
||||
if (incognito != null) {
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_via_contact_link),
|
||||
text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
|
||||
connectDestructive = false
|
||||
text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText,
|
||||
connectDestructive = false,
|
||||
cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -123,18 +132,21 @@ suspend fun planAndConnect(
|
||||
if (incognito != null) {
|
||||
AlertManager.privacySensitive.showAlertDialog(
|
||||
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
|
||||
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address),
|
||||
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText,
|
||||
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
|
||||
destructive = true,
|
||||
onDismiss = cleanup,
|
||||
onDismissRequest = cleanup,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
|
||||
text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address)),
|
||||
connectDestructive = true
|
||||
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText,
|
||||
connectDestructive = true,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -143,9 +155,11 @@ suspend fun planAndConnect(
|
||||
if (incognito != null) {
|
||||
AlertManager.privacySensitive.showAlertDialog(
|
||||
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
|
||||
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address),
|
||||
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText,
|
||||
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
|
||||
onDismiss = cleanup,
|
||||
onDismissRequest = cleanup,
|
||||
destructive = true,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
@ -153,30 +167,41 @@ suspend fun planAndConnect(
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
|
||||
text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address)),
|
||||
connectDestructive = true
|
||||
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText,
|
||||
connectDestructive = true,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
is ContactAddressPlan.ConnectingProhibit -> {
|
||||
Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito")
|
||||
val contact = connectionPlan.contactAddressPlan.contact
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName),
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
if (filterKnownContact != null) {
|
||||
filterKnownContact(contact)
|
||||
} else {
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
}
|
||||
}
|
||||
is ContactAddressPlan.Known -> {
|
||||
Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito")
|
||||
val contact = connectionPlan.contactAddressPlan.contact
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName),
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
if (filterKnownContact != null) {
|
||||
filterKnownContact(contact)
|
||||
} else {
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
}
|
||||
}
|
||||
is ContactAddressPlan.ContactViaAddress -> {
|
||||
Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress, incognito=$incognito")
|
||||
@ -187,6 +212,7 @@ suspend fun planAndConnect(
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false)
|
||||
}
|
||||
cleanup?.invoke()
|
||||
}
|
||||
}
|
||||
is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) {
|
||||
@ -195,33 +221,42 @@ suspend fun planAndConnect(
|
||||
if (incognito != null) {
|
||||
AlertManager.privacySensitive.showAlertDialog(
|
||||
title = generalGetString(MR.strings.connect_via_group_link),
|
||||
text = generalGetString(MR.strings.you_will_join_group),
|
||||
text = generalGetString(MR.strings.you_will_join_group) + linkText,
|
||||
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
|
||||
onDismiss = cleanup,
|
||||
onDismissRequest = cleanup,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_via_group_link),
|
||||
text = AnnotatedString(generalGetString(MR.strings.you_will_join_group)),
|
||||
connectDestructive = false
|
||||
text = generalGetString(MR.strings.you_will_join_group) + linkText,
|
||||
connectDestructive = false,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
is GroupLinkPlan.OwnLink -> {
|
||||
Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito")
|
||||
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
|
||||
ownGroupLinkConfirmConnect(chatModel, rhId, uri, incognito, connectionPlan, groupInfo, close)
|
||||
if (filterKnownGroup != null) {
|
||||
filterKnownGroup(groupInfo)
|
||||
} else {
|
||||
ownGroupLinkConfirmConnect(chatModel, rhId, uri, linkText, incognito, connectionPlan, groupInfo, close, cleanup)
|
||||
}
|
||||
}
|
||||
GroupLinkPlan.ConnectingConfirmReconnect -> {
|
||||
Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito")
|
||||
if (incognito != null) {
|
||||
AlertManager.privacySensitive.showAlertDialog(
|
||||
title = generalGetString(MR.strings.connect_plan_repeat_join_request),
|
||||
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link),
|
||||
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText,
|
||||
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
|
||||
onDismiss = cleanup,
|
||||
onDismissRequest = cleanup,
|
||||
destructive = true,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
@ -229,8 +264,9 @@ suspend fun planAndConnect(
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_plan_repeat_join_request),
|
||||
text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)),
|
||||
connectDestructive = true
|
||||
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText,
|
||||
connectDestructive = true,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -240,37 +276,44 @@ suspend fun planAndConnect(
|
||||
if (groupInfo != null) {
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.connect_plan_group_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName)
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) + linkText
|
||||
)
|
||||
} else {
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.connect_plan_already_joining_the_group),
|
||||
generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link),
|
||||
generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
}
|
||||
cleanup?.invoke()
|
||||
}
|
||||
is GroupLinkPlan.Known -> {
|
||||
Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito")
|
||||
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
|
||||
openKnownGroup(chatModel, rhId, close, groupInfo)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.connect_plan_group_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName),
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
if (filterKnownGroup != null) {
|
||||
filterKnownGroup(groupInfo)
|
||||
} else {
|
||||
openKnownGroup(chatModel, rhId, close, groupInfo)
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.connect_plan_group_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + linkText,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "planAndConnect, plan error")
|
||||
if (incognito != null) {
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close, cleanup)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, rhId, uri, connectionPlan = null, close,
|
||||
title = generalGetString(MR.strings.connect_plan_connect_via_link),
|
||||
connectDestructive = false
|
||||
connectDestructive = false,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -282,7 +325,8 @@ suspend fun connectViaUri(
|
||||
uri: URI,
|
||||
incognito: Boolean,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
close: (() -> Unit)?
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)?,
|
||||
) {
|
||||
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
||||
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
||||
@ -300,6 +344,7 @@ suspend fun connectViaUri(
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
}
|
||||
cleanup?.invoke()
|
||||
}
|
||||
|
||||
fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType {
|
||||
@ -317,8 +362,9 @@ fun askCurrentOrIncognitoProfileAlert(
|
||||
connectionPlan: ConnectionPlan?,
|
||||
close: (() -> Unit)?,
|
||||
title: String,
|
||||
text: AnnotatedString? = null,
|
||||
text: String? = null,
|
||||
connectDestructive: Boolean,
|
||||
cleanup: (() -> Unit)?,
|
||||
) {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
title = title,
|
||||
@ -329,7 +375,7 @@ fun askCurrentOrIncognitoProfileAlert(
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
|
||||
@ -337,18 +383,20 @@ fun askCurrentOrIncognitoProfileAlert(
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = cleanup,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
}
|
||||
@ -367,20 +415,23 @@ fun ownGroupLinkConfirmConnect(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
linkText: String,
|
||||
incognito: Boolean?,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
groupInfo: GroupInfo,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)?,
|
||||
) {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.connect_plan_join_your_group),
|
||||
text = AnnotatedString(String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName)),
|
||||
text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName) + linkText,
|
||||
buttons = {
|
||||
Column {
|
||||
// Open group
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
openKnownGroup(chatModel, rhId, close, groupInfo)
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
@ -389,7 +440,7 @@ fun ownGroupLinkConfirmConnect(
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup)
|
||||
}
|
||||
}) {
|
||||
Text(
|
||||
@ -402,7 +453,7 @@ fun ownGroupLinkConfirmConnect(
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
@ -411,7 +462,7 @@ fun ownGroupLinkConfirmConnect(
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
@ -420,11 +471,13 @@ fun ownGroupLinkConfirmConnect(
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
cleanup?.invoke()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = cleanup,
|
||||
hostDevice = hostDevice(rhId),
|
||||
)
|
||||
}
|
||||
@ -438,77 +491,3 @@ fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, grou
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectContactLayout(
|
||||
chatModel: ChatModel,
|
||||
rh: RemoteHostInfo?,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val incognito = remember { mutableStateOf(incognitoPref.get()) }
|
||||
|
||||
@Composable
|
||||
fun QRCodeScanner(close: () -> Unit) {
|
||||
QRCodeScanner { connReqUri ->
|
||||
try {
|
||||
val uri = URI(connReqUri)
|
||||
withApi {
|
||||
planAndConnect(chatModel, rh?.remoteHostId, uri, incognito = incognito.value, close)
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_QR_code),
|
||||
text = generalGetString(MR.strings.this_QR_code_is_not_a_link)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.scan_QR_code), hostDevice(rh?.remoteHostId), withPadding = false)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = 12.dp)
|
||||
) { QRCodeScanner(close) }
|
||||
|
||||
IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } }
|
||||
|
||||
SectionTextFooter(
|
||||
buildAnnotatedString {
|
||||
append(sharedProfileInfo(chatModel, incognito.value))
|
||||
append("\n\n")
|
||||
append(annotatedStringResource(MR.strings.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link))
|
||||
}
|
||||
)
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
fun URI.getQueryParameter(param: String): String? {
|
||||
if (!query.contains("$param=")) return null
|
||||
return query.substringAfter("$param=").substringBefore("&")
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)*/
|
||||
@Composable
|
||||
fun PreviewConnectContactLayout() {
|
||||
SimpleXTheme {
|
||||
ConnectContactLayout(
|
||||
chatModel = ChatModel,
|
||||
rh = null,
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
close = {},
|
||||
)
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.RemoteHostInfo
|
||||
|
||||
enum class ConnectViaLinkTab {
|
||||
SCAN, PASTE
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun ConnectViaLinkView(m: ChatModel, rh: RemoteHostInfo?, close: () -> Unit)
|
@ -23,7 +23,7 @@ import chat.simplex.common.views.chatlist.deleteContactConnectionAlert
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.PendingContactConnection
|
||||
import chat.simplex.common.platform.shareText
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@ -37,16 +37,19 @@ fun ContactConnectionInfoView(
|
||||
close: () -> Unit
|
||||
) {
|
||||
LaunchedEffect(connReqInvitation) {
|
||||
chatModel.connReqInv.value = connReqInvitation
|
||||
if (connReqInvitation != null) {
|
||||
chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false)
|
||||
}
|
||||
}
|
||||
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv].
|
||||
* Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon.
|
||||
/** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation].
|
||||
* 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.
|
||||
* It applies only to Android because on Desktop center space will not be overlapped by [AddContactLearnMore]
|
||||
**/
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (!ModalManager.center.hasModalsOpen()) {
|
||||
chatModel.connReqInv.value = null
|
||||
if (!ModalManager.center.hasModalsOpen() || appPlatform.isDesktop) {
|
||||
chatModel.showingInvitation.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,14 +64,14 @@ fun ContactConnectionInfoView(
|
||||
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
|
||||
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
|
||||
learnMore = {
|
||||
ModalManager.center.showModal {
|
||||
ModalManager.end.showModalCloseable { close ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
AddContactLearnMore()
|
||||
AddContactLearnMore(close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,11 +138,7 @@ private fun ContactConnectionInfoLayout(
|
||||
|
||||
SectionView {
|
||||
if (!connReq.isNullOrEmpty() && contactConnection.initiated) {
|
||||
SimpleXLinkQRCode(
|
||||
connReq, Modifier
|
||||
.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
|
||||
.aspectRatio(1f)
|
||||
)
|
||||
SimpleXLinkQRCode(connReq)
|
||||
incognitoEnabled()
|
||||
ShareLinkButton(connReq)
|
||||
OneTimeLinkLearnMoreButton(learnMore)
|
||||
@ -158,6 +157,30 @@ private fun ContactConnectionInfoLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareLinkButton(connReqInvitation: String) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_share),
|
||||
stringResource(MR.strings.share_invitation_link),
|
||||
click = {
|
||||
chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(connChatUsed = true)
|
||||
clipboard.shareText(simplexChatLink(connReqInvitation))
|
||||
},
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OneTimeLinkLearnMoreButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_info),
|
||||
stringResource(MR.strings.learn_more),
|
||||
onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
|
@ -1,118 +0,0 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.UserAddressView
|
||||
import chat.simplex.res.MR
|
||||
|
||||
enum class CreateLinkTab {
|
||||
ONE_TIME, LONG_TERM
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateLinkView(m: ChatModel, rh: RemoteHostInfo?, initialSelection: CreateLinkTab) {
|
||||
val selection = remember { mutableStateOf(initialSelection) }
|
||||
val connReqInvitation = rememberSaveable { m.connReqInv }
|
||||
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) }
|
||||
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(selection.value) {
|
||||
if (
|
||||
selection.value == CreateLinkTab.ONE_TIME
|
||||
&& connReqInvitation.value.isNullOrEmpty()
|
||||
&& contactConnection.value == null
|
||||
&& !creatingConnReq.value
|
||||
) {
|
||||
createInvitation(m, rh?.remoteHostId, creatingConnReq, connReqInvitation, contactConnection)
|
||||
}
|
||||
}
|
||||
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv].
|
||||
* Otherwise, it will be called here AFTER [AddContactView] 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.
|
||||
**/
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (!ModalManager.center.hasModalsOpen()) {
|
||||
m.connReqInv.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
val tabTitles = CreateLinkTab.values().map {
|
||||
when {
|
||||
it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() && contactConnection.value == null ->
|
||||
stringResource(MR.strings.create_one_time_link)
|
||||
it == CreateLinkTab.ONE_TIME ->
|
||||
stringResource(MR.strings.one_time_link)
|
||||
it == CreateLinkTab.LONG_TERM ->
|
||||
stringResource(MR.strings.your_simplex_contact_address)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
when (selection.value) {
|
||||
CreateLinkTab.ONE_TIME -> {
|
||||
AddContactView(m, rh,connReqInvitation.value ?: "", contactConnection)
|
||||
}
|
||||
CreateLinkTab.LONG_TERM -> {
|
||||
UserAddressView(m, viaCreateLinkView = true, close = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
TabRow(
|
||||
selectedTabIndex = selection.value.ordinal,
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
) {
|
||||
tabTitles.forEachIndexed { index, it ->
|
||||
Tab(
|
||||
selected = selection.value.ordinal == index,
|
||||
onClick = {
|
||||
selection.value = CreateLinkTab.values()[index]
|
||||
},
|
||||
text = { Text(it, fontSize = 13.sp) },
|
||||
icon = {
|
||||
Icon(
|
||||
if (CreateLinkTab.ONE_TIME.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_all_inclusive),
|
||||
it
|
||||
)
|
||||
},
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
unselectedContentColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInvitation(
|
||||
m: ChatModel,
|
||||
rhId: Long?,
|
||||
creatingConnReq: MutableState<Boolean>,
|
||||
connReqInvitation: MutableState<String?>,
|
||||
contactConnection: MutableState<PendingContactConnection?>
|
||||
) {
|
||||
creatingConnReq.value = true
|
||||
withApi {
|
||||
val r = m.controller.apiAddContact(rhId, incognito = m.controller.appPrefs.incognito.get())
|
||||
if (r != null) {
|
||||
m.updateContactConnection(rhId, r.second)
|
||||
connReqInvitation.value = r.first
|
||||
contactConnection.value = r.second
|
||||
} else {
|
||||
creatingConnReq.value = false
|
||||
}
|
||||
}
|
||||
}
|
@ -42,12 +42,7 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
||||
addContact = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModal { CreateLinkView(chatModel, chatModel.currentRemoteHost.value, CreateLinkTab.ONE_TIME) }
|
||||
},
|
||||
connectViaLink = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, chatModel.currentRemoteHost.value, close) }
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) }
|
||||
},
|
||||
createGroup = {
|
||||
closeNewChatSheet(false)
|
||||
@ -59,18 +54,16 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
||||
}
|
||||
|
||||
private val titles = listOf(
|
||||
MR.strings.share_one_time_link,
|
||||
if (appPlatform.isAndroid) MR.strings.connect_via_link_or_qr else MR.strings.connect_via_link,
|
||||
MR.strings.create_group
|
||||
MR.strings.add_contact_tab,
|
||||
MR.strings.create_group_button
|
||||
)
|
||||
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group)
|
||||
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_group)
|
||||
|
||||
@Composable
|
||||
private fun NewChatSheetLayout(
|
||||
newChatSheetState: StateFlow<AnimatedViewState>,
|
||||
stopped: Boolean,
|
||||
addContact: () -> Unit,
|
||||
connectViaLink: () -> Unit,
|
||||
createGroup: () -> Unit,
|
||||
closeNewChatSheet: (animated: Boolean) -> Unit,
|
||||
) {
|
||||
@ -109,7 +102,7 @@ private fun NewChatSheetLayout(
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
val actions = remember { listOf(addContact, connectViaLink, createGroup) }
|
||||
val actions = remember { listOf(addContact, createGroup) }
|
||||
val backgroundColor = if (isInDarkTheme())
|
||||
blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F)
|
||||
else
|
||||
@ -271,7 +264,6 @@ private fun PreviewNewChatSheet() {
|
||||
MutableStateFlow(AnimatedViewState.VISIBLE),
|
||||
stopped = false,
|
||||
addContact = {},
|
||||
connectViaLink = {},
|
||||
createGroup = {},
|
||||
closeNewChatSheet = {},
|
||||
)
|
||||
|
@ -0,0 +1,436 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.*
|
||||
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.launch
|
||||
import java.net.URI
|
||||
|
||||
enum class NewChatOption {
|
||||
INVITE, CONNECT
|
||||
}
|
||||
|
||||
@Composable
|
||||
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 connReqInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connReq ?: "" } }
|
||||
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
|
||||
val pastedLink = rememberSaveable { mutableStateOf("") }
|
||||
LaunchedEffect(selection.value) {
|
||||
if (
|
||||
selection.value == NewChatOption.INVITE
|
||||
&& connReqInvitation.isEmpty()
|
||||
&& contactConnection.value == null
|
||||
&& !creatingConnReq.value
|
||||
) {
|
||||
createInvitation(rh?.remoteHostId, creatingConnReq, connReqInvitation, contactConnection)
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
/** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation].
|
||||
* 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.
|
||||
* It applies only to Android because on Desktop center space will not be overlapped by [AddContactLearnMore]
|
||||
**/
|
||||
if (chatModel.showingInvitation.value != null && (!ModalManager.center.hasModalsOpen() || appPlatform.isDesktop)) {
|
||||
val conn = contactConnection.value
|
||||
if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.keep_unused_invitation_question),
|
||||
text = generalGetString(MR.strings.you_can_view_invitation_link_again),
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
dismissText = generalGetString(MR.strings.keep_invitation_link),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
val chatInfo = ChatInfo.ContactConnection(conn)
|
||||
controller.deleteChat(Chat(remoteHostId = rh?.remoteHostId, chatInfo = chatInfo, chatItems = listOf()))
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
chatModel.showingInvitation.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
val tabTitles = NewChatOption.values().map {
|
||||
when(it) {
|
||||
NewChatOption.INVITE ->
|
||||
stringResource(MR.strings.add_contact_tab)
|
||||
NewChatOption.CONNECT ->
|
||||
stringResource(MR.strings.connect_via_link)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = bottomPadding)
|
||||
Column(Modifier.align(Alignment.CenterEnd).padding(bottom = bottomPadding, end = DEFAULT_PADDING)) {
|
||||
AddContactLearnMoreButton()
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = selection.value.ordinal,
|
||||
initialPageOffsetFraction = 0f
|
||||
) { NewChatOption.values().size }
|
||||
KeyChangeEffect(pagerState.currentPage) {
|
||||
selection.value = NewChatOption.values()[pagerState.currentPage]
|
||||
}
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
) {
|
||||
tabTitles.forEachIndexed { index, it ->
|
||||
LeadingIconTab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
text = { Text(it, fontSize = 13.sp) },
|
||||
icon = {
|
||||
Icon(
|
||||
if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code),
|
||||
it
|
||||
)
|
||||
},
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
unselectedContentColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top) { index ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connReqInvitation.isEmpty()) Arrangement.Center else Arrangement.Top) {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
when (index) {
|
||||
NewChatOption.INVITE.ordinal -> {
|
||||
PrepareAndInviteView(rh?.remoteHostId, contactConnection, connReqInvitation, creatingConnReq)
|
||||
}
|
||||
NewChatOption.CONNECT.ordinal -> {
|
||||
ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close)
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState<PendingContactConnection?>, connReqInvitation: String, creatingConnReq: MutableState<Boolean>) {
|
||||
if (connReqInvitation.isNotEmpty()) {
|
||||
InviteView(
|
||||
rhId,
|
||||
connReqInvitation = connReqInvitation,
|
||||
contactConnection = contactConnection,
|
||||
)
|
||||
} else if (creatingConnReq.value) {
|
||||
CreatingLinkProgressView()
|
||||
} else {
|
||||
RetryButton { createInvitation(rhId, creatingConnReq, connReqInvitation, contactConnection) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreatingLinkProgressView() {
|
||||
DefaultProgressView(stringResource(MR.strings.creating_link))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RetryButton(onClick: () -> Unit) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
IconButton(onClick, Modifier.size(30.dp)) {
|
||||
Icon(painterResource(MR.images.ic_refresh), null)
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
Text(stringResource(MR.strings.retry_verb))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState<PendingContactConnection?>) {
|
||||
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase()) {
|
||||
LinkTextView(connReqInvitation, true)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase()) {
|
||||
SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() })
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
val incognito = remember { mutableStateOf(controller.appPrefs.incognito.get()) }
|
||||
IncognitoToggle(controller.appPrefs.incognito, incognito) {
|
||||
if (appPlatform.isDesktop) ModalManager.end.closeModals()
|
||||
ModalManager.end.showModal { IncognitoView() }
|
||||
}
|
||||
KeyChangeEffect(incognito.value) {
|
||||
withBGApi {
|
||||
val contactConn = contactConnection.value ?: return@withBGApi
|
||||
val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi
|
||||
contactConnection.value = conn
|
||||
chatModel.updateContactConnection(rhId, conn)
|
||||
}
|
||||
chatModel.markShowingInvitationUsed()
|
||||
}
|
||||
SectionTextFooter(sharedProfileInfo(chatModel, incognito.value))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddContactLearnMoreButton() {
|
||||
IconButton(
|
||||
{
|
||||
if (appPlatform.isDesktop) ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable { close ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
AddContactLearnMore(close)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_info),
|
||||
stringResource(MR.strings.learn_more),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, pastedLink: MutableState<String>, close: () -> Unit) {
|
||||
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase()) {
|
||||
PasteLinkView(rhId, pastedLink, showQRCodeScanner, close)
|
||||
}
|
||||
|
||||
if (appPlatform.isAndroid) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase()) {
|
||||
QRCodeScanner(showQRCodeScanner) { text ->
|
||||
withBGApi {
|
||||
val res = verify(rhId, text, close)
|
||||
if (!res) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_qr_code),
|
||||
text = generalGetString(MR.strings.code_you_scanned_is_not_simplex_link_qr_code)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRCodeScanner: MutableState<Boolean>, close: () -> Unit) {
|
||||
if (pastedLink.value.isEmpty()) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SectionItemView({
|
||||
val str = clipboard.getText()?.text ?: return@SectionItemView
|
||||
val link = strHasSingleSimplexLink(str.trim())
|
||||
if (link != null) {
|
||||
pastedLink.value = link.text
|
||||
showQRCodeScanner.value = false
|
||||
withBGApi { connect(rhId, link.text, close) { pastedLink.value = "" } }
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_contact_link),
|
||||
text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link)
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.tap_to_paste_link))
|
||||
}
|
||||
} else {
|
||||
LinkTextView(pastedLink.value, false)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkTextView(link: String, share: Boolean) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
Row(Modifier.fillMaxWidth().heightIn(min = 46.dp).padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.weight(1f).clickable {
|
||||
chatModel.markShowingInvitationUsed()
|
||||
clipboard.shareText(link)
|
||||
}) {
|
||||
BasicTextField(
|
||||
value = link,
|
||||
onValueChange = { },
|
||||
enabled = false,
|
||||
textStyle = TextStyle(fontSize = 16.sp, color = MaterialTheme.colors.onBackground),
|
||||
singleLine = true,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = link,
|
||||
innerTextField = innerTextField,
|
||||
contentPadding = PaddingValues(),
|
||||
label = null,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
leadingIcon = null,
|
||||
trailingIcon = null,
|
||||
singleLine = true,
|
||||
enabled = false,
|
||||
isError = false,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
)
|
||||
})
|
||||
}
|
||||
// Element Text() can add ellipsis (...) in random place of the string, sometimes even after half of width of a screen.
|
||||
// So using BasicTextField + manual ...
|
||||
Text("…", fontSize = 16.sp)
|
||||
if (share) {
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
IconButton({
|
||||
chatModel.markShowingInvitationUsed()
|
||||
clipboard.shareText(link)
|
||||
}, Modifier.size(20.dp)) {
|
||||
Icon(painterResource(MR.images.ic_share_filled), null, tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boolean {
|
||||
if (text != null && strIsSimplexLink(text)) {
|
||||
connect(rhId, text, close)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null) {
|
||||
planAndConnect(
|
||||
rhId,
|
||||
URI.create(link),
|
||||
close = close,
|
||||
cleanup = cleanup,
|
||||
incognito = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun createInvitation(
|
||||
rhId: Long?,
|
||||
creatingConnReq: MutableState<Boolean>,
|
||||
connReqInvitation: String,
|
||||
contactConnection: MutableState<PendingContactConnection?>
|
||||
) {
|
||||
if (connReqInvitation.isNotEmpty() || contactConnection.value != null || creatingConnReq.value) return
|
||||
creatingConnReq.value = true
|
||||
withApi {
|
||||
val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get())
|
||||
if (r != null) {
|
||||
chatModel.updateContactConnection(rhId, r.second)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false)
|
||||
contactConnection.value = r.second
|
||||
} else {
|
||||
creatingConnReq.value = false
|
||||
if (alert != null) {
|
||||
alert()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun strIsSimplexLink(str: String): Boolean {
|
||||
val parsedMd = parseToMarkdown(str)
|
||||
return parsedMd != null && parsedMd.size == 1 && parsedMd[0].format is Format.SimplexLink
|
||||
}
|
||||
|
||||
fun strHasSingleSimplexLink(str: String): FormattedText? {
|
||||
val parsedMd = parseToMarkdown(str) ?: return null
|
||||
val parsedLinks = parsedMd.filter { it.format?.isSimplexLink ?: false }
|
||||
if (parsedLinks.size != 1) return null
|
||||
|
||||
return parsedLinks[0]
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoToggle(
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
incognito: MutableState<Boolean>,
|
||||
onClickInfo: () -> Unit
|
||||
) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = if (incognito.value) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy),
|
||||
text = null,
|
||||
click = onClickInfo,
|
||||
iconColor = if (incognito.value) Indigo else MaterialTheme.colors.secondary,
|
||||
extraPadding = false
|
||||
) {
|
||||
SharedPreferenceToggleWithIcon(
|
||||
stringResource(MR.strings.incognito),
|
||||
painterResource(MR.images.ic_info),
|
||||
stopped = false,
|
||||
onClickInfo = onClickInfo,
|
||||
preference = incognitoPref,
|
||||
preferenceState = incognito
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun sharedProfileInfo(
|
||||
chatModel: ChatModel,
|
||||
incognito: Boolean
|
||||
): String {
|
||||
val name = chatModel.currentUser.value?.displayName ?: ""
|
||||
return if (incognito) {
|
||||
generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared)
|
||||
} else {
|
||||
String.format(generalGetString(MR.strings.connect__your_profile_will_be_shared), name)
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionTextFooter
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.IncognitoView
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun PasteToConnectView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
val connectionLink = remember { mutableStateOf("") }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
PasteToConnectLayout(
|
||||
chatModel = chatModel,
|
||||
rh = rh,
|
||||
incognitoPref = chatModel.controller.appPrefs.incognito,
|
||||
connectionLink = connectionLink,
|
||||
pasteFromClipboard = {
|
||||
connectionLink.value = clipboard.getText()?.text ?: return@PasteToConnectLayout
|
||||
},
|
||||
close = close
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PasteToConnectLayout(
|
||||
chatModel: ChatModel,
|
||||
rh: RemoteHostInfo?,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
connectionLink: MutableState<String>,
|
||||
pasteFromClipboard: () -> Unit,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val incognito = remember { mutableStateOf(incognitoPref.get()) }
|
||||
val rhId = rh?.remoteHostId
|
||||
fun connectViaLink(connReqUri: String) {
|
||||
try {
|
||||
val uri = URI(connReqUri)
|
||||
withApi {
|
||||
planAndConnect(chatModel, rhId, uri, incognito = incognito.value, close)
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_connection_link),
|
||||
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.connect_via_link), hostDevice(rhId), withPadding = false)
|
||||
|
||||
Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) {
|
||||
TextEditor(
|
||||
connectionLink,
|
||||
Modifier.height(180.dp),
|
||||
contentPadding = PaddingValues(),
|
||||
placeholder = stringResource(MR.strings.paste_the_link_you_received_to_connect_with_your_contact)
|
||||
)
|
||||
}
|
||||
|
||||
if (connectionLink.value == "") {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_content_paste),
|
||||
stringResource(MR.strings.paste_button),
|
||||
click = pasteFromClipboard,
|
||||
)
|
||||
} else {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_close),
|
||||
stringResource(MR.strings.clear_verb),
|
||||
click = { connectionLink.value = "" },
|
||||
)
|
||||
}
|
||||
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_link),
|
||||
stringResource(MR.strings.connect_button),
|
||||
click = { connectViaLink(connectionLink.value) },
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
disabled = connectionLink.value.isEmpty() || connectionLink.value.trim().contains(" ")
|
||||
)
|
||||
|
||||
IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } }
|
||||
|
||||
SectionTextFooter(
|
||||
buildAnnotatedString {
|
||||
append(sharedProfileInfo(chatModel, incognito.value))
|
||||
append("\n\n")
|
||||
append(annotatedStringResource(MR.strings.you_can_also_connect_by_clicking_the_link))
|
||||
}
|
||||
)
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
name = "Dark Mode"
|
||||
)*/
|
||||
@Composable
|
||||
fun PreviewPasteToConnectTextbox() {
|
||||
SimpleXTheme {
|
||||
PasteToConnectLayout(
|
||||
chatModel = ChatModel,
|
||||
rh = null,
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
connectionLink = remember { mutableStateOf("") },
|
||||
pasteFromClipboard = {},
|
||||
close = {}
|
||||
)
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ import boofcv.alg.drawing.FiducialImageEngine
|
||||
import boofcv.alg.fiducial.qrcode.*
|
||||
import chat.simplex.common.model.CryptoFile
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
@ -23,14 +23,18 @@ import kotlinx.coroutines.launch
|
||||
fun SimpleXLinkQRCode(
|
||||
connReq: String,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF),
|
||||
tintColor: Color = Color(0xff062d56),
|
||||
withLogo: Boolean = true
|
||||
withLogo: Boolean = true,
|
||||
onShare: (() -> Unit)? = null,
|
||||
) {
|
||||
QRCode(
|
||||
simplexChatLink(connReq),
|
||||
modifier,
|
||||
padding,
|
||||
tintColor,
|
||||
withLogo
|
||||
withLogo,
|
||||
onShare,
|
||||
)
|
||||
}
|
||||
|
||||
@ -46,22 +50,24 @@ fun simplexChatLink(uri: String): String {
|
||||
fun QRCode(
|
||||
connReq: String,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF),
|
||||
tintColor: Color = Color(0xff062d56),
|
||||
withLogo: Boolean = true
|
||||
withLogo: Boolean = true,
|
||||
onShare: (() -> Unit)? = null,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
BoxWithConstraints(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
val maxWidthInPx = with(LocalDensity.current) { maxWidth.roundToPx() }
|
||||
val qr = remember(maxWidthInPx, connReq, tintColor, withLogo) {
|
||||
qrCodeBitmap(connReq, maxWidthInPx).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
|
||||
.let { if (withLogo) it.addLogo() else it }
|
||||
}
|
||||
val qr = remember(connReq, tintColor, withLogo) {
|
||||
qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
|
||||
.let { if (withLogo) it.addLogo() else it }
|
||||
}
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Image(
|
||||
bitmap = qr,
|
||||
contentDescription = stringResource(MR.strings.image_descr_qr_code),
|
||||
Modifier
|
||||
.widthIn(max = 360.dp)
|
||||
.padding(padding)
|
||||
.widthIn(max = 400.dp)
|
||||
.aspectRatio(1f)
|
||||
.then(modifier)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
@ -70,6 +76,7 @@ fun QRCode(
|
||||
val file = saveTempImageUncompressed(image, true)
|
||||
if (file != null) {
|
||||
shareFile("", CryptoFile.plain(file.absolutePath))
|
||||
onShare?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,7 +88,9 @@ fun qrCodeBitmap(content: String, size: Int = 1024): ImageBitmap {
|
||||
val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate()
|
||||
/** See [QrCodeGeneratorImage.initialize] and [FiducialImageEngine.configure] for size calculation */
|
||||
val numModules = QrCode.totalModules(qrCode.version)
|
||||
val borderModule = 1
|
||||
// Hide border on light themes to make it fit to the same place as camera in QRCodeScanner.
|
||||
// On dark themes better to show the border
|
||||
val borderModule = if (CurrentColors.value.colors.isLight) 0 else 1
|
||||
// val calculatedFinalWidth = (pixelsPerModule * numModules) + 2 * (borderModule * pixelsPerModule)
|
||||
// size = (x * numModules) + 2 * (borderModule * x)
|
||||
// size / x = numModules + 2 * borderModule
|
||||
|
@ -1,6 +1,14 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
|
||||
@Composable
|
||||
expect fun QRCodeScanner(onBarcode: (String) -> Unit)
|
||||
expect fun QRCodeScanner(
|
||||
showQRCodeScanner: MutableState<Boolean> = remember { mutableStateOf(true) },
|
||||
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF),
|
||||
onBarcode: (String) -> Unit
|
||||
)
|
||||
|
@ -84,7 +84,7 @@ private fun CreateSimpleXAddressLayout(
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
if (userAddress != null) {
|
||||
SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
|
||||
SimpleXLinkQRCode(userAddress.connReqContact)
|
||||
ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) }
|
||||
Spacer(Modifier.weight(1f))
|
||||
ShareViaEmailButton { sendEmail(userAddress) }
|
||||
|
@ -339,16 +339,9 @@ private fun DevicesView(deviceName: String, remoteCtrls: SnapshotStateList<Remot
|
||||
@Composable
|
||||
private fun ScanDesktopAddressView(sessionAddress: MutableState<String>) {
|
||||
SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(DEFAULT_PADDING)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
sessionAddress.value = text
|
||||
processDesktopQRCode(sessionAddress, text)
|
||||
}
|
||||
QRCodeScanner { text ->
|
||||
sessionAddress.value = text
|
||||
processDesktopQRCode(sessionAddress, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -187,11 +187,7 @@ private fun ConnectMobileViewLayout(
|
||||
SectionView {
|
||||
if (invitation != null && sessionCode == null && port != null) {
|
||||
Box {
|
||||
QRCode(
|
||||
invitation, Modifier
|
||||
.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF)
|
||||
.aspectRatio(1f)
|
||||
)
|
||||
QRCode(invitation)
|
||||
if (staleQrCode) {
|
||||
Box(Modifier.matchParentSize().background(MaterialTheme.colors.background.copy(alpha = 0.9f)), contentAlignment = Alignment.Center) {
|
||||
SimpleButtonDecorated(stringResource(MR.strings.refresh_qr_code), painterResource(MR.images.ic_refresh), click = refreshQrCode)
|
||||
|
@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.AppBarTitle
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.onboarding.ReadableTextWithLink
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
@ -31,6 +32,7 @@ fun IncognitoLayout() {
|
||||
Text(generalGetString(MR.strings.incognito_info_protects))
|
||||
Text(generalGetString(MR.strings.incognito_info_allows))
|
||||
Text(generalGetString(MR.strings.incognito_info_share))
|
||||
ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode")
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ private fun CustomServer(
|
||||
if (valid.value) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) {
|
||||
QRCode(serverAddress.value, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING))
|
||||
QRCode(serverAddress.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,25 +20,17 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr), withPadding = false)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = 12.dp)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
val res = parseServerAddress(text)
|
||||
if (res != null) {
|
||||
onNext(ServerCfg(remoteHostId = rhId, text, false, null, true))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.smp_servers_invalid_address),
|
||||
text = generalGetString(MR.strings.smp_servers_check_address)
|
||||
)
|
||||
}
|
||||
AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr))
|
||||
QRCodeScanner { text ->
|
||||
val res = parseServerAddress(text)
|
||||
if (res != null) {
|
||||
onNext(ServerCfg(remoteHostId = rhId, text, false, null, true))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.smp_servers_invalid_address),
|
||||
text = generalGetString(MR.strings.smp_servers_check_address)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ private fun UserAddressLayout(
|
||||
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) }
|
||||
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
|
||||
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
|
||||
SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
|
||||
SimpleXLinkQRCode(userAddress.connReqContact)
|
||||
ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) }
|
||||
ShareViaEmailButton { sendEmail(userAddress) }
|
||||
ShareWithContactsButton(shareViaProfile, setProfileAddress)
|
||||
|
@ -147,8 +147,6 @@
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">تم تغيير العنوان من أجلك</string>
|
||||
<string name="cant_delete_user_profile">لا يمكن حذف ملف تعريف المستخدم!</string>
|
||||
<string name="icon_descr_video_asked_to_receive">طلب لاستلام الفيديو</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> إضافة جهة اتصال جديدة </b>: لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b> امسح رمز الاستجابة السريعة </b>: للاتصال بجهة الاتصال التي تعرض لك رمز الاستجابة السريعة.]]></string>
|
||||
<string name="callstatus_in_progress">مكالمتك تحت الإجراء</string>
|
||||
<string name="change_database_passphrase_question">تغيير عبارة مرور قاعدة البيانات؟</string>
|
||||
<string name="cannot_access_keychain">لا يمكن الوصول إلى Keystore لحفظ كلمة مرور قاعدة البيانات</string>
|
||||
@ -198,7 +196,6 @@
|
||||
<string name="smp_server_test_compare_file">قارن الملف</string>
|
||||
<string name="icon_descr_server_status_error">خطأ</string>
|
||||
<string name="create_group">إنشاء مجموعة سرية</string>
|
||||
<string name="create_one_time_link">إنشاء رابط دعوة لمرة واحدة</string>
|
||||
<string name="error_aborting_address_change">خطأ في إحباط تغيير العنوان</string>
|
||||
<string name="auth_enable_simplex_lock">تفعيل قفل SimpleX</string>
|
||||
<string name="auth_confirm_credential">تأكد من بيانات الاعتماد الخاصة بك</string>
|
||||
@ -1324,7 +1321,7 @@
|
||||
<string name="observer_cant_send_message_title">لا يمكنك إرسال رسائل!</string>
|
||||
<string name="you_need_to_allow_to_send_voice">تحتاج إلى السماح لجهة الاتصال الخاصة بك بإرسال رسائل صوتية لتتمكن من إرسالها.</string>
|
||||
<string name="contact_sent_large_file">أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s).</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[يمكنك <font color=#0088ff>الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[يمكنك <font color="#0088ff">الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.]]></string>
|
||||
<string name="smp_servers_your_server">خادمك</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">يُخزن ملف تعريفك على جهازك ومشاركته فقط مع جهات اتصالك. لا تستطيع خوادم SimpleX رؤية ملف تعريفك.</string>
|
||||
<string name="icon_descr_video_off">الفيديو مقفل</string>
|
||||
|
@ -293,9 +293,11 @@
|
||||
<string name="tap_to_start_new_chat">Tap to start a new chat</string>
|
||||
<string name="chat_with_developers">Chat with the developers</string>
|
||||
<string name="you_have_no_chats">You have no chats</string>
|
||||
<string name="loading_chats">Loading chats…</string>
|
||||
<string name="no_filtered_chats">No filtered chats</string>
|
||||
<string name="contact_tap_to_connect">Tap to Connect</string>
|
||||
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
|
||||
<string name="search_or_paste_simplex_link">Search or paste SimpleX link</string>
|
||||
|
||||
<!-- ChatView.kt -->
|
||||
<string name="no_selected_chat">No selected chat</string>
|
||||
@ -427,6 +429,11 @@
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scan or paste from clipboard)</string>
|
||||
<string name="only_stored_on_members_devices">(only stored by group members)</string>
|
||||
|
||||
<!-- QRCodeScanner -->
|
||||
<string name="enable_camera_access">Enable camera access</string>
|
||||
<string name="tap_to_scan">Tap to scan</string>
|
||||
<string name="camera_not_available">Camera not available</string>
|
||||
|
||||
<!-- GetImageView -->
|
||||
<string name="toast_permission_denied">Permission Denied!</string>
|
||||
<string name="use_camera_button">Camera</string>
|
||||
@ -442,8 +449,8 @@
|
||||
<string name="to_start_a_new_chat_help_header">To start a new chat</string>
|
||||
<string name="chat_help_tap_button">Tap button</string>
|
||||
<string name="above_then_preposition_continuation">above, then:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Add new contact</b>: to create your one-time QR Code for your contact.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scan QR code</b>: to connect to your contact who shows QR code to you.]]></string>
|
||||
<string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Add contact</b>: to create a new invitation link, or connect via a link you received.]]></string>
|
||||
<string name="create_group_button_to_create_new_group"><![CDATA[<b>Create group</b>: to create a new group.]]></string>
|
||||
<string name="to_connect_via_link_title">To connect via link</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">If you received SimpleX Chat invitation link, you can open it in your browser:</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 desktop: scan displayed QR code from the app, via <b>Scan QR code</b>.]]></string>
|
||||
@ -546,11 +553,26 @@
|
||||
<string name="this_string_is_not_a_connection_link">This string is not a connection link!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[You can also connect by clicking the link. If it opens in the browser, click <b>Open in mobile app</b> button.]]></string>
|
||||
|
||||
<!-- CreateLinkView.kt -->
|
||||
<string name="create_one_time_link">Create one-time invitation link</string>
|
||||
<!-- NewChatView.kt -->
|
||||
<string name="new_chat">New chat</string>
|
||||
<string name="add_contact_tab">Add contact</string>
|
||||
<string name="one_time_link">One-time invitation link</string>
|
||||
<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="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>
|
||||
<string name="keep_invitation_link">Keep</string>
|
||||
<string name="creating_link">Creating link…</string>
|
||||
<string name="retry_verb">Retry</string>
|
||||
<string name="share_this_1_time_link">Share this 1-time invite link</string>
|
||||
<string name="paste_the_link_you_received">Paste the link you received</string>
|
||||
<string name="the_text_you_pasted_is_not_a_link">The text you pasted is not a SimpleX link.</string>
|
||||
<string name="tap_to_paste_link">Tap to paste link</string>
|
||||
|
||||
<string name="invalid_qr_code">Invalid QR code</string>
|
||||
<string name="code_you_scanned_is_not_simplex_link_qr_code">The code you scanned is not a SimpleX link QR code.</string>
|
||||
|
||||
<!-- ScanCodeView.kt -->
|
||||
<string name="scan_code">Scan code</string>
|
||||
@ -1708,20 +1730,20 @@
|
||||
<!-- Connection plan -->
|
||||
<string name="connect_plan_connect_to_yourself">Connect to yourself?</string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">This is your own one-time link!</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName">You are already connecting to %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[You are already connecting to <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_already_connecting">Already connecting!</string>
|
||||
<string name="connect_plan_you_are_already_connecting_via_this_one_time_link">You are already connecting via this one-time link!</string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">This is your own SimpleX address!</string>
|
||||
<string name="connect_plan_repeat_connection_request">Repeat connection request?</string>
|
||||
<string name="connect_plan_you_have_already_requested_connection_via_this_address">You have already requested connection via this address!</string>
|
||||
<string name="connect_plan_join_your_group">Join your group?</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName">This is your link for group %1$s!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[This is your link for group <b>%1$s</b>!]]></string>
|
||||
<string name="connect_plan_open_group">Open group</string>
|
||||
<string name="connect_plan_repeat_join_request">Repeat join request?</string>
|
||||
<string name="connect_plan_group_already_exists">Group already exists!</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName">You are already joining the group %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[You are already joining the group <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_already_joining_the_group">Already joining the group!</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link.</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName">You are already in group %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[You are already in group <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_connect_via_link">Connect via link?</string>
|
||||
</resources>
|
@ -119,8 +119,6 @@
|
||||
<string name="back">Назад</string>
|
||||
<string name="cancel_verb">Отказ</string>
|
||||
<string name="icon_descr_cancel_live_message">Спри живото съобщение</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Добави нов контакт</b>: за да създадете своя еднократен QR код за вашия контакт.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Сканирай QR код</b>: за да се свържете с вашия контакт, който ви показва QR код.]]></string>
|
||||
<string name="use_camera_button">Камера</string>
|
||||
<string name="if_you_cant_meet_in_person">Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.</string>
|
||||
<string name="icon_descr_cancel_link_preview">спри визуализацията на линка</string>
|
||||
@ -404,7 +402,6 @@
|
||||
<string name="delete_contact_menu_action">Изтрий</string>
|
||||
<string name="delete_group_menu_action">Изтрий</string>
|
||||
<string name="connect_via_link">Свърване чрез линк</string>
|
||||
<string name="create_one_time_link">Създай линк за еднократна покана</string>
|
||||
<string name="database_passphrase_and_export">Парола за базата данни и експортиране</string>
|
||||
<string name="contribute">Допринеси</string>
|
||||
<string name="continue_to_next_step">Продължи</string>
|
||||
@ -794,7 +791,7 @@
|
||||
<string name="mute_chat">Без звук</string>
|
||||
<string name="image_descr_qr_code">QR код</string>
|
||||
<string name="icon_descr_more_button">Повече</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Прочетете повече в <font color=#0088ff>Ръководство за потребителя</font>.]]></string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Прочетете повече в <font color="#0088ff">Ръководство за потребителя</font>.]]></string>
|
||||
<string name="mark_code_verified">Маркирай като проверено</string>
|
||||
<string name="is_not_verified">%s не е потвърдено</string>
|
||||
<string name="is_verified">%s е потвърдено</string>
|
||||
@ -814,7 +811,7 @@
|
||||
<string name="network_use_onion_hosts_no_desc">Няма се използват Onion хостове.</string>
|
||||
<string name="email_invite_subject">Нека да поговорим в SimpleX Chat</string>
|
||||
<string name="password_to_show">Парола за показване</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Прочетете повече в нашето <font color=#0088ff>GitHub хранилище</font>.]]></string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Прочетете повече в нашето <font color="#0088ff">GitHub хранилище</font>.]]></string>
|
||||
<string name="onboarding_notifications_mode_off">Когато приложението работи</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Периодично</string>
|
||||
<string name="paste_the_link_you_received">Постави получения линк</string>
|
||||
@ -1328,7 +1325,7 @@
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим.</string>
|
||||
<string name="you_are_observer">вие сте наблюдател</string>
|
||||
<string name="gallery_video_button">Видео</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Можете да <font color=#0088ff>се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>;.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Можете да <font color="#0088ff">се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>;.]]></string>
|
||||
<string name="contact_wants_to_connect_with_you">иска да се свърже с вас!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона <b>Отваряне в мобилно приложение</b>.]]></string>
|
||||
<string name="xftp_servers">XFTP сървъри</string>
|
||||
|
@ -191,8 +191,6 @@
|
||||
<string name="to_start_a_new_chat_help_header">Pro zahájení nové konverzace</string>
|
||||
<string name="chat_help_tap_button">Klepněte na tlačítko</string>
|
||||
<string name="above_then_preposition_continuation">potom:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Přidejte nový kontakt</b>: vytvořte jednorázý QR kód pro váš kontakt.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Naskenujte QR kód</b>: připojíte se ke kontaktu, který vám QR kód ukázal.]]></string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 počítač: naskenujte QR kód z aplikace přez <b>Skenovat QR kód</b>.]]></string>
|
||||
<string name="clear_chat_question">Vyčistit chat\?</string>
|
||||
<string name="clear_verb">Vyčistit</string>
|
||||
@ -209,7 +207,6 @@
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Budete připojeni, jakmile bude vaše žádost o připojení přijata, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="connection_request_sent">Požadavek na připojení byl odeslán!</string>
|
||||
<string name="create_one_time_link">Vytvořit jednorázovou pozvánku</string>
|
||||
<string name="one_time_link">Jednorázová pozvánka</string>
|
||||
<string name="security_code">Bezpečnostní kód</string>
|
||||
<string name="is_verified">%s je ověřeno</string>
|
||||
|
@ -262,12 +262,10 @@
|
||||
<string name="gallery_video_button">Video</string>
|
||||
<!-- help - ChatHelpView.kt -->
|
||||
<string name="thank_you_for_installing_simplex">Danke, dass Sie SimpleX Chat installiert haben!</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Sie können sich <font color=#0088ff>mit SimpleX-Chat-Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Sie können sich <font color="#0088ff">mit SimpleX-Chat-Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.]]></string>
|
||||
<string name="to_start_a_new_chat_help_header">Um einen neuen Chat zu starten</string>
|
||||
<string name="chat_help_tap_button">Schaltfläche antippen</string>
|
||||
<string name="above_then_preposition_continuation">Danach die gewünschte Aktion auswählen:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Neuen Kontakt hinzufügen</b>: Um Ihren Einmal-QR-Code für Ihren Kontakt zu erstellen.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR-Code scannen</b>: Um sich mit Ihrem Kontakt zu verbinden, der Ihnen seinen QR-Code zeigt.]]></string>
|
||||
<string name="to_connect_via_link_title">Über Link verbinden</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Wenn Sie einen SimpleX-Chat-Einladungslink erhalten haben, können Sie ihn in Ihrem Browser öffnen:</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 Desktop: Angezeigten QR-Code aus der App scannen, über <b>QR-Code scannen</b>.]]></string>
|
||||
@ -341,7 +339,6 @@
|
||||
<string name="this_string_is_not_a_connection_link">Diese Zeichenfolge entspricht keinem gültigen Verbindungslink!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche „<b>In mobiler App öffnen</b>“.]]></string>
|
||||
<!-- CreateLinkView.kt -->
|
||||
<string name="create_one_time_link">Einmal-Einladungslink erstellen</string>
|
||||
<string name="one_time_link">Einmal-Einladungslink</string>
|
||||
<!-- settings - SettingsView.kt -->
|
||||
<string name="your_settings">Ihre Einstellungen</string>
|
||||
@ -1488,14 +1485,14 @@
|
||||
<string name="expand_verb">Erweitern</string>
|
||||
<string name="connect_plan_repeat_connection_request">Verbindungsanfrage wiederholen?</string>
|
||||
<string name="rcv_direct_event_contact_deleted">Gelöschter Kontakt</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName">Sie sind bereits mit %1$s verbunden.</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Sie sind bereits mit <b>%1$s</b> verbunden.]]></string>
|
||||
<string name="error_alert_title">Fehler</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">Sie sind über diesen Link bereits Mitglied der Gruppe.</string>
|
||||
<string name="create_group_button">Gruppe erstellen</string>
|
||||
<string name="create_another_profile_button">Profil erstellen</string>
|
||||
<string name="group_members_2">%s und %s</string>
|
||||
<string name="connect_plan_join_your_group">Ihrer Gruppe beitreten?</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName">Sie sind bereits Mitglied in der Gruppe %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Sie sind bereits Mitglied in der Gruppe <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">Das ist Ihr eigener Einmal-Link!</string>
|
||||
<string name="marked_deleted_items_description">%d Nachrichten als gelöscht markiert</string>
|
||||
<string name="connect_plan_group_already_exists">Gruppe besteht bereits!</string>
|
||||
@ -1510,7 +1507,7 @@
|
||||
<string name="unblock_member_button">Mitglied freigeben</string>
|
||||
<string name="connect_plan_connect_to_yourself">Mit Ihnen selbst verbinden?</string>
|
||||
<string name="contact_tap_to_connect">Zum Verbinden antippen</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName">Sie sind bereits Mitglied in der Gruppe %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Sie sind bereits Mitglied in der Gruppe <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">Das ist Ihre eigene SimpleX-Adresse!</string>
|
||||
<string name="correct_name_to">Richtiger Name für %s?</string>
|
||||
<string name="delete_messages__question">%d Nachrichten löschen?</string>
|
||||
@ -1531,7 +1528,7 @@
|
||||
<string name="block_member_question">Mitglied blockieren?</string>
|
||||
<string name="rcv_group_events_count">%d Gruppenereignisse</string>
|
||||
<string name="invalid_name">Ungültiger Name!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName">Das ist Ihr Link für die Gruppe %1$s!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Das ist Ihr Link für die Gruppe <b>%1$s</b>!]]></string>
|
||||
<string name="unblock_member_confirmation">Freigeben</string>
|
||||
<string name="non_content_uri_alert_title">Ungültiger Datei-Pfad</string>
|
||||
<string name="connect_plan_you_have_already_requested_connection_via_this_address">Sie haben über diese Adresse bereits eine Verbindung beantragt!</string>
|
||||
|
@ -27,7 +27,6 @@
|
||||
<string name="notifications_mode_service">Siempre activo</string>
|
||||
<string name="allow_verb">Permitir</string>
|
||||
<string name="above_then_preposition_continuation">y después:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Añadir nuevo contacto</b>: para crear tu código QR de un solo uso para tu contacto.]]></string>
|
||||
<string name="accept_connection_request__question">¿Aceptar solicitud de conexión\?</string>
|
||||
<string name="accept_contact_incognito_button">Aceptar incógnito</string>
|
||||
<string name="clear_chat_warning">Se eliminarán todos los mensajes SOLO para tí. ¡No podrá deshacerse!</string>
|
||||
@ -79,9 +78,7 @@
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>¡Consume más batería!</b> El servicio en segundo plano se ejecuta continuamente y las notificaciones se mostrarán de inmediato.]]></string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Tanto tú como tu contacto podéis eliminar de forma irreversible los mensajes enviados.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Tanto tú como tu contacto podéis enviar mensajes temporales.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Escanear código QR</b>: para conectar con tu contacto mediante su código QR.]]></string>
|
||||
<string name="create_profile_button">Crear</string>
|
||||
<string name="create_one_time_link">Crea enlace de invitación de un uso</string>
|
||||
<string name="create_group">Crea grupo secreto</string>
|
||||
<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>
|
||||
|
@ -188,7 +188,6 @@
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Salli kontaktiesi lähettää katoavia viestejä.</string>
|
||||
<string name="timed_messages">Katoavat viestit</string>
|
||||
<string name="icon_descr_context">Kontekstikuvake</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Skannaa QR-koodi</b>: muodostaaksesi yhteyden kontaktiisi, joka näyttää QR-koodin sinulle.]]></string>
|
||||
<string name="icon_descr_cancel_live_message">Peruuta live-viesti</string>
|
||||
<string name="configure_ICE_servers">Määritä ICE-palvelimet</string>
|
||||
<string name="add_address_to_your_profile">Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi.</string>
|
||||
@ -231,7 +230,6 @@
|
||||
<string name="clear_verb">Tyhjennä</string>
|
||||
<string name="clear_chat_button">Tyhjennä keskustelu</string>
|
||||
<string name="clear_chat_question">Tyhjennä keskustelu\?</string>
|
||||
<string name="create_one_time_link">Luo kertaluonteinen kutsulinkki</string>
|
||||
<string name="app_version_name">Sovellusversio: v%s</string>
|
||||
<string name="callstatus_calling">soittaa…</string>
|
||||
<string name="delete_chat_profile">Poista keskusteluprofiili</string>
|
||||
@ -271,7 +269,6 @@
|
||||
<string name="icon_descr_server_status_disconnected">Katkaistu</string>
|
||||
<string name="back">Takaisin</string>
|
||||
<string name="connect_via_link_or_qr">Yhdistä linkillä / QR-koodilla</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Lisää uusi kontakti</b>: luo kertakäyttöinen QR-koodi kontaktille.]]></string>
|
||||
<string name="clear_chat_menu_action">Tyhjennä</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 työpöytä: skannaa esitetty QR-koodi sovelluksesta käyttämällä <b>Skannaa QR-koodi</b>.]]></string>
|
||||
<string name="delete_pending_connection__question">Poistetaanko odottava yhteys\?</string>
|
||||
|
@ -206,7 +206,6 @@
|
||||
<string name="gallery_video_button">Vidéo</string>
|
||||
<string name="to_start_a_new_chat_help_header">Pour démarrer une nouvelle discussion</string>
|
||||
<string name="chat_help_tap_button">Appuyez sur le bouton</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scanner un code QR</b> : pour vous connecter à votre contact qui vous montre un code QR.]]></string>
|
||||
<string name="to_connect_via_link_title">Pour se connecter via un lien</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si vous avez reçu un lien d\'invitation SimpleX Chat, vous pouvez l\'ouvrir dans votre navigateur :</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 bureau : scanner le code QR affiché depuis l\'app, via <b>Scanner le code QR</b>.]]></string>
|
||||
@ -246,7 +245,6 @@
|
||||
<string name="paste_button">Coller</string>
|
||||
<string name="this_string_is_not_a_connection_link">Cette chaîne n\'est pas un lien de connexion !</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Vous pouvez aussi vous connecter en cliquant sur le lien. Si il s\'ouvre dans le navigateur, cliquez sur <b>Ouvrir dans l\'app mobile</b>.]]></string>
|
||||
<string name="create_one_time_link">Créer un lien d\'invitation unique</string>
|
||||
<string name="text_field_set_contact_placeholder">Définir le nom du contact…</string>
|
||||
<string name="icon_descr_server_status_disconnected">Déconnecté</string>
|
||||
<string name="icon_descr_server_status_error">Erreur</string>
|
||||
@ -296,7 +294,6 @@
|
||||
<string name="thank_you_for_installing_simplex">Merci d\'avoir installé SimpleX Chat !</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Vous pouvez <font color="#0088ff">vous connecter aux développeurs de SimpleX Chat pour leur poser des questions et recevoir des réponses :</font>.]]></string>
|
||||
<string name="above_then_preposition_continuation">ci-dessus, puis :</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Ajouter un nouveau contact</b> : afin de créer un code QR à usage unique pour votre contact.]]></string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si vous choisissez de la rejeter, l\'expéditeur·rice NE sera PAS notifié·e.</string>
|
||||
<string name="accept_contact_button">Accepter</string>
|
||||
<string name="mute_chat">Muet</string>
|
||||
@ -1407,14 +1404,14 @@
|
||||
<string name="expand_verb">Développer</string>
|
||||
<string name="connect_plan_repeat_connection_request">Répéter la demande de connexion ?</string>
|
||||
<string name="rcv_direct_event_contact_deleted">contact supprimé</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName">Vous êtes déjà connecté(e) à %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Vous êtes déjà connecté(e) à <b>%1$s</b>.]]></string>
|
||||
<string name="error_alert_title">Erreur</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">Vous êtes déjà en train de rejoindre le groupe via ce lien.</string>
|
||||
<string name="create_group_button">Créer un groupe</string>
|
||||
<string name="create_another_profile_button">Créer le profil</string>
|
||||
<string name="group_members_2">%s et %s</string>
|
||||
<string name="connect_plan_join_your_group">Rejoindre votre groupe ?</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName">Vous êtes déjà en train de rejoindre le groupe %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Vous êtes déjà en train de rejoindre le groupe <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">Voici votre propre lien unique !</string>
|
||||
<string name="marked_deleted_items_description">%d messages marqués comme supprimés</string>
|
||||
<string name="connect_plan_group_already_exists">Ce groupe existe déjà !</string>
|
||||
@ -1429,7 +1426,7 @@
|
||||
<string name="unblock_member_button">Débloquer ce membre</string>
|
||||
<string name="connect_plan_connect_to_yourself">Se connecter à soi-même ?</string>
|
||||
<string name="contact_tap_to_connect">Tapez pour vous connecter</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName">Vous êtes déjà dans le groupe %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Vous êtes déjà dans le groupe <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">Voici votre propre adresse SimpleX !</string>
|
||||
<string name="correct_name_to">Corriger le nom pour %s ?</string>
|
||||
<string name="delete_messages__question">Supprimer %d messages ?</string>
|
||||
@ -1450,7 +1447,7 @@
|
||||
<string name="block_member_question">Bloquer ce membre ?</string>
|
||||
<string name="rcv_group_events_count">%d événements de groupe</string>
|
||||
<string name="invalid_name">Nom invalide !</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName">Voici votre lien pour le groupe %1$s !</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Voici votre lien pour le groupe <b>%1$s</b> !]]></string>
|
||||
<string name="unblock_member_confirmation">Débloquer</string>
|
||||
<string name="non_content_uri_alert_title">Chemin du fichier invalide</string>
|
||||
<string name="connect_plan_you_have_already_requested_connection_via_this_address">Vous avez déjà demandé une connexion via cette adresse !</string>
|
||||
|
@ -44,7 +44,6 @@
|
||||
<string name="network_session_mode_user_description"><![CDATA[Külön TCP kapcsolat (és SOCKS bejelentkezési adatok) lesznek használva <b>minden chat profilodra az appban</b>.]]></string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Mindketten, te és az ismerősöd is küldhettek eltűnő üzeneteket.</string>
|
||||
<string name="keychain_is_storing_securely">Az Android Keystore-t jelmondat biztonságos tárolására használják - lehetővé teszi az értesítési szolgáltatás működését.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR-kód beolvasása</b>: kapcsolódás ismerőshöz a megmutatott QR-kódja alapján]]></string>
|
||||
<string name="alert_title_msg_bad_hash">Téves üzenet hash</string>
|
||||
<string name="cant_delete_user_profile">Felhasználói profil törlése nem lehetséges!</string>
|
||||
<string name="color_background">Háttér</string>
|
||||
@ -117,7 +116,6 @@
|
||||
<string name="bold_text">vastagított</string>
|
||||
<string name="app_passcode_replaced_with_self_destruct">Az app számkód helyettesítésre kerül egy önmegsemmisítő számkóddal.</string>
|
||||
<string name="v5_3_new_interface_languages_descr">Arab, bulgár, finn, héber, thai és ukrán - köszönet a felhasználóknak és a Weblate-nek!</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Új ismerős hozzáadása</b>: egyszer használatos QR-kód készítése az ismerős számára.]]></string>
|
||||
<string name="allow_voice_messages_question">Hangüzenetek engedélyezése?</string>
|
||||
<string name="always_use_relay">Mindig használt relay szervert</string>
|
||||
<string name="chat_preferences_always">mindig</string>
|
||||
@ -338,7 +336,6 @@
|
||||
<string name="connected_to_mobile">Csatlakoztatva a mobilhoz</string>
|
||||
<string name="current_passphrase">Jelenlegi jelmondat…</string>
|
||||
<string name="choose_file_title">Fájl választása</string>
|
||||
<string name="create_one_time_link">Egyszer használatos meghívó link létrehozása</string>
|
||||
<string name="delete_image">Kép törlése</string>
|
||||
<string name="smp_server_test_create_file">Fájl létrehozása</string>
|
||||
<string name="create_secret_group_title">Tikos csoport létrehozása</string>
|
||||
@ -1432,7 +1429,7 @@
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">A chat szolgáltatást elindíthatod a beállítások / adatbázis pontban vagy az app újraindításával.</string>
|
||||
<string name="verify_code_on_mobile">Ellenőrizd a kódot a mobilon!</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Csatlakoztál ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz.</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Kapcsolatba léphetsz <font color=#0088ff>a SimpleX Chat fejlesztőivel és kérdezhetsz bármit és értesülhetsz az újdonságokról</font>.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Kapcsolatba léphetsz <font color="#0088ff">a SimpleX Chat fejlesztőivel és kérdezhetsz bármit és értesülhetsz az újdonságokról</font>.]]></string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Opcionális üdvözlő szöveggel.</string>
|
||||
<string name="unknown_database_error_with_info">Ismeretlen adatbázis hiba: %s</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Elrejtheted vagy némíthatod egy felhasználó profilját - tartsd lenyomva a menühöz!</string>
|
||||
@ -1518,13 +1515,13 @@
|
||||
<string name="sync_connection_force_desc">A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Ez a művelet nem vonható vissza - profilod, ismerőseid, üzeneteid és fájljaid visszafordíthatatlanul törlésre kerülnek.</string>
|
||||
<string name="info_row_updated_at">A bejegyzés frissítve</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[További információ a <font color=#0088ff>Felhasználói útmutatóban</font> olvasható.]]></string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[További információ a <font color="#0088ff">Felhasználói útmutatóban</font> olvasható.]]></string>
|
||||
<string name="settings_is_storing_in_clear_text">A jelmondat a beállításokban egyszerű szövegként van tárolva.</string>
|
||||
<string name="terminal_always_visible">Konzol megjelenítése új ablakban</string>
|
||||
<string name="alert_text_msg_bad_hash">Az előző üzenet hash-e más.</string>
|
||||
<string name="receipts_section_description">Ezek a beállítások a jelenlegi profilodra vonatkoznak</string>
|
||||
<string name="loading_remote_file_desc">Kérjük, várj, amíg a fájl betöltődik az összekapcsolt mobilról.</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[További információ a <font color=#0088ff>GitHub tárolónkban</font>.]]></string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[További információ a <font color="#0088ff">GitHub tárolónkban</font>.]]></string>
|
||||
<string name="error_showing_content">hiba a tartalom megjelenítése közben</string>
|
||||
<string name="error_showing_message">hiba az üzenet megjelenítésekor</string>
|
||||
</resources>
|
@ -292,8 +292,6 @@
|
||||
<string name="delete_group_menu_action">Elimina</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">I messaggi diretti tra i membri sono vietati in questo gruppo.</string>
|
||||
<string name="display_name">Inserisci il tuo nome:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Aggiungi un contatto</b>: per creare il tuo codice QR una tantum per il tuo contatto.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scansiona codice QR</b>: per connetterti al contatto che ti mostra il codice QR.]]></string>
|
||||
<string name="choose_file">File</string>
|
||||
<string name="clear_chat_button">Svuota chat</string>
|
||||
<string name="clear_chat_question">Svuotare la chat\?</string>
|
||||
@ -319,7 +317,6 @@
|
||||
<string name="clear_verification">Annulla la verifica</string>
|
||||
<string name="connect_button">Connetti</string>
|
||||
<string name="connect_via_link">Connetti via link</string>
|
||||
<string name="create_one_time_link">Crea link di invito una tantum</string>
|
||||
<string name="database_passphrase_and_export">Password del database ed esportazione</string>
|
||||
<string name="smp_servers_enter_manually">Inserisci il server manualmente</string>
|
||||
<string name="how_to_use_simplex_chat">Come si usa</string>
|
||||
@ -1407,14 +1404,14 @@
|
||||
<string name="expand_verb">Espandi</string>
|
||||
<string name="connect_plan_repeat_connection_request">Ripetere la richiesta di connessione?</string>
|
||||
<string name="rcv_direct_event_contact_deleted">contatto eliminato</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName">Ti stai già connettendo a %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Ti stai già connettendo a <b>%1$s</b>.]]></string>
|
||||
<string name="error_alert_title">Errore</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">Stai già entrando nel gruppo tramite questo link.</string>
|
||||
<string name="create_group_button">Crea gruppo</string>
|
||||
<string name="create_another_profile_button">Crea profilo</string>
|
||||
<string name="group_members_2">%s e %s</string>
|
||||
<string name="connect_plan_join_your_group">Entrare nel tuo gruppo?</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName">Stai già entrando nel gruppo %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Stai già entrando nel gruppo <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">Questo è il tuo link una tantum!</string>
|
||||
<string name="marked_deleted_items_description">%d messaggi contrassegnati eliminati</string>
|
||||
<string name="connect_plan_group_already_exists">Il gruppo esiste già!</string>
|
||||
@ -1429,7 +1426,7 @@
|
||||
<string name="unblock_member_button">Sblocca membro</string>
|
||||
<string name="connect_plan_connect_to_yourself">Connettersi a te stesso?</string>
|
||||
<string name="contact_tap_to_connect">Tocca per connettere</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName">Sei già nel gruppo %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Sei già nel gruppo <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">Questo è il tuo indirizzo SimpleX!</string>
|
||||
<string name="correct_name_to">Correggere il nome a %s?</string>
|
||||
<string name="delete_messages__question">Eliminare %d messaggi?</string>
|
||||
@ -1450,7 +1447,7 @@
|
||||
<string name="block_member_question">Bloccare il membro?</string>
|
||||
<string name="rcv_group_events_count">%d eventi del gruppo</string>
|
||||
<string name="invalid_name">Nome non valido!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName">Questo è il tuo link per il gruppo %1$s!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Questo è il tuo link per il gruppo <b>%1$s</b>!]]></string>
|
||||
<string name="unblock_member_confirmation">Sblocca</string>
|
||||
<string name="non_content_uri_alert_title">Percorso file non valido</string>
|
||||
<string name="connect_plan_you_have_already_requested_connection_via_this_address">Hai già richiesto la connessione tramite questo indirizzo!</string>
|
||||
|
@ -93,7 +93,6 @@
|
||||
<string name="v4_2_auto_accept_contact_requests">אשר אוטומטית בקשות ליצירת קשר.</string>
|
||||
<string name="authentication_cancelled">אימות בוטל</string>
|
||||
<string name="auth_unavailable">אימות לא זמין</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>הוסיפו איש קשר חדש</b>: ליצירת קוד QR חד־פעמי עבור איש הקשר שלכם.]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>הטוב ביותר לסוללה</b>. התראות יוצגו רק כאשר האפליקציה מופעלת (ללא שירות רקע).]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>טוב לסוללה</b>. שירות הרקע ייבדוק הודעות כל 10 דקות. שיחות או הודעות דחופות עלולות להתפספס.]]></string>
|
||||
<string name="both_you_and_your_contacts_can_delete">גם אתם וגם איש הקשר יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</string>
|
||||
@ -103,7 +102,6 @@
|
||||
<string name="cancel_verb">ביטול</string>
|
||||
<string name="icon_descr_cancel_live_message">בטל הודעה חיה</string>
|
||||
<string name="use_camera_button">מצלמה</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>סירקו קוד QR</b>: כדי להתחבר לאיש קשר המציג לכם קוד QR.]]></string>
|
||||
<string name="icon_descr_cancel_link_preview">בטל תצוגה מקדימה של קישורים</string>
|
||||
<string name="callstatus_error">שגיאת שיחה</string>
|
||||
<string name="callstatus_in_progress">שיחה מתמשכת</string>
|
||||
@ -219,7 +217,6 @@
|
||||
<string name="copied">הועתק ללוח</string>
|
||||
<string name="share_one_time_link">צור קישור הזמנה חד־פעמי</string>
|
||||
<string name="create_group">צור קבוצה סודית</string>
|
||||
<string name="create_one_time_link">צור קישור הזמנה חד־פעמי</string>
|
||||
<string name="contribute">תרומה</string>
|
||||
<string name="core_version">גרסת ליבה: v%s</string>
|
||||
<string name="create_address">צור כתובת</string>
|
||||
|
@ -73,12 +73,10 @@
|
||||
<string name="auth_unavailable">認証不可能</string>
|
||||
<string name="auto_accept_images">画像を自動的に受信</string>
|
||||
<string name="notifications_mode_service_desc">バックグラウンド機能が常にオンで、メッセージが到着次第に通知が出ます。</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>新しい連絡先を追加</b>:使い捨てのQRコードを発行]]></string>
|
||||
<string name="turning_off_service_and_periodic">電池省エネをオンに、バックグラウンド機能と定期的な受信依頼をオフにします。設定メニューにて変更できます。</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>電池消費が最少</b>:アプリがアクティブ時のみに通知が出ます(バックグラウンドサービス無し)。]]></string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>設定メニューにてオフにできます。</b> アプリがアクティブ時に通知が出ます。]]></string>
|
||||
<string name="both_you_and_your_contacts_can_delete">あなたと連絡相手が送信済みメッセージを永久削除できます。</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QRコードを読み込み</b>:連絡相手のQRコードをスキャンすると繋がります。]]></string>
|
||||
<string name="chat_archive_section">チャットのアーカイブ</string>
|
||||
<string name="delete_chat_archive_question">チャットのアーカイブを削除しますか?</string>
|
||||
<string name="join_group_incognito_button">シークレットモードで参加</string>
|
||||
@ -524,7 +522,6 @@
|
||||
<string name="mute_chat">ミュート</string>
|
||||
<string name="delete_pending_connection__question">接続待ちの繋がりを削除しますか?</string>
|
||||
<string name="connect_button">接続</string>
|
||||
<string name="create_one_time_link">使い捨てリンクを発行する</string>
|
||||
<string name="one_time_link">使い捨ての招待リンク</string>
|
||||
<string name="database_passphrase_and_export">データベース暗証フレーズとエキスポート</string>
|
||||
<string name="how_to_use_simplex_chat">使い方</string>
|
||||
|
@ -27,7 +27,6 @@
|
||||
<string name="icon_descr_context">컨텍스트 아이콘</string>
|
||||
<string name="icon_descr_server_status_connected">연결됨</string>
|
||||
<string name="back">뒤로</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>새 대화 상대 추가</b> : 대화를 위한 일회용 QR 코드 만들기]]></string>
|
||||
<string name="cancel_verb">취소</string>
|
||||
<string name="icon_descr_cancel_live_message">라이브 메시지 취소</string>
|
||||
<string name="choose_file">파일 선택</string>
|
||||
@ -171,7 +170,6 @@
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>배터리에 가장 좋음</b>. 앱이 실행 중일 때만 알림을 받게 됩니다 (백그라운드에서 실행되지 않음).]]></string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>설정을 통해 비활성화할 수 있습니다.</b> – 앱이 실행되는 동안 알림이 표시됩니다.]]></string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">당신과 대화 상대 모두 사라지는 메시지를 보낼 수 있습니다.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR 코드 스캔</b>: QR 코드를 보여주는 사람과 대화할 수 있습니다.]]></string>
|
||||
<string name="cannot_access_keychain">데이터베이스 암호를 저장하고 있는 Keystore에 접근할 수 없습니다.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>배터리 더욱 사용</b>! 백그라운드 서비스가 항상 실행됩니다. - 메시지를 수신되는 즉시 알림이 표시됩니다.]]></string>
|
||||
<string name="callstatus_ended">통화 종료됨 %1$s</string>
|
||||
@ -202,7 +200,6 @@
|
||||
<string name="status_contact_has_e2e_encryption">대화 상대와 종단간 암호화됨</string>
|
||||
<string name="alert_title_contact_connection_pending">대화 상대와 아직 연결되지 않았습니다!</string>
|
||||
<string name="archive_created_on_ts">%1$s에 생성 완료</string>
|
||||
<string name="create_one_time_link">일회용 초대 링크 생성</string>
|
||||
<string name="create_secret_group_title">비밀 그룹 생성</string>
|
||||
<string name="accept_contact_incognito_button">익명 수락</string>
|
||||
<string name="chat_item_ttl_month">1개월</string>
|
||||
|
@ -117,7 +117,6 @@
|
||||
<string name="image_descr_qr_code">QR kodas</string>
|
||||
<string name="icon_descr_help">pagalba</string>
|
||||
<string name="icon_descr_email">El. paštas</string>
|
||||
<string name="create_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string>
|
||||
<string name="scan_code">Skenuoti kodą</string>
|
||||
<string name="database_passphrase_and_export">Duomenų bazės slaptafrazė ir eksportavimas</string>
|
||||
<string name="smp_servers_delete_server">Ištrinti serverį</string>
|
||||
@ -387,8 +386,6 @@
|
||||
<string name="error_smp_test_certificate">Gali būti, kad liudijimo kontrolinis kodas serverio adrese yra neteisingas</string>
|
||||
<string name="smp_server_test_delete_file">Ištrinti failą</string>
|
||||
<string name="image_decoding_exception_title">Dekodavimo klaida</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Pridėti naują adresatą</b>: norėdami sukurti adresatui vienkartinį QR kodą.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Skenuoti QR kodą</b>: norėdami prisijungti prie adresato, kuris jums rodo QR kodą.]]></string>
|
||||
<string name="clear_chat_button">Išvalyti pokalbį</string>
|
||||
<string name="unmute_chat">Įjungti pranešimus</string>
|
||||
<string name="invalid_QR_code">Neteisingas QR kodas</string>
|
||||
|
@ -66,7 +66,6 @@
|
||||
<string name="allow_voice_messages_question">Spraak berichten toestaan\?</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Goed voor de batterij</b>. Achtergrondservice controleert berichten elke 10 minuten. Mogelijk mist u oproepen of dringende berichten.]]></string>
|
||||
<string name="integrity_msg_bad_hash">Onjuiste bericht hash</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scan QR-code</b>: om verbinding te maken met uw contact die u de QR-code laat zien.]]></string>
|
||||
<string name="integrity_msg_bad_id">Onjuiste bericht-ID</string>
|
||||
<string name="call_already_ended">Oproep al beëindigd!</string>
|
||||
<string name="chat_item_ttl_month">1 maand</string>
|
||||
@ -94,7 +93,6 @@
|
||||
<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="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="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Nieuw contact toevoegen</b>: om een eenmalige QR-code voor uw contact te maken.]]></string>
|
||||
<string name="icon_descr_call_ended">Oproep beëindigd</string>
|
||||
<string name="turning_off_service_and_periodic">Batterijoptimalisatie is actief, waardoor achtergrondservice en periodieke verzoeken om nieuwe berichten worden uitgeschakeld. Je kunt ze weer inschakelen via instellingen.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Het beste voor de batterij</b>. U ontvangt alleen meldingen wanneer de app wordt uitgevoerd (GEEN achtergrondservice).]]></string>
|
||||
@ -203,7 +201,6 @@
|
||||
<string name="clear_verification">Verwijderd verificatie</string>
|
||||
<string name="connect_button">Verbind</string>
|
||||
<string name="connect_via_link">Maak verbinding via link</string>
|
||||
<string name="create_one_time_link">Maak een eenmalige uitnodiging link</string>
|
||||
<string name="colored_text">gekleurd</string>
|
||||
<string name="callstatus_connecting">Oproep verbinden…</string>
|
||||
<string name="create_profile_button">Maak</string>
|
||||
@ -1442,17 +1439,17 @@
|
||||
<string name="terminal_always_visible">Console in nieuw venster weergeven</string>
|
||||
<string name="block_member_desc">Alle nieuwe berichten van %s worden verborgen!</string>
|
||||
<string name="blocked_item_description">geblokkeerd</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName">Je bent al verbonden met %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Je bent al verbonden met <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">Je wordt al lid van de groep via deze link.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName">Je bent al lid van de groep %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Je bent al lid van de groep <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">Dit is uw eigen eenmalige link!</string>
|
||||
<string name="unblock_member_button">Lid deblokkeren</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName">Je zit al in groep %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Je zit al in groep <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">Dit is uw eigen SimpleX adres!</string>
|
||||
<string name="unblock_member_question">Lid deblokkeren?</string>
|
||||
<string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Je maakt al verbinding via deze eenmalige link!</string>
|
||||
<string name="non_content_uri_alert_text">Je hebt een ongeldig bestandslocatie gedeeld. Rapporteer het probleem aan de app-ontwikkelaars.</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName">Dit is jouw link voor groep %1$s!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Dit is jouw link voor groep <b>%1$s</b>!]]></string>
|
||||
<string name="unblock_member_confirmation">Deblokkeren</string>
|
||||
<string name="connect_plan_you_have_already_requested_connection_via_this_address">U heeft al een verbinding aangevraagd via dit adres!</string>
|
||||
<string name="encryption_renegotiation_error">Fout bij heronderhandeling van codering</string>
|
||||
|
@ -229,7 +229,6 @@
|
||||
<string name="accept_contact_button">Akceptuj</string>
|
||||
<string name="accept_contact_incognito_button">Akceptuj incognito</string>
|
||||
<string name="clear_chat_warning">Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Zeskanuj kod QR</b>: aby połączyć się z kontaktem, który pokaże Ci kod QR.]]></string>
|
||||
<string name="icon_descr_cancel_link_preview">anuluj podgląd linku</string>
|
||||
<string name="clear_verb">Wyczyść</string>
|
||||
<string name="clear_chat_menu_action">Wyczyść</string>
|
||||
@ -917,7 +916,6 @@
|
||||
<string name="auth_unavailable">Uwierzytelnianie niedostępne</string>
|
||||
<string name="attach">Dołącz</string>
|
||||
<string name="back">Wstecz</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Dodaj nowy kontakt</b>: aby stworzyć swój jednorazowy kod QR dla kontaktu.]]></string>
|
||||
<string name="turning_off_service_and_periodic">Optymalizacja baterii jest aktywna, wyłącza usługi w tle i okresowe żądania nowych wiadomości. Możesz je ponownie włączyć za pośrednictwem ustawień.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Można je wyłączyć poprzez ustawienia</b> - powiadomienia nadal będą pokazywane podczas działania aplikacji.]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Najlepsze dla baterii</b>. Będziesz otrzymywać powiadomienia tylko wtedy, gdy aplikacja jest uruchomiona (NIE w tle).]]></string>
|
||||
@ -933,7 +931,6 @@
|
||||
<string name="connection_error_auth">Błąd połączenia (UWIERZYTELNIANIE)</string>
|
||||
<string name="connect_via_link_or_qr">Połącz się przez link / kod QR</string>
|
||||
<string name="archive_created_on_ts">Utworzony na %1$s</string>
|
||||
<string name="create_one_time_link">Utwórz jednorazowy link do zaproszenia</string>
|
||||
<string name="create_group">Utwórz tajną grupę</string>
|
||||
<string name="create_secret_group_title">Utwórz tajną grupę</string>
|
||||
<string name="set_password_to_export_desc">Baza danych jest zaszyfrowana przy użyciu losowego hasła. Proszę zmienić je przed eksportem.</string>
|
||||
@ -1427,12 +1424,12 @@
|
||||
<string name="blocked_item_description">zablokowany</string>
|
||||
<string name="expand_verb">Rozszerz</string>
|
||||
<string name="connect_plan_repeat_connection_request">Powtórzyć prośbę połączenia?</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName">Już jesteś połączony z %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Już jesteś połączony z <b>%1$s</b>.]]></string>
|
||||
<string name="error_alert_title">Błąd</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">Już dołączasz do grupy przez ten link.</string>
|
||||
<string name="group_members_2">%s i %s</string>
|
||||
<string name="connect_plan_join_your_group">Dołączyć do twojej grupy?</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName">Już dołączasz do grupy %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Już dołączasz do grupy <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">To jest twój jednorazowy link!</string>
|
||||
<string name="connect_plan_group_already_exists">Grupa już istnieje!</string>
|
||||
<string name="video_decoding_exception_desc">Wideo nie może zostać zdekodowane, spróbuj inne wideo lub skontaktuj się z deweloperami.</string>
|
||||
@ -1440,7 +1437,7 @@
|
||||
<string name="group_members_n">%s, %s i %d członków</string>
|
||||
<string name="unblock_member_button">Odblokuj członka</string>
|
||||
<string name="contact_tap_to_connect">Dotknij aby połączyć</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName">Już jesteś w grupie %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Już jesteś w grupie <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">To jest twój własny adres SimpleX!</string>
|
||||
<string name="remove_member_button">Usuń członka</string>
|
||||
<string name="unblock_member_question">Odblokować członka?</string>
|
||||
@ -1452,7 +1449,7 @@
|
||||
<string name="error_sending_message_contact_invitation">Błąd wysyłania zaproszenia</string>
|
||||
<string name="non_content_uri_alert_text">Udostępniłeś nieprawidłową ścieżkę pliku. Zgłoś problem do deweloperów aplikacji.</string>
|
||||
<string name="invalid_name">Nieprawidłowa nazwa!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName">To jest twój link zaproszenia do grupy %1$s!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[To jest twój link zaproszenia do grupy <b>%1$s</b>!]]></string>
|
||||
<string name="unblock_member_confirmation">Odblokuj</string>
|
||||
<string name="non_content_uri_alert_title">Nieprawidłowa ścieżka pliku</string>
|
||||
<string name="connect_plan_you_have_already_requested_connection_via_this_address">Już prosiłeś o połączenie na ten adres!</string>
|
||||
|
@ -24,8 +24,6 @@
|
||||
<string name="icon_descr_cancel_live_message">Cancelar mensagem ao vivo</string>
|
||||
<string name="back">Voltar</string>
|
||||
<string name="choose_file">Arquivo</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Adicionar novo contato</b>: para criar seu QR code de uso único para seu contato.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Escanear código QR</b>: para se conectar ao seu contato que mostra o código QR para você.]]></string>
|
||||
<string name="accept_contact_button">Aceitar</string>
|
||||
<string name="clear_chat_question">Limpar chat\?</string>
|
||||
<string name="clear_verb">Limpar</string>
|
||||
@ -374,7 +372,6 @@
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">A autenticação do dispositivo está desativada. Desativando o bloqueio SimpleX.</string>
|
||||
<string name="for_everybody">Para todos</string>
|
||||
<string name="notification_preview_mode_hidden">Oculto</string>
|
||||
<string name="create_one_time_link">Gerar um link de convite de uso único.</string>
|
||||
<string name="how_to_use_your_servers">Como usar seus servidores</string>
|
||||
<string name="import_database_confirmation">Importar</string>
|
||||
<string name="import_database_question">Importar banco de dados de chat\?</string>
|
||||
|
@ -211,7 +211,6 @@
|
||||
<string name="info_row_group">Grupo</string>
|
||||
<string name="icon_descr_audio_on">Áudio ligado</string>
|
||||
<string name="authentication_cancelled">Autenticação cancelada</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> Adicionar novo contato</b>: para criar o seu código QR de utilização única para o seu contato.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> Bom para a bateria </b>. O serviço em segundo plano verifica se há mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]></string>
|
||||
<string name="turning_off_service_and_periodic">A otimização da bateria está ativa, desativando o serviço em segundo plano e os pedidos periódicos de novas mensagens. Você pode reativá-los através das definições.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Melhor para a bateria</b>. Apenas receberá notificações enquanto a app estiver em execução (SEM serviço em segundo plano)]]></string>
|
||||
@ -225,7 +224,6 @@
|
||||
<string name="calls_prohibited_with_this_contact">Chamadas de áudio/vídeo são proibidas.</string>
|
||||
<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="la_authenticate">Autenticar</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Leia o código QR </b>: para se conectar ao seu contato que lhe mostra o código QR.]]></string>
|
||||
<string name="callstatus_ended">chamada finalizada %1$s</string>
|
||||
<string name="callstatus_calling">a chamar…</string>
|
||||
<string name="callstatus_error">erro de chamada</string>
|
||||
@ -366,7 +364,6 @@
|
||||
<string name="connection_error">Erro de conexão</string>
|
||||
<string name="connection_local_display_name">conexão %1$d</string>
|
||||
<string name="contact_already_exists">O contato já existe</string>
|
||||
<string name="create_one_time_link">Criar convite de ligação de utilização única</string>
|
||||
<string name="one_time_link">Convite de ligação de utilização única</string>
|
||||
<string name="save_servers_button">Salvar</string>
|
||||
<string name="settings_section_title_incognito">Modo anónimo</string>
|
||||
|
@ -267,12 +267,10 @@
|
||||
<string name="to_start_a_new_chat_help_header">Чтобы начать новый чат</string>
|
||||
<string name="chat_help_tap_button">Нажмите кнопку</string>
|
||||
<string name="above_then_preposition_continuation">сверху, затем:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Добавить новый контакт</b>: чтобы создать одноразовый QR код/ссылку для Вашего контакта.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Сканировать QR код</b>: чтобы соединиться с контактом, который показывает Вам QR код.]]></string>
|
||||
<string name="to_connect_via_link_title">Чтобы соединиться через ссылку</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Если Вы получили ссылку с приглашением из SimpleX Chat, Вы можете открыть ее в браузере:</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 на компьютере: сосканируйте показанный QR код из приложения через <b>Сканировать QR код</b>.]]></string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 на мобильном: намжите кнопку <b>Open in mobile app</b> на веб странице, затем нажмите <b>Соединиться</b> в приложении.]]></string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 на мобильном: нажмите кнопку <b>Open in mobile app</b> на веб странице, затем нажмите <b>Соединиться</b> в приложении.]]></string>
|
||||
<!-- Contact Request Alert Dialogue - CharListNavLinkView.kt -->
|
||||
<string name="accept_connection_request__question">Принять запрос на соединение?</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Отправителю НЕ будет послано уведомление, если Вы отклоните запрос на соединение.</string>
|
||||
@ -339,7 +337,6 @@
|
||||
<string name="connect_button">Соединиться</string>
|
||||
<string name="paste_button">Вставить</string>
|
||||
<!-- CreateLinkView.kt -->
|
||||
<string name="create_one_time_link">Создать одноразовую ссылку</string>
|
||||
<string name="one_time_link">Одноразовая ссылка</string>
|
||||
<!-- settings - SettingsView.kt -->
|
||||
<string name="your_settings">Настройки</string>
|
||||
@ -1566,9 +1563,9 @@
|
||||
<string name="block_member_desc">Все новые сообщения от %s будут скрыты!</string>
|
||||
<string name="desktop_app_version_is_incompatible">Версия настольного приложения %s несовместима с этим приложением.</string>
|
||||
<string name="blocked_item_description">заблокировано</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName">Вы уже соединяетесь с %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Вы уже соединяетесь с <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">Вы уже вступаете в группу по этой ссылке.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName">Вы уже вступаете в группу %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Вы уже вступаете в группу <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">Это ваша собственная одноразовая ссылка!</string>
|
||||
<string name="v5_4_link_mobile_desktop_descr">Через безопасный квантово-устойчивый протокол.</string>
|
||||
<string name="v5_4_block_group_members_descr">Чтобы скрыть нежелательные сообщения.</string>
|
||||
@ -1580,7 +1577,7 @@
|
||||
<string name="unblock_member_button">Разблокировать члена группы</string>
|
||||
<string name="contact_tap_to_connect">Нажмите чтобы соединиться</string>
|
||||
<string name="this_device_name">Имя этого устройства</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName">Вы уже состоите в группе %1$s.</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Вы уже состоите в группе <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">Это ваш собственный адрес SimpleX!</string>
|
||||
<string name="unblock_member_question">Разблокировать члена группы?</string>
|
||||
<string name="settings_section_title_use_from_desktop">Использовать с компьютера</string>
|
||||
@ -1590,7 +1587,7 @@
|
||||
<string name="this_device_name_shared_with_mobile">Имя устройства будет доступно подключенному мобильному клиенту.</string>
|
||||
<string name="verify_code_on_mobile">Сверьте код на мобильном</string>
|
||||
<string name="non_content_uri_alert_text">Указан неверный путь к файлу. Сообщите о проблеме разработчикам приложения.</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName">Это ваша ссылка на группу %1$s!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Это ваша ссылка на группу <b>%1$s</b>!]]></string>
|
||||
<string name="verify_code_with_desktop">Сверьте код с компьютером</string>
|
||||
<string name="scan_qr_code_from_desktop">Сканировать QR код с компьютера</string>
|
||||
<string name="unblock_member_confirmation">Разблокировать</string>
|
||||
|
@ -112,8 +112,6 @@
|
||||
<string name="both_you_and_your_contact_can_add_message_reactions">ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาต่อข้อความได้</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้</string>
|
||||
<string name="use_camera_button">กล้อง</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> เพิ่มผู้ติดต่อใหม่ </b>: เพื่อสร้างรหัส QR แบบใช้ครั้งเดียวสําหรับผู้ติดต่อของคุณ]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>สแกนรหัส QR</b>: เพื่อเชื่อมต่อกับผู้ติดต่อที่แสดงรหัส QR ให้คุณ]]></string>
|
||||
<string name="learn_more_about_address">เกี่ยวกับที่อยู่ SimpleX</string>
|
||||
<string name="bold_text">ตัวหนา</string>
|
||||
<string name="callstatus_calling">กำลังโทร…</string>
|
||||
@ -205,7 +203,6 @@
|
||||
<string name="icon_descr_close_button">ปุ่มปิด</string>
|
||||
<string name="connect_button">เชื่อมต่อ</string>
|
||||
<string name="connect_via_link">เชื่อมต่อผ่านลิงก์</string>
|
||||
<string name="create_one_time_link">สร้างลิงก์เชิญแบบใช้ครั้งเดียว</string>
|
||||
<string name="clear_verification">ล้างการยืนยัน</string>
|
||||
<string name="chat_console">คอนโซลแชท</string>
|
||||
<string name="smp_servers_check_address">ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง</string>
|
||||
|
@ -3,7 +3,6 @@
|
||||
<string name="notifications">Bildirimler</string>
|
||||
<string name="connect_via_link_or_qr">Bağlantı ya da karekod ile bağlan</string>
|
||||
<string name="create_group">Gizli grup oluştur</string>
|
||||
<string name="create_one_time_link">Tek seferlik davet bağlantısı oluştur</string>
|
||||
<string name="your_simplex_contact_address">SimpleX addresin</string>
|
||||
<string name="callstatus_missed">cevapsız çağrı</string>
|
||||
<string name="incoming_video_call">Gelen görüntülü arama</string>
|
||||
@ -736,7 +735,6 @@
|
||||
<string name="v4_3_irreversible_message_deletion">Geri alınamaz mesaj silme</string>
|
||||
<string name="v4_5_italian_interface">İtalyanca arayüz</string>
|
||||
<string name="choose_file_title">Dosya seç</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR kodunu tara</b>: size QR kodunu gösteren kişiyle bağlantı kurmak için.]]></string>
|
||||
<string name="invite_friends">Arkadaşlarınızı davet edin</string>
|
||||
<string name="bold_text">kalın</string>
|
||||
<string name="italic_text">İtalik</string>
|
||||
@ -790,7 +788,6 @@
|
||||
<string name="delete_message_cannot_be_undone_warning">Mesajlar silinecek - bu geri alınamaz!</string>
|
||||
<string name="switch_receiving_address_question">Alıcı adresini değiştir\?</string>
|
||||
<string name="back">Geri</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Yeni kişi ekle</b>: Kişiniz için tek seferlik QR Kodunuzu oluşturmak için.]]></string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Bağlantı isteğiniz kabul edildiğinde bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Kişinizin cihazı çevrimiçi olduğunda bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string>
|
||||
<string name="learn_more">Daha fazla bilgi edinin</string>
|
||||
@ -902,7 +899,7 @@
|
||||
<string name="contact_sent_large_file">Kişiniz desteklenen maksimum boyuttan (%1$s) daha büyük bir dosya gönderdi.</string>
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">Kişiniz yüklemeyi tamamladığında video alınacaktır.</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Ayrıca bağlantıya tıklayarak da bağlanabilirsiniz. Eğer bağlantı tarayıcda açılırsa, <b>mobil uygulamada aç</b> seçeneğine tıklayın.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Soru sormak ve güncellemeleri almak için <font color=#0088ff>SimpleX Chat geliştiricilerine bağlanabilirsiniz</font>.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Soru sormak ve güncellemeleri almak için <font color="#0088ff">SimpleX Chat geliştiricilerine bağlanabilirsiniz</font>.]]></string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Bir kullanıcının profilini gizleyebilir veya sessize alabilirsiniz - menü için basılı tutun.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Sohbet veritabanınızın en son sürümünü SADECE bir cihazda kullanmalısınız, aksi takdirde bazı kişilerden daha fazla mesaj alamayabilirsiniz.</string>
|
||||
<string name="wrong_passphrase">Yanlış veritabanı parolası</string>
|
||||
@ -976,7 +973,7 @@
|
||||
<string name="to_connect_via_link_title">Link ile bağlanmak için</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Bu geçerli bir bağlantı linki değil</string>
|
||||
<string name="this_QR_code_is_not_a_link">Bu QR kodu bir bağlantı değil!</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Daha fazla bilgi için <font color=#0088ff>Kullanıcı Kılavuzu</font>.]]></string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Daha fazla bilgi için <font color="#0088ff">Kullanıcı Kılavuzu</font>.]]></string>
|
||||
<string name="paste_button">Yapıştır</string>
|
||||
<string name="this_string_is_not_a_connection_link">Bu dize bir bağlantı linki değil!</string>
|
||||
<string name="rate_the_app">Uygulamaya puan verin</string>
|
||||
@ -1020,7 +1017,7 @@
|
||||
<string name="unfavorite_chat">Favorilerden çıkar</string>
|
||||
<string name="make_profile_private">Sohbeti gizli yap!</string>
|
||||
<string name="profile_update_will_be_sent_to_contacts">Profil güncellemesi kişilerinize gönderilecektir.</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[<font color=#0088ff>GitHub repomuzda</font> daha fazlasını okuyun.]]></string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[<font color="#0088ff">GitHub repomuzda</font> daha fazlasını okuyun.]]></string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Lütfen geliştiricilere bildirin.</string>
|
||||
<string name="users_delete_with_connections">Profil ve sunucu bağlantıları</string>
|
||||
<string name="user_unhide">gizlemeyi kaldır</string>
|
||||
|
@ -104,7 +104,6 @@
|
||||
<string name="integrity_msg_bad_hash">поганий хеш повідомлення</string>
|
||||
<string name="integrity_msg_bad_id">поганий ідентифікатор повідомлення</string>
|
||||
<string name="color_background">Фон</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Додайте новий контакт</b>: щоб створити одноразовий QR-код для вашого контакту.]]></string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown">Це можна вимкнути у налаштуваннях – сповіщення все одно будуть відображатися, коли програма працює.</string>
|
||||
<string name="notifications_mode_service_desc">Служба фонового режиму завжди активна – сповіщення відображатимуться, як тільки повідомлення будуть доступні.</string>
|
||||
<string name="icon_descr_asked_to_receive">Запит на отримання зображення</string>
|
||||
@ -356,7 +355,6 @@
|
||||
<string name="this_link_is_not_a_valid_connection_link">Це посилання не є дійсним з\'єднувальним посиланням!</string>
|
||||
<string name="connection_request_sent">Запит на з\'єднання відправлено!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Ви також можете підключитися, клацнувши посилання. Якщо воно відкривається у браузері, клацніть кнопку <b>Відкрити у мобільному додатку</b>.]]></string>
|
||||
<string name="create_one_time_link">Створити одноразове запрошення</string>
|
||||
<string name="scan_code">Сканувати код</string>
|
||||
<string name="scan_code_from_contacts_app">Скануйте код безпеки з додатка вашого контакту.</string>
|
||||
<string name="smp_servers_invalid_address">Невірна адреса сервера!</string>
|
||||
@ -529,7 +527,7 @@
|
||||
<string name="image_descr_profile_image">зображення профілю</string>
|
||||
<string name="icon_descr_more_button">Більше</string>
|
||||
<string name="create_profile">Створити профіль</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Докладніше читайте в нашому репозиторії на <font color=#0088ff>GitHub</font>.]]></string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Докладніше читайте в нашому репозиторії на <font color="#0088ff">GitHub</font>.]]></string>
|
||||
<string name="icon_descr_video_on">Відео увімкнено</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Це може трапитися, якщо ви або ваше з\'єднання використовували застарілу резервну копію бази даних.</string>
|
||||
<string name="restore_database">Відновити резервну копію бази даних</string>
|
||||
@ -606,7 +604,6 @@
|
||||
<string name="toast_permission_denied">Доступ відхилено!</string>
|
||||
<string name="use_camera_button">Камера</string>
|
||||
<string name="thank_you_for_installing_simplex">Дякуємо за установку SimpleX Chat!</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Сканувати QR-код</b>: щоб підключитися до вашого контакту, який вам показує QR-код.]]></string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Якщо ви отримали запрошення від SimpleX Chat, ви можете відкрити його у вашому браузері:</string>
|
||||
<string name="clear_chat_menu_action">Очистити</string>
|
||||
<string name="delete_contact_menu_action">Видалити</string>
|
||||
@ -1028,7 +1025,7 @@
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Вас підключать, коли пристрій вашого контакту буде в мережі, зачекайте або перевірте пізніше!</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">Ви не втратите свої контакти, якщо ви пізніше видалите свою адресу.</string>
|
||||
<string name="you_can_accept_or_reject_connection">Коли люди просять про з\'єднання, ви можете його прийняти чи відхилити.</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Докладніше читайте в <font color=#0088ff>Посібнику користувача</font>.]]></string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Докладніше читайте в <font color="#0088ff">Посібнику користувача</font>.]]></string>
|
||||
<string name="simplex_address">SimpleX-адреса</string>
|
||||
<string name="clear_verification">Очистити перевірку</string>
|
||||
<string name="is_verified">%s перевірено</string>
|
||||
@ -1200,7 +1197,7 @@
|
||||
<string name="create_group">Створити секретну групу</string>
|
||||
<string name="to_share_with_your_contact">(щоб поділитися з вашим контактом)</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(сканувати або вставити з буферу обміну)</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Ви можете <font color=#0088ff>підключитися до розробників SimpleX Chat, щоб задати будь-які питання і отримувати оновлення</font>.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Ви можете <font color="#0088ff">підключитися до розробників SimpleX Chat, щоб задати будь-які питання і отримувати оновлення</font>.]]></string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 настільний комп\'ютер: скануйте відображений QR-код з додатка за допомогою <b>Сканувати QR-код</b>.]]></string>
|
||||
<string name="icon_descr_address">Адреса SimpleX</string>
|
||||
<string name="show_QR_code">Показати QR-код</string>
|
||||
|
@ -118,7 +118,6 @@
|
||||
<string name="network_session_mode_entity_description">每个联系人和群组成员 <b>将使用单独的 TCP 连接(和 SOCKS 凭证)</b>。
|
||||
\n<b>请注意</b>:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>添加新联系人</b>:为您的联系人创建一次性二维码。]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> 最长续航 </b>。您只会在应用程序运行时收到通知(无后台服务)。]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> 较长续航 </b>。后台服务每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]></string>
|
||||
<string name="bold_text">加粗</string>
|
||||
@ -129,7 +128,6 @@
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b> 使用更多电量 </b>!后台服务始终运行——一旦收到消息,就会显示通知。]]></string>
|
||||
<string name="impossible_to_recover_passphrase"><![CDATA[<b>请注意</b>:如果您丢失密码,您将无法恢复或者更改密码。]]></string>
|
||||
<string name="call_already_ended">通话已结束!</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>扫描二维码</b> :与向您展示二维码的联系人联系。]]></string>
|
||||
<string name="alert_title_cant_invite_contacts">无法邀请联系人!</string>
|
||||
<string name="invite_prohibited">无法邀请联系人!</string>
|
||||
<string name="cancel_verb">取消</string>
|
||||
@ -338,7 +336,6 @@
|
||||
<string name="create_secret_group_title">创建私密群组</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">不同的名字、头像和传输隔离。</string>
|
||||
<string name="v4_4_french_interface">法语界面</string>
|
||||
<string name="create_one_time_link">创建一次性邀请链接</string>
|
||||
<string name="how_to_use_simplex_chat">如何使用它</string>
|
||||
<string name="server_error">错误</string>
|
||||
<string name="display_name_connecting">连接中……</string>
|
||||
@ -1407,14 +1404,14 @@
|
||||
<string name="expand_verb">展开</string>
|
||||
<string name="connect_plan_repeat_connection_request">重复连接请求吗?</string>
|
||||
<string name="rcv_direct_event_contact_deleted">已删除联系人</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName">你已经在连接到 %1$s。</string>
|
||||
<string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[你已经在连接到 <b>%1$s</b>。]]></string>
|
||||
<string name="error_alert_title">错误</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">你已经在通过此链接加入该群。</string>
|
||||
<string name="create_group_button">建群</string>
|
||||
<string name="create_another_profile_button">创建个人资料</string>
|
||||
<string name="group_members_2">%s 和 %s</string>
|
||||
<string name="connect_plan_join_your_group">加入你的群吗?</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName">你已经在加入 %1$s 群。</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[你已经在加入 <b>%1$s</b> 群。]]></string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">这是你自己的一次性链接!</string>
|
||||
<string name="marked_deleted_items_description">%d 条消息被标记为删除</string>
|
||||
<string name="connect_plan_group_already_exists">群已存在!</string>
|
||||
@ -1428,7 +1425,7 @@
|
||||
<string name="unblock_member_button">解封成员</string>
|
||||
<string name="connect_plan_connect_to_yourself">连接到你自己?</string>
|
||||
<string name="contact_tap_to_connect">轻按连接</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName">你已经在%1$s 群内。</string>
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[你已经在<b>%1$s</b> 群内。]]></string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">这是你自己的 SimpleX 地址!</string>
|
||||
<string name="correct_name_to">更正名称为 %s?</string>
|
||||
<string name="delete_messages__question">删除 %d 条消息吗?</string>
|
||||
@ -1449,7 +1446,7 @@
|
||||
<string name="block_member_question">封禁成员吗?</string>
|
||||
<string name="rcv_group_events_count">%d 个群事件</string>
|
||||
<string name="invalid_name">无效名称!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName">这是给你的 %1$s 群链接!</string>
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[这是给你的 <b>%1$s</b> 群链接!]]></string>
|
||||
<string name="unblock_member_confirmation">解封</string>
|
||||
<string name="non_content_uri_alert_title">无效的文件路径</string>
|
||||
<string name="connect_plan_you_have_already_requested_connection_via_this_address">你已经请求通过此地址进行连接!</string>
|
||||
|
@ -47,8 +47,6 @@
|
||||
<string name="allow_voice_messages_question">允許使用語音訊息?</string>
|
||||
<string name="cancel_verb">取消</string>
|
||||
<string name="icon_descr_cancel_live_message">取消實況訊息</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>新增新的聯絡人</b>:建立你的一次性二維碼給你的聯絡人。]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>掃描二維碼</b>:連接到向你出示二維碼的聯絡人。]]></string>
|
||||
<string name="choose_file">選擇檔案</string>
|
||||
<string name="use_camera_button">相機</string>
|
||||
<string name="from_gallery_button">從圖片庫選擇圖片</string>
|
||||
@ -424,7 +422,6 @@
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 桌面版:在應用程式內掃描一個已存在的二維碼,透過 <b>掃描二維碼</b>。]]></string>
|
||||
<string name="icon_descr_settings">設定</string>
|
||||
<string name="this_QR_code_is_not_a_link">這個二維碼不是一個連結!</string>
|
||||
<string name="create_one_time_link">建立一次性邀請連結</string>
|
||||
<string name="network_use_onion_hosts_prefer">當可行的時候</string>
|
||||
<string name="core_version">核心版本:v%s</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
|
@ -4,6 +4,8 @@ import androidx.compose.foundation.contextMenuOpenDetector
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
@ -36,3 +38,5 @@ onExternalDrag(enabled) {
|
||||
}
|
||||
|
||||
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = contextMenuOpenDetector { action() }
|
||||
|
||||
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = Modifier.pointerHoverIcon(PointerIcon.Hand)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import SectionDivider
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
@ -33,11 +34,11 @@ actual fun ChatListNavLinkLayout(
|
||||
dropdownMenuItems: (@Composable () -> Unit)?,
|
||||
showMenu: MutableState<Boolean>,
|
||||
stopped: Boolean,
|
||||
selectedChat: State<Boolean>
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!stopped) modifier = modifier
|
||||
.background(color = if (selectedChat.value) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified)
|
||||
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true }
|
||||
CompositionLocalProvider(
|
||||
@ -52,10 +53,17 @@ actual fun ChatListNavLinkLayout(
|
||||
) {
|
||||
chatLinkPreview()
|
||||
}
|
||||
if (selectedChat.value) {
|
||||
Box(Modifier.matchParentSize().background(MaterialTheme.colors.onBackground.copy(0.05f)))
|
||||
}
|
||||
if (dropdownMenuItems != null) {
|
||||
DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
if (selectedChat.value || nextChatSelected.value) {
|
||||
Divider()
|
||||
} else {
|
||||
SectionDivider()
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,20 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.Density
|
||||
import chat.simplex.common.model.CIFile
|
||||
import chat.simplex.common.model.readCryptoFile
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.simplexWindowState
|
||||
import kotlinx.coroutines.delay
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
import javax.imageio.ImageIO
|
||||
@ -17,6 +23,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
private val bStyle = SpanStyle(fontWeight = FontWeight.Bold)
|
||||
private val iStyle = SpanStyle(fontStyle = FontStyle.Italic)
|
||||
private val uStyle = SpanStyle(textDecoration = TextDecoration.Underline)
|
||||
private fun fontStyle(color: String) =
|
||||
SpanStyle(color = Color(color.replace("#", "ff").toLongOrNull(16) ?: Color.White.toArgb().toLong()))
|
||||
|
||||
@ -54,6 +61,22 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat
|
||||
}
|
||||
break
|
||||
}
|
||||
text.substringSafe(innerI, 2) == "u>" -> {
|
||||
val textStart = innerI + 2
|
||||
for (insideTagI in textStart until text.length) {
|
||||
if (text[insideTagI] == '<') {
|
||||
withStyle(uStyle) { append(text.substring(textStart, insideTagI)) }
|
||||
skipTil = insideTagI + 4
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
text.substringSafe(innerI, 3) == "br>" -> {
|
||||
val textStart = innerI + 3
|
||||
append("\n")
|
||||
skipTil = textStart
|
||||
}
|
||||
text.substringSafe(innerI, 4) == "font" -> {
|
||||
var textStart = innerI + 5
|
||||
var color = "#000000"
|
||||
@ -85,6 +108,18 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat
|
||||
AnnotatedString(text)
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun SetupClipboardListener() {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
chatModel.clipboardHasText.value = clipboard.hasText()
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
chatModel.clipboardHasText.value = clipboard.hasText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual fun getAppFileUri(fileName: String): URI {
|
||||
val rh = chatModel.currentRemoteHost.value
|
||||
return if (rh == null) {
|
||||
|
@ -1,11 +0,0 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.RemoteHostInfo
|
||||
|
||||
@Composable
|
||||
actual fun ConnectViaLinkView(m: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
// TODO this should close if remote host changes in model
|
||||
PasteToConnectView(m, rh, close)
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.*
|
||||
|
||||
@Composable
|
||||
actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
||||
actual fun QRCodeScanner(
|
||||
showQRCodeScanner: MutableState<Boolean>,
|
||||
padding: PaddingValues,
|
||||
onBarcode: (String) -> Unit
|
||||
) {
|
||||
//LALAL
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.RemoteHostInfo
|
||||
|
||||
@Composable
|
||||
actual fun ScanToConnectView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
ConnectContactLayout(
|
||||
chatModel = chatModel,
|
||||
rh = rh,
|
||||
incognitoPref = chatModel.controller.appPrefs.incognito,
|
||||
close = close
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user