review round on UX coverage

This commit is contained in:
Alain Brenzikofer committed 2026-09-29 15:15:58 +02:00
1 parent 42a4cc8adf
commit baf485b8d6
17 files changed
+336 -167

No files matched your search

@@ -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
)
}
}
@@ -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 }
)
}
}
+91 -64
View File
@@ -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
)
@@ -1018,10 +1018,10 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, 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<TextFieldValue>, 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<TextFieldValue>,
searchChatFilteredBySimplexLink: MutableState<Set<String>>,
connectNameCandidate: MutableState<String?>,
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<TextFieldValue>, connectNameCandidate: MutableState<String?>, close: (() -> Unit)?) {
internal fun ConnectByNameRow(name: String, searchChatFilteredBySimplexLink: MutableState<Set<String>>, close: (() -> Unit)?) {
val view = LocalMultiplatformView()
Row(
Modifier
@@ -1216,10 +1217,8 @@ internal fun ConnectByNameRow(name: String, searchText: MutableState<TextFieldVa
chatModel.remoteHostId(),
name,
close = close,
cleanup = {
searchText.value = TextFieldValue()
connectNameCandidate.value = null
},
filterKnownContact = { searchChatFilteredBySimplexLink.value = setOf(it.id) },
filterKnownGroup = { searchChatFilteredBySimplexLink.value = setOf(it.id) },
)
}
}
@@ -22,6 +22,8 @@ import chat.simplex.common.views.usersettings.simplexTeamUri
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.datetime.*
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
enum class ConnectionLinkType {
INVITATION, CONTACT, GROUP
@@ -61,7 +63,7 @@ suspend fun planAndConnect(
}
private fun nameDate(t: Instant): String =
t.toLocalDateTime(TimeZone.currentSystemDefault()).date.toString()
t.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG))
private fun namePrice(price: NamePrice): String {
val dollars = "$" + (price.amount / 100).toString() + (if (price.amount % 100 == 0L) "" else ".%02d".format(price.amount % 100))
@@ -110,12 +112,12 @@ private fun showNameWarningAlert(
is NameWarning.Expired -> 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()
}
@@ -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()
}
@@ -18,6 +18,7 @@
<string name="connect_plan_open_new_chat">Open new chat</string>
<string name="connect_plan_open_group">Open group</string>
<string name="connect_plan_open_new_group">Open new group</string>
<string name="connect_plan_open_new_channel">Open new channel</string>
<string name="connect_plan_you_are_observer">You are an observer</string>
<string name="connect_plan_you_are_member">You are a member</string>
<string name="connect_plan_you_are_moderator">You are a moderator</string>
@@ -207,10 +208,10 @@
<!-- SimpleX name lookup - ConnectPlan.kt -->
<string name="simplex_name_expired">Name expired</string>
<string name="simplex_name_expired_desc">%1$s expired on %2$s. Its owner can renew it until %3$s.</string>
<string name="simplex_name_expired_no_date_desc">%1$s has expired. Its owner can still renew it for a limited time.</string>
<string name="simplex_name_expired_no_date_desc">%1$s expired on %2$s.</string>
<string name="simplex_name_own_expired">Your name has expired</string>
<string name="simplex_name_own_expired_desc">Your name %1$s expired on %2$s. Renew it before %3$s.</string>
<string name="simplex_name_own_expired_no_date_desc">Your name %1$s has expired. Renew it before the grace period ends.</string>
<string name="simplex_name_own_expired_no_date_desc">Your name %1$s expired on %2$s.</string>
<string name="simplex_name_own_available_desc">Your name %1$s is no longer registered. It is available for registration for %2$s.</string>
<string name="simplex_name_not_registered">Name not registered</string>
<string name="simplex_name_no_longer_registered">Name no longer registered</string>
@@ -218,7 +219,9 @@
<string name="simplex_name_price_for_years">%1$s for %2$d years</string>
<string name="simplex_name_reserved_community_desc">%1$s is reserved for community. Please contact SimpleX Chat if you are interested in using it.</string>
<string name="simplex_name_address_changed">%1$s now leads to a new address.</string>
<string name="simplex_name_also_leads_to">%1$s also leads to %2$s</string>
<string name="simplex_name_channel_changed">%1$s now leads to a new channel.</string>
<string name="simplex_name_also_leads_to_channel">%1$s also leads to channel %2$s</string>
<string name="simplex_name_also_leads_to_contact">%1$s also leads to contact %2$s</string>
<string name="simplex_name_register">Register</string>
<string name="simplex_name_re_register">Re-register</string>
<string name="simplex_name_renew">Renew</string>
+1 -1
View File
@@ -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)
@@ -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
@@ -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(
+9 -9
View File
@@ -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 — `<Noun>Not<Predicate>`, 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 — `<Noun>Not<Predicate>`, 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
@@ -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": {}
+8 -7
View File
@@ -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
+14 -7
View File
@@ -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
@@ -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=?)
+102 -3
View File
@@ -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 <message> 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 <message> 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 <message> 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 <message> 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 <message> 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
+6
View File
@@ -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}