iterate on /goal

This commit is contained in:
Alain Brenzikofer
2026-09-29 16:32:33 +02:00
parent baf485b8d6
commit 8028247e87
17 changed files with 476 additions and 234 deletions
@@ -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,
@@ -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()
@@ -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
@@ -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()
@@ -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>