From d468b34fd6ff7fdfa52a281d4885790d224717b8 Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:58:53 +0000 Subject: [PATCH] refactor, fix --- .../Views/Chat/Group/GroupChatInfoView.swift | 12 +- .../Views/UserSettings/UserAddressView.swift | 128 +++++++----------- .../chat/simplex/common/views/WelcomeView.kt | 4 +- .../views/chat/group/GroupChatInfoView.kt | 25 +--- .../views/usersettings/SetSimplexNameView.kt | 25 ++-- .../views/usersettings/UserAddressView.kt | 33 ++--- 6 files changed, 88 insertions(+), 139 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index a66ad244a3..db9addaa20 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -160,13 +160,11 @@ struct GroupChatInfoView: View { } if groupInfo.useRelays && groupInfo.isOwner && groupLink != nil { - if groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName != nil { - Section(header: Text("Channel SimpleX name").foregroundColor(theme.colors.secondary)) { - channelSimplexNameButton() - } - } else { - Section { - channelSimplexNameButton() + Section { + channelSimplexNameButton() + } header: { + if groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName != nil { + Text("Channel SimpleX name").foregroundColor(theme.colors.secondary) } } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 27a8b5273a..122b420169 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -191,21 +191,36 @@ struct UserAddressView: View { } } - if let d = chatModel.currentUser?.profile.contactDomain?.domain { - Section { - setSimplexNameButton { + Section { + NavigationLink { + let simplexName = if let d = chatModel.currentUser?.profile.contactDomain?.domain { "@\(d)" } else { "" } + SetSimplexDomainView( + title: "Your SimpleX name", + footer: "Let people connect to you via name registered with your SimpleX address.", + prompt: "@yourname.testing", + simplexName: simplexName, + broadcastWarning: NSLocalizedString("Profile update will be sent to your SimpleX contacts.", comment: "alert title"), + save: { simplexDomain in + do { + let u = try await apiSetUserDomain(simplexDomain) + await MainActor.run { chatModel.updateUser(u) } + return true + } catch { + return false + } + } + ) + } label: { + if let d = chatModel.currentUser?.profile.contactDomain?.domain { Label("\(d)", systemImage: "at") - } - } header: { - Text("Your SimpleX name") - .foregroundColor(theme.colors.secondary) - } - } else { - Section { - setSimplexNameButton { + } else { Label("Get SimpleX name (BETA)", systemImage: "at") } } + } header: { + if chatModel.currentUser?.profile.contactDomain?.domain != nil { + Text("Your SimpleX name").foregroundColor(theme.colors.secondary) + } } Section { @@ -227,47 +242,6 @@ struct UserAddressView: View { } } - private func setSimplexNameButton(@ViewBuilder label: () -> L) -> some View { - NavigationLink { - let simplexName = if let d = chatModel.currentUser?.profile.contactDomain?.domain { "@\(d)" } else { "" } - SetSimplexDomainView( - title: "Your SimpleX name", - footer: "Let people connect to you via name registered with your SimpleX address.", - prompt: "@yourname.testing", - simplexName: simplexName, - save: { simplexDomain in - if simplexDomain != chatModel.currentUser?.profile.contactDomain?.domain { - let confirmed = await withCheckedContinuation { (cont: CheckedContinuation) in - DispatchQueue.main.async { - showAlert( - NSLocalizedString("Profile update will be sent to your SimpleX contacts.", comment: "alert title"), - actions: {[ - UIAlertAction(title: NSLocalizedString("Save", comment: "alert action"), style: .default) { _ in - cont.resume(returning: true) - }, - UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert action"), style: .cancel) { _ in - cont.resume(returning: false) - } - ]} - ) - } - } - if !confirmed { return false } - } - do { - let u = try await apiSetUserDomain(simplexDomain) - await MainActor.run { chatModel.updateUser(u) } - return true - } catch { - return false - } - } - ) - } label: { - label() - } - } - @ViewBuilder private func onboardingAddressView(_ userAddress: UserContactLink) -> some View { Section { HStack(spacing: 8) { @@ -751,6 +725,7 @@ struct SetSimplexDomainView: View { let footer: LocalizedStringKey let prompt: String @State var simplexName: String + let broadcastWarning: String? let save: (String?) async -> Bool @Environment(\.dismiss) var dismiss @EnvironmentObject var theme: AppTheme @@ -760,11 +735,12 @@ struct SetSimplexDomainView: View { @State private var editing = false @FocusState private var nameFocused: Bool - init(title: LocalizedStringKey, footer: LocalizedStringKey, prompt: String, simplexName: String, save: @escaping (String?) async -> Bool) { + init(title: LocalizedStringKey, footer: LocalizedStringKey, prompt: String, simplexName: String, broadcastWarning: String? = nil, save: @escaping (String?) async -> Bool) { self.title = title self.footer = footer self.prompt = prompt self._simplexName = State(initialValue: simplexName) + self.broadcastWarning = broadcastWarning self.save = save self._original = State(initialValue: simplexName) self._editing = State(initialValue: simplexName.isEmpty) @@ -822,16 +798,13 @@ struct SetSimplexDomainView: View { } } Button { - saving = true - Task { - let ok = await save(normalized(simplexName)) - await MainActor.run { - saving = false - if ok { - didSave = true - dismiss() - } - } + if let w = broadcastWarning, changed { + showAlert(w, actions: {[ + UIAlertAction(title: NSLocalizedString("Save", comment: "alert action"), style: .default) { _ in saveAndDismiss() }, + UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert action"), style: .cancel) + ]}) + } else { + saveAndDismiss() } } label: { Text("Save") @@ -859,6 +832,7 @@ struct SetSimplexDomainView: View { let saveName = save showAlert( NSLocalizedString("Save SimpleX name?", comment: "alert title"), + message: broadcastWarning, actions: {[ UIAlertAction(title: NSLocalizedString("Save", comment: "alert action"), style: .default) { _ in Task { _ = await saveName(domain) } @@ -870,6 +844,20 @@ struct SetSimplexDomainView: View { } } + private func saveAndDismiss() { + saving = true + Task { + let ok = await save(normalized(simplexName)) + await MainActor.run { + saving = false + if ok { + didSave = true + dismiss() + } + } + } + } + private func normalized(_ s: String) -> String? { let t = s.trimmingCharacters(in: .whitespacesAndNewlines) return t.isEmpty @@ -893,19 +881,7 @@ struct SetSimplexDomainView: View { private func isValidNameLabel(_ label: Substring) -> Bool { if label.isEmpty || label.utf8.count > 63 { return false } - var prev: Unicode.Scalar? = nil - var isFirst = true - for u in label.unicodeScalars { - let v = u.value - if v == 45 { - if isFirst || prev?.value == 45 { return false } - } else if !((v >= 97 && v <= 122) || (v >= 65 && v <= 90) || (v >= 48 && v <= 57)) { - return false - } - prev = u - isFirst = false - } - return prev?.value != 45 + return label.range(of: "^[A-Za-z0-9]+(-[A-Za-z0-9]+)*$", options: .regularExpression) != nil } } 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 0744145663..3e4b8ce1db 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 @@ -410,7 +410,7 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () } @Composable -fun ProfileNameField(name: MutableState, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null, onInvalidTap: (() -> Unit)? = null) { +fun ProfileNameField(name: MutableState, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) { var valid by rememberSaveable { mutableStateOf(true) } var focused by rememberSaveable { mutableStateOf(false) } val strokeColor by remember { @@ -451,7 +451,7 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal leadingIcon = null, trailingIcon = if (!valid && placeholder != "") { { - IconButton((onInvalidTap ?: { showInvalidNameAlert(mkValidName(name.value), name) }), Modifier.size(20.dp)) { + IconButton({ showInvalidNameAlert(mkValidName(name.value), name) }, Modifier.size(20.dp)) { Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 3e70131bc3..afa111f91c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -666,24 +666,13 @@ fun ModalData.GroupChatInfoLayout( if (groupInfo.isOwner && groupLink != null) { SectionDividerSpaced() val channelDomain = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName - if (channelDomain != null) { - SectionView(stringResource(MR.strings.channel_simplex_name)) { - SettingsActionItem( - painterResource(MR.images.ic_tag), - channelDomain, - setSimplexName, - iconColor = MaterialTheme.colors.secondary - ) - } - } else { - SectionView { - SettingsActionItem( - painterResource(MR.images.ic_tag), - stringResource(MR.strings.get_simplex_name_beta), - setSimplexName, - iconColor = MaterialTheme.colors.secondary - ) - } + SectionView(title = if (channelDomain != null) generalGetString(MR.strings.channel_simplex_name) else null) { + SettingsActionItem( + painterResource(MR.images.ic_tag), + channelDomain ?: generalGetString(MR.strings.get_simplex_name_beta), + setSimplexName, + iconColor = MaterialTheme.colors.secondary + ) } } } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt index 106869ce50..d5d88467b1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.* 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.focus.FocusRequester import androidx.compose.ui.platform.LocalClipboardManager @@ -41,6 +40,7 @@ fun SetSimplexDomainView( placeholder: String, simplexName: String, registerBackgroundClose: Boolean = false, + broadcastWarning: String? = null, save: suspend (String?) -> Boolean, close: () -> Unit ) { @@ -58,8 +58,8 @@ fun SetSimplexDomainView( val t = s.trim() return when { t.isEmpty() -> null - t.startsWith("@") || t.startsWith("#") -> addSimplexTLD(t.substring(1)) - else -> addSimplexTLD(t) + t.startsWith("@") || t.startsWith("#") -> addSimplexTLD(t.substring(1).lowercase()) + else -> addSimplexTLD(t.lowercase()) } } @@ -86,7 +86,7 @@ fun SetSimplexDomainView( } saving.value = false if (ok) withContext(Dispatchers.Main) { - chatModel.centerPanelBackgroundClickHandler = null + if (registerBackgroundClose) chatModel.centerPanelBackgroundClickHandler = null close() } } @@ -100,17 +100,18 @@ fun SetSimplexDomainView( return if (changed && valid) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.save_simplex_name_question), + text = broadcastWarning, confirmText = generalGetString(MR.strings.save_verb), onConfirm = { doSave(close) }, dismissText = generalGetString(MR.strings.exit_without_saving), onDismiss = { - chatModel.centerPanelBackgroundClickHandler = null + if (registerBackgroundClose) chatModel.centerPanelBackgroundClickHandler = null close() } ) true } else { - chatModel.centerPanelBackgroundClickHandler = null + if (registerBackgroundClose) chatModel.centerPanelBackgroundClickHandler = null close() false } @@ -123,7 +124,7 @@ fun SetSimplexDomainView( } } onDispose { - chatModel.centerPanelBackgroundClickHandler = null + if (registerBackgroundClose) chatModel.centerPanelBackgroundClickHandler = null } } @@ -138,11 +139,11 @@ fun SetSimplexDomainView( focusRequester.requestFocus() } SectionItemViewSpaceBetween(click = { focusRequester.requestFocus() }) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { + Box(Modifier.weight(1f)) { PlainTextEditor(name, placeholder = placeholder, contentPadding = PaddingValues(), focusRequester = focusRequester) - if (!isValid) { - Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error) - } + } + if (!isValid) { + Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error) } } } else { @@ -164,7 +165,7 @@ fun SetSimplexDomainView( Text(stringResource(MR.strings.register_test_name), color = MaterialTheme.colors.primary) } } - SectionItemView({ doSave(close) }, disabled = unchanged || saving.value || !isValid) { + SectionItemView({ if (broadcastWarning != null && !unchanged) AlertManager.shared.showAlertDialog(title = broadcastWarning, confirmText = generalGetString(MR.strings.save_verb), onConfirm = { doSave(close) }) else doSave(close) }, disabled = unchanged || saving.value || !isValid) { Text( stringResource(MR.strings.save_verb), color = if (unchanged || saving.value || !isValid) MaterialTheme.colors.secondary else MaterialTheme.colors.primary diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index bfe42f7078..5394bf1040 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -35,9 +35,7 @@ import chat.simplex.common.views.newchat.* import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext -import kotlin.coroutines.resume @Composable fun UserAddressView( @@ -378,29 +376,16 @@ private fun UserAddressLayout( placeholder = "@yourname.testing", simplexName = if (domain == null) "" else "@$domain", registerBackgroundClose = true, + broadcastWarning = generalGetString(MR.strings.profile_update_will_be_sent_to_contacts), save = { simplexDomain -> - val changed = simplexDomain != domain - val proceed = if (changed) { - suspendCancellableCoroutine { cont -> - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.profile_update_will_be_sent_to_contacts), - confirmText = generalGetString(MR.strings.save_verb), - onConfirm = { if (cont.isActive) cont.resume(true) }, - onDismiss = { if (cont.isActive) cont.resume(false) }, - onDismissRequest = { if (cont.isActive) cont.resume(false) }, - ) - } - } else true - if (proceed) { - try { - val u = chatModel.controller.apiSetUserDomain(user?.remoteHostId, simplexDomain) - withContext(Dispatchers.Main) { chatModel.updateUser(u) } - true - } catch (e: Exception) { - Log.e(TAG, "apiSetUserDomain: ${e.message}") - false - } - } else false + try { + val u = chatModel.controller.apiSetUserDomain(user?.remoteHostId, simplexDomain) + withContext(Dispatchers.Main) { chatModel.updateUser(u) } + true + } catch (e: Exception) { + Log.e(TAG, "apiSetUserDomain: ${e.message}") + false + } }, close = close )