android: onboarding (#624)

* android: onboarding views

* create profile

* creating profile works

* make connection view

* onboarding layout

* add translations
This commit is contained in:
Evgeny Poberezkin
2022-05-10 08:03:43 +01:00
committed by GitHub
parent 69e21781df
commit 412982cc01
29 changed files with 699 additions and 192 deletions

View File

@@ -9,25 +9,27 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.AndroidViewModel
import androidx.work.*
import chat.simplex.app.model.*
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.NtfManager
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.SplashView
import chat.simplex.app.views.WelcomeView
import chat.simplex.app.views.chat.ChatView
import chat.simplex.app.views.chatlist.ChatListView
import chat.simplex.app.views.chatlist.openChat
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.connectViaUri
import chat.simplex.app.views.newchat.withUriAction
import chat.simplex.app.views.onboarding.*
import java.util.concurrent.TimeUnit
//import kotlinx.serialization.decodeFromString
class MainActivity: ComponentActivity() {
@@ -83,12 +85,18 @@ class SimplexViewModel(application: Application): AndroidViewModel(application)
@Composable
fun MainPage(chatModel: ChatModel) {
Box {
when (chatModel.userCreated.value) {
null -> SplashView()
false -> WelcomeView(chatModel)
true ->
val onboarding = chatModel.onboardingStage.value
val userCreated = chatModel.userCreated.value
when {
onboarding == null || userCreated == null -> SplashView()
onboarding == OnboardingStage.OnboardingComplete && userCreated ->
if (chatModel.chatId.value == null) ChatListView(chatModel)
else ChatView(chatModel)
onboarding == OnboardingStage.Step1_SimpleXInfo ->
Box(Modifier.padding(horizontal = 20.dp)) {
SimpleXInfo(chatModel, onboarding = true)
}
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel)
}
ModalManager.shared.showInView()
AlertManager.shared.showInView()

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.*
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.getFilesDirectory
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.onboarding.OnboardingStage
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*
@@ -46,7 +47,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
withApi {
val user = chatController.apiGetActiveUser()
if (user != null) {
if (user == null) {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
} else {
chatController.startChat(user)
SimplexService.start(applicationContext)
chatController.showBackgroundServiceNotice()

View File

@@ -8,6 +8,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.onboarding.OnboardingStage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -65,7 +66,9 @@ class SimplexService: Service() {
withApi {
try {
val user = chatController.apiGetActiveUser()
if (user != null) {
if (user == null) {
chatController.chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
} else {
Log.w(TAG, "Starting foreground service")
chatController.startChat(user)
chatController.startReceiver()

View File

@@ -12,6 +12,7 @@ import chat.simplex.app.ui.theme.SecretColor
import chat.simplex.app.ui.theme.SimplexBlue
import chat.simplex.app.views.call.*
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.app.views.onboarding.OnboardingStage
import kotlinx.datetime.*
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
@@ -20,6 +21,7 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
class ChatModel(val controller: ChatController) {
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
val currentUser = mutableStateOf<User?>(null)
val userCreated = mutableStateOf<Boolean?>(null)
val chats = mutableStateListOf<Chat>()

View File

@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -18,6 +17,7 @@ import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.views.call.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
@@ -49,6 +49,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
chatModel.chats.addAll(chats)
chatModel.currentUser.value = user
chatModel.userCreated.value = true
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
Log.d(TAG, "started chat")
} catch(e: Error) {
Log.e(TAG, "failed starting chat $e")
@@ -949,7 +950,9 @@ sealed class ChatErrorType {
val string: String get() = when (this) {
is InvalidConnReq -> "invalidConnReq"
is ContactGroups -> "groupNames $groupNames"
is NoActiveUser -> "noActiveUser"
}
@Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType()
@Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType()
@Serializable @SerialName("contactGroups") class ContactGroups(val contact: Contact, val groupNames: List<String>): ChatErrorType()
}

View File

@@ -1,15 +1,22 @@
package chat.simplex.app.views
import androidx.compose.foundation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBackIosNew
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
@@ -18,69 +25,12 @@ import chat.simplex.app.R
import chat.simplex.app.SimplexService
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Profile
import chat.simplex.app.views.helpers.getKeyboardState
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.views.helpers.withApi
import com.google.accompanist.insets.ProvideWindowInsets
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.navigationBarsWithImePadding
import kotlinx.coroutines.launch
@Composable
fun WelcomeView(chatModel: ChatModel) {
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colors.background)
) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.verticalScroll(scrollState)
.fillMaxSize()
.background(color = MaterialTheme.colors.background)
.padding(12.dp)
) {
Image(
painter = painterResource(R.drawable.logo),
contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier.padding(vertical = 15.dp)
)
Text(
stringResource(R.string.you_control_your_chat),
style = MaterialTheme.typography.h4,
color = MaterialTheme.colors.onBackground
)
Text(
stringResource(R.string.the_messaging_and_app_platform_protecting_your_privacy_and_security),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground
)
Spacer(Modifier.height(8.dp))
Text(
stringResource(R.string.we_do_not_store_contacts_or_messages_on_servers),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground
)
Spacer(Modifier.height(24.dp))
CreateProfilePanel(chatModel)
}
if (savedKeyboardState != keyboardState) {
LaunchedEffect(keyboardState) {
scope.launch {
savedKeyboardState = keyboardState
scrollState.animateScrollTo(scrollState.maxValue)
}
}
}
}
}
}
fun isValidDisplayName(name: String) : Boolean {
return (name.firstOrNull { it.isWhitespace() }) == null
@@ -88,91 +38,106 @@ fun isValidDisplayName(name: String) : Boolean {
@Composable
fun CreateProfilePanel(chatModel: ChatModel) {
var displayName by remember { mutableStateOf("") }
var fullName by remember { mutableStateOf("") }
val displayName = remember { mutableStateOf("") }
val fullName = remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
Column(
modifier=Modifier.fillMaxSize()
) {
Text(
stringResource(R.string.create_profile),
style = MaterialTheme.typography.h4,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(vertical = 5.dp)
)
Text(
stringResource(R.string.your_profile_is_stored_on_your_decide_and_shared_only_with_your_contacts),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground
)
Spacer(Modifier.height(10.dp))
Text(
stringResource(R.string.display_name),
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 3.dp)
)
BasicTextField(
value = displayName,
onValueChange = { displayName = it },
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.secondary)
.height(40.dp)
.clip(RoundedCornerShape(5.dp))
.padding(8.dp)
.navigationBarsWithImePadding(),
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true
)
val errorText = if(!isValidDisplayName(displayName)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
Column(
modifier = Modifier.fillMaxSize()
) {
Text(
stringResource(R.string.create_profile),
style = MaterialTheme.typography.h4,
modifier = Modifier.padding(vertical = 5.dp)
)
ReadableText(R.string.your_profile_is_stored_on_your_device)
ReadableText(R.string.profile_is_only_shared_with_your_contacts)
Spacer(Modifier.height(10.dp))
Text(
stringResource(R.string.display_name),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 3.dp)
)
ProfileNameField(displayName, focusRequester)
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
Text(
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.height(3.dp))
Text(
stringResource(R.string.full_name_optional__prompt),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 5.dp)
)
ProfileNameField(fullName)
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
SimpleButton(
text = stringResource(R.string.about_simplex),
icon = Icons.Outlined.ArrowBackIosNew
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
Text(
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(3.dp))
Text(
stringResource(R.string.full_name_optional__prompt),
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 5.dp)
)
BasicTextField(
value = fullName,
onValueChange = { fullName = it },
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.secondary)
.height(40.dp)
.clip(RoundedCornerShape(3.dp))
.padding(8.dp)
.navigationBarsWithImePadding(),
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true
)
Spacer(Modifier.height(20.dp))
Button(onClick = {
withApi {
val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null)
)
chatModel.controller.startChat(user)
SimplexService.start(chatModel.controller.appContext)
chatModel.controller.showBackgroundServiceNotice()
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val createModifier: Modifier
val createColor: Color
if (enabled) {
createModifier = Modifier.padding(8.dp).clickable { createProfile(chatModel, displayName.value, fullName.value) }
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
createColor = HighOrLowlight
}
Surface(shape = RoundedCornerShape(20.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor)
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = createColor)
}
}
}
},
enabled = (displayName.isNotEmpty() && isValidDisplayName(displayName))
) { Text(stringResource(R.string.create_profile_button)) }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
}
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String) {
withApi {
val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null)
)
chatModel.controller.startChat(user)
SimplexService.start(chatModel.controller.appContext)
// TODO show it later?
chatModel.controller.showBackgroundServiceNotice()
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
}
}
@Composable
fun ProfileNameField(name: MutableState<String>, focusRequester: FocusRequester? = null) {
val modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.secondary)
.height(40.dp)
.clip(RoundedCornerShape(5.dp))
.padding(8.dp)
.navigationBarsWithImePadding()
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true
)
}

View File

@@ -1,7 +1,7 @@
package chat.simplex.app.views.call
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.model.Contact
import chat.simplex.app.views.helpers.generalGetString
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -1,10 +1,11 @@
import androidx.compose.foundation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource

View File

@@ -7,7 +7,8 @@ import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Edit
@@ -24,7 +25,8 @@ import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
sealed class ComposePreview {
object NoPreview: ComposePreview()

View File

@@ -6,7 +6,7 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector

View File

@@ -23,8 +23,9 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable
fun SendMsgView(

View File

@@ -1,12 +1,14 @@
import android.graphics.Bitmap
import androidx.compose.foundation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.MoreHoriz
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -16,7 +18,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.model.CIFile
import chat.simplex.app.model.CIFileStatus
import chat.simplex.app.views.helpers.*
@Composable

View File

@@ -18,7 +18,8 @@ import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*

View File

@@ -1,7 +1,8 @@
import android.graphics.Bitmap
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

View File

@@ -21,6 +21,7 @@ import chat.simplex.app.ui.theme.ToolbarDark
import chat.simplex.app.ui.theme.ToolbarLight
import chat.simplex.app.views.helpers.ModalManager
import chat.simplex.app.views.newchat.NewChatSheet
import chat.simplex.app.views.onboarding.MakeConnection
import chat.simplex.app.views.usersettings.SettingsView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -88,8 +89,7 @@ fun ChatListView(chatModel: ChatModel) {
if (chatModel.chats.isNotEmpty()) {
ChatList(chatModel)
} else {
val user = chatModel.currentUser.value
Help(scaffoldCtrl, displayName = user?.profile?.displayName)
MakeConnection(chatModel)
}
}
if (scaffoldCtrl.expanded.value) {

View File

@@ -9,14 +9,12 @@ import androidx.compose.material.icons.outlined.Link
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.model.PendingContactConnection
import chat.simplex.app.model.getTimestampText
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.ChatInfoImage
import chat.simplex.app.views.helpers.ProfileImage
@Composable

View File

@@ -10,7 +10,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.model.ChatInfo
import chat.simplex.app.model.getTimestampText
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.ChatInfoImage

View File

@@ -19,7 +19,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.Chat
import chat.simplex.app.model.ChatInfo
import chat.simplex.app.ui.theme.SimpleXTheme

View File

@@ -11,7 +11,8 @@ import androidx.compose.runtime.Composable
import androidx.core.content.ContextCompat
import chat.simplex.app.R
import chat.simplex.app.model.CIFile
import java.io.*
import java.io.BufferedOutputStream
import java.io.File
fun shareText(cxt: Context, text: String) {
val sendIntent: Intent = Intent().apply {

View File

@@ -19,19 +19,24 @@ import androidx.compose.ui.unit.dp
fun SimpleButton(text: String, icon: ImageVector,
color: Color = MaterialTheme.colors.primary,
click: () -> Unit) {
SimpleButtonFrame(click) {
Icon(
icon, text, tint = color,
modifier = Modifier.padding(end = 8.dp)
)
Text(text, style = MaterialTheme.typography.caption, color = color)
}
}
@Composable
fun SimpleButtonFrame(click: () -> Unit, content: @Composable () -> Unit) {
Surface(shape = RoundedCornerShape(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { click() }
.padding(8.dp)
) {
Icon(
icon, text, tint = color,
modifier = Modifier.padding(end = 8.dp)
)
Text(text, style = MaterialTheme.typography.caption, color = color)
}
) { content() }
}
}

View File

@@ -0,0 +1,70 @@
package chat.simplex.app.views.onboarding
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.User
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.ModalManager
import chat.simplex.app.views.helpers.annotatedStringResource
@Composable
fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = null) {
Column(Modifier.fillMaxHeight(), horizontalAlignment = Alignment.Start) {
Text(stringResource(R.string.how_simplex_works), style = MaterialTheme.typography.h1, modifier = Modifier.padding(bottom = 8.dp))
ReadableText(R.string.many_people_asked_how_can_it_deliver)
ReadableText(R.string.to_protect_privacy_simplex_has_ids_for_queues)
ReadableText(R.string.you_control_servers_to_receive_your_contacts_to_send)
ReadableText(R.string.only_client_devices_store_contacts_groups_e2e_encrypted_messages)
if (onboardingStage == null) {
val uriHandler = LocalUriHandler.current
Text(
annotatedStringResource(R.string.read_more_in_github_with_link),
modifier = Modifier.padding(bottom = 12.dp).clickable { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat#readme") },
lineHeight = 22.sp
)
} else {
ReadableText(R.string.read_more_in_github)
}
Spacer(Modifier.fillMaxHeight().weight(1f))
if (onboardingStage != null) {
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.shared.closeModal() })
}
Spacer(Modifier.fillMaxHeight().weight(1f))
}
}
}
@Composable
fun ReadableText(@StringRes stringResId: Int) {
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(bottom = 12.dp), lineHeight = 22.sp)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
@Composable
fun PreviewHowItWorks() {
SimpleXTheme {
HowItWorks(user = null)
}
}

View File

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

View File

@@ -0,0 +1,47 @@
package chat.simplex.app.views.onboarding
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.CreateProfilePanel
import chat.simplex.app.views.helpers.getKeyboardState
import com.google.accompanist.insets.ProvideWindowInsets
import kotlinx.coroutines.launch
enum class OnboardingStage {
Step1_SimpleXInfo,
Step2_CreateProfile,
OnboardingComplete
}
@Composable
fun CreateProfile(chatModel: ChatModel) {
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colors.background)
.padding(20.dp)
) {
CreateProfilePanel(chatModel)
if (savedKeyboardState != keyboardState) {
LaunchedEffect(keyboardState) {
scope.launch {
savedKeyboardState = keyboardState
scrollState.animateScrollTo(scrollState.maxValue)
}
}
}
}
}
}

View File

@@ -0,0 +1,139 @@
package chat.simplex.app.views.onboarding
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Info
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.User
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ModalManager
@Composable
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
SimpleXInfoLayout(
user = chatModel.currentUser.value,
onboardingStage = if (onboarding) chatModel.onboardingStage else null,
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
)
}
@Composable
fun SimpleXInfoLayout(
user: User?,
onboardingStage: MutableState<OnboardingStage?>?,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
) {
Column(Modifier.fillMaxHeight(), horizontalAlignment = Alignment.Start) {
Image(
painter = painterResource(R.drawable.logo),
contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier
.padding(vertical = 20.dp)
.fillMaxWidth(0.80f)
)
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 16.dp))
InfoRow("🎭", R.string.privacy_redefined, R.string.first_platform_without_user_ids)
InfoRow("📭", R.string.immune_to_spam_and_abuse, R.string.people_can_connect_only_via_links_you_share)
InfoRow("🤝", R.string.decentralized, R.string.opensource_protocol_and_code_anybody_can_run_servers)
Spacer(
Modifier
.fillMaxHeight()
.weight(1f))
if (onboardingStage != null) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
OnboardingActionButton(user, onboardingStage)
}
Spacer(
Modifier
.fillMaxHeight()
.weight(1f))
}
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
SimpleButton(text = stringResource(R.string.how_it_works), icon = Icons.Outlined.Info,
click = showModal { HowItWorks(user, onboardingStage) })
}
}
}
@Composable
private fun InfoRow(emoji: String, @StringRes titleId: Int, @StringRes textId: Int) {
Row(Modifier.padding(bottom = 20.dp), verticalAlignment = Alignment.Top) {
Text(emoji, fontSize = 36.sp, modifier = Modifier
.width(60.dp)
.padding(end = 16.dp))
Column(horizontalAlignment = Alignment.Start) {
Text(stringResource(titleId), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h3, lineHeight = 24.sp)
Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.caption)
}
}
}
@Composable
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
if (user == null) {
ActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
} else {
ActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
}
}
@Composable
private fun ActionButton(
@StringRes labelId: Int,
onboarding: OnboardingStage?,
onboardingStage: MutableState<OnboardingStage?>,
onclick: (() -> Unit)?
) {
SimpleButtonFrame(click = {
onclick?.invoke()
onboardingStage.value = onboarding
}) {
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary)
Icon(
Icons.Outlined.ArrowForwardIos, "next stage", tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(end = 8.dp)
)
}
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
@Composable
fun PreviewSimpleXInfo() {
SimpleXTheme {
SimpleXInfoLayout(
user = null,
onboardingStage = null,
showModal = {{}}
)
}
}

View File

@@ -26,6 +26,7 @@ import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.TerminalView
import chat.simplex.app.views.call.VideoCallView
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.SimpleXInfo
@Composable
fun SettingsView(chatModel: ChatModel) {
@@ -111,6 +112,15 @@ fun SettingsLayout(
Text(stringResource(R.string.how_to_use_simplex_chat))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showModal { SimpleXInfo(it, onboarding = false) }) {
Icon(
Icons.Outlined.Info,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.about_simplex_chat))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showModal { MarkdownHelpView() }) {
Icon(
Icons.Outlined.TextFormat,

View File

@@ -214,7 +214,8 @@
<!-- settings - SettingsView.kt -->
<string name="your_settings">Настройки</string>
<string name="your_simplex_contact_address">Ваш <xliff:g id="appName">SimpleX</xliff:g> адрес</string>
<string name="how_to_use_simplex_chat">Как использовать <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="about_simplex_chat">Информация о <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="how_to_use_simplex_chat">Как использовать</string>
<string name="markdown_help">Форматирование сообщений</string>
<string name="markdown_in_messages">Форматирование сообщений</string>
<string name="chat_with_the_founder">Соединиться с разработчиками</string>
@@ -256,11 +257,13 @@
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Платформа для сообщений и приложений, которая защищает вашу личную информацию и безопасность.</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">Мы не храним ваши контакты и сообщения (после доставки) на серверах.</string>
<string name="create_profile">Создать профиль</string>
<string name="your_profile_is_stored_on_your_decide_and_shared_only_with_your_contacts">Ваш профиль хранится на вашем устройстве и отправляется только вашим контактам.</string>
<string name="your_profile_is_stored_on_your_device">Ваш профиль, контакты и доставленные сообщения хранятся на вашем устройстве.</string>
<string name="profile_is_only_shared_with_your_contacts">Профиль отправляется только вашим контактам.</string>
<string name="display_name_cannot_contain_whitespace">Имя профиля не может содержать пробелы.</string>
<string name="display_name">Имя профиля</string>
<string name="full_name_optional__prompt">Полное имя (не обязательно)</string>
<string name="create_profile_button">Создать</string>
<string name="about_simplex">О SimpleX</string>
<!-- markdown demo - MarkdownHelpView.kt -->
<string name="how_to_use_markdown">Как форматировать</string>
@@ -293,4 +296,37 @@
<string name="callstate_received_answer">получен ответ…</string>
<string name="callstate_connecting">соединяется…</string>
<string name="callstate_connected">соединено</string>
<!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">Новое поколение приватных сообщений</string>
<string name="privacy_redefined">Более конфиденциальный</string>
<string name="first_platform_without_user_ids">Первая в мире платформа без идентификаторов пользователей.</string>
<string name="immune_to_spam_and_abuse">Защищен от спама</string>
<string name="people_can_connect_only_via_links_you_share">С вами можно соединиться только через созданные вами ссылки.</string>
<string name="decentralized">Децентрализованный</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Открытый протокол и код - кто угодно может запустить сервер.</string>
<string name="create_your_profile">Создать профиль</string>
<string name="make_private_connection">Добавьте контакт</string>
<string name="how_it_works">Как это работает</string>
<!-- How SimpleX Works -->
<string name="how_simplex_works">Как <xliff:g id="appName">SimpleX</xliff:g> работает</string>
<string name="many_people_asked_how_can_it_deliver">Много пользователей спросили: <i>как <xliff:g id="appName">SimpleX</xliff:g> доставляет сообщения без идентификаторов пользователей?</i></string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Чтобы защитить вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, <xliff:g id="appName">SimpleX</xliff:g> использует ID для очередей сообщений, разные для каждого контакта.</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">Вы определяете через какие серверы вы <b>получаете сообщения</b>, ваши контакты - серверы, которые вы используете для отправки.</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются <b>с двухуровневым end-to-end шифрованием</b>.</string>
<string name="read_more_in_github">Узнайте больше из нашего GitHub репозитория.</string>
<string name="read_more_in_github_with_link">Узнайте больше из нашего <font color="#0088ff">GitHub репозитория</font>.</string>
<!-- MakeConnection -->
<string name="to_make_your_first_private_connection_choose">Чтобы добавить ваш первый контакт, выберите <b>одно из</b>:</string>
<string name="create_1_time_link_qr_code">Создать ссылку / QR код</string>
<string name="it_s_secure_to_share__only_one_contact_can_use_it">Ей безопасно поделиться - только один контакт может использовать её.</string>
<string name="paste_the_link_you_received">Вставьте полученную ссылку</string>
<string name="or_open_the_link_in_the_browser_and_tap_open_in_mobile">Или откройте ссылку в браузере и нажмите <b>Open in mobile</b>.</string>
<string name="scan_contact_s_qr_code">Сосканировать QR код контакта</string>
<string name="in_person_or_via_a_video_call__the_most_secure_way_to_connect">При встрече или в видеозвонке самый безопасный способ установить соединение</string>
<string name="or">или</string>
<string name="connect_with_the_developers">Соединиться с разработчиками</string>
<string name="to_ask_any_questions_and_to_receive_simplex_chat_updates">Чтобы задать вопросы и получать уведомления о <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
</resources>

View File

@@ -219,7 +219,8 @@
<!-- settings - SettingsView.kt -->
<string name="your_settings">Your settings</string>
<string name="your_simplex_contact_address">Your <xliff:g id="appName">SimpleX</xliff:g> contact address</string>
<string name="how_to_use_simplex_chat">How to use <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="about_simplex_chat">About <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="how_to_use_simplex_chat">How to use it</string>
<string name="markdown_help">Markdown help</string>
<string name="markdown_in_messages">Markdown in messages</string>
<string name="chat_with_the_founder">Connect to the developers</string>
@@ -261,11 +262,13 @@
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">The messaging and application platform protecting your privacy and security.</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">We don\'t store any of your contacts or messages (once delivered) on the servers.</string>
<string name="create_profile">Create profile</string>
<string name="your_profile_is_stored_on_your_decide_and_shared_only_with_your_contacts">Your profile is stored on your device and shared only with your contacts.</string>
<string name="your_profile_is_stored_on_your_device">Your profile, contacts and delivered messages are stored on your device.</string>
<string name="profile_is_only_shared_with_your_contacts">The profile is only shared with your contacts.</string>
<string name="display_name_cannot_contain_whitespace">Display name cannot contain whitespace.</string>
<string name="display_name">Display Name</string>
<string name="full_name_optional__prompt">Full Name (Optional)</string>
<string name="full_name_optional__prompt">Full Name (optional)</string>
<string name="create_profile_button">Create</string>
<string name="about_simplex">About SimpleX</string>
<!-- markdown demo - MarkdownHelpView.kt -->
<string name="how_to_use_markdown">How to use markdown</string>
@@ -294,4 +297,37 @@
<string name="callstate_received_answer">received answer…</string>
<string name="callstate_connecting">connecting…</string>
<string name="callstate_connected">connected</string>
<!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">The next generation of private messaging</string>
<string name="privacy_redefined">Privacy redefined</string>
<string name="first_platform_without_user_ids">The 1st platform without any user identifiers private by design.</string>
<string name="immune_to_spam_and_abuse">Immune to spam and abuse</string>
<string name="people_can_connect_only_via_links_you_share">People can connect to you only via the links you share.</string>
<string name="decentralized">Decentralized</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol and code anybody can run the servers.</string>
<string name="create_your_profile">Create your profile</string>
<string name="make_private_connection">Make a private connection</string>
<string name="how_it_works">How it works</string>
<!-- How SimpleX Works -->
<string name="how_simplex_works">How <xliff:g id="appName">SimpleX</xliff:g> works</string>
<string name="many_people_asked_how_can_it_deliver">Many people asked: <i>if <xliff:g id="appName">SimpleX</xliff:g> has no user identifiers, how can it deliver messages?</i></string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">To protect privacy, instead of user IDs used by all other platforms, <xliff:g id="appName">SimpleX</xliff:g> has identifiers for message queues, separate for each of your contacts.</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">You control through which server(s) <b>to receive</b> the messages, your contacts the servers you use to message them.</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Only client devices store user profiles, contacts, groups, and messages sent with <b>2-layer end-to-end encryption</b>.</string>
<string name="read_more_in_github">Read more in our GitHub repository.</string>
<string name="read_more_in_github_with_link">Read more in our <font color="#0088ff">GitHub repository</font>.</string>
<!-- MakeConnection -->
<string name="to_make_your_first_private_connection_choose">To make your first private connection, choose <b>one of the following</b>:</string>
<string name="create_1_time_link_qr_code">Create 1-time link / QR code</string>
<string name="it_s_secure_to_share__only_one_contact_can_use_it">It\'s secure to share - only one contact can use it.</string>
<string name="paste_the_link_you_received">Paste the link you received</string>
<string name="or_open_the_link_in_the_browser_and_tap_open_in_mobile">Or open the link in the browser and tap <b>Open in mobile</b>.</string>
<string name="scan_contact_s_qr_code">Scan contact\'s QR code</string>
<string name="in_person_or_via_a_video_call__the_most_secure_way_to_connect">In person or via a video call the most secure way to connect.</string>
<string name="or">or</string>
<string name="connect_with_the_developers">Connect with the developers</string>
<string name="to_ask_any_questions_and_to_receive_simplex_chat_updates">To ask any questions and to receive <xliff:g id="appNameFull">SimpleX Chat</xliff:g> updates.</string>
</resources>

View File

@@ -782,7 +782,7 @@
</trans-unit>
<trans-unit id="The 1st platform without any user identifiers private by design." xml:space="preserve">
<source>The 1st platform without any user identifiers private by design.</source>
<target>Первая в мире платформа без идентификации пользователей.</target>
<target>Первая в мире платформа без идентификаторов пользователей.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">

View File

@@ -552,7 +552,7 @@
"Thank you for installing SimpleX Chat!" = "Спасибо, что установили SimpleX Chat!";
/* No comment provided by engineer. */
"The 1st platform without any user identifiers private by design." = "Первая в мире платформа без идентификации пользователей.";
"The 1st platform without any user identifiers private by design." = "Первая в мире платформа без идентификаторов пользователей.";
/* No comment provided by engineer. */
"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках.";