From 34e1e44338e77f038821d4e6a86bc70df7c6ba22 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 6 Oct 2023 04:49:18 +0800 Subject: [PATCH] android, desktop: profile names (remove full name) (#3177) * desktop, android: profile names (remove full name) * rename back * disallow spaces only in names * ios: disallow spaces only in names * changes --- .../Views/Chat/Group/GroupProfileView.swift | 6 +- .../Shared/Views/NewChat/AddGroupView.swift | 3 +- .../Views/UserSettings/UserProfile.swift | 2 +- .../src/commonMain/cpp/android/simplex-api.c | 9 + .../src/commonMain/cpp/desktop/simplex-api.c | 11 +- .../kotlin/chat/simplex/common/App.kt | 3 +- .../chat/simplex/common/platform/Core.kt | 1 + .../chat/simplex/common/views/WelcomeView.kt | 276 ++++++++++++------ .../views/chat/group/GroupProfileView.kt | 46 +-- .../common/views/newchat/AddGroupView.kt | 31 +- .../common/views/onboarding/OnboardingView.kt | 40 --- .../common/views/usersettings/SettingsView.kt | 2 +- .../views/usersettings/UserProfileView.kt | 64 ++-- .../views/usersettings/UserProfilesView.kt | 3 +- .../commonMain/resources/MR/base/strings.xml | 11 +- 15 files changed, 288 insertions(+), 220 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 6ff3af66a..7e123c389 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -80,7 +80,7 @@ struct GroupProfileView: View { HStack(spacing: 20) { Button("Cancel") { dismiss() } Button("Save group profile") { saveProfile() } - .disabled(groupProfile.displayName == "" || !validNewProfileName()) + .disabled(!canUpdateProfile()) } } .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) @@ -134,6 +134,10 @@ struct GroupProfileView: View { .onTapGesture { hideKeyboard() } } + private func canUpdateProfile() -> Bool { + groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName() + } + private func validNewProfileName() -> Bool { groupProfile.displayName == groupInfo.groupProfile.displayName || validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces)) diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 5cea52cc8..186a24e99 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -179,7 +179,8 @@ struct AddGroupView: View { } func canCreateProfile() -> Bool { - profile.displayName != "" && validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces)) + let name = profile.displayName.trimmingCharacters(in: .whitespaces) + return name != "" && validDisplayName(name) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 88be0b6a7..b1a362a5a 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -162,7 +162,7 @@ struct UserProfile: View { } private func canSaveProfile(_ user: User) -> Bool { - profile.displayName != "" && validNewProfileName(user) + profile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName(user) } func saveProfile() { diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index dbfee7025..351ed93c9 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -46,6 +46,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); +extern char *chat_valid_name(const char *name); extern char *chat_write_file(const char *path, char *ptr, int length); extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_encrypt_file(const char *from_path, const char *to_path); @@ -121,6 +122,14 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz, jstring name) { + const char *_name = (*env)->GetStringUTFChars(env, name, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_valid_name(_name)); + (*env)->ReleaseStringUTFChars(env, name, _name); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index ddc5c92f9..f36c86c36 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -21,6 +21,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); +extern char *chat_valid_name(const char *name); extern char *chat_write_file(const char *path, char *ptr, int length); extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_encrypt_file(const char *from_path, const char *to_path); @@ -75,7 +76,7 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass cla jstring res = decode_to_utf8_string(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl)); (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); - (*env)->ReleaseStringUTFChars(env, dbKey, _confirm); + (*env)->ReleaseStringUTFChars(env, confirm, _confirm); // Creating array of Object's (boxed values can be passed, eg. Long instead of long) jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); @@ -133,6 +134,14 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass cl return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz, jstring name) { + const char *_name = encode_to_utf8_chars(env, name); + jstring res = decode_to_utf8_string(env, chat_valid_name(_name)); + (*env)->ReleaseStringUTFChars(env, name, _name); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { const char *_path = encode_to_utf8_chars(env, path); diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index c08ad5f91..4531f88f9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -17,6 +17,7 @@ import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.CreateFirstProfile import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.common.views.SplashView import chat.simplex.common.views.call.ActiveCallView @@ -135,7 +136,7 @@ fun MainScreen() { ModalManager.fullscreen.showInView() } } - onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {} + onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel) onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 801a0270e..2bed24b1f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -20,6 +20,7 @@ external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String external fun chatPasswordHash(pwd: String, salt: String): String +external fun chatValidName(name: String): String external fun chatWriteFile(path: String, buffer: ByteBuffer): String external fun chatReadFile(path: String, key: String, nonce: String): Array external fun chatEncryptFile(fromPath: String, toPath: String): String diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 13ce16d0a..504ecac89 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -1,10 +1,10 @@ package chat.simplex.common.views +import SectionTextFooter import androidx.compose.foundation.* 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.MaterialTheme.colors import androidx.compose.runtime.* @@ -18,115 +18,160 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatModel import chat.simplex.common.model.Profile -import chat.simplex.common.platform.appPlatform -import chat.simplex.common.platform.navigationBarsWithImePadding +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.OnboardingStage -import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.views.onboarding.* +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged - -fun isValidDisplayName(name: String) : Boolean { - return (name.firstOrNull { it.isWhitespace() }) == null && !name.startsWith("@") && !name.startsWith("#") -} +import kotlinx.coroutines.launch @Composable -fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) { - val displayName = rememberSaveable { mutableStateOf("") } - val fullName = rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } +fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + val keyboardState by getKeyboardState() + var savedKeyboardState by remember { mutableStateOf(keyboardState) } - Column( - modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) - ) { - /*CloseSheetBar(close = { - if (chatModel.users.isEmpty()) { - chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo - } else { - close() - } - })*/ - Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) - ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1) - ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1) - Spacer(Modifier.height(DEFAULT_PADDING)) - Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - stringResource(MR.strings.display_name), - fontSize = 16.sp - ) - if (!isValidDisplayName(displayName.value)) { - Text( - stringResource(MR.strings.no_spaces), - fontSize = 16.sp, - color = Color.Red - ) - } - } - ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) - Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.full_name_optional__prompt), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(fullName, "") - } - Spacer(Modifier.fillMaxHeight().weight(1f)) - Row { - if (chatModel.users.isEmpty()) { - SimpleButtonDecorated( - text = stringResource(MR.strings.about_simplex), - icon = painterResource(MR.images.ic_arrow_back_ios_new), - textDecoration = TextDecoration.None, - fontWeight = FontWeight.Medium - ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } - } - Spacer(Modifier.fillMaxWidth().weight(1f)) - val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) - val createModifier: Modifier - val createColor: Color - if (enabled) { - createModifier = Modifier.clickable { - if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { - createProfileInProfiles(chatModel, displayName.value, fullName.value, close) - } else { - createProfileOnboarding(chatModel, displayName.value, fullName.value, close) + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 20.dp) + ) { + val displayName = rememberSaveable { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) + ) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) + Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(MR.strings.display_name), + fontSize = 16.sp + ) + val name = displayName.value.trim() + val validName = mkValidName(name) + Spacer(Modifier.height(20.dp)) + if (name != validName) { + IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } + ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester) + } + SettingsActionItem( + painterResource(MR.images.ic_check), + stringResource(MR.strings.create_another_profile_button), + disabled = !canCreateProfile(displayName.value), + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary, + click = { createProfileInProfiles(chatModel, displayName.value, close) }, + ) + SectionTextFooter(generalGetString(MR.strings.your_profile_is_stored_on_your_device)) + SectionTextFooter(generalGetString(MR.strings.profile_is_only_shared_with_your_contacts)) + + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() + } + } + if (savedKeyboardState != keyboardState) { + LaunchedEffect(keyboardState) { + scope.launch { + savedKeyboardState = keyboardState + scrollState.animateScrollTo(scrollState.maxValue) } - }.padding(8.dp) - createColor = MaterialTheme.colors.primary - } else { - createModifier = Modifier.padding(8.dp) - createColor = MaterialTheme.colors.secondary - } - Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { - Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium) - Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor) } } - } - - LaunchedEffect(Unit) { - delay(300) - focusRequester.requestFocus() } } } -fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { +@Composable +fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + val keyboardState by getKeyboardState() + var savedKeyboardState by remember { mutableStateOf(keyboardState) } + + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 20.dp) + ) { + val displayName = rememberSaveable { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) + ) { + /*CloseSheetBar(close = { + if (chatModel.users.isEmpty()) { + chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo + } else { + close() + } + })*/ + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) + ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + Spacer(Modifier.height(DEFAULT_PADDING)) + Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(MR.strings.display_name), + fontSize = 16.sp + ) + val name = displayName.value.trim() + val validName = mkValidName(name) + Spacer(Modifier.height(20.dp)) + if (name != validName) { + IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } + ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester) + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + OnboardingButtons(displayName, close) + + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() + } + } + LaunchedEffect(Unit) { + setLastVersionDefault(chatModel) + } + if (savedKeyboardState != keyboardState) { + LaunchedEffect(keyboardState) { + scope.launch { + savedKeyboardState = keyboardState + scrollState.animateScrollTo(scrollState.maxValue) + } + } + } + } + } +} + +fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { withApi { val user = chatModel.controller.apiCreateActiveUser( - Profile(displayName, fullName, null) + Profile(displayName.trim(), "", null) ) ?: return@withApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { @@ -142,10 +187,10 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: } } -fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { +fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) { withApi { chatModel.controller.apiCreateActiveUser( - Profile(displayName, fullName, null) + Profile(displayName.trim(), "", null) ) ?: return@withApi val onboardingStage = chatModel.controller.appPrefs.onboardingStage if (chatModel.users.isEmpty()) { @@ -163,6 +208,28 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: } } +@Composable +fun OnboardingButtons(displayName: MutableState, close: () -> Unit) { + Row { + SimpleButtonDecorated( + text = stringResource(MR.strings.about_simplex), + icon = painterResource(MR.images.ic_arrow_back_ios_new), + textDecoration = TextDecoration.None, + fontWeight = FontWeight.Medium + ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } + Spacer(Modifier.fillMaxWidth().weight(1f)) + val enabled = canCreateProfile(displayName.value) + val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp) + val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { + Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium) + Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor) + } + } + } +} + @Composable fun ProfileNameField(name: MutableState, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) { var valid by rememberSaveable { mutableStateOf(true) } @@ -195,10 +262,6 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal onValueChange = { name.value = it }, modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester), textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - autoCorrect = false - ), singleLine = true, cursorBrush = SolidColor(MaterialTheme.colors.secondary) ) @@ -211,3 +274,28 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal } } } + +private fun canCreateProfile(displayName: String): Boolean { + val name = displayName.trim() + return name.isNotEmpty() && mkValidName(name) == name +} + +fun showInvalidNameAlert(name: String, displayName: MutableState) { + if (name.isEmpty()) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_name), + ) + } else { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.invalid_name), + text = generalGetString(MR.strings.correct_name_to).format(name), + onConfirm = { + displayName.value = name + } + ) + } +} + +fun isValidDisplayName(name: String) : Boolean = mkValidName(name.trim()) == name + +fun mkValidName(s: String): String = chatValidName(s) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index ac13dd483..5376cb092 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -19,12 +19,12 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.ProfileNameField +import chat.simplex.common.views.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.net.URI @@ -65,13 +65,13 @@ fun GroupProfileLayout( fullName.value == groupProfile.fullName && groupProfile.image == profileImage.value val closeWithAlert = { - if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) { + if (dataUnchanged || !canUpdateProfile(displayName.value, groupProfile)) { close() } else { showUnsavedChangesAlert({ saveProfile( groupProfile.copy( - displayName = displayName.value, + displayName = displayName.value.trim(), fullName = fullName.value, image = profileImage.value ) @@ -125,32 +125,32 @@ fun GroupProfileLayout( stringResource(MR.strings.group_display_name_field), fontSize = 16.sp ) - if (!isValidDisplayName(displayName.value)) { + if (!isValidNewProfileName(displayName.value, groupProfile)) { Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - Text( - stringResource(MR.strings.no_spaces), - fontSize = 16.sp, - color = Color.Red - ) + IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } } } - ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) + ProfileNameField(displayName, "", { isValidNewProfileName(it, groupProfile) }, focusRequester) + if (groupProfile.fullName.isNotEmpty() && groupProfile.fullName != groupProfile.displayName) { + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.group_full_name_field), + fontSize = 16.sp, + modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) + ) + ProfileNameField(fullName) + } Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.group_full_name_field), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(fullName) - Spacer(Modifier.height(DEFAULT_PADDING)) - val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) + val enabled = !dataUnchanged && canUpdateProfile(displayName.value, groupProfile) if (enabled) { Text( stringResource(MR.strings.save_group_profile), modifier = Modifier.clickable { saveProfile( groupProfile.copy( - displayName = displayName.value, + displayName = displayName.value.trim(), fullName = fullName.value, image = profileImage.value ) @@ -178,6 +178,12 @@ fun GroupProfileLayout( } } +private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = + displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, groupProfile) + +private fun isValidNewProfileName(displayName: String, groupProfile: GroupProfile): Boolean = + displayName == groupProfile.displayName || isValidDisplayName(displayName.trim()) + private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { AlertManager.shared.showAlertDialogStacked( title = generalGetString(MR.strings.save_preferences_question), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 3ab5ef9b2..6ad919c27 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -19,15 +19,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.ProfileNameField import chat.simplex.common.views.chat.group.AddGroupMembersView import chat.simplex.common.views.chatlist.setGroupMembers import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.usersettings.DeleteImageButton import chat.simplex.common.views.usersettings.EditImageButton import chat.simplex.common.platform.* +import chat.simplex.common.views.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -60,7 +59,6 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() val displayName = rememberSaveable { mutableStateOf("") } - val fullName = rememberSaveable { mutableStateOf("") } val chosenImage = rememberSaveable { mutableStateOf(null) } val profileImage = rememberSaveable { mutableStateOf(null) } val focusRequester = remember { FocusRequester() } @@ -110,31 +108,22 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { stringResource(MR.strings.group_display_name_field), fontSize = 16.sp ) - if (!isValidDisplayName(displayName.value)) { + if (!isValidDisplayName(displayName.value.trim())) { Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - Text( - stringResource(MR.strings.no_spaces), - fontSize = 16.sp, - color = Color.Red - ) + IconButton({ showInvalidNameAlert(mkValidName(displayName.value.trim()), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } } } - ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) - Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.group_full_name_field), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(fullName, "") + ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) Spacer(Modifier.height(8.dp)) - val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) + val enabled = canCreateProfile(displayName.value) if (enabled) { CreateGroupButton(MaterialTheme.colors.primary, Modifier .clickable { createGroup(GroupProfile( - displayName = displayName.value, - fullName = fullName.value, + displayName = displayName.value.trim(), + fullName = "", image = profileImage.value )) } @@ -167,6 +156,8 @@ fun CreateGroupButton(color: Color, modifier: Modifier) { } } +fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim()) + @Preview @Composable fun PreviewAddGroupLayout() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt index 119ed8cd4..0a48bbd1e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt @@ -1,16 +1,5 @@ package chat.simplex.common.views.onboarding -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.ProvideWindowInsets -import chat.simplex.common.views.CreateProfilePanel -import chat.simplex.common.platform.getKeyboardState -import kotlinx.coroutines.launch - enum class OnboardingStage { Step1_SimpleXInfo, Step2_CreateProfile, @@ -19,32 +8,3 @@ enum class OnboardingStage { Step4_SetNotificationsMode, OnboardingComplete } - -@Composable -fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { - val scope = rememberCoroutineScope() - val scrollState = rememberScrollState() - val keyboardState by getKeyboardState() - var savedKeyboardState by remember { mutableStateOf(keyboardState) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = 20.dp) - ) { - CreateProfilePanel(chatModel, close) - LaunchedEffect(Unit) { - setLastVersionDefault(chatModel) - } - if (savedKeyboardState != keyboardState) { - LaunchedEffect(keyboardState) { - scope.launch { - savedKeyboardState = keyboardState - scrollState.animateScrollTo(scrollState.maxValue) - } - } - } - } - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index eebe7b3f4..d949f800b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -364,7 +364,7 @@ fun AppVersionItem(showVersion: () -> Unit) { maxLines = 1, overflow = TextOverflow.Ellipsis ) - if (profileOf.fullName.isNotEmpty()) { + if (profileOf.fullName.isNotEmpty() && profileOf.fullName != profileOf.displayName) { Text( profileOf.fullName, Modifier.padding(vertical = 5.dp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index a7ae3a675..38c9e58ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -17,14 +17,12 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.ProfileNameField import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.onboarding.ReadableText -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.Profile import chat.simplex.common.platform.* +import chat.simplex.common.views.* import chat.simplex.res.MR import kotlinx.coroutines.launch import java.net.URI @@ -39,7 +37,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { close, saveProfile = { displayName, fullName, image -> withApi { - val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image)) + val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName.trim(), fullName = fullName, image = image)) if (updated != null) { val (newProfile, _) = updated chatModel.updateCurrentUser(newProfile) @@ -89,7 +87,7 @@ fun UserProfileLayout( profile.image == profileImage.value val closeWithAlert = { - if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) { + if (dataUnchanged || !canSaveProfile(displayName.value, profile)) { close() } else { showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, profileImage.value) }, close) @@ -128,36 +126,27 @@ fun UserProfileLayout( stringResource(MR.strings.display_name__field), fontSize = 16.sp ) - if (!isValidDisplayName(displayName.value)) { - Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - Text( - stringResource(MR.strings.no_spaces), - fontSize = 16.sp, - color = Color.Red - ) + if (!isValidNewProfileName(displayName.value, profile)) { + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } } } - ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) - Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.full_name__field), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(fullName) - - Spacer(Modifier.height(DEFAULT_PADDING)) - val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) - val saveModifier: Modifier - val saveColor: Color - if (enabled) { - saveModifier = Modifier - .clickable { saveProfile(displayName.value, fullName.value, profileImage.value) } - saveColor = MaterialTheme.colors.primary - } else { - saveModifier = Modifier - saveColor = MaterialTheme.colors.secondary + ProfileNameField(displayName, "", { isValidNewProfileName(it, profile) }, focusRequester) + if (showFullName(profile)) { + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.full_name__field), + fontSize = 16.sp, + modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) + ) + ProfileNameField(fullName) } + Spacer(Modifier.height(DEFAULT_PADDING)) + val enabled = !dataUnchanged && canSaveProfile(displayName.value, profile) + val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, profileImage.value) } + val saveColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary Text( stringResource(MR.strings.save_and_notify_contacts), modifier = saveModifier, @@ -216,6 +205,15 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { ) } +private fun isValidNewProfileName(displayName: String, profile: Profile): Boolean = + displayName == profile.displayName || isValidDisplayName(displayName.trim()) + +private fun showFullName(profile: Profile): Boolean = + profile.fullName.isNotEmpty() && profile.fullName != profile.displayName + +private fun canSaveProfile(displayName: String, profile: Profile): Boolean = + displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, profile) + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index 7929413c9..7d3239700 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -28,9 +28,8 @@ import chat.simplex.common.views.chatlist.UserProfilePickerItem import chat.simplex.common.views.chatlist.UserProfileRow import chat.simplex.common.views.database.PassphraseField import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.CreateProfile -import chat.simplex.common.model.* import chat.simplex.common.platform.appPlatform +import chat.simplex.common.views.CreateProfile import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.delay diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index d57211f4a..7912a5ad1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -675,7 +675,7 @@ Your contacts in SimpleX will see it.\nYou can change it in Settings. - Display name: + Profile name: Full name: Your current profile Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. @@ -703,11 +703,12 @@ Create profile Your profile, contacts and delivered messages are stored on your device. The profile is only shared with your contacts. - No spaces! Display name cannot contain whitespace. - Display Name - Full Name (optional) + Enter your name: Create + Create profile + Invalid name! + Correct name to %s? About SimpleX @@ -1290,7 +1291,7 @@ Create secret group The group is fully decentralized – it is visible only to the members. - Group display name: + Enter group name: Group full name: Your chat profile will be sent to group members