refactor, fix

This commit is contained in:
Evgeny @ SimpleX Chat
2026-07-11 21:58:53 +00:00
parent 17e20b6920
commit d468b34fd6
6 changed files with 88 additions and 139 deletions
@@ -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)
}
}
}
@@ -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<L: View>(@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<Bool, Never>) 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
}
}
@@ -410,7 +410,7 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: ()
}
@Composable
fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null, onInvalidTap: (() -> Unit)? = null) {
fun ProfileNameField(name: MutableState<String>, 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<String>, 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)
}
}
@@ -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 {
@@ -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
@@ -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<Boolean> { 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
)