use domain without prefix in profiles and in database, rename fields and types

This commit is contained in:
Evgeny Poberezkin committed 2026-07-02 22:03:31 +01:00
1 parent 80ddf976bd
commit c863283b86
56 files changed
+816 -884

No files matched your search

+9 -9
View File
@@ -156,9 +156,9 @@ enum ChatCommand: ChatCmdProtocol {
case apiAddMyAddressShortLink(userId: Int64)
case apiSetProfileAddress(userId: Int64, on: Bool)
case apiSetAddressSettings(userId: Int64, addressSettings: AddressSettings)
case apiSetUserName(userId: Int64, name: String?)
case apiVerifyContactName(contactId: Int64)
case apiVerifyPublicGroupName(groupId: Int64)
case apiSetUserDomain(userId: Int64, simplexDomain: String?)
case apiVerifyContactDomain(contactId: Int64)
case apiVerifyGroupDomain(groupId: Int64)
case apiAcceptContact(incognito: Bool, contactReqId: Int64)
case apiRejectContact(contactReqId: Int64)
// WebRTC calls
@@ -374,10 +374,10 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiAddMyAddressShortLink(userId): return "/_short_link_address \(userId)"
case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))"
case let .apiSetAddressSettings(userId, addressSettings): return "/_address_settings \(userId) \(encodeJSON(addressSettings))"
case let .apiSetUserName(userId, name): return "/_set_name \(userId)" + (name.map { " " + $0 } ?? "")
case let .apiSetUserDomain(userId, simplexDomain): return "/_set domain \(userId)" + (simplexDomain.map { " " + $0 } ?? "")
case let .apiSetPublicGroupAccess(groupId, access): return "/_public group access #\(groupId) \(encodeJSON(access))"
case let .apiVerifyContactName(contactId): return "/_verify name @\(contactId)"
case let .apiVerifyPublicGroupName(groupId): return "/_verify name #\(groupId)"
case let .apiVerifyContactDomain(contactId): return "/_verify domain @\(contactId)"
case let .apiVerifyGroupDomain(groupId): return "/_verify domain #\(groupId)"
case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)"
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
@@ -560,9 +560,9 @@ enum ChatCommand: ChatCmdProtocol {
case .apiAddMyAddressShortLink: return "apiAddMyAddressShortLink"
case .apiSetProfileAddress: return "apiSetProfileAddress"
case .apiSetAddressSettings: return "apiSetAddressSettings"
case .apiSetUserName: return "apiSetUserName"
case .apiVerifyContactName: return "apiVerifyContactName"
case .apiVerifyPublicGroupName: return "apiVerifyPublicGroupName"
case .apiSetUserDomain: return "apiSetUserDomain"
case .apiVerifyContactDomain: return "apiVerifyContactDomain"
case .apiVerifyGroupDomain: return "apiVerifyGroupDomain"
case .apiAcceptContact: return "apiAcceptContact"
case .apiRejectContact: return "apiRejectContact"
case .apiSendCallInvitation: return "apiSendCallInvitation"
+15 -15
View File
@@ -1083,17 +1083,17 @@ private func apiConnectResponseAlert<R>(_ r: APIResult<R>) -> Alert {
title: "Unsupported connection link",
message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link."
)
case let .error(.simplexName(name, err)):
case let .error(.simplexDomain(domain, err)):
switch err {
case .noValidLink:
mkAlert(
title: "No valid link",
message: "The SimpleX name \(name.shortName) is registered, but it has no valid link."
message: "The SimpleX name \(domain.fullDomainName) is registered, but it has no valid link."
)
case .unknownName:
case .unknownDomain:
mkAlert(
title: "Unconfirmed name",
message: "The SimpleX name \(name.shortName) is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner."
message: "The SimpleX name \(domain.fullDomainName) is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner."
)
}
case .errorAgent(.NO_NAME_SERVERS):
@@ -1368,19 +1368,19 @@ func apiSetProfileAddress(on: Bool) async throws -> User? {
// name is the encoded SimplexName (e.g. "@alice.simplex"); nil clears it
// owner-specific SNENoValidLink wording; everything else reuses the general apiConnectResponseAlert
func showSetSimplexNameError<R>(_ r: APIResult<R>, isChannel: Bool) {
if case let .error(.simplexName(name, .noValidLink)) = r.unexpected {
if case let .error(.simplexDomain(domain, .noValidLink)) = r.unexpected {
let format = isChannel
? NSLocalizedString("The SimpleX name %@ is registered without channel link. Add channel link to the name via the registration page.", comment: "alert message")
: NSLocalizedString("The SimpleX name %@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.", comment: "alert message")
showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, name.shortName))
? NSLocalizedString("The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page.", comment: "alert message")
: NSLocalizedString("The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.", comment: "alert message")
showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName))
} else {
AlertManager.shared.showAlert(apiConnectResponseAlert(r))
}
}
func apiSetUserName(_ name: String?) async throws -> User {
let userId = try currentUserId("apiSetUserName")
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiSetUserName(userId: userId, name: name))
func apiSetUserDomain(_ simplexDomain: String?) async throws -> User {
let userId = try currentUserId("apiSetUserDomain")
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiSetUserDomain(userId: userId, simplexDomain: simplexDomain))
switch r {
case let .result(.userProfileUpdated(user, _, _, _)): return user
case let .result(.userProfileNoChange(user)): return user
@@ -1390,14 +1390,14 @@ func apiSetUserName(_ name: String?) async throws -> User {
}
}
func apiVerifyContactName(_ contactId: Int64) async throws -> (Contact, String?) {
let r: ChatResponse2 = try await chatSendCmd(.apiVerifyContactName(contactId: contactId))
func apiVerifyContactDomain(_ contactId: Int64) async throws -> (Contact, String?) {
let r: ChatResponse2 = try await chatSendCmd(.apiVerifyContactDomain(contactId: contactId))
if case let .contactNameVerified(_, contact, verificationFailure) = r { return (contact, verificationFailure) }
throw r.unexpected
}
func apiVerifyPublicGroupName(_ groupId: Int64) async throws -> (GroupInfo, String?) {
let r: ChatResponse2 = try await chatSendCmd(.apiVerifyPublicGroupName(groupId: groupId))
func apiVerifyGroupDomain(_ groupId: Int64) async throws -> (GroupInfo, String?) {
let r: ChatResponse2 = try await chatSendCmd(.apiVerifyGroupDomain(groupId: groupId))
if case let .groupNameVerified(_, groupInfo, verificationFailure) = r { return (groupInfo, verificationFailure) }
throw r.unexpected
}
+17 -17
View File
@@ -392,21 +392,21 @@ struct ChatInfoView: View {
.lineLimit(3)
.padding(.bottom, 2)
}
if let claim = contact.profile.simplexName, contact.profile.simplexNameVerification != nil || claim.proof != nil {
if let domain = contact.profile.contactDomain,
contact.profile.contactDomainVerified != nil || domain.proof != nil {
SimplexNameView(
name: claim.shortName,
verification: contact.profile.simplexNameVerification,
autoVerify: UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES),
simplexName: "@\(domain.shortName)",
verified: contact.profile.contactDomainVerified,
verify: {
do {
let (ct, reason) = try await apiVerifyContactName(contact.contactId)
let (ct, reason) = try await apiVerifyContactDomain(contact.contactId)
await MainActor.run {
chatModel.updateContact(ct)
contact = ct
}
return (ct.profile.simplexNameVerification, reason)
return (ct.profile.contactDomainVerified, reason)
} catch {
logger.error("apiVerifyContactName: \(responseError(error))")
logger.error("apiVerifyContactDomain: \(responseError(error))")
return nil
}
}
@@ -1382,31 +1382,31 @@ private func deleteNotReadyContact(
}
struct SimplexNameView: View {
let name: String
let verification: Bool?
let autoVerify: Bool
let verify: () async -> (Bool?, String?)?
@EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) var autoVerify = false
let simplexName: String
let verified: Bool?
let verify: () async -> (Bool?, String?)?
@State private var inFlight = false
@State private var showSpinner = false
var body: some View {
HStack(spacing: 6) {
Text(name)
.font(verification == true ? .subheadline : .system(.subheadline, design: .monospaced))
.foregroundColor(verification == true ? theme.colors.primary : theme.colors.secondary)
Text(simplexName)
.font(verified == true ? .subheadline : .system(.subheadline, design: .monospaced))
.foregroundColor(verified == true ? theme.colors.primary : theme.colors.secondary)
indicator()
}
.padding(.bottom, 2)
.onAppear { if autoVerify && verification == nil { runVerify(manual: false) } }
.onAppear { if autoVerify && verified == nil { runVerify(manual: false) } }
}
@ViewBuilder private func indicator() -> some View {
if showSpinner {
ProgressView()
} else if verification == true {
} else if verified == true {
Image(systemName: "checkmark")
} else if verification == false {
} else if verified == false {
Image(systemName: "xmark")
.foregroundColor(.red)
.onTapGesture { runVerify(manual: true) }
@@ -148,7 +148,7 @@ struct ChannelWebAccessView: View {
let existingAccess = pg.publicGroupAccess
pg.publicGroupAccess = PublicGroupAccess(
groupWebPage: trimmedPage.isEmpty ? nil : trimmedPage,
simplexName: existingAccess?.simplexName,
groupDomainClaim: existingAccess?.groupDomainClaim,
domainWebPage: existingAccess?.domainWebPage ?? false,
allowEmbedding: allowEmbedding
)
@@ -112,6 +112,7 @@ struct GroupChatInfoView: View {
// TODO [relays] allow other owners to manage channel link (requires protocol changes to share link ownership)
if groupInfo.isOwner && groupLink != nil {
channelLinkButton()
channelSimplexNameButton()
} else if let link = groupInfo.groupProfile.publicGroup?.groupLink {
SimpleXLinkQRCode(uri: link)
Button {
@@ -247,32 +248,6 @@ struct GroupChatInfoView: View {
if groupInfo.useRelays && groupInfo.isOwner {
Section(header: Text("Advanced options").foregroundColor(theme.colors.secondary)) {
channelWebAccessButton()
if groupInfo.groupProfile.publicGroup != nil {
NavigationLink {
SetSimplexNameView(
titleKey: "SimpleX name",
footer: "Let people join via name registered with this channel link.",
prefix: "#",
nameText: groupInfo.groupProfile.publicGroup?.publicGroupAccess?.simplexName?.shortName ?? "",
save: { name in
do {
var access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?? PublicGroupAccess()
access.simplexName = name.map { SimplexNameClaim(name: $0) }
let gInfo = try await apiSetPublicGroupAccess(groupInfo.groupId, access: access)
await MainActor.run {
chatModel.updateGroup(gInfo)
groupInfo = gInfo
}
return true
} catch {
return false
}
}
)
} label: {
Label("SimpleX name", systemImage: "number")
}
}
}
}
@@ -358,22 +333,21 @@ struct GroupChatInfoView: View {
.fixedSize(horizontal: false, vertical: true)
}
if let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess,
let groupName = access.simplexName?.shortName,
groupInfo.simplexNameVerification != nil || access.simplexName?.proof != nil {
let domain = access.groupDomainClaim?.shortName,
groupInfo.groupDomainVerified != nil || access.groupDomainClaim?.proof != nil {
SimplexNameView(
name: groupName,
verification: groupInfo.simplexNameVerification,
autoVerify: UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES),
simplexName: "#\(domain)",
verified: groupInfo.groupDomainVerified,
verify: {
do {
let (gInfo, reason) = try await apiVerifyPublicGroupName(groupInfo.groupId)
let (gInfo, reason) = try await apiVerifyGroupDomain(groupInfo.groupId)
await MainActor.run {
chatModel.updateGroup(gInfo)
groupInfo = gInfo
}
return (gInfo.simplexNameVerification, reason)
return (gInfo.groupDomainVerified, reason)
} catch {
logger.error("apiVerifyPublicGroupName: \(responseError(error))")
logger.error("apiVerifyGroupDomain: \(responseError(error))")
return nil
}
}
@@ -722,6 +696,34 @@ struct GroupChatInfoView: View {
}
}
private func channelSimplexNameButton() -> some View {
NavigationLink {
let domain = if let d = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName { "#\(d)" } else { "" }
SetSimplexDomainView(
title: "SimpleX name",
footer: "Let people join via name registered with this channel link.",
prompt: "#channelname.testing",
simplexName: domain,
save: { domain in
do {
var access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?? PublicGroupAccess()
access.groupDomainClaim = domain.map { SimplexDomainClaim(domain: $0) }
let gInfo = try await apiSetPublicGroupAccess(groupInfo.groupId, access: access)
await MainActor.run {
chatModel.updateGroup(gInfo)
groupInfo = gInfo
}
return true
} catch {
return false
}
}
)
} label: {
Label("SimpleX name", systemImage: "number")
}
}
private func groupLinkDestinationView() -> some View {
GroupLinkView(
groupId: groupInfo.groupId,
@@ -16,7 +16,7 @@ struct PrivacySettings: View {
@AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS, store: groupDefaults) private var useLinkPreviews = true
@AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
@AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) private var verifySimplexNames = true
@AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) private var verifySimplexNames = false
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
@@ -100,7 +100,7 @@ let appDefaults: [String: Any] = [
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue,
DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true,
DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES: true,
DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES: false,
DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true,
DEFAULT_PRIVACY_PROTECT_SCREEN: false,
DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false,
@@ -193,14 +193,15 @@ struct UserAddressView: View {
Section {
NavigationLink {
SetSimplexNameView(
titleKey: "Your SimpleX name",
let simplexName = if let d = chatModel.currentUser?.profile.contactDomain?.shortName { "@\(d)" } else { "" }
SetSimplexDomainView(
title: "Your SimpleX name",
footer: "Let people connect to you via name registered with your SimpleX address.",
prefix: "@",
nameText: chatModel.currentUser?.profile.simplexName?.shortName ?? "",
save: { name in
prompt: "@yourname.testing",
simplexName: simplexName,
save: { simplexDomain in
do {
let u = try await apiSetUserName(name)
let u = try await apiSetUserDomain(simplexDomain)
await MainActor.run { chatModel.updateUser(u) }
return true
} catch {
@@ -710,14 +711,11 @@ private func saveAddressSettings(_ settings: AddressSettingsState, _ savedSettin
}
}
// Set the user's own (prefix "@") or a channel's (prefix "#") SimpleX name.
// The field is prefilled with the full prefixed name; `save` receives the encoded name (or nil to
// clear) and returns a failure message (nil on success). The prefix is normalised on save.
struct SetSimplexNameView: View {
let titleKey: LocalizedStringKey
struct SetSimplexDomainView: View {
let title: LocalizedStringKey
let footer: LocalizedStringKey
let prefix: String
@State var nameText: String
let prompt: String
@State var simplexName: String
let save: (String?) async -> Bool
@Environment(\.dismiss) var dismiss
@EnvironmentObject var theme: AppTheme
@@ -726,7 +724,7 @@ struct SetSimplexNameView: View {
var body: some View {
List {
Section {
TextField(prefix + "name.simplex", text: $nameText)
TextField(prompt, text: $simplexName)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
} header: {
@@ -750,16 +748,19 @@ struct SetSimplexNameView: View {
.disabled(saving)
}
}
.navigationTitle(titleKey)
.navigationTitle(title)
.navigationBarTitleDisplayMode(.large)
}
// ensure the correct type prefix so a contact name is not parsed as a group (or vice versa)
private func normalized() -> String? {
let s = nameText.trimmingCharacters(in: .whitespacesAndNewlines)
if s.isEmpty { return nil }
if s.hasPrefix("@") || s.hasPrefix("#") { return prefix + s.dropFirst() }
return prefix + s
let s = simplexName.trimmingCharacters(in: .whitespacesAndNewlines)
return s.isEmpty
? nil
: addSimplexTLD(s.hasPrefix("@") || s.hasPrefix("#") ? String(s.dropFirst()) : s)
}
private func addSimplexTLD(_ d: String) -> String {
if d.contains(".") { d } else { "\(d).simplex" }
}
}