From c278d22ce3409b9e40b9c3fb98bb1edad42dc2ac Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Sun, 12 Jul 2026 06:32:21 +0000 Subject: [PATCH] refactor --- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 86 ++++++++++++++----- apps/ios/Shared/Views/Chat/ChatView.swift | 52 +---------- .../Views/Chat/Group/GroupChatInfoView.swift | 33 +------ .../simplex/common/views/chat/ChatInfoView.kt | 15 +--- .../simplex/common/views/chat/ChatView.kt | 51 +---------- .../common/views/chat/SimplexNameView.kt | 58 ++++++++++++- .../views/chat/group/GroupChatInfoView.kt | 27 +----- 7 files changed, 128 insertions(+), 194 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 2078e9048a..c12740d364 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -393,26 +393,7 @@ struct ChatInfoView: View { .lineLimit(3) .padding(.bottom, 2) } - if let domain = contact.profile.contactDomain, - contact.profile.contactDomainVerified != nil || domain.proof != nil { - SimplexNameView( - simplexName: "@\(domain.domain)", - verified: contact.profile.contactDomainVerified, - verify: { - do { - let (ct, reason) = try await apiVerifyContactDomain(contact.contactId) - await MainActor.run { - chatModel.updateContact(ct) - contact = ct - } - return (ct.profile.contactDomainVerified, reason) - } catch { - logger.error("apiVerifyContactDomain: \(responseError(error))") - return nil - } - } - ) - } + contactSimplexNameView(contact) { contact = $0 } if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) @@ -1374,19 +1355,80 @@ private func deleteNotReadyContact( )) } +@ViewBuilder func contactSimplexNameView(_ contact: Contact, verifiable: Bool = true, onUpdate: ((Contact) -> Void)? = nil) -> some View { + if let domain = contact.profile.contactDomain, + contact.profile.contactDomainVerified != nil || domain.proof != nil { + SimplexNameView( + simplexName: "@\(domain.domain)", + verified: contact.profile.contactDomainVerified, + verify: { + do { + let (ct, reason) = try await apiVerifyContactDomain(contact.contactId) + await MainActor.run { + ChatModel.shared.updateContact(ct) + onUpdate?(ct) + } + return (ct.profile.contactDomainVerified, reason) + } catch { + logger.error("apiVerifyContactDomain: \(responseError(error))") + return nil + } + }, + verifiable: verifiable + ) + } +} + +@ViewBuilder func groupSimplexNameView(_ groupInfo: GroupInfo, verifiable: Bool = true, onUpdate: ((GroupInfo) -> Void)? = nil) -> some View { + if groupInfo.businessChat == nil { + if let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess, + let domain = access.groupDomainClaim?.shortName, + groupInfo.groupDomainVerified != nil || access.groupDomainClaim?.proof != nil { + SimplexNameView( + simplexName: "#\(domain)", + verified: groupInfo.groupDomainVerified, + verify: { + do { + let (gInfo, reason) = try await apiVerifyGroupDomain(groupInfo.groupId) + await MainActor.run { + ChatModel.shared.updateGroup(gInfo) + onUpdate?(gInfo) + } + return (gInfo.groupDomainVerified, reason) + } catch { + logger.error("apiVerifyGroupDomain: \(responseError(error))") + return nil + } + }, + verifiable: verifiable + ) + } + } else if let claim = groupInfo.businessChat?.businessDomain, + groupInfo.groupDomainVerified != nil || claim.proof != nil { + // A business presents as a contact, so the name retains its .simplex suffix; it cannot be re-verified. + SimplexNameView( + simplexName: "@\(claim.domain)", + verified: groupInfo.groupDomainVerified, + verify: { nil }, + verifiable: false + ) + } +} + struct SimplexNameView: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) var autoVerify = false let simplexName: String let verified: Bool? let verify: () async -> (Bool?, String?)? + var verifiable: Bool = true @State private var inFlight = false @State private var showSpinner = false var body: some View { content .padding(.bottom, 2) - .onAppear { if autoVerify && verified == nil { runVerify(manual: false) } } + .onAppear { if verifiable && autoVerify && verified == nil { runVerify(manual: false) } } } private var nameText: Text { @@ -1415,6 +1457,8 @@ struct SimplexNameView: View { UIPasteboard.general.string = simplexName UIImpactFeedbackGenerator(style: .rigid).impactOccurred() } + } else if !verifiable { + nameText } else if verified == false { HStack(alignment: .firstTextBaseline, spacing: 4) { nameText diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index cf13f83b65..21586d9533 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -1039,57 +1039,9 @@ struct ChatView: View { switch chat.chatInfo { case let .direct(contact): - if let domain = contact.profile.contactDomain, - contact.profile.contactDomainVerified != nil || domain.proof != nil { - SimplexNameView( - simplexName: "@\(domain.domain)", - verified: contact.profile.contactDomainVerified, - verify: { - do { - let (ct, reason) = try await apiVerifyContactDomain(contact.contactId) - await MainActor.run { - ChatModel.shared.updateContact(ct) - } - return (ct.profile.contactDomainVerified, reason) - } catch { - logger.error("apiVerifyContactDomain: \(responseError(error))") - return nil - } - } - ) - } + contactSimplexNameView(contact, verifiable: false) case let .group(groupInfo, _): - if groupInfo.businessChat == nil { - if let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess, - let domain = access.groupDomainClaim?.shortName, - groupInfo.groupDomainVerified != nil || access.groupDomainClaim?.proof != nil { - SimplexNameView( - simplexName: "#\(domain)", - verified: groupInfo.groupDomainVerified, - verify: { - do { - let (gInfo, reason) = try await apiVerifyGroupDomain(groupInfo.groupId) - await MainActor.run { - ChatModel.shared.updateGroup(gInfo) - } - return (gInfo.groupDomainVerified, reason) - } catch { - logger.error("apiVerifyGroupDomain: \(responseError(error))") - return nil - } - } - ) - } - } else { - if let claim = groupInfo.businessChat?.businessDomain, - groupInfo.groupDomainVerified != nil || claim.proof != nil { - SimplexNameView( - simplexName: "@\(claim.domain)", - verified: groupInfo.groupDomainVerified, - verify: { nil } - ) - } - } + groupSimplexNameView(groupInfo, verifiable: false) default: EmptyView() } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index db9addaa20..1be8070259 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -341,38 +341,7 @@ struct GroupChatInfoView: View { .lineLimit(4) .fixedSize(horizontal: false, vertical: true) } - if let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess, - let domain = access.groupDomainClaim?.shortName, - groupInfo.groupDomainVerified != nil || access.groupDomainClaim?.proof != nil { - SimplexNameView( - simplexName: "#\(domain)", - verified: groupInfo.groupDomainVerified, - verify: { - do { - let (gInfo, reason) = try await apiVerifyGroupDomain(groupInfo.groupId) - await MainActor.run { - chatModel.updateGroup(gInfo) - groupInfo = gInfo - } - return (gInfo.groupDomainVerified, reason) - } catch { - logger.error("apiVerifyGroupDomain: \(responseError(error))") - return nil - } - } - ) - } - if let claim = groupInfo.businessChat?.businessDomain, - groupInfo.groupDomainVerified != nil || claim.proof != nil { - // A business presents as a contact, so the name retains its .simplex suffix. The tick comes from - // groupDomainVerified (set at connect); its domain proof is not received on the wire yet, so - // re-verification is not wired. - SimplexNameView( - simplexName: "@\(claim.domain)", - verified: groupInfo.groupDomainVerified, - verify: { nil } - ) - } + groupSimplexNameView(groupInfo) { groupInfo = $0 } if let webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage, let url = URL(string: webPage) { Link(destination: url) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index bfcbcdefac..a5cce3c258 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -757,20 +757,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) ) ChatInfoDescription(cInfo, displayName, copyNameToClipboard) - val domain = contact.profile.contactDomain - if (domain != null && (contact.profile.contactDomainVerified != null || domain.proof != null)) { - SimplexNameView( - simplexName = "@${domain.domain}", - verified = contact.profile.contactDomainVerified, - verify = { - val rhId = chatModel.remoteHostId() - chatModel.controller.apiVerifyContactDomain(rhId, contact.contactId)?.let { (ct, reason) -> - chatModel.chatsContext.updateContact(rhId, ct) - ct.profile.contactDomainVerified to reason - } - } - ) - } + ContactSimplexNameView(contact) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index d646a6791d..c1603f2bdc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -2344,55 +2344,8 @@ fun BoxScope.ChatItemsList( } when (chatInfo) { - is ChatInfo.Direct -> { - val contact = chatInfo.contact - val domain = contact.profile.contactDomain - if (domain != null && (contact.profile.contactDomainVerified != null || domain.proof != null)) { - SimplexNameView( - simplexName = "@${domain.domain}", - verified = contact.profile.contactDomainVerified, - verify = { - val rhId = chatModel.remoteHostId() - chatModel.controller.apiVerifyContactDomain(rhId, contact.contactId)?.let { (ct, reason) -> - chatModel.chatsContext.updateContact(rhId, ct) - ct.profile.contactDomainVerified to reason - } - } - ) - } - } - is ChatInfo.Group -> { - val groupInfo = chatInfo.groupInfo - if (groupInfo.businessChat == null) { - val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess - val domain = access?.groupDomainClaim?.shortName - if (domain != null && (groupInfo.groupDomainVerified != null || access.groupDomainClaim?.proof != null)) { - SimplexNameView( - simplexName = "#${domain}", - verified = groupInfo.groupDomainVerified, - verify = { - val rhId = chatModel.remoteHostId() - chatModel.controller.apiVerifyGroupDomain(rhId, groupInfo.groupId)?.let { (gInfo, reason) -> - chatModel.chatsContext.updateGroup(rhId, gInfo) - gInfo.groupDomainVerified to reason - } - } - ) - } - } else { - val businessClaim = groupInfo.businessChat?.businessDomain - if (businessClaim != null && (groupInfo.groupDomainVerified != null || businessClaim.proof != null)) { - // A business presents as a contact, so the name retains its .simplex suffix. The tick comes from - // groupDomainVerified (set at connect); its domain proof is not received on the wire yet, so - // re-verification is not wired. - SimplexNameView( - simplexName = "@${businessClaim.domain}", - verified = groupInfo.groupDomainVerified, - verify = { null } - ) - } - } - } + is ChatInfo.Direct -> ContactSimplexNameView(chatInfo.contact, verifiable = false) + is ChatInfo.Group -> GroupSimplexNameView(chatInfo.groupInfo, verifiable = false) else -> {} } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt index 397778b8ee..d3e3ffbe61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.* import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.ImageResource -import chat.simplex.common.model.SimplexNameInfo +import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.common.views.helpers.* @@ -28,6 +28,7 @@ import kotlinx.coroutines.* fun SimplexNameView( simplexName: String, verified: Boolean?, + verifiable: Boolean = true, verify: suspend () -> Pair? ) { val scope = rememberCoroutineScope() @@ -60,7 +61,7 @@ fun SimplexNameView( } LaunchedEffect(Unit) { - if (chatModel.controller.appPrefs.privacyVerifySimplexNames.get() && verified == null) runVerify(manual = false) + if (verifiable && chatModel.controller.appPrefs.privacyVerifySimplexNames.get() && verified == null) runVerify(manual = false) } val clipboard = LocalClipboardManager.current @@ -82,6 +83,7 @@ fun SimplexNameView( clipboard.setText(AnnotatedString(simplexName)) showToast(generalGetString(MR.strings.copied)) } + !verifiable -> Text(simplexName, style = nameStyle) verified == false -> SimplexNameWithIcon(simplexName, nameStyle, MR.images.ic_close, Color.Red) { runVerify(manual = true) } else -> { @@ -113,3 +115,55 @@ private fun SimplexNameWithIcon(name: String, style: TextStyle, icon: ImageResou ) } } + +@Composable +fun ContactSimplexNameView(contact: Contact, verifiable: Boolean = true) { + val domain = contact.profile.contactDomain + if (domain != null && (contact.profile.contactDomainVerified != null || domain.proof != null)) { + SimplexNameView( + simplexName = "@${domain.domain}", + verified = contact.profile.contactDomainVerified, + verifiable = verifiable, + verify = { + val rhId = chatModel.remoteHostId() + chatModel.controller.apiVerifyContactDomain(rhId, contact.contactId)?.let { (ct, reason) -> + chatModel.chatsContext.updateContact(rhId, ct) + ct.profile.contactDomainVerified to reason + } + } + ) + } +} + +@Composable +fun GroupSimplexNameView(groupInfo: GroupInfo, verifiable: Boolean = true) { + if (groupInfo.businessChat == null) { + val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess + val domain = access?.groupDomainClaim?.shortName + if (domain != null && (groupInfo.groupDomainVerified != null || access.groupDomainClaim?.proof != null)) { + SimplexNameView( + simplexName = "#${domain}", + verified = groupInfo.groupDomainVerified, + verifiable = verifiable, + verify = { + val rhId = chatModel.remoteHostId() + chatModel.controller.apiVerifyGroupDomain(rhId, groupInfo.groupId)?.let { (gInfo, reason) -> + chatModel.chatsContext.updateGroup(rhId, gInfo) + gInfo.groupDomainVerified to reason + } + } + ) + } + } else { + val businessClaim = groupInfo.businessChat?.businessDomain + if (businessClaim != null && (groupInfo.groupDomainVerified != null || businessClaim.proof != null)) { + // A business presents as a contact, so the name retains its .simplex suffix; it cannot be re-verified. + SimplexNameView( + simplexName = "@${businessClaim.domain}", + verified = groupInfo.groupDomainVerified, + verifiable = false, + verify = { null } + ) + } + } +} 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 afa111f91c..cfa97eabba 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 @@ -979,32 +979,7 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) { modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) ) ChatInfoDescription(cInfo, displayName, copyNameToClipboard) - val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess - val domain = access?.groupDomainClaim?.shortName - if (domain != null && (groupInfo.groupDomainVerified != null || access.groupDomainClaim?.proof != null)) { - SimplexNameView( - simplexName = "#${domain}", - verified = groupInfo.groupDomainVerified, - verify = { - val rhId = chatModel.remoteHostId() - chatModel.controller.apiVerifyGroupDomain(rhId, groupInfo.groupId)?.let { (gInfo, reason) -> - chatModel.chatsContext.updateGroup(rhId, gInfo) - gInfo.groupDomainVerified to reason - } - } - ) - } - val businessClaim = groupInfo.businessChat?.businessDomain - if (businessClaim != null && (groupInfo.groupDomainVerified != null || businessClaim.proof != null)) { - // A business presents as a contact, so the name retains its .simplex suffix. The tick comes from - // groupDomainVerified (set at connect); its domain proof is not received on the wire yet, so - // re-verification is not wired. - SimplexNameView( - simplexName = "@${businessClaim.domain}", - verified = groupInfo.groupDomainVerified, - verify = { null } - ) - } + GroupSimplexNameView(groupInfo) val webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage if (webPage != null) { val uriHandler = LocalUriHandler.current