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:
Stanislav Dmitrenko 2023-12-29 22:46:45 +07:00 committed by GitHub
parent 2bacc00a06
commit 9c061508a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1670 additions and 1466 deletions

View File

@ -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()
}

View File

@ -26,3 +26,5 @@ actual fun Modifier.desktopOnExternalDrag(
): Modifier = this
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this

View File

@ -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

View File

@ -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)
}

View File

@ -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,
)
}
}
}
}

View File

@ -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))
}
}
}

View File

@ -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
)
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -23,3 +23,5 @@ expect fun Modifier.desktopOnExternalDrag(
): Modifier
expect fun Modifier.onRightClick(action: () -> Unit): Modifier
expect fun Modifier.desktopPointerHoverIconHand(): Modifier

View File

@ -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))

View File

@ -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

View File

@ -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))
}
}

View File

@ -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))

View File

@ -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,

View File

@ -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(

View File

@ -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(

View File

@ -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) }
)
}
}

View File

@ -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
}
}
}

View File

@ -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,

View File

@ -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)
}
}
}
}

View File

@ -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,

View File

@ -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) })
}
}

View File

@ -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,

View File

@ -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

View File

@ -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()
}
}

View File

@ -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 = {},
)
}
}

View File

@ -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 = {},
)
}
}

View File

@ -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)

View File

@ -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(

View File

@ -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
}
}
}

View File

@ -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 = {},
)

View File

@ -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)
}
}

View File

@ -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 = {}
)
}
}

View File

@ -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

View File

@ -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
)

View File

@ -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) }

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -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)
)
}
}
}

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
)
}