mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-05 20:57:44 +00:00
iterate on /goal
This commit is contained in:
@@ -920,7 +920,7 @@ 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 and connects online, and a chat found by the name stays filtered in the list.
|
||||
// name; tapping hides the keyboard and connects online.
|
||||
struct ConnectByNameRow: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var name: String
|
||||
|
||||
@@ -97,8 +97,7 @@ struct NewChatSheet: View {
|
||||
ConnectByNameRow(
|
||||
name: candidate,
|
||||
searchFocussed: $searchFocussed,
|
||||
dismiss: true,
|
||||
filterKnownContact: { searchChatFilteredBySimplexLink = $0.id }
|
||||
dismiss: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1224,7 +1224,7 @@ private func showPrepareGroupAlert(
|
||||
cancelTitle: openExistingChat == nil ? NSLocalizedString("Cancel", comment: "new chat action") : NSLocalizedString("Open existing chat", comment: "alert action"),
|
||||
confirmTitle: isChannel
|
||||
? (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")),
|
||||
: NSLocalizedString("Open group", comment: "new chat action"),
|
||||
secondTitle: connectOtherButton,
|
||||
onCancel: openExistingChat ?? { cleanup?() },
|
||||
onConfirm: {
|
||||
@@ -1366,12 +1366,12 @@ private func showNameWarningAlert(
|
||||
showAlert(title, message: message, actions: {
|
||||
var actions: [UIAlertAction] = []
|
||||
if let action {
|
||||
actions.append(UIAlertAction(title: action.title, style: .default) { _ in action.handler() })
|
||||
actions.append(UIAlertAction(title: action.title, style: .default) { _ in action.handler(); cleanup?() })
|
||||
}
|
||||
if let openExistingChat {
|
||||
actions.append(UIAlertAction(title: NSLocalizedString("Open existing chat", comment: "alert action"), style: .default) { _ in openExistingChat() })
|
||||
}
|
||||
actions.append(UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default) { _ in cleanup?() })
|
||||
actions.append(okCleanupAlertAction(cleanup: cleanup))
|
||||
return actions
|
||||
})
|
||||
}
|
||||
@@ -1428,28 +1428,26 @@ private func showNameWarningAlert(
|
||||
}
|
||||
}
|
||||
|
||||
// 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)?) {
|
||||
private func showOtherNameAlert(_ otherSimplexName: SimplexNameInfo, connectOtherButton: String, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)?) {
|
||||
showAlert(
|
||||
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.nameDomain.fullDomainName,
|
||||
otherSimplexName.shortStr
|
||||
),
|
||||
actions: {[
|
||||
UIAlertAction(title: connectOtherButton, style: .default) { _ in
|
||||
planAndConnect(otherSimplexName.shortStr, theme: theme, dismiss: dismiss, cleanup: cleanup)
|
||||
},
|
||||
UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default)
|
||||
okAlertAction
|
||||
]}
|
||||
)
|
||||
}
|
||||
@@ -1508,28 +1506,28 @@ func planAndConnect(
|
||||
}
|
||||
let nameDomain: SimplexDomain? = if case let .nameNotConnectable(simplexDomain, _) = connectionPlan { simplexDomain } else { planSimplexName?.nameDomain }
|
||||
if let nameWarning = connectionPlan.nameWarning, let nameDomain {
|
||||
let knownContact: Contact? = if case let .contactAddress(.known(contact), _) = connectionPlan { contact } else { nil }
|
||||
let knownGroup: GroupInfo? = if case let .groupLink(.known(groupInfo), _) = connectionPlan { groupInfo } else { nil }
|
||||
await MainActor.run {
|
||||
var openExisting: (() -> Void)? = nil
|
||||
if let contact = knownContact {
|
||||
switch connectionPlan {
|
||||
case let .contactAddress(.known(contact), _):
|
||||
if ChatModel.shared.getContactChat(contact.contactId) == nil {
|
||||
ChatModel.shared.addChat(Chat(chatInfo: .direct(contact: contact)))
|
||||
}
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
openExisting = { openKnownContact(contact, dismiss: dismiss, cleanup: nil) }
|
||||
openExisting = { openKnownContact(contact, dismiss: dismiss, cleanup: cleanup) }
|
||||
}
|
||||
} else if let groupInfo = knownGroup {
|
||||
case let .groupLink(.known(groupInfo), _):
|
||||
if ChatModel.shared.getGroupChat(groupInfo.groupId) == nil {
|
||||
ChatModel.shared.addChat(Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil)))
|
||||
}
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
} else {
|
||||
openExisting = { openKnownGroup(groupInfo, dismiss: dismiss, cleanup: nil) }
|
||||
openExisting = { openKnownGroup(groupInfo, dismiss: dismiss, cleanup: cleanup) }
|
||||
}
|
||||
default: ()
|
||||
}
|
||||
showNameWarningAlert(domain: nameDomain, warning: nameWarning, openExistingChat: openExisting, cleanup: cleanup)
|
||||
}
|
||||
@@ -1682,8 +1680,8 @@ func planAndConnect(
|
||||
}
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
if let planSimplexName, let otherSimplexName = result.otherSimplexName, let connectOtherButton {
|
||||
showOtherNameAlert(planSimplexName, otherSimplexName, connectOtherButton: connectOtherButton, theme: theme, dismiss: dismiss, cleanup: cleanup)
|
||||
if let otherSimplexName = result.otherSimplexName, let connectOtherButton {
|
||||
showOtherNameAlert(otherSimplexName, connectOtherButton: connectOtherButton, theme: theme, dismiss: dismiss, cleanup: cleanup)
|
||||
}
|
||||
} else {
|
||||
showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink)
|
||||
@@ -1775,8 +1773,8 @@ func planAndConnect(
|
||||
}
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
if let planSimplexName, let otherSimplexName = result.otherSimplexName, let connectOtherButton {
|
||||
showOtherNameAlert(planSimplexName, otherSimplexName, connectOtherButton: connectOtherButton, theme: theme, dismiss: dismiss, cleanup: cleanup)
|
||||
if let otherSimplexName = result.otherSimplexName, let connectOtherButton {
|
||||
showOtherNameAlert(otherSimplexName, connectOtherButton: connectOtherButton, theme: theme, dismiss: dismiss, cleanup: cleanup)
|
||||
}
|
||||
} else {
|
||||
showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink)
|
||||
|
||||
-15
@@ -5252,21 +5252,6 @@ enum class SimplexTLD {
|
||||
@SerialName("web") web
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class NameWarning {
|
||||
@Serializable @SerialName("expired") class Expired(val expiredAt: Instant, val graceUntil: Instant? = null): NameWarning()
|
||||
@Serializable @SerialName("ownExpired") class OwnExpired(val expiredAt: Instant, val graceUntil: Instant? = null): NameWarning()
|
||||
@Serializable @SerialName("available") class Available(val price: NamePrice): NameWarning()
|
||||
@Serializable @SerialName("noLongerRegistered") class NoLongerRegistered(val price: NamePrice): NameWarning()
|
||||
@Serializable @SerialName("ownAvailable") class OwnAvailable(val price: NamePrice): NameWarning()
|
||||
@Serializable @SerialName("reservedForCommunity") object ReservedForCommunity: NameWarning()
|
||||
@Serializable @SerialName("notRegistered") object NotRegistered: NameWarning()
|
||||
@Serializable @SerialName("noValidLink") object NoValidLink: NameWarning()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NamePrice(val amount: Long, val years: Int)
|
||||
|
||||
@Serializable
|
||||
enum class SimplexNameType {
|
||||
@SerialName("publicGroup") publicGroup,
|
||||
|
||||
+15
@@ -7454,6 +7454,21 @@ sealed class ConnectionPlan {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class NameWarning {
|
||||
@Serializable @SerialName("expired") class Expired(val expiredAt: Instant, val graceUntil: Instant? = null): NameWarning()
|
||||
@Serializable @SerialName("ownExpired") class OwnExpired(val expiredAt: Instant, val graceUntil: Instant? = null): NameWarning()
|
||||
@Serializable @SerialName("available") class Available(val price: NamePrice): NameWarning()
|
||||
@Serializable @SerialName("noLongerRegistered") class NoLongerRegistered(val price: NamePrice): NameWarning()
|
||||
@Serializable @SerialName("ownAvailable") class OwnAvailable(val price: NamePrice): NameWarning()
|
||||
@Serializable @SerialName("reservedForCommunity") object ReservedForCommunity: NameWarning()
|
||||
@Serializable @SerialName("notRegistered") object NotRegistered: NameWarning()
|
||||
@Serializable @SerialName("noValidLink") object NoValidLink: NameWarning()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NamePrice(val amount: Long, val years: Int)
|
||||
|
||||
@Serializable
|
||||
sealed class InvitationLinkPlan {
|
||||
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): InvitationLinkPlan()
|
||||
|
||||
+5
-13
@@ -1021,7 +1021,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
TagsOrConnectByName(searchText, searchChatFilteredBySimplexLink, connectNameCandidate) { candidate ->
|
||||
TagsView(searchText)
|
||||
Divider()
|
||||
ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = null)
|
||||
ConnectByNameRow(candidate) { connect(candidate, searchChatFilteredBySimplexLink, cleanup = null) }
|
||||
}
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate)
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
||||
@@ -1030,7 +1030,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate)
|
||||
// top toolbar: search bar above, so on desktop the connect row goes above the tags
|
||||
TagsOrConnectByName(searchText, searchChatFilteredBySimplexLink, connectNameCandidate) { candidate ->
|
||||
ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = null)
|
||||
ConnectByNameRow(candidate) { connect(candidate, searchChatFilteredBySimplexLink, cleanup = null) }
|
||||
Divider()
|
||||
TagsView(searchText)
|
||||
}
|
||||
@@ -1199,28 +1199,20 @@ private fun TagsOrConnectByName(
|
||||
val candidate = connectNameCandidate.value
|
||||
when {
|
||||
candidate == null -> TagsView(searchText)
|
||||
!appPlatform.isDesktop -> ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = null)
|
||||
!appPlatform.isDesktop -> ConnectByNameRow(candidate) { connect(candidate, searchChatFilteredBySimplexLink, cleanup = null) }
|
||||
else -> desktopView(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ConnectByNameRow(name: String, searchChatFilteredBySimplexLink: MutableState<Set<String>>, close: (() -> Unit)?) {
|
||||
internal fun ConnectByNameRow(name: String, onClick: () -> Unit) {
|
||||
val view = LocalMultiplatformView()
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
planAndConnect(
|
||||
chatModel.remoteHostId(),
|
||||
name,
|
||||
close = close,
|
||||
filterKnownContact = { searchChatFilteredBySimplexLink.value = setOf(it.id) },
|
||||
filterKnownGroup = { searchChatFilteredBySimplexLink.value = setOf(it.id) },
|
||||
)
|
||||
}
|
||||
onClick()
|
||||
}
|
||||
.padding(vertical = DEFAULT_PADDING_HALF),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
+17
-19
@@ -75,6 +75,7 @@ private fun openNameHowTo(uriHandler: UriHandler) = openBrowserAlert(SIMPLEX_NAM
|
||||
private const val SIMPLEX_NAMES_HOWTO_URL = "https://simplex.domains/#testing"
|
||||
|
||||
private fun showNameWarningAlert(
|
||||
rhId: Long?,
|
||||
domain: SimplexDomain,
|
||||
warning: NameWarning,
|
||||
openExistingChat: (() -> Unit)?,
|
||||
@@ -90,12 +91,12 @@ private fun showNameWarningAlert(
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Column {
|
||||
if (action != null) {
|
||||
SectionItemView({ AlertManager.privacySensitive.hideAlert(); action.second(uriHandler) }) {
|
||||
SectionItemView({ dismiss(); action.second(uriHandler) }) {
|
||||
Text(action.first, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
if (openExistingChat != null) {
|
||||
SectionItemView({ AlertManager.privacySensitive.hideAlert(); openExistingChat() }) {
|
||||
SectionItemView({ dismiss(); openExistingChat() }) {
|
||||
Text(generalGetString(MR.strings.connect_plan_open_existing_chat), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
@@ -105,7 +106,7 @@ private fun showNameWarningAlert(
|
||||
}
|
||||
}
|
||||
fun alert(title: String, text: String, action: Pair<String, (UriHandler) -> Unit>? = null) {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(title = title, text = text, onDismissRequest = { cleanup?.invoke() }, buttons = buttons(action))
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(title = title, text = text, onDismissRequest = { cleanup?.invoke() }, hostDevice = hostDevice(rhId), buttons = buttons(action))
|
||||
}
|
||||
val register = generalGetString(MR.strings.simplex_name_register) to { uh: UriHandler -> openNameHowTo(uh) }
|
||||
when (warning) {
|
||||
@@ -172,7 +173,7 @@ private suspend fun planAndConnectTask(
|
||||
connectProgressManager.stopConnectProgress()
|
||||
if (!inProgress.value) { return completable }
|
||||
if (result != null) {
|
||||
val (connectionLink_, planSimplexName, otherSimplexName, connectionPlan) = result
|
||||
val (connectionLink, planSimplexName, otherSimplexName, connectionPlan) = result
|
||||
val target = strConnectTarget(shortOrFullLink.trim())
|
||||
val linkText = if (target is ConnectTarget.Link) "<br><br><u>${target.linkText}</u>" else ""
|
||||
// the name can also resolve to the other kind; its type picks the verb, its short form the label and target
|
||||
@@ -203,10 +204,10 @@ private suspend fun planAndConnectTask(
|
||||
if (filterKnownGroup != null) filterKnownGroup(knownGroup)
|
||||
else openExisting = { openKnownGroup(chatModel, rhId, close, knownGroup) }
|
||||
}
|
||||
showNameWarningAlert(nameDomain, nameWarning, openExisting, cleanup)
|
||||
showNameWarningAlert(rhId, nameDomain, nameWarning, openExisting, cleanup)
|
||||
return completable
|
||||
}
|
||||
val connectionLink = connectionLink_ ?: run {
|
||||
if (connectionLink == null) {
|
||||
cleanup()
|
||||
return completable
|
||||
}
|
||||
@@ -288,7 +289,7 @@ private suspend fun planAndConnectTask(
|
||||
connectOtherButton = connectOtherButton,
|
||||
connectOtherLink = connectOtherLink,
|
||||
addressChanged = connectionPlan.contactAddressPlan.addressChanged,
|
||||
openExistingChat = if (filterKnownContact == null && connectionPlan.contactAddressPlan.addressChanged) openNameChat(rhId, planSimplexName, close, cleanup) else null,
|
||||
openExistingChat = if (filterKnownContact == null && connectionPlan.contactAddressPlan.addressChanged) localNameChat(rhId, planSimplexName)?.let { chat -> { openChat_(chatModel, rhId, close, chat); cleanup() } } else null,
|
||||
close,
|
||||
cleanup
|
||||
)
|
||||
@@ -350,7 +351,7 @@ private suspend fun planAndConnectTask(
|
||||
}
|
||||
if (filterKnownContact != null) {
|
||||
filterKnownContact(contact)
|
||||
if (planSimplexName != null && otherSimplexName != null && connectOtherButton != null) showOtherNameAlert(rhId, planSimplexName, otherSimplexName, connectOtherButton, close, cleanup)
|
||||
if (otherSimplexName != null && connectOtherButton != null) showOtherNameAlert(rhId, otherSimplexName, connectOtherButton, close, cleanup)
|
||||
} else {
|
||||
showOpenKnownContactAlert(chatModel, rhId, close, contact, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink)
|
||||
cleanup()
|
||||
@@ -386,7 +387,7 @@ private suspend fun planAndConnectTask(
|
||||
connectOtherButton = connectOtherButton,
|
||||
connectOtherLink = connectOtherLink,
|
||||
addressChanged = connectionPlan.groupLinkPlan.addressChanged,
|
||||
openExistingChat = if (filterKnownGroup == null && connectionPlan.groupLinkPlan.addressChanged) openNameChat(rhId, planSimplexName, close, cleanup) else null,
|
||||
openExistingChat = if (filterKnownGroup == null && connectionPlan.groupLinkPlan.addressChanged) localNameChat(rhId, planSimplexName)?.let { chat -> { openChat_(chatModel, rhId, close, chat); cleanup() } } else null,
|
||||
close,
|
||||
cleanup
|
||||
)
|
||||
@@ -458,7 +459,7 @@ private suspend fun planAndConnectTask(
|
||||
}
|
||||
if (filterKnownGroup != null) {
|
||||
filterKnownGroup(groupInfo)
|
||||
if (planSimplexName != null && otherSimplexName != null && connectOtherButton != null) showOtherNameAlert(rhId, planSimplexName, otherSimplexName, connectOtherButton, close, cleanup)
|
||||
if (otherSimplexName != null && connectOtherButton != null) showOtherNameAlert(rhId, otherSimplexName, connectOtherButton, close, cleanup)
|
||||
} else {
|
||||
showOpenKnownGroupAlert(chatModel, rhId, close, groupInfo, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink)
|
||||
cleanup()
|
||||
@@ -885,20 +886,17 @@ fun showPrepareContactAlert(
|
||||
)
|
||||
}
|
||||
|
||||
// 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() } }
|
||||
}
|
||||
private suspend fun localNameChat(rhId: Long?, name: SimplexNameInfo?): Chat? =
|
||||
name?.let { knownChatId(rhId, chatModel.controller.apiConnectPlan(rhId, it.shortStr, PlanResolveMode.PRMNever, inProgress = mutableStateOf(false))) }?.let { chatModel.getChat(it) }
|
||||
|
||||
// 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)?) {
|
||||
private fun showOtherNameAlert(rhId: Long?, otherSimplexName: SimplexNameInfo, connectOtherButton: String, close: (() -> Unit)?, cleanup: (() -> Unit)?) {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
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.nameDomain.fullDomainName,
|
||||
otherSimplexName.shortStr
|
||||
),
|
||||
hostDevice = hostDevice(rhId),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
@@ -949,7 +947,7 @@ fun showPrepareGroupAlert(
|
||||
).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)
|
||||
else MR.strings.connect_plan_open_group
|
||||
),
|
||||
onConfirm = {
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
|
||||
+2
-2
@@ -319,7 +319,7 @@ private fun ModalData.NewChatSheetLayout(
|
||||
)
|
||||
connectNameCandidate.value?.let { candidate ->
|
||||
Divider()
|
||||
ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = close)
|
||||
ConnectByNameRow(candidate) { withBGApi { planAndConnect(chatModel.remoteHostId(), candidate, close = close) } }
|
||||
}
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
||||
}
|
||||
@@ -410,7 +410,7 @@ private fun ModalData.NewChatSheetLayout(
|
||||
)
|
||||
connectNameCandidate.value?.let { candidate ->
|
||||
Divider()
|
||||
ConnectByNameRow(candidate, searchChatFilteredBySimplexLink, close = close)
|
||||
ConnectByNameRow(candidate) { withBGApi { planAndConnect(chatModel.remoteHostId(), candidate, close = close) } }
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<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_open_existing_chat">Open existing chat</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>
|
||||
@@ -205,7 +206,6 @@
|
||||
<string name="simplex_name_resolver_error_desc">Resolver error: %1$s</string>
|
||||
<string name="simplex_name_no_valid_link">No valid link</string>
|
||||
<string name="simplex_name_no_valid_link_desc">The SimpleX name %1$s is registered, but it has no valid link.</string>
|
||||
<!-- 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 expired on %2$s.</string>
|
||||
@@ -226,7 +226,6 @@
|
||||
<string name="simplex_name_re_register">Re-register</string>
|
||||
<string name="simplex_name_renew">Renew</string>
|
||||
<string name="simplex_name_connect_simplex_team">Connect to SimpleX team</string>
|
||||
<string name="connect_plan_open_existing_chat">Open existing chat</string>
|
||||
<string name="simplex_name_unconfirmed">Unconfirmed name</string>
|
||||
<string name="simplex_name_unconfirmed_desc">The SimpleX name %1$s is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.</string>
|
||||
<string name="channel_temporarily_unavailable">Channel temporarily unavailable</string>
|
||||
|
||||
@@ -181,7 +181,7 @@ 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. 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.
|
||||
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. The plan passes the expiry of the registration it resolved, and `updateGroupFromLinkData` takes it from its callers, since it must not resolve; `/_verify domain` and `APISetPublicGroupAccess` pass `NULL`. 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.
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
# Name warnings in core (#7525 review, 2026-09-28)
|
||||
|
||||
EP's review: the connection plan should say in core which name scenario applies, as a type with the minimal data it needs. The CLI renders it, and each app only maps its constructors to strings. The freshness check should sit where each kind's chat is found, not in one place for both. Bare names (kind unknown) and business chats should be visible in the logic.
|
||||
|
||||
The canvas was reviewed against this model on 2026-09-28, story by story (§4).
|
||||
|
||||
## Table of contents
|
||||
|
||||
1. Executive summary
|
||||
2. Terms
|
||||
3. Scenarios and answers
|
||||
4. Changes against today
|
||||
5. The type
|
||||
6. Plan logic, as a story per target
|
||||
7. CLI
|
||||
8. Apps
|
||||
9. Canvas changes
|
||||
10. What goes away
|
||||
11. Decisions
|
||||
12. Tests
|
||||
13. Order of work
|
||||
14. Done means
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
- **`NameWarning` replaces `NameRegistration` in the plan.** `CPContactAddress` and `CPGroupLink` have `nameWarning_ :: Maybe NameWarning`, and `CPNameNotConnectable` has `nameWarning :: NameWarning`. An app shows an alert exactly when the plan has a warning. It does not compare dates, lengths or registrations.
|
||||
- **Two pure functions decide the warning.** One computes a registration's link, or the warning when nothing is local. The other maps that warning to the one for the user's own name or for a chat. Both are tested directly.
|
||||
- **A typed name (`@d`, `#d`) is planned for its kind only.**
|
||||
- A chat at the name's link is confirmed.
|
||||
- A chat, own address or own channel at another link gives the new link's plan with `addressChanged` (3c).
|
||||
- Nothing local gives the new link's plan (2a).
|
||||
- **A bare name (`d`) is planned for both kinds.**
|
||||
- It looks up both kinds locally and resolves the name once.
|
||||
- It plans the kind that matches locally (the channel first), otherwise the kind the name has a link for.
|
||||
- It offers the other kind when the name has a link of it that the user does not have.
|
||||
- **The local lookup of each kind also returns whether the chat's name was resolved within a day.** It reads this where it finds the chat: the contact's resolution for a contact, the group's for a business chat or channel. No function checks freshness for both kinds.
|
||||
- **Accepted:**
|
||||
- An answer for a chat can be up to a day old.
|
||||
- Removing the link of a chat's kind from the name is not reported.
|
||||
- Reservations for reasons other than community, and label lengths, are not reported for a chat or the own name.
|
||||
|
||||
## 2. Terms
|
||||
|
||||
- **name:** `d`, e.g. `bakery.simplex`. It is typed as `@d` (contact name), `#d` (channel name), or bare `d`.
|
||||
- **kind:** contact or channel. `@d` has the contact kind, `#d` the channel kind. **The other kind** is the channel for `@d` and the contact for `#d`.
|
||||
- **the name's link of a kind:** the first entry in the registry record's list for that kind (`nrSimplexContact`, `nrSimplexChannel`) that parses as a short link of that kind's type. Entries that do not parse are ignored. **No link of a kind:** no such entry.
|
||||
- **links of local things:**
|
||||
- **a chat's link:** the short link stored when the user connected or joined (`conn_short_link_to_connect`);
|
||||
- **the own address's or own channel's link:** its `short_link_contact`;
|
||||
- **the same link:** equal after the server address is normalized (`serverShortLink`);
|
||||
- **a new link:** the name's link differs from the local one;
|
||||
- **leads to:** "the name leads to X" means the name's link of X's kind is X's link.
|
||||
- **registry answers:**
|
||||
- **registered:** the registry answers `NRRegistered`;
|
||||
- **live:** registered, and `expires` is absent or not earlier than now;
|
||||
- **expired:** registered, and `expires` is earlier than now, whether or not the grace period (`graceUntil`) has passed;
|
||||
- **available:** `NRAvailable`, and the label (the part before the TLD, without subnames) has at least `minLabelLength` characters;
|
||||
- **reserved for community:** `NRReserved community`;
|
||||
- **not registered:** `NRReserved` for another reason (internal, trademark, unknown), or `NRAvailable` with a shorter label;
|
||||
- **request failed:** the resolver request returns an error (network, server or resolver).
|
||||
- **address:** a SimpleX contact address, i.e. a short link of the contact type, including business addresses.
|
||||
- **own address:** the user's contact address (not a group link). It counts for `d` when the user's profile claims `d`.
|
||||
- **channel:** a public group of the channel type, joined through a short link of the channel type.
|
||||
- **own channel:** a group the user has the group link for, whose profile claims `d`.
|
||||
- **chat** (for a name, of a kind), not counting the own address or channel:
|
||||
- contact kind: a contact that is not deleted, or a business chat the user is still a member of;
|
||||
- channel kind: a joined channel (not a business chat) the user is still a member of.
|
||||
|
||||
In both cases the name `d` is verified, and a short link is stored. A contact connected by a one-time invitation, or with an unverified name, is not a chat for the name.
|
||||
- **local:** a chat, the own address or the own channel, of the kind, for the name. **Nothing local:** none of them.
|
||||
- **fresh:** the chat's name was resolved within the last 24 hours, and the expiry stored with it has not passed. The own address and channel are never fresh, so an owner sees a change on every lookup.
|
||||
- **from search, from a message:** the canvas entry points 1a and 1b.
|
||||
|
||||
## 3. Scenarios and answers
|
||||
|
||||
From `plans/sketches/2026-09-18-names-lookup-flows.excalidraw`.
|
||||
|
||||
**A typed name `@d` or `#d`, of kind K**
|
||||
|
||||
| Name | Local of kind K | Answer | Warning | Screen |
|
||||
|---|---|---|---|---|
|
||||
| live, link L | nothing | L's plan | — | 2a |
|
||||
| live, link L | chat at L | the chat, confirmed | — | 3a |
|
||||
| live, link L | chat at another link | L's plan, `addressChanged` | — | 3c |
|
||||
| live, link L | own at L | own | — | 4a |
|
||||
| live, link L | own at another link | L's plan, `addressChanged` | — | 3c |
|
||||
| live, link L whose profile does not claim `d` | nothing | error `SDEUnknownDomain` | — | 2g, 4b |
|
||||
| live, link L whose profile does not claim `d` | chat or own | the local one | — | 3a, 4a |
|
||||
| live, no link of kind K | nothing | not connectable | `NWNoValidLink` | 2f |
|
||||
| live, no link of kind K | chat or own | the local one | — | 3a, 4a |
|
||||
| expired | nothing | not connectable | `NWExpired` | 2b |
|
||||
| expired | chat | the chat | `NWExpired` | 3b, 1d |
|
||||
| expired | own | own | `NWOwnExpired` | 4c |
|
||||
| available | nothing | not connectable | `NWAvailable` | 2c |
|
||||
| available | chat | the chat | `NWNoLongerRegistered` | 3d |
|
||||
| available | own | own | `NWOwnAvailable` | 4d |
|
||||
| reserved for community | nothing or chat | not connectable, or the chat | `NWReservedForCommunity` | 2d, 3d |
|
||||
| reserved for community | own | own | `NWReservedForCommunity` (N13) | 2d's alert |
|
||||
| not registered | nothing | not connectable | `NWNotRegistered` | 2e |
|
||||
| not registered | chat or own | the local one | — | 3a, 4a |
|
||||
| request failed | chat | the chat | — | 3a |
|
||||
| request failed | own or nothing | error | — | 2h |
|
||||
| not asked: a fresh chat in the default mode, or `resolve=never` | chat or own | the local one | — | 3a, 4a |
|
||||
|
||||
A typed name has no `otherSimplexName`: `@` and `#` state the user's intent.
|
||||
|
||||
**A bare name `d`**
|
||||
|
||||
The match is what the channel kind's local lookup found, else what the contact kind's found.
|
||||
|
||||
| Local | Answer |
|
||||
|---|---|
|
||||
| `resolve=never` | the match, or `CENotResolvedLocally` |
|
||||
| default mode, and the match is a fresh chat | the chat, from the store |
|
||||
| any other match: own, a chat that is not fresh, or `resolve=all` | the resolved registration, planned as the typed name of the match's kind (the rows above) |
|
||||
| nothing local | the channel's plan if the name has a live channel link, falling back to the contact kind if it fails; otherwise the contact kind's plan if the name has a live contact link; otherwise not connectable with the "nothing" row's warning; a failed request is an error |
|
||||
|
||||
When the name is resolved and has a live link of the kind not planned, `otherSimplexName` is that kind's name, unless the user has a chat, own address or own channel of that kind at that link. The plan's screen shows it:
|
||||
- the second button of 2a, 3c and 4a;
|
||||
- 3e for a chat (new).
|
||||
|
||||
## 4. Changes against today
|
||||
|
||||
"Today" is #7525 as of `b78d76ae6`, with its apps. Prices are the registry's per-year price for the label's length, times 2 years.
|
||||
|
||||
| # | Story | Today | New |
|
||||
|---|---|---|---|
|
||||
| 1 | The name is live and also reserved for community, whatever is local | the community alert instead of the plan | the plan, as for any live name |
|
||||
| 4 | Bare name; a chat that is not fresh; the name also leads to the other kind at a link the user has no chat at (e.g. channel `#bakery`, the name now has only a contact link; or contact `@bakery`, the name has both) | the channel's plan if the name has a channel link, else the contact's; the local chat is shown only as the other kind's button, if at all | the local chat, and 3e: "bakery.simplex also leads to …", with Join channel or Connect |
|
||||
| 7 | A chat; the name leads to a new link whose profile does not claim the name | the "Unconfirmed name" error alert | the chat, no alert |
|
||||
| 8 | A chat that is not fresh; the request fails | the "SimpleX name error" alert | the chat, no alert |
|
||||
| 9 | A chat; the name leads to a new link; from a message | 3c with Open new chat, Cancel | 3c with Open new chat, Open existing chat (opens the chat that the name's local lookup finds), and no Cancel |
|
||||
| 10 | A chat; the name is available | "Name no longer registered", "from $X per year" | the same alert, "$Y for 2 years" |
|
||||
| 11 | Own address or channel; the name leads to another link | "Connect to yourself?", or the own channel | 3c: "alice.simplex now leads to a new address", as for a chat, including 9 |
|
||||
| 14 | Own; the name is available | "Your name has expired", "from $X per year" | the same alert, "$Y for 2 years" |
|
||||
| 15 | Bare name; own address; the name also has a channel link the user has no chat at | the channel's join sheet | "Connect to yourself?" (4a) with Join channel |
|
||||
|
||||
Unchanged, and checked in the review:
|
||||
- typed names never offer the other kind;
|
||||
- nothing local and no link of kind K: 2f, with no other kind;
|
||||
- a chat or own, and the name is not registered: no warning;
|
||||
- own, no link of its kind: no warning;
|
||||
- own and the request failed: the error alert.
|
||||
|
||||
## 5. The type
|
||||
|
||||
```haskell
|
||||
data NameWarning
|
||||
= NWExpired {expiredAt :: UTCTime, graceUntil :: Maybe UTCTime}
|
||||
| NWOwnExpired {expiredAt :: UTCTime, graceUntil :: Maybe UTCTime}
|
||||
| NWAvailable {price :: NamePrice}
|
||||
| NWNoLongerRegistered {price :: NamePrice}
|
||||
| NWOwnAvailable {price :: NamePrice}
|
||||
| NWReservedForCommunity
|
||||
| NWNotRegistered
|
||||
| NWNoValidLink
|
||||
|
||||
data NamePrice = NamePrice {amount :: USDCents, years :: Int}
|
||||
```
|
||||
|
||||
- `expiredAt` is present whenever a name is expired, because a registration without `expires` counts as live. A missing `graceUntil` is the canvas's dateless variant.
|
||||
- `NamePrice` is the price of the 2-year term (N3): the registry's per-year price for the label's length (or its base price), times `years = 2`.
|
||||
- The domain is not repeated. It is `planSimplexName`, or `simplexDomain` on `CPNameNotConnectable`.
|
||||
|
||||
**Deciding the warning.** Two pure functions, next to `setAddressChanged`:
|
||||
|
||||
```haskell
|
||||
nameLinkOrWarning :: SystemSeconds -> SimplexNameInfo -> NameRegistration -> Either NameWarning ShortLinkContact
|
||||
setNameWarning :: NameWarning -> ConnectionPlan -> ConnectionPlan
|
||||
```
|
||||
|
||||
- **`nameLinkOrWarning`** returns the name's link of the kind, or the warning for nothing local (the "nothing" rows of §3). The label length check moves from the apps into it.
|
||||
- **`setNameWarning`** sets the warning for the local plan, as one `case` over the plan constructors:
|
||||
- own: `NWExpired` becomes `NWOwnExpired`, `NWAvailable` becomes `NWOwnAvailable`, and `NWReservedForCommunity` stays (N13); the others become no warning;
|
||||
- a chat: `NWAvailable` becomes `NWNoLongerRegistered`, and `NWExpired` and `NWReservedForCommunity` stay; the others become no warning.
|
||||
|
||||
## 6. Plan logic, as a story per target
|
||||
|
||||
**The local lookups.** Two functions, one per kind, moved from the kind branches to the `where` of the `SCMContact` equation, so the bare name path can use them:
|
||||
- **the contact kind:** the own address, else a contact, else a business chat (`getGroupToConnect`, which matches `business_chat IS NOT NULL` for `@` names);
|
||||
- **the channel kind:** the own channel, else a joined channel.
|
||||
|
||||
Each returns the plan for what it finds, and whether it is fresh. It reads the contact's resolution for a contact, and the group's for a business chat or channel. The own address and channel are not fresh.
|
||||
|
||||
**A typed name `@d` or `#d`**
|
||||
|
||||
1. Look up the name's kind locally.
|
||||
2. With `resolve=never`, answer with what was found, or fail with `CENotResolvedLocally`.
|
||||
3. In the default mode, answer with a fresh chat.
|
||||
4. Resolve the registration, unless the bare name path passed it. If the request fails, answer with a chat, or fail.
|
||||
5. `nameLinkOrWarning` gives either:
|
||||
- **a link L:**
|
||||
- own at L is answered as found, and a chat at L is confirmed (`setContactDomainVerified`, `setGroupDomainVerified`, or the channel's refresh from its link data);
|
||||
- otherwise the plan for L, with `addressChanged` if something was local; if L's profile does not claim the name and something was local, answer with it instead;
|
||||
- **a warning:** the local plan with `setNameWarning`, or `CPNameNotConnectable d` with the warning.
|
||||
|
||||
**A link target** keeps today's steps: a local chat is answered, and `resolve=all` refreshes a known channel from its link data.
|
||||
|
||||
**A bare name `d`**
|
||||
|
||||
1. Look up both kinds locally. The match is what the channel kind's lookup found, else what the contact kind's found.
|
||||
2. With `resolve=never`, answer with the match, or fail with `CENotResolvedLocally`.
|
||||
3. In the default mode, answer with the match if it is a fresh chat.
|
||||
4. Resolve the registration once. If the request fails, answer with the match if it is a chat, or fail.
|
||||
5. Plan the kind:
|
||||
- the match's kind, if there is a match;
|
||||
- otherwise the channel if the name has a live channel link, falling back to the contact kind if that fails;
|
||||
- otherwise the contact kind if the name has a live contact link.
|
||||
|
||||
It is planned as the typed name, passing the registration. With no kind to plan, answer `CPNameNotConnectable d` with the "nothing" warning.
|
||||
6. Set `otherSimplexName` to the other kind's name if the name has a live link of it, unless the local lookup of that kind found something at that link.
|
||||
|
||||
## 7. CLI
|
||||
|
||||
`viewNameWarning` prints one line after the plan, replacing today's `registered …` / `available …` / `reserved …` lines:
|
||||
|
||||
- `SimpleX name bakery.simplex expired on 2027-06-24, its owner can renew it until 2027-09-22`
|
||||
- `your SimpleX name alice.simplex expired on 2027-06-24, renew it before 2027-09-22`
|
||||
- `SimpleX name sunflower.simplex is available: $20 for 2 years`
|
||||
- `SimpleX name bakery.simplex is no longer registered, available: $20 for 2 years`
|
||||
- `your SimpleX name alice.simplex is no longer registered, available: $20 for 2 years`
|
||||
- `SimpleX name privacy.simplex is reserved for community`
|
||||
- `SimpleX name acme.simplex is not registered`
|
||||
- `SimpleX name boogaloo.simplex has no valid link`
|
||||
|
||||
A missing `graceUntil` drops the second clause of the expiry lines. `otherSimplexNameNote` is unchanged. The prices are the test registry's ($10 per year).
|
||||
|
||||
## 8. Apps
|
||||
|
||||
- **Alert.** `showNameRegistrationAlert` becomes a plain `case` from `NameWarning` to title, message and action (Renew, Register, Re-register, Connect to SimpleX team). It keeps "Open existing chat" when the plan has a chat (1d).
|
||||
- **Flow.** `planAndConnect` shows the alert when the plan has a warning. Otherwise:
|
||||
- a plan for a chat (`CAPKnown`, `GLPKnown`) with `otherSimplexName` shows 3e (N14);
|
||||
- every other plan proceeds as today.
|
||||
|
||||
There is no `isOwn`, `notConnectable`, `hasLocalChat`, expiry or length logic in either app.
|
||||
- **3c from a message.** The buttons are Open new chat (Open new channel) and Open existing chat, with no Cancel. Open existing chat is offered only when the name's local lookup (`resolve=never`) finds a chat, so the own address gets Cancel (N20).
|
||||
- **Name search.** The "Connect to" row passes the filters, as a pasted link does (N17).
|
||||
- **Types.** Kotlin and Swift get `NameWarning` and `NamePrice` in place of `NameRegistration` and `NamePricing`. The hand-written Swift decoder for `NameRegistration` goes away: `NameWarning` is chat's own type and derives like its neighbours.
|
||||
- **Strings.** The price strings change from "from %s per year" to "%s for %d years". 3e needs a title, a message and its buttons.
|
||||
|
||||
## 9. Canvas changes
|
||||
|
||||
- **2a, 3c, 4a:** the other kind's button is shown for bare names only.
|
||||
- **3c:** also applies to the own address and channel. From a message, it shows Open new chat and Open existing chat, with no Cancel.
|
||||
- **3e (new):** a bare name matches a chat, and the name also leads to the other kind: "bakery.simplex also leads to channel #bakery.simplex", Join channel, OK. From a message it also shows Open existing chat.
|
||||
- **Prices:** "$X for 2 years", computed from the registry's price. The amounts on the canvas are examples.
|
||||
- **3a:** unchanged: "Still leads to your chat, or not found, no valid link, another name, or the request failed" matches §3.
|
||||
|
||||
## 10. What goes away
|
||||
|
||||
- `nameRegistration_` on `CPContactAddress`/`CPGroupLink`, `nameRegistration` on `CPNameNotConnectable`, and `setPlanRegistration`.
|
||||
- The `PRMUnknown` equation with `resolvedRecently`.
|
||||
- The pre-resolve branch of `CTShortContact`, `resolveNameLink`, `nameHasLink` and `nameExpired`.
|
||||
- `viewNameRegistration`, replaced by `viewNameWarning`.
|
||||
- In both apps: the decisions in `showNameRegistrationAlert`, `NameRegistration.expired`, `reservedForCommunity`, `centsPerYear`, `nameCentsPerYear`, and the Swift `NameRegistration` decoder.
|
||||
|
||||
## 11. Decisions
|
||||
|
||||
Decided:
|
||||
|
||||
| # | Question | Answer |
|
||||
|---|---|---|
|
||||
| N1 | Where the warning is | `nameWarning_ :: Maybe NameWarning` on `CPContactAddress`/`CPGroupLink`, `nameWarning :: NameWarning` on `CPNameNotConnectable` |
|
||||
| N2 | Own and chat variants | separate constructors |
|
||||
| N3 | Price | the registry's per-year price for the label's length, times 2 years |
|
||||
| N4 | A live name with `reservedReason_ = community` | no warning |
|
||||
| N5 | 3c | `addressChanged :: Bool` stays |
|
||||
| N6 | Answers up to a day old for a chat | accepted |
|
||||
| N7 | The name no longer has a link of a chat's or own's kind | not reported |
|
||||
| N8 | The other kind | offered for bare names only, unless the user has a chat, own address or own channel of that kind at its link |
|
||||
| N9 | Own address or channel at another link than the name's | 3c, as for a chat |
|
||||
| N10 | 3c from a message | Open new chat, and Open existing chat (the chat found by the name's local lookup), no Cancel |
|
||||
| N11 | Not registered (reserved for another reason, or too short), with a chat or own | not reported |
|
||||
| N12 | The new link does not claim the name, with a chat or own | the local one, no alert |
|
||||
| N15 | The request failed | with a chat: the chat, no alert; with own or nothing: the error alert |
|
||||
| N16 | Where the bare name's lookups and freshness are | the two local lookups move to the equation's `where` and return freshness; the bare name path uses both |
|
||||
| N17 | Name search ("Connect to" row) | behaves as the canvas's search (1c): it passes the filters, so found chats stay filtered, dismissing keeps the search, 3c shows Cancel and 3e is the alert |
|
||||
| N18 | `/c` when the name moved (3c) or the own name has a warning | the plan is shown instead of connecting (`connectionPlanProceed`) |
|
||||
| N19 | A chat, and the name's link data cannot be fetched | the chat, no alert, as N15 |
|
||||
| N20 | 3c for the own address from a message | Cancel: there is no chat to open |
|
||||
| N21 | 3c for a channel | "… now leads to a new channel." in the information line; Open new channel, and Open existing chat from a message |
|
||||
| N22 | Bare name, the channel failed and the contact kind is planned | the channel is not offered as the other kind |
|
||||
| N23 | A chat answered without a warning (N7, N11) | stored as resolved, so it is not re-resolved for a day |
|
||||
| N24 | Expired without a grace date | "expired on <date>", without a renew clause, as the CLI |
|
||||
| N25 | Dates in the alerts | one long localized format in both apps |
|
||||
| N26 | Bot clients | `connLink` is optional in Python and Node; `resolve=allGroups` still parses |
|
||||
| N13 | Own name reserved for community after its registration ended | `NWReservedForCommunity`, 2d's alert |
|
||||
| N14 | What shows 3e | the app, for a chat's plan with `otherSimplexName` |
|
||||
|
||||
## 12. Tests
|
||||
|
||||
- **Unit tests:**
|
||||
- `nameLinkOrWarning`: one per "nothing" row of §3, plus the dateless, too-short, 2-year price and live-community cases;
|
||||
- `setNameWarning` for the own address (`CPContactAddress CAPOwnLink`).
|
||||
|
||||
The chat rows need a `Contact` or `GroupInfo`, and are covered by CLI tests.
|
||||
- **CLI tests:** "connection plan: the name lookup answers" asserts the warning lines instead of the registration lines. Tests are added for:
|
||||
- §4 stories 4, 7, 8, 11 and 15, and 10 and 14 through the price in the warning lines; story 1 is a unit test;
|
||||
- a chat with a name reserved for community (3d);
|
||||
- a chat and own with a name not registered (no warning);
|
||||
- a business chat, fresh and not fresh;
|
||||
- a bare name with a fresh chat (no resolution).
|
||||
- **Unchanged:** the freshness tests (§9 of the lookup plan) pass as they are.
|
||||
|
||||
## 13. Order of work
|
||||
|
||||
1. Core:
|
||||
- `NameWarning`, `NamePrice`, `nameLinkOrWarning` and `setNameWarning`, with their unit tests;
|
||||
- the local lookups;
|
||||
- the typed name and bare name paths;
|
||||
- the View;
|
||||
- the CLI tests.
|
||||
2. Regenerate the bot API types.
|
||||
3. Kotlin, then Swift: the `case`, 3e, 3c from a message, the strings.
|
||||
4. Update the lookup plan (`plans/2026-09-22-name-lookup-core-api.md`) §2–§4, and the canvas (§9).
|
||||
|
||||
## 14. Done means
|
||||
|
||||
- No app reads a `NameRegistration`, compares dates or lengths, or decides whether to alert.
|
||||
- The typed name and bare name paths read as §6. Freshness is read only by the local lookups.
|
||||
- Every row of §3, and every story of §4 except 9 (apps only), has a unit test or a CLI assertion.
|
||||
@@ -710,7 +710,7 @@ data ChatCommand
|
||||
|
||||
data PlanResolveMode
|
||||
= PRMAll -- always resolve, also known chats
|
||||
| PRMUnknown -- only resolve if chat is unknown (default)
|
||||
| PRMUnknown -- resolve unknown chats, and names of known chats not resolved within a day (default)
|
||||
| PRMNever -- do not resolve links and names, only do local search
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -1490,7 +1490,7 @@ data ChatError
|
||||
| ChatErrorRemoteHost {rhKey :: RHKey, remoteHostError :: RemoteHostError}
|
||||
deriving (Show, Exception)
|
||||
|
||||
-- why a resolved SimpleX name could not be used (the name itself resolved; in a connection plan, a name with nothing to connect is CPNameNotConnectable)
|
||||
-- why a resolved SimpleX name could not be used (the name itself resolved; an unregistered name is the agent's NAME NOT_FOUND)
|
||||
data SimplexDomainError
|
||||
= SDENoValidLink -- the name's record has no usable contact/channel link
|
||||
| SDEUnknownDomain -- the resolved link's profile has no name, or a different name
|
||||
|
||||
@@ -1606,7 +1606,7 @@ processChatCommand cxt nm = \case
|
||||
UserContactLink {shortLinkDataSet, connLinkContact = CCLink _ sl_} <- withFastStore (`getUserAddress` user)
|
||||
case sl_ of
|
||||
Just sl | shortLinkDataSet -> do
|
||||
(NameRecord {nrSimplexContact}, _) <- resolveNameRecord user nm domain
|
||||
NameRecord {nrSimplexContact} <- resolveNameRecord user nm domain
|
||||
unless (nameResolvesTo sl nrSimplexContact) $ throwChatError $ CESimplexDomainNotReady domain SDENoValidLink
|
||||
pure $ Just (CLShort sl)
|
||||
_ -> throwCmdError "create the address short link and add it to name"
|
||||
@@ -2411,21 +2411,21 @@ processChatCommand cxt nm = \case
|
||||
let connLink_ = preparedContact >>= \PreparedContact {connLinkToConnect = ACCL m (CCLink _ sLnk_)} -> ACSL m <$> sLnk_
|
||||
domain <- maybe (throwCmdError "contact has no name to verify") pure contactDomain
|
||||
(verified, reason) <- verifyEntityDomain user nm NTContact domain connLink_
|
||||
ct' <- maybe (pure ct) (\(v, expiresAt) -> withFastStore' $ \db -> setContactDomainVerified db user ct v expiresAt) verified
|
||||
ct' <- maybe (pure ct) (\v -> withFastStore' $ \db -> setContactDomainVerified db user ct v Nothing) verified
|
||||
pure $ CRContactDomainVerified user ct' reason
|
||||
APIVerifyGroupDomain groupId -> withUser $ \user -> do
|
||||
g@GroupInfo {groupProfile = GroupProfile {publicGroup}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
|
||||
PublicGroupProfile {groupLink, publicGroupAccess} <- maybe (throwCmdError "not a public group") pure publicGroup
|
||||
claim <- maybe (throwCmdError "group has no name to verify") pure $ publicGroupAccess >>= groupDomainClaim
|
||||
-- checks the profile link, not the link we joined through (which may have rotated)
|
||||
(verified, reason, expiresAt) <-
|
||||
(verified, reason) <-
|
||||
tryAllErrors (resolveNameRecord user nm (claimDomain claim)) >>= \case
|
||||
Right (NameRecord {nrSimplexChannel}, expiresAt)
|
||||
| nameResolvesTo groupLink nrSimplexChannel -> pure (True, Nothing, expiresAt)
|
||||
| otherwise -> pure (False, Just "the name does not resolve to the link in the group profile", expiresAt)
|
||||
Left (ChatErrorAgent {agentError = SMP _ (NAME SMP.NOT_FOUND)}) -> pure (False, Just "the name is not registered", Nothing)
|
||||
Right NameRecord {nrSimplexChannel}
|
||||
| nameResolvesTo groupLink nrSimplexChannel -> pure (True, Nothing)
|
||||
| otherwise -> pure (False, Just "the name does not resolve to the link in the group profile")
|
||||
Left (ChatErrorAgent {agentError = SMP _ (NAME SMP.NOT_FOUND)}) -> pure (False, Just "the name is not registered")
|
||||
Left e -> throwError e
|
||||
g' <- withFastStore' $ \db -> setGroupDomainVerified db user g verified expiresAt
|
||||
g' <- withFastStore' $ \db -> setGroupDomainVerified db user g verified Nothing
|
||||
pure $ CRGroupDomainVerified user g' reason
|
||||
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
|
||||
ct@Contact {profile = LocalProfile {contactLink}, groupDirectInv} <- withFastStore $ \db -> getContact db cxt user contactId
|
||||
@@ -3280,7 +3280,7 @@ processChatCommand cxt nm = \case
|
||||
processChatCommand cxt nm $ APIListGroups userId (contactId' <$> ct_) search_
|
||||
APIUpdateGroupProfile groupId p' -> withUser $ \user -> do
|
||||
gInfo <- withFastStore $ \db -> getGroupInfoKeys db cxt user groupId
|
||||
runUpdateGroupProfile user gInfo p' Nothing
|
||||
runUpdateGroupProfile user gInfo p' False
|
||||
UpdateGroupNames gName GroupProfile {displayName, fullName, shortDescr} ->
|
||||
updateGroupProfileByName gName $ \p -> p {displayName, fullName, shortDescr}
|
||||
ShowGroupProfile gName -> withUser $ \user ->
|
||||
@@ -3294,11 +3294,11 @@ processChatCommand cxt nm = \case
|
||||
case publicGroup of
|
||||
Just pg@PublicGroupProfile {groupLink, publicGroupAccess = existingAccess} -> do
|
||||
let domainChanged = (claimDomain <$> newClaim) /= (claimDomain <$> (existingAccess >>= groupDomainClaim))
|
||||
verifiedExpiry_ <- forM (if domainChanged then claimDomain <$> newClaim else Nothing) $ \newDomain -> do
|
||||
(NameRecord {nrSimplexChannel}, expiresAt) <- resolveNameRecord user nm newDomain
|
||||
unless (nameResolvesTo groupLink nrSimplexChannel) $ throwChatError $ CESimplexDomainNotReady newDomain SDENoValidLink
|
||||
pure expiresAt
|
||||
runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}} verifiedExpiry_
|
||||
forM_ (claimDomain <$> newClaim) $ \newDomain ->
|
||||
when domainChanged $ do
|
||||
NameRecord {nrSimplexChannel} <- resolveNameRecord user nm newDomain
|
||||
unless (nameResolvesTo groupLink nrSimplexChannel) $ throwChatError $ CESimplexDomainNotReady newDomain SDENoValidLink
|
||||
runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}} (isJust newClaim && domainChanged)
|
||||
Nothing -> throwChatError $ CECommandError "not a public group"
|
||||
APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do
|
||||
gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
|
||||
@@ -4099,8 +4099,8 @@ processChatCommand cxt nm = \case
|
||||
void (sendDirectContactMessage user ct' $ XInfo p Nothing) `catchAllErrors` eToView
|
||||
lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct'
|
||||
pure $ CRContactPrefsUpdated user ct ct'
|
||||
runUpdateGroupProfile :: User -> GroupInfoKeys -> GroupProfile -> Maybe (Maybe UTCTime) -> CM ChatResponse
|
||||
runUpdateGroupProfile user (GIK gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} gks) p'@GroupProfile {displayName = n', image = img', memberAdmission = ma'} verifiedExpiry_ = do
|
||||
runUpdateGroupProfile :: User -> GroupInfoKeys -> GroupProfile -> Bool -> CM ChatResponse
|
||||
runUpdateGroupProfile user (GIK gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} gks) p'@GroupProfile {displayName = n', image = img', memberAdmission = ma'} domainVerified = do
|
||||
assertUserGroupRole gInfo GROwner
|
||||
when (n /= n') $ checkValidName n'
|
||||
checkProfileImageSize img'
|
||||
@@ -4109,7 +4109,7 @@ processChatCommand cxt nm = \case
|
||||
-- updateGroupProfile clears domain verification; re-set it when the caller already re-resolved the name
|
||||
gInfo' <- withStore $ \db -> do
|
||||
g <- updateGroupProfile db user gInfo p'
|
||||
maybe (pure g) (liftIO . setGroupDomainVerified db user g True) verifiedExpiry_
|
||||
if domainVerified then liftIO $ setGroupDomainVerified db user g True Nothing else pure g
|
||||
msg <- case businessChat of
|
||||
Just BusinessChatInfo {businessId} -> do
|
||||
ms <- withStore' $ \db -> getGroupMembers db cxt user gInfo'
|
||||
@@ -4202,7 +4202,7 @@ processChatCommand cxt nm = \case
|
||||
applicable = if channel then groupFeatureInChannel feature else groupFeatureInRegularGroup feature
|
||||
unless applicable $
|
||||
throwCmdError $ T.unpack (groupFeatureNameText feature) <> " is not available in " <> (if channel then "channels" else "groups")
|
||||
runUpdateGroupProfile user gInfo (update p) Nothing
|
||||
runUpdateGroupProfile user gInfo (update p) False
|
||||
withCurrentCall :: ContactId -> (User -> Contact -> Call -> CM (Maybe Call)) -> CM ChatResponse
|
||||
withCurrentCall ctId action = do
|
||||
(user, ct) <- withStore $ \db -> do
|
||||
@@ -4506,10 +4506,12 @@ processChatCommand cxt nm = \case
|
||||
CM (Maybe ACreatedConnLink, ConnectionPlan)
|
||||
shortLinkPlan confirmKnown resolvedPlan knownFresh_ = case nl' of
|
||||
CTLink l -> first Just <$> case known_ of
|
||||
Just r@(_, p) | not (resolveMode == PRMAll && knownChat p) -> pure r
|
||||
_ -> do
|
||||
Just r@(_, p)
|
||||
| resolveMode == PRMAll && knownChat p -> confirmKnown Nothing l r
|
||||
| otherwise -> pure r
|
||||
Nothing -> do
|
||||
when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally
|
||||
linkPlan Nothing l
|
||||
resolvedPlan Nothing l
|
||||
CTName ni -> case knownFresh_ of
|
||||
Just ((l, p), fresh) | resolveMode == PRMNever || (resolveMode == PRMUnknown && fresh) -> pure (Just l, p)
|
||||
Nothing | resolveMode == PRMNever -> throwChatError CENotResolvedLocally
|
||||
@@ -5138,10 +5140,10 @@ resolveNameRegistration user nm domain =
|
||||
registration <$> withAgent (\a -> resolveSimplexName a nm (aUserId user) domain)
|
||||
|
||||
-- the resolver now also reports names that are not registered, which stay the agent's NAME NOT_FOUND
|
||||
resolveNameRecord :: User -> NetworkRequestMode -> SimplexDomain -> CM (NameRecord, Maybe UTCTime)
|
||||
resolveNameRecord :: User -> NetworkRequestMode -> SimplexDomain -> CM NameRecord
|
||||
resolveNameRecord user nm domain =
|
||||
resolveNameRegistration user nm domain >>= \case
|
||||
reg@NRRegistered {nameRecord} -> pure (nameRecord, nameExpiresAt reg)
|
||||
NRRegistered {nameRecord} -> pure nameRecord
|
||||
_ -> throwError $ chatErrorAgent $ SMP "" (NAME SMP.NOT_FOUND)
|
||||
|
||||
nameExpiresAt :: NameRegistration -> Maybe UTCTime
|
||||
@@ -5189,24 +5191,24 @@ setNameWarning w = \case
|
||||
|
||||
setAddressChanged :: ConnectionPlan -> ConnectionPlan
|
||||
setAddressChanged = \case
|
||||
CPContactAddress (CAPOk cld ov _) nr -> CPContactAddress (CAPOk cld ov True) nr
|
||||
CPGroupLink (GLPOk li gld ov _) nr -> CPGroupLink (GLPOk li gld ov True) nr
|
||||
CPContactAddress (CAPOk cld ov _) w_ -> CPContactAddress (CAPOk cld ov True) w_
|
||||
CPGroupLink (GLPOk li gld ov _) w_ -> CPGroupLink (GLPOk li gld ov True) w_
|
||||
p -> p
|
||||
|
||||
verifyEntityDomain :: User -> NetworkRequestMode -> SimplexNameType -> SimplexDomainClaim -> Maybe AConnShortLink -> CM (Maybe (Bool, Maybe UTCTime), Maybe Text)
|
||||
verifyEntityDomain :: User -> NetworkRequestMode -> SimplexNameType -> SimplexDomainClaim -> Maybe AConnShortLink -> CM (Maybe Bool, Maybe Text)
|
||||
verifyEntityDomain user nm nameType SimplexDomainClaim {domain = StrJSON domain, proof = proof_} connLink_ = case (proof_, connLink_) of
|
||||
(Nothing, _) -> pure (Nothing, Just "no name proof to verify")
|
||||
(_, Nothing) -> pure (Nothing, Just "no connection link to check the name against")
|
||||
(Just proof, Just (ACSL SCMContact profileSLnk)) -> do
|
||||
(NameRecord {nrSimplexContact, nrSimplexChannel}, expiresAt) <- resolveNameRecord user nm domain
|
||||
NameRecord {nrSimplexContact, nrSimplexChannel} <- resolveNameRecord user nm domain
|
||||
let resolvedLinks = case nameType of
|
||||
NTContact -> nrSimplexContact
|
||||
NTPublicGroup -> nrSimplexChannel
|
||||
if not (nameResolvesTo profileSLnk resolvedLinks)
|
||||
then pure (Just (False, expiresAt), Just "the name does not resolve to this address")
|
||||
then pure (Just False, Just "the name does not resolve to this address")
|
||||
else do
|
||||
ok <- verifyDomainProof proof profileSLnk
|
||||
pure (Just (ok, expiresAt), if ok then Nothing else Just "the name proof was not signed by this address's owner")
|
||||
pure (Just ok, if ok then Nothing else Just "the name proof was not signed by this address's owner")
|
||||
(Just _, Just _) -> pure (Nothing, Just "unexpected connection link type for name verification")
|
||||
where
|
||||
verifyDomainProof :: SimplexDomainProof -> ShortLinkContact -> CM Bool
|
||||
@@ -5827,7 +5829,7 @@ sendServiceRequestBytes nm user sendTarget requestTimeout signKey request = do
|
||||
_ -> throwCmdError "service request target must be a contact"
|
||||
CTDomain d -> resolveDomain d
|
||||
resolveDomain d = do
|
||||
(nr, _) <- resolveNameRecord user nm d
|
||||
nr <- resolveNameRecord user nm d
|
||||
case firstNameLink CCTContact (nrSimplexContact nr) of
|
||||
Just sLnk -> resolveShortLink sLnk
|
||||
Nothing -> throwChatError $ CESimplexDomainNotReady d SDENoValidLink
|
||||
|
||||
@@ -7883,7 +7883,7 @@ Query: UPDATE contact_profiles SET contact_domain = ?, updated_at = ? WHERE user
|
||||
Plan:
|
||||
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE contact_profiles SET contact_domain_expires_at = datetime('now', '-1 hours')
|
||||
Query: UPDATE contact_profiles SET contact_domain_resolved_at = datetime('now'), contact_domain_expires_at = datetime('now', '-1 hours')
|
||||
Plan:
|
||||
SCAN contact_profiles
|
||||
|
||||
|
||||
+56
-108
@@ -30,7 +30,7 @@ chatNamesTests = do
|
||||
it "connect by resolved name" testConnectByName
|
||||
it "connect by name not claimed in link profile is rejected" testConnectByNameNotClaimed
|
||||
it "connect by name to a known contact not claimed in profile is rejected" testConnectByNameKnownContactNotClaimed
|
||||
it "connect by unregistered name reports the registration" testConnectByNameNotFound
|
||||
it "connect by unregistered name reports it is available" testConnectByNameNotFound
|
||||
it "set name not resolving to own address is rejected" testSetNameNotOwnAddress
|
||||
it "channel name is not verified just by joining via link" testChannelDomainLinkJoinUnverified
|
||||
it "verify channel name, fail on re-point, retain status on refresh" testChannelDomainVerify
|
||||
@@ -39,8 +39,6 @@ chatNamesTests = do
|
||||
it "connect by name resolving to direct contact (primary) and channel" testConnectByNameContactAndChannel
|
||||
it "connect by name resolving to business (primary) and channel" testConnectByNameBusinessAndChannel
|
||||
describe "connection plan: the name lookup answers" $ do
|
||||
it "expired, no local chat" testPlanNameExpired
|
||||
it "available, no local chat" testPlanNameAvailable
|
||||
it "reserved for community" testPlanNameReservedCommunity
|
||||
it "reserved for another reason" testPlanNameReservedOther
|
||||
it "registered with no usable link" testPlanNameNoValidLink
|
||||
@@ -49,9 +47,8 @@ chatNamesTests = do
|
||||
it "known chat, name moved to a new address" testPlanKnownNameAddressChanged
|
||||
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 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
|
||||
@@ -60,6 +57,7 @@ chatNamesTests = do
|
||||
it "own name, now available" testPlanOwnNameAvailable
|
||||
it "the request failed" testPlanNameResolverFailed
|
||||
it "resolve=never: local hit and miss" testPlanNameResolveNever
|
||||
describe "name warnings" nameWarningTests
|
||||
|
||||
testConnectByName :: HasCallStack => TestParams -> IO ()
|
||||
testConnectByName ps = withSmpServerAndNames $ \reg ->
|
||||
@@ -172,7 +170,7 @@ testChannelDomainLinkJoinUnverified ps = withSmpServerAndNames $ \reg ->
|
||||
cath <## "updated public group access: domain=team.simplex"
|
||||
memberJoinChannel "team" [cath] [alice] shortLink fullLink bob
|
||||
-- a link-data refresh must not mark the self-claimed name verified
|
||||
bob ##> ("/_connect plan 1 " <> shortLink <> " resolve=all")
|
||||
bob ##> ("/_connect plan 1 " <> shortLink <> " resolve=allGroups")
|
||||
bob <## "group link: known group #team"
|
||||
bob <## "use #team <message> to send messages" -- no "SimpleX name" line: status stays unknown
|
||||
where
|
||||
@@ -201,7 +199,7 @@ testChannelDomainVerify ps = withSmpServerAndNames $ \reg ->
|
||||
bob ##> "/_verify domain #1"
|
||||
bob <## "SimpleX name #team not verified: the name does not resolve to the link in the group profile"
|
||||
-- a link-data refresh keeps the failed status, not overwritten with verified
|
||||
bob ##> ("/_connect plan 1 " <> shortLink <> " resolve=all")
|
||||
bob ##> ("/_connect plan 1 " <> shortLink <> " resolve=allGroups")
|
||||
bob <## "group link: known group #team"
|
||||
bob <## "SimpleX name: #team (verification failed)"
|
||||
bob <## "use #team <message> to send messages"
|
||||
@@ -294,8 +292,8 @@ testConnectByNameChannelAndContact ps = withSmpServerAndNames $ \reg ->
|
||||
|
||||
-- The bare name "acme.simplex" resolves to both a channel and a direct contact. The channel is tried
|
||||
-- first but its group profile does not claim the domain, so the channel side of the plan fails; the
|
||||
-- plan falls back to the direct contact as primary (planSimplexName) while otherSimplexName is the
|
||||
-- channel #acme, shown as "You can also join channel #acme". The channel link is a real, fetchable
|
||||
-- plan falls back to the direct contact as primary (planSimplexName) without offering the channel; the
|
||||
-- owner's plan offers channel #acme, shown as "You can also join channel #acme". The channel link is a real, fetchable
|
||||
-- #acme channel, so the failure is the faithful "channel does not claim this domain" case, not a broken link.
|
||||
testConnectByNameContactAndChannel :: HasCallStack => TestParams -> IO ()
|
||||
testConnectByNameContactAndChannel ps = withSmpServerAndNames $ \reg ->
|
||||
@@ -382,12 +380,23 @@ withAliceName test ps = withSmpServerAndNames $ \reg ->
|
||||
where
|
||||
setup reg alice bob = do
|
||||
mapM_ enableNamesRole [alice, bob]
|
||||
alice ##> "/ad"
|
||||
(shortLink, _) <- getContactLinks alice True
|
||||
registerName reg aliceSimplexName (contactNameRecord "alice.simplex" (T.pack shortLink))
|
||||
alice ##> "/_set domain 1 alice.simplex"
|
||||
alice <## "new contact address set"
|
||||
test reg (T.pack shortLink) alice bob
|
||||
shortLink <- setAliceName reg alice
|
||||
test reg shortLink alice bob
|
||||
|
||||
setAliceName :: HasCallStack => NameRegistry -> TestCC -> IO Text
|
||||
setAliceName reg alice = do
|
||||
alice ##> "/ad"
|
||||
(shortLink, _) <- getContactLinks alice True
|
||||
registerName reg aliceSimplexName (contactNameRecord "alice.simplex" (T.pack shortLink))
|
||||
alice ##> "/_set domain 1 alice.simplex"
|
||||
alice <## "new contact address set"
|
||||
pure $ T.pack shortLink
|
||||
|
||||
knownAlicePlan :: HasCallStack => TestCC -> IO ()
|
||||
knownAlicePlan bob = do
|
||||
bob <## "contact address: known contact alice"
|
||||
bob <## "SimpleX name: @alice.simplex (verified)"
|
||||
bob <## "use @alice <message> to send messages"
|
||||
|
||||
connectBobByName :: HasCallStack => TestCC -> TestCC -> IO ()
|
||||
connectBobByName alice bob = do
|
||||
@@ -402,22 +411,6 @@ connectBobByName alice bob = do
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
|
||||
testPlanNameExpired :: HasCallStack => TestParams -> IO ()
|
||||
testPlanNameExpired = withAliceName $ \reg shortLink _alice bob -> do
|
||||
registerExpiredName reg aliceSimplexName (contactNameRecord "alice.simplex" shortLink)
|
||||
bob ##> "/_connect plan 1 @alice.simplex"
|
||||
bob <## "SimpleX name alice.simplex: nothing to connect to"
|
||||
bob <##. "SimpleX name alice.simplex expired on "
|
||||
|
||||
testPlanNameAvailable :: HasCallStack => TestParams -> IO ()
|
||||
testPlanNameAvailable = withAliceName $ \reg _l _alice bob -> do
|
||||
registerAvailableName reg sunflower 3
|
||||
bob ##> "/_connect plan 1 @sunflower.simplex"
|
||||
bob <## "SimpleX name sunflower.simplex: nothing to connect to"
|
||||
bob <## "SimpleX name sunflower.simplex is available: $20 for 2 years"
|
||||
where
|
||||
sunflower = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "sunflower" [])
|
||||
|
||||
testPlanNameReservedCommunity :: HasCallStack => TestParams -> IO ()
|
||||
testPlanNameReservedCommunity = withAliceName $ \reg _l _alice bob -> do
|
||||
registerReservedName reg privacy NRRCommunity
|
||||
@@ -449,18 +442,14 @@ testPlanKnownNameLive :: HasCallStack => TestParams -> IO ()
|
||||
testPlanKnownNameLive = withAliceName $ \_reg _l alice bob -> do
|
||||
connectBobByName alice bob
|
||||
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"
|
||||
knownAlicePlan bob
|
||||
|
||||
testPlanKnownNameExpired :: HasCallStack => TestParams -> IO ()
|
||||
testPlanKnownNameExpired = withAliceName $ \reg shortLink alice bob -> do
|
||||
connectBobByName alice bob
|
||||
registerExpiredName reg aliceSimplexName (contactNameRecord "alice.simplex" shortLink)
|
||||
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"
|
||||
knownAlicePlan bob
|
||||
bob <##. "SimpleX name alice.simplex expired on "
|
||||
|
||||
testPlanKnownNameAvailable :: HasCallStack => TestParams -> IO ()
|
||||
@@ -468,9 +457,7 @@ testPlanKnownNameAvailable = withAliceName $ \reg _l alice bob -> do
|
||||
connectBobByName alice bob
|
||||
unregisterName reg aliceSimplexName
|
||||
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"
|
||||
knownAlicePlan bob
|
||||
bob <## "SimpleX name alice.simplex is no longer registered, available: $20 for 2 years"
|
||||
|
||||
testPlanKnownNameReserved :: HasCallStack => TestParams -> IO ()
|
||||
@@ -478,59 +465,49 @@ 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
|
||||
bob ##> "/_connect plan 1 @alice.simplex resolve=all"
|
||||
knownAlicePlan 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"
|
||||
knownAlicePlan bob
|
||||
registerReservedName reg aliceSimplexName NRRTrademark
|
||||
planKnownAlice bob
|
||||
bob ##> "/_connect plan 1 @alice.simplex resolve=all"
|
||||
knownAlicePlan bob
|
||||
alice ##> "/_connect plan 1 @alice.simplex"
|
||||
alice <## "contact address: own address"
|
||||
registerReservedName reg aliceSimplexName NRRCommunity
|
||||
planKnownAlice bob
|
||||
bob ##> "/_connect plan 1 @alice.simplex resolve=all"
|
||||
knownAlicePlan bob
|
||||
bob <## "SimpleX name alice.simplex is reserved for community"
|
||||
alice ##> "/_connect plan 1 @alice.simplex"
|
||||
alice <## "contact address: own address"
|
||||
alice <## "SimpleX name alice.simplex is reserved for community"
|
||||
where
|
||||
planKnownAlice bob = do
|
||||
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"
|
||||
|
||||
testPlanKnownNameResolverFailed :: HasCallStack => TestParams -> IO ()
|
||||
testPlanKnownNameResolverFailed = withAliceName $ \reg _l alice bob -> do
|
||||
connectBobByName alice bob
|
||||
failNameResolution reg aliceSimplexName
|
||||
withCCTransaction bob $ \db -> DB.execute_ db "UPDATE contact_profiles SET contact_domain_resolved_at = datetime('now', '-2 days')"
|
||||
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"
|
||||
bob ##> "/_connect plan 1 @alice.simplex resolve=all"
|
||||
knownAlicePlan bob
|
||||
alice ##> "/_connect plan 1 @alice.simplex"
|
||||
alice .<## "smpErr = NAME {nameErr = RESOLVER {resolverErr = \"HTTP 500\"}}}"
|
||||
|
||||
testPlanKnownNameStale :: HasCallStack => TestParams -> IO ()
|
||||
testPlanKnownNameStale = withAliceName $ \_reg _l alice bob -> do
|
||||
testPlanKnownNameStale = withAliceName $ \reg shortLink alice bob -> do
|
||||
connectBobByName alice bob
|
||||
planKnownAlice bob
|
||||
registerExpiredName reg aliceSimplexName (contactNameRecord "alice.simplex" shortLink)
|
||||
bob ##> "/_connect plan 1 @alice.simplex"
|
||||
knownAlicePlan bob
|
||||
withCCTransaction bob $ \db -> DB.execute_ db "UPDATE contact_profiles SET contact_domain_resolved_at = datetime('now', '-2 days')"
|
||||
planKnownAlice bob
|
||||
planKnownAlice bob
|
||||
withCCTransaction bob $ \db -> DB.execute_ db "UPDATE contact_profiles SET contact_domain_expires_at = datetime('now', '-1 hours')"
|
||||
planKnownAlice bob
|
||||
planKnownAlice bob
|
||||
where
|
||||
planKnownAlice bob = do
|
||||
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"
|
||||
bob ##> "/_connect plan 1 @alice.simplex"
|
||||
knownAlicePlan bob
|
||||
bob <##. "SimpleX name alice.simplex expired on "
|
||||
withCCTransaction bob $ \db -> DB.execute_ db "UPDATE contact_profiles SET contact_domain_resolved_at = datetime('now'), contact_domain_expires_at = datetime('now', '-1 hours')"
|
||||
bob ##> "/_connect plan 1 @alice.simplex"
|
||||
knownAlicePlan bob
|
||||
bob <##. "SimpleX name alice.simplex expired on "
|
||||
|
||||
testPlanNameResolvedEveryCall :: HasCallStack => TestParams -> IO ()
|
||||
testPlanNameResolvedEveryCall = withAliceName $ \reg shortLink _alice bob -> do
|
||||
@@ -571,9 +548,7 @@ testPlanNameResolveNever :: HasCallStack => TestParams -> IO ()
|
||||
testPlanNameResolveNever = withAliceName $ \_reg _l alice bob -> do
|
||||
connectBobByName alice bob
|
||||
bob ##> "/_connect plan 1 @alice.simplex resolve=never"
|
||||
bob <## "contact address: known contact alice"
|
||||
bob <## "SimpleX name: @alice.simplex (verified)"
|
||||
bob <## "use @alice <message> to send messages"
|
||||
knownAlicePlan bob
|
||||
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"
|
||||
@@ -585,19 +560,13 @@ testPlanKnownNameAddressChanged ps = withSmpServerAndNames $ \reg ->
|
||||
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"
|
||||
_ <- setAliceName reg alice
|
||||
connectBobByName alice bob
|
||||
cath ##> "/ad"
|
||||
(cathLink, _) <- getContactLinks cath True
|
||||
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"
|
||||
knownAlicePlan bob
|
||||
alice ##> "/_connect plan 1 @alice.simplex"
|
||||
alice <## "contact address: own address"
|
||||
cath ##> "/_set domain 1 alice.simplex"
|
||||
@@ -606,16 +575,11 @@ testPlanKnownNameAddressChanged ps = withSmpServerAndNames $ \reg ->
|
||||
alice <## "contact address: ok to connect, address changed"
|
||||
_ <- getTermLine alice
|
||||
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"
|
||||
knownAlicePlan bob
|
||||
bob ##> "/_connect plan 1 @alice.simplex resolve=all"
|
||||
bob <## "contact address: ok to connect, address changed"
|
||||
_ <- getTermLine bob -- the new address's short link data (JSON, printed in test view)
|
||||
withCCTransaction bob $ \db -> DB.execute_ db "UPDATE contact_profiles SET contact_domain_resolved_at = datetime('now', '-2 days')"
|
||||
bob ##> "/_connect plan 1 @alice.simplex"
|
||||
bob <## "contact address: ok to connect, address changed"
|
||||
_ <- getTermLine bob
|
||||
withCCTransaction bob $ \db -> DB.execute_ db "UPDATE contact_profiles SET contact_domain_resolved_at = datetime('now', '-2 days')"
|
||||
bob ##> "/_connect plan 1 @alice.simplex"
|
||||
bob <## "contact address: ok to connect, address changed"
|
||||
_ <- getTermLine bob
|
||||
@@ -623,9 +587,7 @@ testPlanKnownNameAddressChanged ps = withSmpServerAndNames $ \reg ->
|
||||
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)"
|
||||
bob <## "use @alice <message> to send messages"
|
||||
knownAlicePlan bob
|
||||
|
||||
testPlanNameResolverFailed :: HasCallStack => TestParams -> IO ()
|
||||
testPlanNameResolverFailed = withAliceName $ \reg _l _alice bob -> do
|
||||
@@ -643,11 +605,7 @@ testPlanKnownNameLinkFailed ps = withSmpServerAndNames $ \reg ->
|
||||
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"
|
||||
_ <- setAliceName reg alice
|
||||
connectBobByName alice bob
|
||||
cath ##> "/ad"
|
||||
(cathLink, _) <- getContactLinks cath True
|
||||
@@ -656,17 +614,7 @@ testPlanKnownNameLinkFailed ps = withSmpServerAndNames $ \reg ->
|
||||
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 ()
|
||||
knownAlicePlan bob
|
||||
|
||||
testPlanChannelNameMoved :: HasCallStack => TestParams -> IO ()
|
||||
testPlanChannelNameMoved ps = withSmpServerAndNames $ \reg ->
|
||||
@@ -712,7 +660,7 @@ testPlanChannelNameMoved ps = withSmpServerAndNames $ \reg ->
|
||||
where
|
||||
teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" [])
|
||||
|
||||
nameWarningTests :: Spec
|
||||
nameWarningTests :: SpecWith TestParams
|
||||
nameWarningTests = do
|
||||
it "warning for a name with no local chat" $ \_ -> testNameLinkOrWarning
|
||||
it "warning for own name" $ \_ -> testOwnNameWarning
|
||||
|
||||
+4
-18
@@ -1,10 +1,9 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | Local HTTP names resolver for chat tests, copied from simplexmq's
|
||||
-- NamesResolverServer and made dynamic: it answers /v2/resolve/<query> from a
|
||||
-- mutable name -> NameRecord registry, so a test can resolve a name to the
|
||||
-- mutable name -> answer registry, so a test can resolve a name to the
|
||||
-- address it just created.
|
||||
module NameResolver
|
||||
( NameRegistry,
|
||||
@@ -12,8 +11,6 @@ module NameResolver
|
||||
registerName,
|
||||
registerExpiredName,
|
||||
registerReservedName,
|
||||
registerReservedLiveName,
|
||||
registerAvailableName,
|
||||
unregisterName,
|
||||
failNameResolution,
|
||||
emptyRecord,
|
||||
@@ -57,10 +54,9 @@ withNameResolver action = do
|
||||
["v2", "resolve", q] -> answer . M.lookup q <$> readTVarIO reg
|
||||
_ -> pure (notFound404, "{}")
|
||||
send $ responseLBS st [(hContentType, "application/json")] body
|
||||
answer = \case
|
||||
Just AnswerFails -> (internalServerError500, "{}")
|
||||
Just (AnswerRegistration registration) -> (ok200, J.encode NameResponse {lastBlockTs = Nothing, registration})
|
||||
Nothing -> (ok200, J.encode NameResponse {lastBlockTs = Nothing, registration = NRAvailable {pricing = testPricing 1}})
|
||||
answer (Just AnswerFails) = (internalServerError500, "{}")
|
||||
answer (Just (AnswerRegistration registration)) = (ok200, J.encode NameResponse {lastBlockTs = Nothing, registration})
|
||||
answer Nothing = (ok200, J.encode NameResponse {lastBlockTs = Nothing, registration = NRAvailable {pricing = NamePricing {registrationPrices = M.empty, basePrice = USDCents 1000, minLabelLength = 1}}})
|
||||
|
||||
-- | Register a name's domain to resolve to the given record.
|
||||
registerName :: NameRegistry -> SimplexNameInfo -> NameRecord -> IO ()
|
||||
@@ -80,13 +76,6 @@ 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}
|
||||
|
||||
failNameResolution :: NameRegistry -> SimplexNameInfo -> IO ()
|
||||
failNameResolution reg ni = atomically $ modifyTVar' reg $ M.insert (registryKey ni) AnswerFails
|
||||
@@ -98,9 +87,6 @@ registryKey :: SimplexNameInfo -> Text
|
||||
registryKey SimplexNameInfo {nameDomain = SimplexDomain {nameTLD, domain}} =
|
||||
decodeLatin1 $ strEncode (labelHash domain) <> strEncode nameTLD
|
||||
|
||||
testPricing :: Int -> NamePricing
|
||||
testPricing minLabelLength = NamePricing {registrationPrices = M.empty, basePrice = USDCents 1000, minLabelLength}
|
||||
|
||||
contactNameRecord :: Text -> Text -> NameRecord
|
||||
contactNameRecord name link = (emptyRecord name) {nrSimplexContact = [link]}
|
||||
|
||||
|
||||
+1
-2
@@ -15,7 +15,7 @@ import Bots.DirectoryTests
|
||||
import ChatClient
|
||||
import ChatTests
|
||||
import ChatTests.DBUtils
|
||||
import ChatTests.Names (chatNamesTests, nameWarningTests)
|
||||
import ChatTests.Names (chatNamesTests)
|
||||
import ChatTests.Utils (xdescribe'')
|
||||
import Control.Logger.Simple
|
||||
import Data.Time.Clock.System
|
||||
@@ -83,7 +83,6 @@ main = do
|
||||
describe "SimpleX chat view" viewTests
|
||||
describe "SimpleX chat protocol" protocolTests
|
||||
describe "Valid names" validNameTests
|
||||
describe "Name warnings" nameWarningTests
|
||||
describe "Message batching" batchingTests
|
||||
describe "Operators" operatorTests
|
||||
describe "Random servers" randomServersTests
|
||||
|
||||
Reference in New Issue
Block a user