This commit is contained in:
Evgeny @ SimpleX Chat
2026-07-12 06:32:31 +00:00
parent 09bb412c06
commit c278d22ce3
7 changed files with 128 additions and 194 deletions
+65 -21
View File
@@ -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
+2 -50
View File
@@ -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()
}
@@ -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) {
@@ -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)
}
}
@@ -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 -> {}
}
@@ -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<Boolean?, String?>?
) {
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 }
)
}
}
}
@@ -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