diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 506f59dc80..fa9df3a59a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -772,10 +772,10 @@ struct ChatListSearchBar: View { if oneHandUI, let candidate = connectNameCandidate { ConnectByNameRow( name: candidate, - searchText: $searchText, - connectNameCandidate: $connectNameCandidate, searchFocussed: $searchFocussed, - dismiss: false + dismiss: false, + filterKnownContact: { searchChatFilteredBySimplexLink = [$0.id] }, + filterKnownGroup: { searchChatFilteredBySimplexLink = [$0.id] } ) } else { ScrollView([.horizontal], showsIndicators: false) { TagsView(parentSheet: $parentSheet, searchText: $searchText) } @@ -817,10 +817,10 @@ struct ChatListSearchBar: View { if !oneHandUI, let candidate = connectNameCandidate { ConnectByNameRow( name: candidate, - searchText: $searchText, - connectNameCandidate: $connectNameCandidate, searchFocussed: $searchFocussed, - dismiss: false + dismiss: false, + filterKnownContact: { searchChatFilteredBySimplexLink = [$0.id] }, + filterKnownGroup: { searchChatFilteredBySimplexLink = [$0.id] } ) } } @@ -920,14 +920,14 @@ struct ChatListSearchBar: View { // Row shown when the search text is a SimpleX name — in place of the list tags in the chat list, below // the search field in the new chat sheet. The @ icon marks a contact name, the tag icon a channel/other -// name; tapping hides the keyboard, connects online, and clears the field. +// name; tapping hides the keyboard and connects online, and a chat found by the name stays filtered in the list. struct ConnectByNameRow: View { @EnvironmentObject var theme: AppTheme var name: String - @Binding var searchText: String - @Binding var connectNameCandidate: String? @FocusState.Binding var searchFocussed: Bool var dismiss: Bool + var filterKnownContact: ((Contact) -> Void)? = nil + var filterKnownGroup: ((GroupInfo) -> Void)? = nil var body: some View { HStack(spacing: 4) { @@ -945,10 +945,8 @@ struct ConnectByNameRow: View { name, theme: theme, dismiss: dismiss, - cleanup: { - searchText = "" - connectNameCandidate = nil - } + filterKnownContact: filterKnownContact, + filterKnownGroup: filterKnownGroup ) } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 416dc32308..c3def19e8c 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -96,10 +96,9 @@ struct NewChatSheet: View { if let candidate = connectNameCandidate { ConnectByNameRow( name: candidate, - searchText: $searchText, - connectNameCandidate: $connectNameCandidate, searchFocussed: $searchFocussed, - dismiss: true + dismiss: true, + filterKnownContact: { searchChatFilteredBySimplexLink = $0.id } ) } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index d32e3fe0aa..a749687aec 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -974,35 +974,45 @@ private func showAskCurrentOrIncognitoProfileSheet( connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, ownerVerification: OwnerVerification? = nil, + connectOtherButton: String? = nil, + onConnectOther: (() -> Void)? = nil, dismiss: Bool, cleanup: (() -> Void)? ) { showSheet( title, message: ownerVerificationMessage(ownerVerification), - actions: {[ - UIAlertAction( - title: NSLocalizedString("Use current profile", comment: "new chat action"), - style: actionStyle, - handler: { _ in - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) - } - ), - UIAlertAction( - title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), - style: actionStyle, - handler: { _ in - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) - } - ), - UIAlertAction( - title: NSLocalizedString("Cancel", comment: "new chat action"), - style: .default, - handler: { _ in - cleanup?() - } + actions: { + var actions = [ + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: actionStyle, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: actionStyle, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ) + ] + if let connectOtherButton, let onConnectOther { + actions.append(UIAlertAction(title: connectOtherButton, style: .default) { _ in onConnectOther() }) + } + actions.append( + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } + ) ) - ]} + return actions + } ) } @@ -1046,6 +1056,8 @@ private func showOwnGroupLinkConfirmConnectSheet( groupInfo: GroupInfo, connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, + connectOtherButton: String? = nil, + onConnectOther: (() -> Void)? = nil, dismiss: Bool, cleanup: (() -> Void)? ) { @@ -1055,22 +1067,30 @@ private func showOwnGroupLinkConfirmConnectSheet( NSLocalizedString("This is your link for channel %@!", comment: "new chat action"), groupInfo.displayName ), - actions: {[ - UIAlertAction( - title: NSLocalizedString("Open channel", comment: "new chat action"), - style: .default, - handler: { _ in - openKnownGroup(groupInfo, dismiss: dismiss, cleanup: cleanup) - } - ), - UIAlertAction( - title: NSLocalizedString("Cancel", comment: "new chat action"), - style: .default, - handler: { _ in - cleanup?() - } + actions: { + var actions = [ + UIAlertAction( + title: NSLocalizedString("Open channel", comment: "new chat action"), + style: .default, + handler: { _ in + openKnownGroup(groupInfo, dismiss: dismiss, cleanup: cleanup) + } + ) + ] + if let connectOtherButton, let onConnectOther { + actions.append(UIAlertAction(title: connectOtherButton, style: .default) { _ in onConnectOther() }) + } + actions.append( + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } + ) ) - ]} + return actions + } ) } else { showSheet( @@ -1177,11 +1197,15 @@ private func showPrepareGroupAlert( verifiedDomain: SimplexDomain? = nil, connectOtherButton: String? = nil, connectOtherLink: String? = nil, + addressChanged: Bool = false, + openExistingChat: (() -> Void)? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? ) { let isChannel = !(groupShortLinkInfo?.direct ?? true) + let channelChanged = addressChanged ? verifiedDomain.map { String.localizedStringWithFormat(NSLocalizedString("%@ now leads to a new channel.", comment: "alert information"), $0.fullDomainName) } : nil + let information = [channelChanged, ownerVerificationMessage(ownerVerification)].compactMap { $0 }.joined(separator: "\n") let subscriberCount = groupShortLinkData.publicGroupData.map { "\($0.publicMemberCount) subscribers" } showOpenChatAlert( profileName: groupShortLinkData.groupProfile.displayName, @@ -1196,13 +1220,13 @@ private func showPrepareGroupAlert( ), theme: theme, subtitle: isChannel ? subscriberCount : nil, - information: ownerVerificationMessage(ownerVerification), - cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), + information: information.isEmpty ? nil : information, + cancelTitle: openExistingChat == nil ? NSLocalizedString("Cancel", comment: "new chat action") : NSLocalizedString("Open existing chat", comment: "alert action"), confirmTitle: isChannel - ? NSLocalizedString("Open channel", comment: "new chat action") - : NSLocalizedString("Open group", comment: "new chat action"), + ? (addressChanged ? NSLocalizedString("Open new channel", comment: "new chat action") : NSLocalizedString("Open channel", comment: "new chat action")) + : (addressChanged ? NSLocalizedString("Open new group", comment: "new chat action") : NSLocalizedString("Open group", comment: "new chat action")), secondTitle: connectOtherButton, - onCancel: { cleanup?() }, + onCancel: openExistingChat ?? { cleanup?() }, onConfirm: { Task { do { @@ -1323,7 +1347,7 @@ private func showOpenKnownGroupAlert( private let simplexNamesHowToURL = "https://simplex.domains/#testing" private func nameDate(_ date: Date) -> String { - date.formatted(date: .abbreviated, time: .omitted) + date.formatted(date: .long, time: .omitted) } private func namePrice(_ price: NamePrice) -> String { @@ -1358,14 +1382,14 @@ private func showNameWarningAlert( let message = if let graceUntil { String.localizedStringWithFormat(NSLocalizedString("%1$@ expired on %2$@. Its owner can renew it until %3$@.", comment: "alert message"), nameStr, nameDate(expiredAt), nameDate(graceUntil)) } else { - String.localizedStringWithFormat(NSLocalizedString("%@ has expired. Its owner can still renew it for a limited time.", comment: "alert message"), nameStr) + String.localizedStringWithFormat(NSLocalizedString("%1$@ expired on %2$@.", comment: "alert message"), nameStr, nameDate(expiredAt)) } alert(NSLocalizedString("Name expired", comment: "alert title"), message) case let .ownExpired(expiredAt, graceUntil): let message = if let graceUntil { String.localizedStringWithFormat(NSLocalizedString("Your name %1$@ expired on %2$@. Renew it before %3$@.", comment: "alert message"), nameStr, nameDate(expiredAt), nameDate(graceUntil)) } else { - String.localizedStringWithFormat(NSLocalizedString("Your name %@ has expired. Renew it before the grace period ends.", comment: "alert message"), nameStr) + String.localizedStringWithFormat(NSLocalizedString("Your name %1$@ expired on %2$@.", comment: "alert message"), nameStr, nameDate(expiredAt)) } alert(NSLocalizedString("Your name has expired", comment: "alert title"), message, action: (NSLocalizedString("Renew", comment: "alert action"), openHowTo)) case let .available(price): @@ -1404,24 +1428,23 @@ private func showNameWarningAlert( } } -// a name tapped in a message has no filtered list behind the alert, so the chat the name had is found and opened here -private func openLocalNameChat(_ name: SimplexNameInfo, dismiss: Bool, cleanup: (() -> Void)?) { - Task { - let chatId = knownChatId(await apiConnectPlan(connLink: name.shortStr, resolveMode: .never, inProgress: BoxedValue(false))) - await MainActor.run { - if let chatId { - openKnownChat(chatId, dismiss: dismiss, cleanup: cleanup) - } else { - cleanup?() - } - } - } +// a name tapped in a message has no filtered list behind the alert, so 3c offers the chat the name had, if there is one +private func nameChatId(_ name: SimplexNameInfo?) async -> ChatId? { + guard let name else { return nil } + let plan = await apiConnectPlan(connLink: name.shortStr, resolveMode: .never, inProgress: BoxedValue(false)) + return await MainActor.run { knownChatId(plan) } } // a bare name found as a local chat also leads to the other kind, which the user does not have private func showOtherNameAlert(_ planSimplexName: SimplexNameInfo, _ otherSimplexName: SimplexNameInfo, connectOtherButton: String, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)?) { showAlert( - String.localizedStringWithFormat(NSLocalizedString("%1$@ also leads to %2$@", comment: "alert title"), planSimplexName.nameDomain.fullDomainName, otherSimplexName.shortStr), + String.localizedStringWithFormat( + otherSimplexName.nameType == .publicGroup + ? NSLocalizedString("%1$@ also leads to channel %2$@", comment: "alert title") + : NSLocalizedString("%1$@ also leads to contact %2$@", comment: "alert title"), + planSimplexName.nameDomain.fullDomainName, + otherSimplexName.shortStr + ), actions: {[ UIAlertAction(title: connectOtherButton, style: .default) { _ in planAndConnect(otherSimplexName.shortStr, theme: theme, dismiss: dismiss, cleanup: cleanup) @@ -1587,11 +1610,8 @@ func planAndConnect( case let .ok(contactSLinkData_, ownerVerification, addressChanged): if let contactSLinkData = contactSLinkData_ { logger.debug("planAndConnect, .contactAddress, .ok, short link data present") + let existingChatId = await nameChatId(filterKnownContact == nil && addressChanged ? planSimplexName : nil) await MainActor.run { - var openExisting: (() -> Void)? = nil - if filterKnownContact == nil && addressChanged, let planSimplexName { - openExisting = { openLocalNameChat(planSimplexName, dismiss: dismiss, cleanup: cleanup) } - } showPrepareContactAlert( connectionLink: connectionLink, contactShortLinkData: contactSLinkData, @@ -1600,7 +1620,7 @@ func planAndConnect( connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink, addressChanged: addressChanged, - openExistingChat: openExisting, + openExistingChat: existingChatId.map { chatId in { openKnownChat(chatId, dismiss: dismiss, cleanup: cleanup) } }, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1627,6 +1647,8 @@ func planAndConnect( actionStyle: .destructive, connectionLink: connectionLink, connectionPlan: connectionPlan, + connectOtherButton: connectOtherButton, + onConnectOther: connectOtherLink.map { link in { planAndConnect(link, theme: theme, dismiss: dismiss, cleanup: cleanup) } }, dismiss: dismiss, cleanup: cleanup ) @@ -1679,9 +1701,10 @@ func planAndConnect( } case let .groupLink(glp, _): switch glp { - case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification, _): + case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification, addressChanged): if let groupSLinkData = groupSLinkData_ { logger.debug("planAndConnect, .groupLink, .ok, short link data present") + let existingChatId = await nameChatId(filterKnownGroup == nil && addressChanged ? planSimplexName : nil) await MainActor.run { showPrepareGroupAlert( connectionLink: connectionLink, @@ -1691,6 +1714,8 @@ func planAndConnect( verifiedDomain: planSimplexName?.nameDomain, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink, + addressChanged: addressChanged, + openExistingChat: existingChatId.map { chatId in { openKnownChat(chatId, dismiss: dismiss, cleanup: cleanup) } }, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1719,6 +1744,8 @@ func planAndConnect( groupInfo: groupInfo, connectionLink: connectionLink, connectionPlan: connectionPlan, + connectOtherButton: connectOtherButton, + onConnectOther: connectOtherLink.map { link in { planAndConnect(link, theme: theme, dismiss: dismiss, cleanup: cleanup) } }, dismiss: dismiss, cleanup: cleanup ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 5f296655b7..b1bb25a67a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -1018,10 +1018,10 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { Divider() // bottom toolbar: search bar below, so on desktop the connect row goes below the tags - TagsOrConnectByName(searchText, connectNameCandidate) { candidate -> + TagsOrConnectByName(searchText, searchChatFilteredBySimplexLink, connectNameCandidate) { candidate -> TagsView(searchText) Divider() - ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = null) } ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) @@ -1029,8 +1029,8 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat } else { ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate) // top toolbar: search bar above, so on desktop the connect row goes above the tags - TagsOrConnectByName(searchText, connectNameCandidate) { candidate -> - ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + TagsOrConnectByName(searchText, searchChatFilteredBySimplexLink, connectNameCandidate) { candidate -> + ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = null) Divider() TagsView(searchText) } @@ -1192,19 +1192,20 @@ internal suspend fun knownChatId(rhId: Long?, result: ConnectionPlanResult?): St @Composable private fun TagsOrConnectByName( searchText: MutableState, + searchChatFilteredBySimplexLink: MutableState>, connectNameCandidate: MutableState, desktopView: @Composable (candidate: String) -> Unit, ) { val candidate = connectNameCandidate.value when { candidate == null -> TagsView(searchText) - !appPlatform.isDesktop -> ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + !appPlatform.isDesktop -> ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = null) else -> desktopView(candidate) } } @Composable -internal fun ConnectByNameRow(name: String, searchText: MutableState, connectNameCandidate: MutableState, close: (() -> Unit)?) { +internal fun ConnectByNameRow(name: String, searchChatFilteredBySimplexLink: MutableState>, close: (() -> Unit)?) { val view = LocalMultiplatformView() Row( Modifier @@ -1216,10 +1217,8 @@ internal fun ConnectByNameRow(name: String, searchText: MutableState alert( generalGetString(MR.strings.simplex_name_expired), if (warning.graceUntil != null) String.format(generalGetString(MR.strings.simplex_name_expired_desc), nameStr, nameDate(warning.expiredAt), nameDate(warning.graceUntil)) - else String.format(generalGetString(MR.strings.simplex_name_expired_no_date_desc), nameStr) + else String.format(generalGetString(MR.strings.simplex_name_expired_no_date_desc), nameStr, nameDate(warning.expiredAt)) ) is NameWarning.OwnExpired -> alert( generalGetString(MR.strings.simplex_name_own_expired), if (warning.graceUntil != null) String.format(generalGetString(MR.strings.simplex_name_own_expired_desc), nameStr, nameDate(warning.expiredAt), nameDate(warning.graceUntil)) - else String.format(generalGetString(MR.strings.simplex_name_own_expired_no_date_desc), nameStr), + else String.format(generalGetString(MR.strings.simplex_name_own_expired_no_date_desc), nameStr, nameDate(warning.expiredAt)), generalGetString(MR.strings.simplex_name_renew) to { uh: UriHandler -> openNameHowTo(uh) } ) is NameWarning.Available -> alert( @@ -189,9 +191,15 @@ private suspend fun planAndConnectTask( val knownGroup = (connectionPlan as? ConnectionPlan.GroupLink)?.groupLinkPlan.let { it as? GroupLinkPlan.Known }?.groupInfo var openExisting: (() -> Unit)? = null if (knownContact != null) { + if (chatModel.getContactChat(knownContact.contactId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(knownContact), chatItems = emptyList())) + } if (filterKnownContact != null) filterKnownContact(knownContact) else openExisting = { openKnownContact(chatModel, rhId, close, knownContact) } } else if (knownGroup != null) { + if (chatModel.getGroupChat(knownGroup.groupId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(knownGroup, groupChatScope = null), chatItems = emptyList())) + } if (filterKnownGroup != null) filterKnownGroup(knownGroup) else openExisting = { openKnownGroup(chatModel, rhId, close, knownGroup) } } @@ -280,9 +288,7 @@ private suspend fun planAndConnectTask( connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink, addressChanged = connectionPlan.contactAddressPlan.addressChanged, - openExistingChat = if (filterKnownContact == null && connectionPlan.contactAddressPlan.addressChanged && planSimplexName != null) { - { openLocalNameChat(rhId, planSimplexName, close, cleanup) } - } else null, + openExistingChat = if (filterKnownContact == null && connectionPlan.contactAddressPlan.addressChanged) openNameChat(rhId, planSimplexName, close, cleanup) else null, close, cleanup ) @@ -379,6 +385,8 @@ private suspend fun planAndConnectTask( planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink, + addressChanged = connectionPlan.groupLinkPlan.addressChanged, + openExistingChat = if (filterKnownGroup == null && connectionPlan.groupLinkPlan.addressChanged) openNameChat(rhId, planSimplexName, close, cleanup) else null, close, cleanup ) @@ -877,20 +885,20 @@ fun showPrepareContactAlert( ) } -// a name tapped in a message has no filtered list behind the alert, so the chat the name had is found and opened here -private fun openLocalNameChat(rhId: Long?, name: SimplexNameInfo, close: (() -> Unit)?, cleanup: (() -> Unit)?) { - withBGApi { - val chatId = knownChatId(rhId, chatModel.controller.apiConnectPlan(rhId, name.shortStr, PlanResolveMode.PRMNever, inProgress = mutableStateOf(false))) - val chat = chatId?.let { chatModel.getChat(it) } - if (chat != null) withContext(Dispatchers.Main) { openChat_(chatModel, rhId, close, chat) } - cleanup?.invoke() - } +// a name tapped in a message has no filtered list behind the alert, so the chat the name had is opened from it, if there is one +private suspend fun openNameChat(rhId: Long?, name: SimplexNameInfo?, close: (() -> Unit)?, cleanup: (() -> Unit)?): (() -> Unit)? { + val chat = name?.let { knownChatId(rhId, chatModel.controller.apiConnectPlan(rhId, it.shortStr, PlanResolveMode.PRMNever, inProgress = mutableStateOf(false))) }?.let { chatModel.getChat(it) } + return chat?.let { { openChat_(chatModel, rhId, close, it); cleanup?.invoke() } } } // a bare name found as a local chat also leads to the other kind, which the user does not have private fun showOtherNameAlert(rhId: Long?, planSimplexName: SimplexNameInfo, otherSimplexName: SimplexNameInfo, connectOtherButton: String, close: (() -> Unit)?, cleanup: (() -> Unit)?) { AlertManager.privacySensitive.showAlertDialogButtonsColumn( - title = String.format(generalGetString(MR.strings.simplex_name_also_leads_to), planSimplexName.nameDomain.fullDomainName, otherSimplexName.shortStr), + title = String.format( + generalGetString(if (otherSimplexName.nameType == SimplexNameType.publicGroup) MR.strings.simplex_name_also_leads_to_channel else MR.strings.simplex_name_also_leads_to_contact), + planSimplexName.nameDomain.fullDomainName, + otherSimplexName.shortStr + ), buttons = { Column { SectionItemView({ @@ -916,6 +924,8 @@ fun showPrepareGroupAlert( planSimplexName: SimplexNameInfo? = null, connectOtherButton: String? = null, connectOtherLink: String? = null, + addressChanged: Boolean = false, + openExistingChat: (() -> Unit)? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -933,8 +943,14 @@ fun showPrepareGroupAlert( }, nameCaption = planSimplexName?.shortStr, subtitle = subscriberCount, - information = ownerVerificationMessage(ownerVerification), - confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_channel else MR.strings.connect_plan_open_group), + information = listOfNotNull( + if (addressChanged && planSimplexName != null) String.format(generalGetString(MR.strings.simplex_name_channel_changed), planSimplexName.nameDomain.fullDomainName) else null, + ownerVerificationMessage(ownerVerification) + ).joinToString("\n").ifEmpty { null }, + confirmText = generalGetString( + if (isChannel) (if (addressChanged) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_channel) + else (if (addressChanged) MR.strings.connect_plan_open_new_group else MR.strings.connect_plan_open_group) + ), onConfirm = { AlertManager.privacySensitive.hideAlert() withBGApi { @@ -958,6 +974,8 @@ fun showPrepareGroupAlert( }, connectOtherButton = connectOtherButton, onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close, cleanup = cleanup) } } }, + dismissText = generalGetString(if (openExistingChat != null) MR.strings.connect_plan_open_existing_chat else MR.strings.cancel_verb), + onDismissButton = openExistingChat, onDismiss = { cleanup?.invoke() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 4d97e3a054..df55adcc35 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -319,7 +319,7 @@ private fun ModalData.NewChatSheetLayout( ) connectNameCandidate.value?.let { candidate -> Divider() - ConnectByNameRow(candidate, searchText, connectNameCandidate, close = close) + ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = close) } Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } @@ -410,7 +410,7 @@ private fun ModalData.NewChatSheetLayout( ) connectNameCandidate.value?.let { candidate -> Divider() - ConnectByNameRow(candidate, searchText, connectNameCandidate, close = close) + ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = close) } Divider() } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 7c1b29f737..ae6ce3a0ff 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -18,6 +18,7 @@ Open new chat Open group Open new group + Open new channel You are an observer You are a member You are a moderator @@ -207,10 +208,10 @@ Name expired %1$s expired on %2$s. Its owner can renew it until %3$s. - %1$s has expired. Its owner can still renew it for a limited time. + %1$s expired on %2$s. Your name has expired Your name %1$s expired on %2$s. Renew it before %3$s. - Your name %1$s has expired. Renew it before the grace period ends. + Your name %1$s expired on %2$s. Your name %1$s is no longer registered. It is available for registration for %2$s. Name not registered Name no longer registered @@ -218,7 +219,9 @@ %1$s for %2$d years %1$s is reserved for community. Please contact SimpleX Chat if you are interested in using it. %1$s now leads to a new address. - %1$s also leads to %2$s + %1$s now leads to a new channel. + %1$s also leads to channel %2$s + %1$s also leads to contact %2$s Register Re-register Renew diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index 8d4e75b181..39e75c3ab8 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -717,7 +717,7 @@ export class ChatApi { * Determine SimpleX link type and if the bot is already connected via this link. * Network usage: interactive. */ - async apiConnectPlan(userId: number, connectionLink: string): Promise<[T.ConnectionPlan, T.CreatedConnLink]> { + async apiConnectPlan(userId: number, connectionLink: string): Promise<[T.ConnectionPlan, T.CreatedConnLink | undefined]> { const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectTarget: connectionLink, resolveMode: T.PlanResolveMode.Unknown})) if (r.type === "connectionPlan") return [r.connectionPlan, r.connLink] throw new ChatCommandError("error getting connect plan", r) diff --git a/packages/simplex-chat-nodejs/tests/bot.test.ts b/packages/simplex-chat-nodejs/tests/bot.test.ts index 5a7faa663f..6b1c2e6af7 100644 --- a/packages/simplex-chat-nodejs/tests/bot.test.ts +++ b/packages/simplex-chat-nodejs/tests/bot.test.ts @@ -36,7 +36,7 @@ describe("Bot tests (use preset servers)", () => { // connect to bot const [plan, link] = await alice.apiConnectPlan(aliceUser.userId, util.contactAddressStr(botAddress.connLinkContact)) assert(plan.type === "contactAddress") - await expect(alice.apiConnect(aliceUser.userId, false, link)).resolves.toBe(api.ConnReqType.Contact) + await expect(alice.apiConnect(aliceUser.userId, false, link!)).resolves.toBe(api.ConnReqType.Contact) const [botContact, aliceContact] = await Promise.all([ (await alice.wait("contactConnected")).contact, (await chat.wait("contactConnected")).contact diff --git a/packages/simplex-chat-python/src/simplex_chat/api.py b/packages/simplex-chat-python/src/simplex_chat/api.py index 876230ef39..9897c35369 100644 --- a/packages/simplex-chat-python/src/simplex_chat/api.py +++ b/packages/simplex-chat-python/src/simplex_chat/api.py @@ -489,14 +489,14 @@ class ChatApi: async def api_connect_plan( self, user_id: int, connection_link: str - ) -> tuple[T.ConnectionPlan, T.CreatedConnLink]: + ) -> tuple[T.ConnectionPlan, T.CreatedConnLink | None]: r = await self.send_chat_cmd( CC.APIConnectPlan_cmd_string( {"userId": user_id, "connectTarget": connection_link, "resolveMode": "unknown"} ) ) if r["type"] == "connectionPlan": - return (r["connectionPlan"], r["connLink"]) + return (r["connectionPlan"], r.get("connLink")) raise ChatCommandError("error getting connect plan", r) async def api_connect( diff --git a/plans/2026-09-22-name-lookup-core-api.md b/plans/2026-09-22-name-lookup-core-api.md index e371aaeb8e..b5f274506c 100644 --- a/plans/2026-09-22-name-lookup-core-api.md +++ b/plans/2026-09-22-name-lookup-core-api.md @@ -143,9 +143,9 @@ Link targets keep today's `PRMUnknown` meaning: a known chat is answered from th ## 6. What stays an error -`SDEUnknownDomain` stays, for 2g and 4b. It is a mismatch between a resolved link's profile and the name asked for, not a property of the registry answer, so it cannot become a `NameRegistration`. Per the review thread it stays nullary — which name was claimed is not carried, because it is not shown. +`SDEUnknownDomain` stays, for 2g and 4b. It is a mismatch between a resolved link's profile and the name asked for, not a property of the registry answer, so it cannot become a `NameWarning`. Per the review thread it stays nullary — which name was claimed is not carried, because it is not shown. -`SDENoValidLink` stays. It becomes unreachable from the plan path, since 2f is now `CPNameNotConnectable` and its band-3 twin is `CAPKnown`, but `resolveNameLink`, `verifyEntityDomain` (`:5094`) and `/_set domain` (`:1607`) still raise it. +`SDENoValidLink` stays for `verifyEntityDomain` and `/_set domain`. The plan path no longer raises it: 2f is `CPNameNotConnectable` with `NWNoValidLink`, and a chat or own name is answered without a warning. Registry and network failures stay `CPError` (2h). The canvas shows the resolver's own text, which `chatErrorAgent` already carries. @@ -153,13 +153,13 @@ Registry and network failures stay `CPError` (2h). The canvas shows the resolver ## 7. Decisions taken -1. **The term is dropped from lookup wording, not added to the API.** Adding a term to `NamePricing` means a simplexmq change and a re-pin for one string; hardcoding "2 years" in three clients means three silent lies the day the term moves. Per-year pricing is what the registry actually returns. +1. **Reversed on 2026-09-28: the price is the 2-year term, computed in core.** `NamePrice {amount, years}` is the registry's per-year price for the label's length times `years = 2`, so the term is in one place and no client hardcodes it (`plans/2026-09-28-name-warnings.md`, N3). 2. **3c is a `Bool` on the `Ok` plans, not a revived `ownerChanged`.** It carries no owner and needs nothing persisted, so it does not reopen `f3bcd4a16`, and it is all the canvas claims. 3. **Dateless alerts rather than suppressed ones.** A user on an old router still learns the name expired; only the two dates go. 4. **Registration does not go through `ConnectionPlan`.** On the sibling canvas, 5a and 5b are the only registration states that would need one — 5a is `CPContactAddress (CAPKnown ct) (Just NRRegistered)`, 5b is `CPContactAddress (CAPOk …) (Just NRRegistered)` — and both are proposed for dropping (`b898b991d`, "suggest to drop 5a & 5b"). With them gone the registration check is a plain name-status call, this API keeps one consumer, and the two surfaces stop competing. Nothing here becomes removable as a result: every field 5a and 5b would have used is independently required by 3b and 3d. 5. **Consequently the lookup canvas's footer line "registration will always resolve" is obsolete** and should come off the sketch with this change. 6. **Reversed on review (2026-09-24): the freshness rule is in core, not the UIs.** Kept in the apps it was duplicated, lost on export, and keyed by domain name alone, so it was shared across user profiles and, under remote access, across hosts. §9. -7. **The constructor is `CPNameNotConnectable`, renamed from `CPSimplexName`.** All five states it carries share one invariant — you cannot connect — and the attached `NameRegistration` says why. Rejected, with reasons, so they are not re-litigated: *`CPUnregisteredSimplexName`* is false for 2b and 2f, which are registered; *`CPNonResolvingName`* is false for 2b, where both `resolveNameRecord` and `resolveNameLink` succeed and the refusal is policy, and it collides with `CENotResolvedLocally` and with 2h, the cases that genuinely do not resolve; *`CPSimplexName`* reads as a sibling of `CPContactAddress` / `CPGroupLink` naming the target kind, but a name that resolves produces those instead. `CPSimplexDomain`, asked for in review on `ea721d33f`, is vague rather than wrong and remains the fallback if the thread is reopened. The ordering follows the file's dominant negation pattern — `Not`, 20 constructors including the close sibling `CESimplexDomainNotReady`; a `Non` prefix appears nowhere in `src/`. +7. **The constructor is `CPNameNotConnectable`, renamed from `CPSimplexName`.** All five states it carries share one invariant — you cannot connect — and the attached `NameWarning` says why. Rejected, with reasons, so they are not re-litigated: *`CPUnregisteredSimplexName`* is false for 2b and 2f, which are registered; *`CPNonResolvingName`* is false for 2b, where both `resolveNameRecord` and `resolveNameLink` succeed and the refusal is policy, and it collides with `CENotResolvedLocally` and with 2h, the cases that genuinely do not resolve; *`CPSimplexName`* reads as a sibling of `CPContactAddress` / `CPGroupLink` naming the target kind, but a name that resolves produces those instead. `CPSimplexDomain`, asked for in review on `ea721d33f`, is vague rather than wrong and remains the fallback if the thread is reopened. The ordering follows the file's dominant negation pattern — `Not`, 20 constructors including the close sibling `CESimplexDomainNotReady`; a `Non` prefix appears nowhere in `src/`. --- @@ -171,7 +171,7 @@ Registry and network failures stay `CPError` (2h). The canvas shows the resolver - `CAPOk` / `GLPOk` construction sites take `addressChanged`. - Regenerate the client types: `bots/api/TYPES.md:1903-1922`, `packages/simplex-chat-client/types/typescript/src/types.ts`, `packages/simplex-chat-python/src/simplex_chat/types/_types.py` all still describe the three-constructor plan. Note that `bots/src/API/Docs/Commands.hs:142` and both generated clients never emit `resolve=`, which is fine and stays. -**Encoding note for the app work that follows.** `ConnectionPlan` derives through `sumTypeJSON`, which is `_owsf`-tagged on iOS and `type`-tagged elsewhere, but `NameRegistration` derives through `taggedObjectJSON` unconditionally — deliberately, since it is the RNAME payload. So one iOS response mixes both forms: `{"_owsf":"contactAddress","contactAddress":{…,"nameRegistration_":{"type":"registered",…}}}`. The Swift decoder for `NameRegistration` is therefore hand-written for the `type` form, as `MsgChatLink`'s is (#6821) — a protocol type in the same position — and follows it: `private enum CodingKeys`, `forKey: .type`, a `container` binding, and an `"Unknown … type"` error. simplexmq cannot switch to `sumTypeJSON`: that is platform-conditional in the core too, so an iOS build would expect `_owsf` while the relay forwards the resolver's `type` form. Its `reservedReason` is a bare string that may hold anything up to 32 characters (`NRRUnknown`), so both clients need a default branch. +**Encoding note.** Superseded on 2026-09-28: the plan has `NameWarning`, which is chat's own type and derives through `sumTypeJSON`, so the Swift decoder is synthesized like its neighbours and the hand-written `NameRegistration` decoder is gone. --- @@ -181,11 +181,11 @@ Review on 2026-09-24 reversed decision 6. Each decision below was taken by the a 1. **Storage.** `contact_profiles.contact_domain_resolved_at` and `contact_domain_expires_at`; `groups.group_domain_resolved_at` and `group_domain_expires_at` — beside `contact_domain_verified` and `group_domain_verified`, which record whether the name checked out; these record when it was last resolved and when its registration expires. `TEXT` in SQLite, `TIMESTAMPTZ` in Postgres, `_at` as in `badge_purchases.expires_at`. One migration per backend. 2. **Rule.** Under `PRMUnknown`, a known chat reached by a name is re-resolved when `resolved_at` is `NULL` or over a day old, or `expires_at` has passed. `PRMAll` always resolves; `PRMNever` never does. A name with no chat is resolved on every call and nothing is stored; the user's own address is not a chat, so it too is resolved on every call. -3. **Writes.** Wherever core sets a verification flag after a resolution, it also sets `resolved_at` to now and `expires_at` to the registration's expiry, or `NULL` when the caller does not have it — then only the one-day limit applies until the next resolution. That is `setContactDomainVerified`, `setGroupDomainVerified`, and `createPreparedContact`, which inserts a chat created by name already verified. To supply the expiry, `resolveNameRecord` returns it beside the record, for `/_verify domain` (both kinds) and `APISetPublicGroupAccess` to pass on, and `updateGroupFromLinkData` takes it from its callers, since it must not resolve. One path gains a write: a re-resolution confirming the name still resolves to the known chat, which writes nothing today. It re-sets the flag to `True`, a no-op, since only verified chats are found by name. +3. **Writes.** Wherever core sets a verification flag after a resolution, it also sets `resolved_at` to now and `expires_at` to the registration's expiry, or `NULL` when the caller does not have it — then only the one-day limit applies until the next resolution. That is `setContactDomainVerified`, `setGroupDomainVerified`, and `createPreparedContact`, which inserts a chat created by name already verified. To supply the expiry, `resolveNameRecord` returns it beside the record, for `/_verify domain` (both kinds) and `APISetPublicGroupAccess` to pass on, and `updateGroupFromLinkData` takes it from its callers, since it must not resolve. Two paths gain a write: a re-resolution confirming the name still resolves to the known chat, and one that answers the chat without a warning because the name no longer has a link of its kind or is not registered (`plans/2026-09-28-name-warnings.md`, N7 and N11). Both re-set the flag to `True`, a no-op, since only verified chats are found by name. 4. **Moved name.** When re-resolution finds the name resolves elsewhere (3c), nothing is written to the old chat. It stays stale, so each default lookup re-resolves and reports the new address; `resolve=never` still returns the old chat. 5. **Reading.** A store function reads the two columns for the one chat being planned. `LocalProfile` and `GroupInfo` do not change, so the columns never reach the UIs; loading them there would touch 19 queries in 6 store files and both types' JSON. 6. **UIs.** Both apps lose the cache — the preference, `SimplexNameResolved`, the local probe before resolving, and the invalidation in `UserAddressView` — and plan a name with the default mode. -7. **iOS decoding.** Unchanged in substance, per §8. +7. **iOS decoding.** Superseded, per the note in §8. **Tests**, in core. A name re-pointed with `registerName` shows whether core queried the registry; a stored time is backdated with `withCCTransaction … DB.execute "UPDATE …"`, as `tests/ChatTests/Groups.hs:8825` does: - a fresh known chat is answered from the store: after re-pointing, the default plan still returns the old contact; @@ -218,8 +218,8 @@ Review on 2026-09-24 reversed decision 6. Each decision below was taken by the a - `tests/NameResolver.hs` can answer expired, reserved and available, not only registered-without-dates - every row of §4 is produced by core and asserted by a test - `CPNameNotConnectable` carries a domain and is returned only when no local chat claims the name -- `nameRegistration_` is `Just` for every name target the registry was asked about, and `Nothing` for every link target and every known chat answered from the store -- `PRMAll` re-resolves a known chat, `PRMAllGroups` is removed, `PRMNever` is unchanged, and `PRMUnknown` applies the rule in §9 +- `nameWarning_` is `Just` exactly when the lookup canvas shows an alert (`plans/2026-09-28-name-warnings.md` §3) +- `PRMAll` re-resolves a known chat and replaces `PRMAllGroups` (`allGroups` still parses), `PRMNever` is unchanged, and `PRMUnknown` applies the rule in §9 - an expired name never yields a connectable plan, and an absent `expires` is treated as live - `resolve=all` on a known chat whose name moved returns an `Ok` plan with `addressChanged = True` - `cabal build` and `cabal test` are clean, and the generated client types match diff --git a/plans/sketches/2026-09-18-names-lookup-flows.excalidraw b/plans/sketches/2026-09-18-names-lookup-flows.excalidraw index c7a4f238c4..c43d1a4d9c 100644 --- a/plans/sketches/2026-09-18-names-lookup-flows.excalidraw +++ b/plans/sketches/2026-09-18-names-lookup-flows.excalidraw @@ -15,7 +15,7 @@ {"id":"te321463628892","type":"text","x":8.321842955737054,"y":446.294092339979,"width":67.81,"height":11.05,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["search"],"text":"bakery.simplex","fontSize":9,"fontFamily":9,"index":"aN","strokeWidth":2,"lineHeight":1.25}, {"id":"te231930574466","type":"text","x":148.70184295573705,"y":446.294092339979,"width":32.26,"height":11.05,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["search"],"text":"Cancel","fontSize":9,"fontFamily":9,"index":"aP","strokeWidth":2,"lineHeight":1.25}, {"id":"te146535658481","type":"text","x":-17.678157044262946,"y":511.844092339979,"width":115.33,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["search"],"text":"1a. From search","fontSize":13,"fontFamily":9,"index":"aW","strokeWidth":2,"lineHeight":1.25}, - {"id":"te9023925185","type":"text","x":-17.678157044262946,"y":528.484092339979,"width":271.44,"height":68.75,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["search"],"text":"Typing filters to the chats found by this name,\nand Connect to resolves it, both as today. A name\nyou have a chat for is resolved at most once a\nday; any other name on every tap, so an owner\ncan check a change.","fontSize":11,"fontFamily":9,"index":"aX","strokeWidth":2,"lineHeight":1.25}, + {"id":"te9023925185","type":"text","x":-17.678157044262946,"y":528.484092339979,"width":241.56,"height":82.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["search"],"text":"Typing filters to the chats found by this name,\nand Connect to resolves it, both as today. A name\nyou have a chat for is resolved at most once a\nday; any other name on every tap, so an owner\ncan check a change. The row hides once the\nname is a chat of every kind.","fontSize":11,"fontFamily":9,"index":"aX","strokeWidth":2,"lineHeight":1.25}, {"id":"re137734344677","type":"rectangle","x":332.5,"y":298.4,"width":209.99,"height":132.06,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["over","over-alert"],"index":"b03","strokeWidth":2,"boundElements":[{"id":"ar625193548486","type":"arrow"},{"id":"ar909442594990","type":"arrow"}]}, {"id":"te719661998578","type":"text","x":396.791,"y":314.78,"width":81.408,"height":14.82,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["over","over-alert"],"text":"Name expired","fontSize":12,"fontFamily":9,"index":"b04","strokeWidth":2,"lineHeight":1.25}, {"id":"te79947822704","type":"text","x":342.41,"y":337.89,"width":190.17,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["over","over-alert"],"text":"bakery.simplex expired on 24 June 2027.","fontSize":9,"fontFamily":9,"index":"b05","strokeWidth":2,"lineHeight":1.25,"boundElements":[{"id":"ar909442594990","type":"arrow"},{"id":"ar625193548486","type":"arrow"}]}, @@ -43,20 +43,20 @@ {"id":"re832390381940","type":"rectangle","x":476.9483165664566,"y":714.4455739971563,"width":65.6,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["fromchat"],"index":"b0l","strokeWidth":2}, {"id":"te235708960196","type":"text","x":486.46831656645656,"y":717.2455739971563,"width":46.56,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["fromchat"],"text":"changed","fontSize":10,"fontFamily":9,"index":"b0m","strokeWidth":2,"lineHeight":1.25}, {"id":"te987142809072","type":"text","x":332.5483165664566,"y":918.8855739971564,"width":260.11,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["fromchat"],"text":"1d. From a message, with chats found","fontSize":13,"fontFamily":9,"index":"b0p","strokeWidth":2,"lineHeight":1.25}, - {"id":"te71774133772","type":"text","x":332.5483165664566,"y":935.5255739971564,"width":229.17,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["fromchat"],"text":"No list to filter, so the alert adds Open\nexisting chat. With several chats, it opens\nsearch filtered by the name.","fontSize":11,"fontFamily":9,"index":"b0q","strokeWidth":2,"lineHeight":1.25}, + {"id":"te71774133772","type":"text","x":332.5483165664566,"y":935.5255739971564,"width":183.58,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["fromchat"],"text":"No list to filter, so the alert adds Open\nexisting chat.","fontSize":11,"fontFamily":9,"index":"b0q","strokeWidth":2,"lineHeight":1.25}, {"id":"re911452534945","type":"rectangle","x":681.5956360464527,"y":140,"width":1490,"height":905,"strokeColor":"transparent","fillStyle":"solid","roughness":0,"index":"b0t","strokeWidth":2,"boundElements":[{"id":"ar625193548486","type":"arrow"},{"id":"ar11920695389","type":"arrow"},{"id":"ar967582753031","type":"arrow"},{"id":"ar998006064859","type":"arrow"},{"id":"ar996082616004","type":"arrow"},{"id":"ar1094370764209","type":"arrow"},{"id":"ar802878277106","type":"arrow"},{"id":"ar641649257354","type":"arrow"},{"id":"ar428625206945","type":"arrow"},{"id":"ar95247738220","type":"arrow"}],"backgroundColor":"#eef8fb"}, - {"id":"re573918155529","type":"rectangle","x":682.7272727272726,"y":1075.3513829104986,"width":1490,"height":485,"strokeColor":"transparent","fillStyle":"solid","roughness":0,"index":"b0u","strokeWidth":2,"boundElements":[{"id":"ar174135805467","type":"arrow"},{"id":"ar467310991719","type":"arrow"},{"id":"ar365433195697","type":"arrow"},{"id":"ar26133587117","type":"arrow"}],"backgroundColor":"#f4f3fe"}, + {"id":"re573918155529","type":"rectangle","x":682.7272727272726,"y":1075.3513829104986,"width":1490,"height":515,"strokeColor":"transparent","fillStyle":"solid","roughness":0,"index":"b0u","strokeWidth":2,"boundElements":[{"id":"ar174135805467","type":"arrow"},{"id":"ar467310991719","type":"arrow"},{"id":"ar365433195697","type":"arrow"},{"id":"ar26133587117","type":"arrow"}],"backgroundColor":"#f4f3fe"}, {"id":"re000000000004","type":"rectangle","x":687.9561340504179,"y":1609.1459278594327,"width":1490,"height":454,"strokeColor":"transparent","fillStyle":"solid","roughness":0,"index":"b0uV","strokeWidth":2,"backgroundColor":"#faf3ec"}, {"id":"re787333526056","type":"rectangle","x":780,"y":330,"width":209.99,"height":203.36,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["newres"],"boundElements":[{"id":"ar967582753031","type":"arrow"}],"index":"b0v","strokeWidth":2}, {"id":"te307986224675","type":"text","x":864.58,"y":414.36,"width":40.9,"height":13.17,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"Bakery","fontSize":10,"fontFamily":9,"index":"b0z","strokeWidth":2,"lineHeight":1.25}, {"id":"te714291846370","type":"text","x":847.55,"y":430.76,"width":74.95,"height":10.85,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"@bakery.simplex","fontSize":9,"fontFamily":9,"index":"b10","strokeWidth":2,"lineHeight":1.25}, {"id":"te704456730618","type":"text","x":848.89,"y":450.83,"width":72.28,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"Open new chat","fontSize":10,"fontFamily":9,"index":"b11","strokeWidth":2,"lineHeight":1.25}, - {"id":"te208111920569","type":"text","x":810.77,"y":476.87,"width":148.52,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"Join channel #bakery.simplex","fontSize":10,"fontFamily":9,"index":"b12","strokeWidth":2,"lineHeight":1.25}, + {"id":"te208111920569","type":"text","x":837.77,"y":476.87,"width":94.52,"height":12.5,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"Join channel #bakery","fontSize":10,"fontFamily":9,"index":"b12","strokeWidth":2,"lineHeight":1.25}, {"id":"te295621174921","type":"text","x":867.05,"y":502.91,"width":35.95,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"Cancel","fontSize":10,"fontFamily":9,"index":"b13","strokeWidth":2,"lineHeight":1.25}, {"id":"re872483412553","type":"rectangle","x":942.8,"y":311,"width":47.2,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["newres"],"index":"b14","strokeWidth":2}, {"id":"te127256360443","type":"text","x":950.76,"y":313.8,"width":31.27,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"today","fontSize":10,"fontFamily":9,"index":"b15","strokeWidth":2,"lineHeight":1.25}, {"id":"te591116195349","type":"text","x":780,"y":540.74,"width":133.55,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"2a. A new resource","fontSize":13,"fontFamily":9,"index":"b16","strokeWidth":2,"lineHeight":1.25}, - {"id":"te407194617630","type":"text","x":780,"y":557.38,"width":374.90771484375,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"As today. A name that also leads to a channel\noffers it too. The second button is only shown if it's both channel and address","fontSize":11,"fontFamily":9,"index":"b17","strokeWidth":2,"lineHeight":1.25}, + {"id":"te407194617630","type":"text","x":780,"y":557.38,"width":233.77,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["newres"],"text":"As today. A name typed without @ or # that also\nleads to a channel offers it too, unless you\nhave that channel.","fontSize":11,"fontFamily":9,"index":"b17","strokeWidth":2,"lineHeight":1.25}, {"id":"re281213594903","type":"rectangle","x":1070,"y":330,"width":209.99,"height":132.06,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["expired"],"boundElements":[{"id":"ar998006064859","type":"arrow"}],"index":"b19","strokeWidth":2}, {"id":"te828974930601","type":"text","x":1134.78,"y":346.38,"width":80.5,"height":14.72,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["expired"],"text":"Name expired","fontSize":12,"fontFamily":9,"index":"b1A","strokeWidth":2,"lineHeight":1.25}, {"id":"te41722046585","type":"text","x":1079.94,"y":369.49,"width":190.17,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["expired"],"text":"bakery.simplex expired on 24 June 2027.","fontSize":9,"fontFamily":9,"index":"b1B","strokeWidth":2,"lineHeight":1.25}, @@ -66,7 +66,7 @@ {"id":"re933698042467","type":"rectangle","x":1245,"y":311,"width":35,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["expired"],"index":"b1F","strokeWidth":2}, {"id":"te248705394105","type":"text","x":1251.95,"y":313.8,"width":21.1,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["expired"],"text":"new","fontSize":10,"fontFamily":9,"index":"b1G","strokeWidth":2,"lineHeight":1.25}, {"id":"te983258046414","type":"text","x":1070,"y":469.44,"width":83.08,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["expired"],"text":"2b. Expired","fontSize":13,"fontFamily":9,"index":"b1H","strokeWidth":2,"lineHeight":1.25}, - {"id":"te494479197905","type":"text","x":1070,"y":486.08,"width":236.45,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["expired"],"text":"It does not connect while only its owner can\nrenew it, and a visitor can tell the owner.","fontSize":11,"fontFamily":9,"index":"b1I","strokeWidth":2,"lineHeight":1.25}, + {"id":"te494479197905","type":"text","x":1070,"y":486.08,"width":217.88,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["expired"],"text":"It does not connect while only its owner can\nrenew it, and a visitor can tell the owner.\nWithout a grace date, only the first sentence.","fontSize":11,"fontFamily":9,"index":"b1I","strokeWidth":2,"lineHeight":1.25}, {"id":"re602913776992","type":"rectangle","x":1360,"y":330,"width":209.99,"height":145.08,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["avail"],"boundElements":[{"id":"ar996082616004","type":"arrow"}],"index":"b1K","strokeWidth":2}, {"id":"te553532468077","type":"text","x":1404.619592036599,"y":346.38,"width":106.90858459472656,"height":14.75,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["avail"],"text":"Name not registered","fontSize":12,"fontFamily":9,"index":"b1L","strokeWidth":2,"lineHeight":1.25}, {"id":"te377717712024","type":"text","x":1388.1,"y":369.49,"width":153.87,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["avail"],"text":"sunflower.simplex is available for","fontSize":9,"fontFamily":9,"index":"b1M","strokeWidth":2,"lineHeight":1.25}, @@ -76,7 +76,7 @@ {"id":"re267204074199","type":"rectangle","x":1504.4,"y":311,"width":65.6,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["avail"],"index":"b1Q","strokeWidth":2}, {"id":"te98700466532","type":"text","x":1513.92,"y":313.8,"width":46.56,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["avail"],"text":"changed","fontSize":10,"fontFamily":9,"index":"b1R","strokeWidth":2,"lineHeight":1.25}, {"id":"te1076048035035","type":"text","x":1360,"y":482.44,"width":94.67,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["avail"],"text":"2c. Available","fontSize":13,"fontFamily":9,"index":"b1S","strokeWidth":2,"lineHeight":1.25}, - {"id":"te289138893717","type":"text","x":1360,"y":499.08,"width":250.29,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["avail"],"text":"The price for the label's length for the 2 years\na registration lasts, or for free, as for .testing.\nRegister opens the .testing how-to.","fontSize":11,"fontFamily":9,"index":"b1T","strokeWidth":2,"lineHeight":1.25}, + {"id":"te289138893717","type":"text","x":1360,"y":499.08,"width":212.81,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["avail"],"text":"The registry's price for the label's length, for\nthe 2 years a registration lasts. Register\nopens the .testing how-to.","fontSize":11,"fontFamily":9,"index":"b1T","strokeWidth":2,"lineHeight":1.25}, {"id":"re461925029464","type":"rectangle","x":1650,"y":330,"width":209.99,"height":158.1,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["community"],"boundElements":[{"id":"ar1094370764209","type":"arrow"}],"index":"b1W","strokeWidth":2}, {"id":"te1011410885570","type":"text","x":1693.5041760394142,"y":345.26458400281524,"width":106.90858459472656,"height":14.75,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["community"],"text":"Name not registered","fontSize":12,"fontFamily":9,"index":"b1X","strokeWidth":2,"lineHeight":1.25}, {"id":"te956957970208","type":"text","x":1683.4,"y":369.49,"width":143.25,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["community"],"text":"privacy.simplex is reserved for","fontSize":9,"fontFamily":9,"index":"b1Y","strokeWidth":2,"lineHeight":1.25}, @@ -87,7 +87,7 @@ {"id":"re746637013554","type":"rectangle","x":1794.4,"y":311,"width":65.6,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["community"],"index":"b1d","strokeWidth":2}, {"id":"te970429595740","type":"text","x":1803.92,"y":313.8,"width":46.56,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["community"],"text":"changed","fontSize":10,"fontFamily":9,"index":"b1e","strokeWidth":2,"lineHeight":1.25}, {"id":"te759990831097","type":"text","x":1650,"y":495.44000000000005,"width":193.91,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["community"],"text":"2d. Reserved for community","fontSize":13,"fontFamily":9,"index":"b1f","strokeWidth":2,"lineHeight":1.25}, - {"id":"te481987303758","type":"text","x":1650,"y":512.08,"width":238.4,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["community"],"text":"The only reserved reason that is shown.\nConnect to SimpleX team opens the support\nchat.","fontSize":11,"fontFamily":9,"index":"b1g","strokeWidth":2,"lineHeight":1.25}, + {"id":"te481987303758","type":"text","x":1650,"y":512.08,"width":216.03,"height":68.75,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["community"],"text":"The only reserved reason that is shown.\nConnect to SimpleX team opens the support\nchat. Also for your own name once it is back\nwith the registry. A live name that is also\nreserved connects as usual.","fontSize":11,"fontFamily":9,"index":"b1g","strokeWidth":2,"lineHeight":1.25}, {"id":"re1000055747578","type":"rectangle","x":1940,"y":330,"width":209.99,"height":119.04,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notfound"],"boundElements":[{"id":"ar802878277106","type":"arrow"}],"index":"b1j","strokeWidth":2}, {"id":"te735778138446","type":"text","x":1980.15792804786,"y":345.8222920014077,"width":106.90858459472656,"height":14.75,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notfound"],"text":"Name not registered","fontSize":12,"fontFamily":9,"index":"b1k","strokeWidth":2,"lineHeight":1.25}, {"id":"te957113106198","type":"text","x":1960,"y":369.49,"width":170.05,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notfound"],"text":"This SimpleX name is not registered.","fontSize":9,"fontFamily":9,"index":"b1l","strokeWidth":2,"lineHeight":1.25}, @@ -131,7 +131,7 @@ {"id":"re1034366287668","type":"rectangle","x":942.8,"y":1281,"width":47.2,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["still"],"index":"b2O","strokeWidth":2}, {"id":"te776486861809","type":"text","x":950.76,"y":1283.8,"width":31.27,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["still"],"text":"today","fontSize":10,"fontFamily":9,"index":"b2P","strokeWidth":2,"lineHeight":1.25}, {"id":"te688924689147","type":"text","x":780,"y":1379.34,"width":158.37,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["still"],"text":"3a. Nothing actionable","fontSize":13,"fontFamily":9,"index":"b2Q","strokeWidth":2,"lineHeight":1.25}, - {"id":"te103155488047","type":"text","x":780,"y":1395.98,"width":245.68,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["still"],"text":"Still leads to your chat, or not found, no valid\nlink, another name, or the request failed.","fontSize":11,"fontFamily":9,"index":"b2R","strokeWidth":2,"lineHeight":1.25}, + {"id":"te103155488047","type":"text","x":780,"y":1395.98,"width":216.66,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["still"],"text":"Still leads to your chat, or not found, no valid\nlink, another name, its new link fails, or the\nrequest failed.","fontSize":11,"fontFamily":9,"index":"b2R","strokeWidth":2,"lineHeight":1.25}, {"id":"re399296144194","type":"rectangle","x":1070,"y":1300,"width":209.99,"height":132.06,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["graced"],"boundElements":[{"id":"ar467310991719","type":"arrow"}],"index":"b2T","strokeWidth":2}, {"id":"te697956584978","type":"text","x":1134.78,"y":1316.38,"width":80.5,"height":14.72,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["graced"],"text":"Name expired","fontSize":12,"fontFamily":9,"index":"b2U","strokeWidth":2,"lineHeight":1.25}, {"id":"te1097376107493","type":"text","x":1079.94,"y":1339.49,"width":190.17,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["graced"],"text":"bakery.simplex expired on 24 June 2027.","fontSize":9,"fontFamily":9,"index":"b2V","strokeWidth":2,"lineHeight":1.25}, @@ -152,11 +152,11 @@ {"id":"re965099215536","type":"rectangle","x":1535,"y":1281,"width":35,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"index":"b2o","strokeWidth":2}, {"id":"te1033495344727","type":"text","x":1541.95,"y":1283.8,"width":21.1,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"text":"new","fontSize":10,"fontFamily":9,"index":"b2p","strokeWidth":2,"lineHeight":1.25}, {"id":"te208733714710","type":"text","x":1360,"y":1518.14,"width":191.47,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"text":"3c. Leads to a new address","fontSize":13,"fontFamily":9,"index":"b2q","strokeWidth":2,"lineHeight":1.25}, - {"id":"te377882734292","type":"text","x":1360,"y":1534.78,"width":284.31884765625,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"text":"The chat you have stays in the list. Only the\naddress change is known, not who made it. Chat is filtered","fontSize":11,"fontFamily":9,"index":"b2r","strokeWidth":2,"lineHeight":1.25}, + {"id":"te377882734292","type":"text","x":1360,"y":1534.78,"width":224.53,"height":55.0,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["moved"],"text":"From search your chat stays filtered in the list;\nfrom a message Cancel is Open existing chat.\nAlso for your own address or channel. A\nchannel reads \"now leads to a new channel\".","fontSize":11,"fontFamily":9,"index":"b2r","strokeWidth":2,"lineHeight":1.25}, {"id":"re819241109796","type":"rectangle","x":1650,"y":1300,"width":209.99,"height":145.08,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"boundElements":[{"id":"ar26133587117","type":"arrow"}],"index":"b2t","strokeWidth":2}, {"id":"te1029996043274","type":"text","x":1678.36,"y":1316.38,"width":153.33,"height":14.72,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"text":"Name no longer registered","fontSize":12,"fontFamily":9,"index":"b2u","strokeWidth":2,"lineHeight":1.25}, {"id":"te364627884637","type":"text","x":1683.96,"y":1339.49,"width":142.14,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"text":"bakery.simplex is available for","fontSize":9,"fontFamily":9,"index":"b2v","strokeWidth":2,"lineHeight":1.25}, - {"id":"te406711063858","type":"text","x":1683.13,"y":1352.51,"width":143.79,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"text":"registration for $20 for 2 years.","fontSize":9,"fontFamily":9,"index":"b2w","strokeWidth":2,"lineHeight":1.25}, + {"id":"te406711063858","type":"text","x":1691.28,"y":1352.51,"width":127.5,"height":11.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"text":"registration for $100 for 2 years.","fontSize":9,"fontFamily":9,"index":"b2w","strokeWidth":2,"lineHeight":1.25}, {"id":"te200855129744","type":"text","x":1733.48,"y":1388.59,"width":43.09,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["lost"],"text":"Register","fontSize":10,"fontFamily":9,"index":"b2x","strokeWidth":2,"lineHeight":1.25}, {"id":"te204967153830","type":"text","x":1746.88,"y":1414.63,"width":16.29,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["lost"],"text":"OK","fontSize":10,"fontFamily":9,"index":"b2y","strokeWidth":2,"lineHeight":1.25}, {"id":"re332180152275","type":"rectangle","x":1825,"y":1281,"width":35,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["lost"],"index":"b2z","strokeWidth":2}, @@ -168,22 +168,22 @@ {"id":"te549755328637","type":"text","x":805.58,"y":1839.49,"width":158.9,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"This is your own SimpleX address!","fontSize":9,"fontFamily":9,"index":"b36","strokeWidth":2,"lineHeight":1.25}, {"id":"te131860867684","type":"text","x":838.77,"y":1875.57,"width":92.51,"height":12.4,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"Use current profile","fontSize":10,"fontFamily":9,"index":"b37","strokeWidth":2,"lineHeight":1.25}, {"id":"te635569632893","type":"text","x":822.98,"y":1901.61,"width":124.09,"height":12.4,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"Use new incognito profile","fontSize":10,"fontFamily":9,"index":"b38","strokeWidth":2,"lineHeight":1.25}, - {"id":"te118607224489","type":"text","x":815.53,"y":1927.65,"width":138.99,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"Join channel #alice.simplex","fontSize":10,"fontFamily":9,"index":"b39","strokeWidth":2,"lineHeight":1.25}, + {"id":"te118607224489","type":"text","x":842.49,"y":1927.65,"width":85.06,"height":12.5,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"Join channel #alice","fontSize":10,"fontFamily":9,"index":"b39","strokeWidth":2,"lineHeight":1.25}, {"id":"te486433536921","type":"text","x":867.05,"y":1953.69,"width":35.95,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"Cancel","fontSize":10,"fontFamily":9,"index":"b3A","strokeWidth":2,"lineHeight":1.25}, {"id":"re833440366279","type":"rectangle","x":942.8,"y":1781,"width":47.2,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"index":"b3B","strokeWidth":2}, {"id":"te10203453787","type":"text","x":950.76,"y":1783.8,"width":31.27,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"today","fontSize":10,"fontFamily":9,"index":"b3C","strokeWidth":2,"lineHeight":1.25}, {"id":"te301725264139","type":"text","x":780,"y":1991.44,"width":264.27,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"4a. Leads to you, and shows the name","fontSize":13,"fontFamily":9,"index":"b3D","strokeWidth":2,"lineHeight":1.25}, - {"id":"te595067966648","type":"text","x":780,"y":2008.08,"width":233.13,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"As today, still offering the channel with the\nsame name.","fontSize":11,"fontFamily":9,"index":"b3E","strokeWidth":2,"lineHeight":1.25}, + {"id":"te595067966648","type":"text","x":780,"y":2008.08,"width":233.77,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownshown"],"text":"As today. A name typed without @ or # that also\nleads to a channel offers it too.","fontSize":11,"fontFamily":9,"index":"b3E","strokeWidth":2,"lineHeight":1.25}, {"id":"re111648126497","type":"rectangle","x":1070,"y":1800,"width":209.99,"height":145.08,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"boundElements":[{"id":"ar383945949602","type":"arrow"}],"index":"b3G","strokeWidth":2}, {"id":"te712146501069","type":"text","x":1120.99,"y":1817.324193000603,"width":108.07,"height":14.72,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"Unconfirmed name","fontSize":12,"fontFamily":9,"index":"b3H","strokeWidth":2,"lineHeight":1.25}, - {"id":"te737005807706","type":"text","x":1094.46,"y":1840.4341930006028,"width":146.96978759765625,"height":11.625,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"Your SimpleX name alice.simplex is","fontSize":9,"fontFamily":9,"index":"b3I","strokeWidth":2,"lineHeight":1.25}, - {"id":"te421100556529","type":"text","x":1091.7516139987945,"y":1853.4541930006028,"width":154.0718994140625,"height":11.625,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"registered, but not added to any chat.","fontSize":9,"fontFamily":9,"index":"b3J","strokeWidth":2,"lineHeight":1.25}, - {"id":"te421593695898","type":"text","x":1087.4772545039186,"y":1866.4741930006028,"width":166.4688720703125,"height":11.625,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"Please add it to your address or channel","fontSize":9,"fontFamily":9,"index":"b3K","strokeWidth":2,"lineHeight":1.25}, + {"id":"te737005807706","type":"text","x":1098.19,"y":1840.4341930006028,"width":139.5,"height":11.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"The SimpleX name alice.simplex is","fontSize":9,"fontFamily":9,"index":"b3I","strokeWidth":2,"lineHeight":1.25}, + {"id":"te421100556529","type":"text","x":1099.04,"y":1853.4541930006028,"width":139.5,"height":11.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"registered, but not added to profile.","fontSize":9,"fontFamily":9,"index":"b3J","strokeWidth":2,"lineHeight":1.25}, + {"id":"te421593695898","type":"text","x":1090.21,"y":1866.4741930006028,"width":161.0,"height":11.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"Please add it to your address or channel","fontSize":9,"fontFamily":9,"index":"b3K","strokeWidth":2,"lineHeight":1.25}, {"id":"te655459991027","type":"text","x":1166.88,"y":1915.574193000603,"width":16.29,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"OK","fontSize":10,"fontFamily":9,"index":"b3M","strokeWidth":2,"lineHeight":1.25}, {"id":"re545225797687","type":"rectangle","x":1232.8,"y":1781.9441930006028,"width":47.2,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"index":"b3N","strokeWidth":2}, {"id":"te440349073619","type":"text","x":1240.76,"y":1784.7441930006028,"width":31.27,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"today","fontSize":10,"fontFamily":9,"index":"b3O","strokeWidth":2,"lineHeight":1.25}, {"id":"te581390719059","type":"text","x":1070,"y":1953.3841930006029,"width":195.61,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"4b. Leads to you, not shown","fontSize":13,"fontFamily":9,"index":"b3P","strokeWidth":2,"lineHeight":1.25}, - {"id":"te299166426085","type":"text","x":1070,"y":1970.0241930006027,"width":240.41,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"As today. Adding the name stays in address\nand channel settings.","fontSize":11,"fontFamily":9,"index":"b3Q","strokeWidth":2,"lineHeight":1.25}, + {"id":"te299166426085","type":"text","x":1070,"y":1970.0241930006027,"width":230.72,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"As today: the 2g alert, whose text also\naddresses the owner. Adding the name stays in\naddress and channel settings.","fontSize":11,"fontFamily":9,"index":"b3Q","strokeWidth":2,"lineHeight":1.25}, {"id":"re27041288037","type":"rectangle","x":1360,"y":1800,"width":209.99,"height":158.1,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownexp"],"boundElements":[{"id":"ar534696206681","type":"arrow"}],"index":"b3S","strokeWidth":2}, {"id":"te641002492075","type":"text","x":1399.8938769933684,"y":1816.38,"width":120.92106628417969,"height":14.75,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownexp"],"text":"Your name has expired","fontSize":12,"fontFamily":9,"index":"b3T","strokeWidth":2,"lineHeight":1.25}, {"id":"te447349136667","type":"text","x":1376.09,"y":1839.49,"width":177.88,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownexp"],"text":"Your name alice.simplex expired on 24","fontSize":9,"fontFamily":9,"index":"b3U","strokeWidth":2,"lineHeight":1.25}, @@ -194,12 +194,12 @@ {"id":"re792118738511","type":"rectangle","x":1535,"y":1781,"width":35,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownexp"],"index":"b3Z","strokeWidth":2}, {"id":"te833108413170","type":"text","x":1541.95,"y":1783.8,"width":21.1,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownexp"],"text":"new","fontSize":10,"fontFamily":9,"index":"b3a","strokeWidth":2,"lineHeight":1.25}, {"id":"te109621994091","type":"text","x":1360,"y":1965.44,"width":129.41,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownexp"],"text":"4c. Yours, expired","fontSize":13,"fontFamily":9,"index":"b3b","strokeWidth":2,"lineHeight":1.25}, - {"id":"te999536547743","type":"text","x":1360,"y":1982.08,"width":248.33,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownexp"],"text":"Renew opens the how-to until the wallet lands,\nthen renews here.","fontSize":11,"fontFamily":9,"index":"b3c","strokeWidth":2,"lineHeight":1.25}, + {"id":"te999536547743","type":"text","x":1360,"y":1982.08,"width":237.47,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["ownexp"],"text":"Renew opens the how-to until the wallet lands,\nthen renews here. Without a grace date, only the\nfirst sentence.","fontSize":11,"fontFamily":9,"index":"b3c","strokeWidth":2,"lineHeight":1.25}, {"id":"re775686655556","type":"rectangle","x":1650,"y":1800,"width":209.99,"height":158.1,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"boundElements":[{"id":"ar51174435892","type":"arrow"}],"index":"b3e","strokeWidth":2}, {"id":"te457947658010","type":"text","x":1687.9,"y":1816.38,"width":134.25,"height":14.72,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"text":"Your name has expired","fontSize":12,"fontFamily":9,"index":"b3f","strokeWidth":2,"lineHeight":1.25}, {"id":"te945767504999","type":"text","x":1670.28,"y":1839.49,"width":169.5,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"text":"Your name alice.simplex is no longer","fontSize":9,"fontFamily":9,"index":"b3g","strokeWidth":2,"lineHeight":1.25}, {"id":"te169771895521","type":"text","x":1661.91,"y":1852.51,"width":186.23,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"text":"registered. It is available for registration","fontSize":9,"fontFamily":9,"index":"b3h","strokeWidth":2,"lineHeight":1.25}, - {"id":"te152225942241","type":"text","x":1710.76,"y":1865.53,"width":88.53,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"text":"for $20 for 2 years.","fontSize":9,"fontFamily":9,"index":"b3i","strokeWidth":2,"lineHeight":1.25}, + {"id":"te152225942241","type":"text","x":1714.78,"y":1865.53,"width":80.5,"height":11.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"text":"for $100 for 2 years.","fontSize":9,"fontFamily":9,"index":"b3i","strokeWidth":2,"lineHeight":1.25}, {"id":"te659086583391","type":"text","x":1726.94,"y":1901.61,"width":56.18,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"text":"Re-register","fontSize":10,"fontFamily":9,"index":"b3j","strokeWidth":2,"lineHeight":1.25}, {"id":"te749824996886","type":"text","x":1746.88,"y":1927.65,"width":16.29,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"text":"OK","fontSize":10,"fontFamily":9,"index":"b3k","strokeWidth":2,"lineHeight":1.25}, {"id":"re819385127968","type":"rectangle","x":1825,"y":1781,"width":35,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["notyours"],"index":"b3l","strokeWidth":2}, @@ -214,7 +214,7 @@ {"id":"te845115350481","type":"text","x":774.5454545454545,"y":1112.8872727272728,"width":572.56,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"text":"only an actionable alert shows over the filtered chats; from a message it adds Open existing chat","fontSize":12,"fontFamily":9,"index":"b47","strokeWidth":2,"lineHeight":1.25}, {"id":"te822161367289","type":"text","x":780,"y":1618.6,"width":156,"height":25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"text":"Your own name","fontSize":20,"fontFamily":9,"index":"b48","strokeWidth":2,"lineHeight":1.25}, {"id":"te166687152361","type":"text","x":780,"y":1645.16,"width":197.96,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"text":"your address or channel claims it","fontSize":12,"fontFamily":9,"index":"b49","strokeWidth":2,"lineHeight":1.25}, - {"id":"te921930439761","type":"text","x":60,"y":2112.98,"width":1923.76,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"text":"Tags compare with today's app: today, changed, new. The two tinted bands split the answers by whether a chat for the name is on the device. Greyed: needs the wallet (simplex-chat#7475), which has not landed. Design document, not screenshots; alerts are drawn in the app's iOS idiom. A lookup returns the chats found on the device and, on Connect to or a\ntapped name, one resolution from the registry: always for a name with no local chat, and for a name with one only when its cached answer is older than a day or past its expiry; registration will always resolve. The API is in simplex-name-lookup-alert-diff.md. Registering, owning, recovering and giving names are separate flows.","fontSize":11,"fontFamily":9,"index":"b4A","strokeWidth":2,"lineHeight":1.25}, + {"id":"te921930439761","type":"text","x":60,"y":2112.98,"width":1745.5,"height":27.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"text":"Tags compare with today's app: today, changed, new. The two tinted bands split the answers by whether a chat for the name is on the device. Greyed: needs the wallet (simplex-chat#7475), which has not landed. Design document, not screenshots; alerts are drawn in the app's iOS idiom. A lookup returns the chats found on the device and, on Connect to or a\ntapped name, one resolution from the registry: always for a name with no local chat and for your own name, and for a chat only when its cached answer is older than a day or past its expiry. The API is in plans/2026-09-28-name-warnings.md. Registering, owning, recovering and giving names are separate flows.","fontSize":11,"fontFamily":9,"index":"b4A","strokeWidth":2,"lineHeight":1.25}, {"id":"te719096292165","type":"text","x":237.33057968099348,"y":341.55499999999995,"width":53.20068359375,"height":13.75,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"text":"Connect to","fontSize":11,"fontFamily":9,"textAlign":"center","verticalAlign":"middle","containerId":"ar909442594990","index":"b4C","strokeWidth":2,"lineHeight":1.25}, {"id":"ar909442594990","type":"arrow","x":196.361842955737,"y":364.42999999999995,"width":135.138157044263,"height":0,"strokeColor":"#007aff","fillStyle":"solid","strokeWidth":2,"roughness":0,"boundElements":[{"id":"te719096292165","type":"text"}],"points":[[0,0],[135.138157044263,0]],"endArrowhead":"arrow","elbowed":true,"fixedSegments":null,"index":"b4D","startBinding":{"elementId":"re698762997543","fixedPoint":[1.023918867202449,0.49964387464387444],"focus":0,"gap":0},"endBinding":{"elementId":"re137734344677","fixedPoint":[-0.004762,0.5],"focus":0,"gap":1}}, {"id":"te597337641794","type":"text","x":300.05,"y":791,"width":56.87451171875,"height":13.75,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"text":"chats found","fontSize":11,"fontFamily":9,"textAlign":"center","verticalAlign":"middle","containerId":"ar507222825509","index":"b4E","strokeWidth":2,"lineHeight":1.25}, @@ -263,7 +263,19 @@ {"id":"el687117614750","type":"ellipse","x":855.27,"y":343.64,"width":59.52,"height":59.52,"strokeColor":"#1e1e1e","fillStyle":"solid","strokeWidth":2,"roughness":0,"groupIds":["newres"],"boundElements":[{"id":"ar967582753031","type":"arrow"}],"index":"b4|"}, {"id":"el430900442039","type":"ellipse","x":1435.27,"y":1313.64,"width":59.52,"height":59.52,"strokeColor":"#1e1e1e","fillStyle":"solid","strokeWidth":2,"roughness":0,"groupIds":["moved"],"boundElements":[{"id":"ar365433195697","type":"arrow"}],"index":"b4}"}, {"id":"tx109101110116","type":"text","x":26.0,"y":808.62,"width":64.59310344827585,"height":10,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["message"],"text":"@bakery.simplex","fontSize":8,"fontFamily":9,"index":"b4~","strokeWidth":2,"lineHeight":1.25}, - {"id":"i9ADag2YXvSUyLm-vKlDH","type":"rectangle","x":644.952908536373,"y":210.90187429288562,"width":0.25151942394802523,"height":1857.6832590651504,"strokeColor":"#1971c2","backgroundColor":"#a5d8ff","fillStyle":"solid","strokeWidth":2,"roughness":1,"index":"b40","roundness":{"type":3},"boundElements":[{"id":"ar625193548486","type":"arrow"},{"id":"ar967582753031","type":"arrow"},{"id":"ar998006064859","type":"arrow"},{"id":"ar996082616004","type":"arrow"},{"id":"ar1094370764209","type":"arrow"},{"id":"ar802878277106","type":"arrow"},{"id":"ar641649257354","type":"arrow"},{"id":"ar428625206945","type":"arrow"},{"id":"ar95247738220","type":"arrow"},{"id":"ar11920695389","type":"arrow"},{"id":"ar174135805467","type":"arrow"},{"id":"ar467310991719","type":"arrow"},{"id":"ar365433195697","type":"arrow"},{"id":"ar26133587117","type":"arrow"},{"id":"ar537817736884","type":"arrow"},{"id":"ar383945949602","type":"arrow"},{"id":"ar534696206681","type":"arrow"},{"id":"ar51174435892","type":"arrow"}]} + {"id":"i9ADag2YXvSUyLm-vKlDH","type":"rectangle","x":644.952908536373,"y":210.90187429288562,"width":0.25151942394802523,"height":1857.6832590651504,"strokeColor":"#1971c2","backgroundColor":"#a5d8ff","fillStyle":"solid","strokeWidth":2,"roughness":1,"index":"b40","roundness":{"type":3},"boundElements":[{"id":"ar625193548486","type":"arrow"},{"id":"ar967582753031","type":"arrow"},{"id":"ar998006064859","type":"arrow"},{"id":"ar996082616004","type":"arrow"},{"id":"ar1094370764209","type":"arrow"},{"id":"ar802878277106","type":"arrow"},{"id":"ar641649257354","type":"arrow"},{"id":"ar428625206945","type":"arrow"},{"id":"ar95247738220","type":"arrow"},{"id":"ar11920695389","type":"arrow"},{"id":"ar174135805467","type":"arrow"},{"id":"ar467310991719","type":"arrow"},{"id":"ar365433195697","type":"arrow"},{"id":"ar26133587117","type":"arrow"},{"id":"ar537817736884","type":"arrow"},{"id":"ar383945949602","type":"arrow"},{"id":"ar534696206681","type":"arrow"},{"id":"ar51174435892","type":"arrow"},{"id":"ar3e0000000001","type":"arrow"}]}, + {"id":"te4b0000000001","type":"text","x":1110.94,"y":1879.4741930006028,"width":128.17,"height":11.62,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["unconf"],"text":"profile, if you are the owner.","fontSize":9,"fontFamily":9,"index":"b50","strokeWidth":2,"lineHeight":1.25}, + {"id":"re3e0000000000","type":"rectangle","x":1940,"y":1300,"width":209.99,"height":125.08,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"index":"b51","strokeWidth":2,"boundElements":[{"id":"ar3e0000000001","type":"arrow"}]}, + {"id":"te3e0000000001","type":"text","x":1970.11,"y":1316.38,"width":149.83,"height":15.0,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"text":"bakery.simplex also leads to","fontSize":12,"fontFamily":9,"index":"b52","strokeWidth":2,"lineHeight":1.25}, + {"id":"te3e0000000002","type":"text","x":2000.97,"y":1332.38,"width":88.05,"height":15.0,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"text":"channel #bakery","fontSize":12,"fontFamily":9,"index":"b53","strokeWidth":2,"lineHeight":1.25}, + {"id":"te3e0000000004","type":"text","x":1997.74,"y":1368.59,"width":94.52,"height":12.5,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"text":"Join channel #bakery","fontSize":10,"fontFamily":9,"index":"b55","strokeWidth":2,"lineHeight":1.25}, + {"id":"te3e0000000005","type":"text","x":2036.88,"y":1394.63,"width":16.29,"height":12.4,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"text":"OK","fontSize":10,"fontFamily":9,"index":"b56","strokeWidth":2,"lineHeight":1.25}, + {"id":"re3e0000000006","type":"rectangle","x":2115,"y":1281,"width":35,"height":15,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"index":"b57","strokeWidth":2}, + {"id":"te3e0000000007","type":"text","x":2121.95,"y":1283.8,"width":21.1,"height":12.5,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"text":"new","fontSize":10,"fontFamily":9,"index":"b58","strokeWidth":2,"lineHeight":1.25}, + {"id":"te3e0000000008","type":"text","x":1940,"y":1432.44,"width":181.44,"height":16.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"text":"3e. Also leads to the other kind","fontSize":13,"fontFamily":9,"index":"b59","strokeWidth":2,"lineHeight":1.25}, + {"id":"te3e0000000009","type":"text","x":1940,"y":1449.08,"width":217.53,"height":41.25,"strokeColor":"#1e1e1e","fillStyle":"solid","roughness":0,"groupIds":["alsoleads"],"text":"A name typed without @ or # that also leads\nto a channel or contact you don't have. From\nsearch, over the filtered list.","fontSize":11,"fontFamily":9,"index":"b5A","strokeWidth":2,"lineHeight":1.25}, + {"id":"ar3e0000000001","type":"arrow","x":655,"y":1198,"width":1389.995,"height":76,"strokeColor":"#007aff","fillStyle":"solid","strokeWidth":2,"roughness":0,"boundElements":[{"id":"te3e0000000099","type":"text"}],"points":[[0,0],[1389.995,0],[1389.995,76]],"endArrowhead":"arrow","elbowed":true,"fixedSegments":null,"endBinding":{"elementId":"re3e0000000000","focus":0,"gap":26,"fixedPoint":[0.5001,-0.179211]},"index":"b5K","startBinding":{"elementId":"i9ADag2YXvSUyLm-vKlDH","fixedPoint":[39.945589,0.53135975731614],"focus":0,"gap":0}}, + {"id":"te3e0000000099","type":"text","x":1979.2,"y":1266,"width":131.59,"height":13.75,"strokeColor":"#007aff","fillStyle":"solid","roughness":0,"text":"also leads to the other kind","fontSize":11,"fontFamily":9,"textAlign":"center","verticalAlign":"middle","containerId":"ar3e0000000001","index":"b5L","strokeWidth":2,"lineHeight":1.25} ], "appState": {"gridSize": 20, "gridStep": 5, "gridModeEnabled": false, "viewBackgroundColor": "#ffffff"}, "files": {} diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 72617ac752..f3ff1796e4 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -41,7 +41,7 @@ import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import Data.Set (Set) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, isNothing) import Data.String import Data.Text (Text) import Data.Text.Encoding (decodeLatin1) @@ -718,6 +718,7 @@ planResolveModeP :: A.Parser PlanResolveMode planResolveModeP = A.takeTill (== ' ') >>= \case "all" -> pure PRMAll + "allGroups" -> pure PRMAll "on" -> pure PRMAll "unknown" -> pure PRMUnknown "off" -> pure PRMUnknown @@ -1229,15 +1230,15 @@ connectionPlanProceed = \case ILPOk {} -> True ILPOwnLink -> True _ -> False - CPContactAddress cap _ -> case cap of - CAPOk {} -> True - CAPOwnLink -> True + CPContactAddress cap w_ -> case cap of + CAPOk {addressChanged} -> not addressChanged + CAPOwnLink -> isNothing w_ CAPConnectingConfirmReconnect -> True CAPContactViaAddress _ -> True _ -> False - CPGroupLink glp _ -> case glp of - GLPOk {} -> True - GLPOwnLink _ -> True + CPGroupLink glp w_ -> case glp of + GLPOk {addressChanged} -> not addressChanged + GLPOwnLink _ -> isNothing w_ GLPConnectingConfirmReconnect -> True GLPNoRelays _ -> False GLPUpdateRequired _ -> False diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index a60a32616c..c549b0fe16 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -4429,7 +4429,8 @@ processChatCommand cxt nm = \case Right reg -> do now <- liftIO getSystemSeconds let linkOrWarning ni = serverShortLink <$> nameLinkOrWarning now ni reg - namePlan ni = (\(l, planName, _, p) -> (l, planName, otherName ni, p)) <$> connectPlan user (nameTarget ni) resolveMode sig_ (Just reg) + namePlanWith other ni = (\(l, planName, _, p) -> (l, planName, other, p)) <$> connectPlan user (nameTarget ni) resolveMode sig_ (Just reg) + namePlan ni = namePlanWith (otherName ni) ni otherName SimplexNameInfo {nameType} = case nameType of NTPublicGroup -> newName contactName contact_ NTContact -> newName channelName channel_ @@ -4439,7 +4440,7 @@ processChatCommand cxt nm = \case case match_ of Just (ni, _) -> namePlan ni Nothing -> case (linkOrWarning channelName, linkOrWarning contactName) of - (Right _, Right _) -> namePlan channelName `catchAllErrors` \e -> (namePlan contactName `catchAllErrors` \_ -> throwError e) + (Right _, Right _) -> namePlan channelName `catchAllErrors` \e -> (namePlanWith Nothing contactName `catchAllErrors` \_ -> throwError e) (Right _, Left _) -> namePlan channelName (Left _, Right _) -> namePlan contactName (Left w, Left _) -> pure (Nothing, Nothing, Nothing, CPNameNotConnectable d w) @@ -4514,16 +4515,22 @@ processChatCommand cxt nm = \case Nothing | resolveMode == PRMNever -> throwChatError CENotResolvedLocally _ -> tryAllErrors (maybe (resolveNameRegistration user nm (nameDomain ni)) pure nameReg_) >>= \case - Left e -> case known_ of - Just r@(_, p) | knownChat p -> pure $ first Just r - _ -> throwError e + Left e -> first Just <$> knownChatOrThrow e Right reg -> do now <- liftIO getSystemSeconds case nameLinkOrWarning now ni reg of - Right l' -> first Just <$> linkPlan (nameExpiresAt reg) (serverShortLink l') - Left w -> pure $ maybe (Nothing, CPNameNotConnectable (nameDomain ni) w) (bimap Just (setNameWarning w)) known_ + Right l' -> first Just <$> (linkPlan (nameExpiresAt reg) (serverShortLink l') `catchAllErrors` knownChatOrThrow) + Left w -> case known_ of + Just (l, p) -> do + let p' = setNameWarning w p + when (isNothing $ nameWarning_ p') $ setKnownVerified (nameExpiresAt reg) p' + pure (Just l, p') + Nothing -> pure (Nothing, CPNameNotConnectable (nameDomain ni) w) where known_ = fst <$> knownFresh_ + knownChatOrThrow e = case known_ of + Just r@(_, p) | knownChat p -> pure r + _ -> throwError e linkPlan expiresAt l' = case known_ of Just r@(l, _) | knownLinkOf l == Just l' -> confirmKnown expiresAt l' r diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index 5c2204d1bc..833a1ae602 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -1366,10 +1366,6 @@ Query: UPDATE connections SET smp_agent_version = ?, pq_support = ?, enable_ntfs Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) -Query: UPDATE deleted_snd_chunk_replicas SET delay = ?, retries = retries + 1, updated_at = ? WHERE deleted_snd_chunk_replica_id = ? -Plan: -SEARCH deleted_snd_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE messages SET msg_body = x'' WHERE conn_id = ? AND internal_id = ? Plan: SEARCH messages USING PRIMARY KEY (conn_id=? AND internal_id=?) @@ -1378,6 +1374,10 @@ Query: UPDATE ratchets SET ratchet_state = ? WHERE conn_id = ? Plan: SEARCH ratchets USING PRIMARY KEY (conn_id=?) +Query: UPDATE rcv_file_chunk_replicas SET delay = ?, retries = retries + 1, updated_at = ? WHERE rcv_file_chunk_replica_id = ? +Plan: +SEARCH rcv_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE rcv_file_chunk_replicas SET received = 1, updated_at = ? WHERE rcv_file_chunk_replica_id = ? Plan: SEARCH rcv_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) diff --git a/tests/ChatTests/Names.hs b/tests/ChatTests/Names.hs index b4f243a506..0796a6d142 100644 --- a/tests/ChatTests/Names.hs +++ b/tests/ChatTests/Names.hs @@ -6,7 +6,7 @@ module ChatTests.Names where import ChatClient import ChatTests.DBUtils -import ChatTests.Groups (memberJoinChannel, prepareChannel1Relay) +import ChatTests.Groups (memberJoinChannel, prepareChannel', prepareChannel1Relay) import ChatTests.Utils import Control.Concurrent.Async (concurrently_) import Data.Int (Int64) @@ -50,6 +50,9 @@ chatNamesTests = do it "known chat, name now available" testPlanKnownNameAvailable it "known chat and own name, name without link or reserved" testPlanKnownNameReserved it "known chat resolved over a day ago, the request failed" testPlanKnownNameResolverFailed + it "known chat, the name's new link cannot be fetched" testPlanKnownNameLinkFailed + it "live name also reserved for community" testPlanNameLiveCommunity + it "own channel expired, joined channel moved to a new channel" testPlanChannelNameMoved it "known chat, resolved over a day ago or past expiry" testPlanKnownNameStale it "no local chat, resolved on every call" testPlanNameResolvedEveryCall it "own name, live" testPlanOwnNameLive @@ -275,6 +278,10 @@ testConnectByNameChannelAndContact ps = withSmpServerAndNames $ \reg -> bob <## "SimpleX name: #team (verified)" bob <## "use #team to send messages" bob <## "You can also connect to @team.simplex in direct chat" + bob ##> "/_connect plan 1 #team.simplex resolve=all" + bob <## "group link: known group #team" + bob <## "SimpleX name: #team (verified)" + bob <## "use #team to send messages" registerName reg teamName (contactNameRecord "team.simplex" (T.pack contactLink)) withCCTransaction bob $ \db -> DB.execute_ db "UPDATE groups SET group_domain_resolved_at = datetime('now', '-2 days')" bob ##> "/_connect plan 1 team.simplex" @@ -305,7 +312,6 @@ testConnectByNameContactAndChannel ps = withSmpServerAndNames $ \reg -> bob ##> "/_connect plan 1 acme.simplex" bob <## "contact address: ok to connect" _ <- getTermLine bob -- contact short link data (JSON, printed in test view) - bob <## "You can also join channel #acme" alice ##> "/_connect plan 1 acme.simplex" alice <## "contact address: own address" alice <## "You can also join channel #acme" @@ -329,7 +335,6 @@ testConnectByNameBusinessAndChannel ps = withSmpServerAndNames $ \reg -> bob ##> "/_connect plan 1 biz.simplex" bob <## "business address: ok to connect" contactSLinkData <- getTermLine bob -- contact short link data (JSON, printed in test view) - bob <## "You can also join channel #biz" -- preparing the business by name saves its domain on the group, so it is then found by local name search bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> contactLink <> " domain=biz.simplex " <> contactSLinkData) bob <## "#alice: group is prepared" @@ -472,9 +477,15 @@ testPlanKnownNameReserved :: HasCallStack => TestParams -> IO () testPlanKnownNameReserved = withAliceName $ \reg _l alice bob -> do connectBobByName alice bob registerName reg aliceSimplexName (emptyRecord "alice.simplex") + withCCTransaction bob $ \db -> DB.execute_ db "UPDATE contact_profiles SET contact_domain_resolved_at = datetime('now', '-2 days')" planKnownAlice bob alice ##> "/_connect plan 1 @alice.simplex" alice <## "contact address: own address" + registerExpiredName reg aliceSimplexName (emptyRecord "alice.simplex") + bob ##> "/_connect plan 1 @alice.simplex" + bob <## "contact address: known contact alice" + bob <## "SimpleX name: @alice.simplex (verified)" + bob <## "use @alice to send messages" registerReservedName reg aliceSimplexName NRRTrademark planKnownAlice bob alice ##> "/_connect plan 1 @alice.simplex" @@ -526,6 +537,9 @@ testPlanNameResolvedEveryCall = withAliceName $ \reg shortLink _alice bob -> do bob ##> "/_connect plan 1 @alice.simplex" bob <## "contact address: ok to connect" _ <- getTermLine bob + bob ##> "/_connect plan 1 alice.simplex" + bob <## "contact address: ok to connect" + _ <- getTermLine bob registerExpiredName reg aliceSimplexName (contactNameRecord "alice.simplex" shortLink) bob ##> "/_connect plan 1 @alice.simplex" bob <## "SimpleX name alice.simplex: nothing to connect to" @@ -542,6 +556,9 @@ testPlanOwnNameExpired = withAliceName $ \reg shortLink alice _bob -> do alice ##> "/_connect plan 1 @alice.simplex resolve=all" alice <## "contact address: own address" alice <##. "your SimpleX name alice.simplex expired on " + alice ##> "/c @alice.simplex" + alice <## "contact address: own address" + alice <##. "your SimpleX name alice.simplex expired on " testPlanOwnNameAvailable :: HasCallStack => TestParams -> IO () testPlanOwnNameAvailable = withAliceName $ \reg _l alice _bob -> do @@ -559,6 +576,8 @@ testPlanNameResolveNever = withAliceName $ \_reg _l alice bob -> do bob <## "use @alice to send messages" bob ##> "/_connect plan 1 @nobody.simplex resolve=never" bob <## "no matching chat found, name resolution is disabled" + bob ##> "/_connect plan 1 nobody.simplex resolve=never" + bob <## "no matching chat found, name resolution is disabled" testPlanKnownNameAddressChanged :: HasCallStack => TestParams -> IO () testPlanKnownNameAddressChanged ps = withSmpServerAndNames $ \reg -> @@ -600,6 +619,9 @@ testPlanKnownNameAddressChanged ps = withSmpServerAndNames $ \reg -> bob ##> "/_connect plan 1 @alice.simplex" bob <## "contact address: ok to connect, address changed" _ <- getTermLine bob + bob ##> "/c @alice.simplex" + bob <## "contact address: ok to connect, address changed" + _ <- getTermLine bob bob ##> "/_connect plan 1 @alice.simplex resolve=never" bob <## "contact address: known contact alice" bob <## "SimpleX name: @alice.simplex (verified)" @@ -610,9 +632,86 @@ testPlanNameResolverFailed = withAliceName $ \reg _l _alice bob -> do failNameResolution reg broken bob ##> "/_connect plan 1 @broken.simplex" bob .<## "smpErr = NAME {nameErr = RESOLVER {resolverErr = \"HTTP 500\"}}}" + bob ##> "/_connect plan 1 broken.simplex" + bob .<## "smpErr = NAME {nameErr = RESOLVER {resolverErr = \"HTTP 500\"}}}" where broken = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "broken" []) +testPlanKnownNameLinkFailed :: HasCallStack => TestParams -> IO () +testPlanKnownNameLinkFailed ps = withSmpServerAndNames $ \reg -> + testChat3 aliceProfile bobProfile cathProfile (test reg) ps + where + test reg alice bob cath = do + mapM_ enableNamesRole [alice, bob, cath] + alice ##> "/ad" + (aliceLink, _) <- getContactLinks alice True + registerName reg aliceSimplexName (contactNameRecord "alice.simplex" (T.pack aliceLink)) + alice ##> "/_set domain 1 alice.simplex" + alice <## "new contact address set" + connectBobByName alice bob + cath ##> "/ad" + (cathLink, _) <- getContactLinks cath True + cath ##> "/da" + cath <## "Your chat address is deleted - accepted contacts will remain connected." + cath <## "To create a new chat address use /ad" + registerName reg aliceSimplexName (contactNameRecord "alice.simplex" (T.pack cathLink)) + bob ##> "/_connect plan 1 @alice.simplex resolve=all" + bob <## "contact address: known contact alice" + bob <## "SimpleX name: @alice.simplex (verified)" + bob <## "use @alice to send messages" + +testPlanNameLiveCommunity :: HasCallStack => TestParams -> IO () +testPlanNameLiveCommunity = withAliceName $ \reg shortLink _alice bob -> do + registerReservedLiveName reg aliceSimplexName NRRCommunity (contactNameRecord "alice.simplex" shortLink) + bob ##> "/_connect plan 1 @alice.simplex" + bob <## "contact address: ok to connect" + _ <- getTermLine bob + pure () + +testPlanChannelNameMoved :: HasCallStack => TestParams -> IO () +testPlanChannelNameMoved ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + mapM_ enableNamesRole [alice, cath, bob] + (shortLink, _) <- prepareChannel1Relay "team" alice cath + registerName reg teamName (channelNameRecord "team.simplex" (T.pack shortLink)) + alice ##> "/public group access #team domain=team.simplex" + alice <## "updated public group access: domain=team.simplex" + cath <## "alice updated group #team: (signed)" + cath <## "updated public group access: domain=team.simplex" + bob ##> "/c #team.simplex" + bob <## "#team: connection started" + concurrentlyN_ + [ bob + <### [ "#team: joining the group (connecting to relay cath)...", + "#team: you joined the group (connected to relay cath)" + ] + , do + cath <## "bob (Bob): accepting request to join group #team..." + cath <## "#team: bob joined the group" + , alice <### [EndsWith "introduced bob (Bob) in the channel"] + ] + registerExpiredName reg teamName (channelNameRecord "team.simplex" (T.pack shortLink)) + alice ##> "/_connect plan 1 #team.simplex" + alice <## "group link: own link for group #team" + alice <##. "your SimpleX name team.simplex expired on " + alice ##> "/c #team.simplex" + alice <## "group link: own link for group #team" + alice <##. "your SimpleX name team.simplex expired on " + (shortLink2, _) <- prepareChannel' 2 "team2" alice cath + registerName reg teamName (channelNameRecord "team.simplex" (T.pack shortLink2)) + alice ##> "/public group access #team2 domain=team.simplex" + alice <## "updated public group access: domain=team.simplex" + cath <## "alice_1 updated group #team2: (signed)" + cath <## "updated public group access: domain=team.simplex" + bob ##> "/_connect plan 1 #team.simplex resolve=all" + bob <## "group link: ok to connect via relays, address changed" + _ <- getTermLine bob + pure () + where + teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" []) + nameWarningTests :: Spec nameWarningTests = do it "warning for a name with no local chat" $ \_ -> testNameLinkOrWarning diff --git a/tests/NameResolver.hs b/tests/NameResolver.hs index 9b13e2f918..36407fc982 100644 --- a/tests/NameResolver.hs +++ b/tests/NameResolver.hs @@ -12,6 +12,7 @@ module NameResolver registerName, registerExpiredName, registerReservedName, + registerReservedLiveName, registerAvailableName, unregisterName, failNameResolution, @@ -79,6 +80,11 @@ registerExpiredName reg ni nameRecord = do registerReservedName :: NameRegistry -> SimplexNameInfo -> NameReservedReason -> IO () registerReservedName reg ni reservedReason = registerRegistration reg ni NRReserved {reservedReason} +-- | A live registration also held back by the registry, which is why it will not free up at expiry. +registerReservedLiveName :: NameRegistry -> SimplexNameInfo -> NameReservedReason -> NameRecord -> IO () +registerReservedLiveName reg ni reason nameRecord = + registerRegistration reg ni NRRegistered {expires = Nothing, graceUntil = Nothing, reservedReason_ = Just reason, nameRecord} + registerAvailableName :: NameRegistry -> SimplexNameInfo -> Int -> IO () registerAvailableName reg ni minLen = registerRegistration reg ni NRAvailable {pricing = testPricing minLen}