rfc: namespace (#7001)

* rfc: namespace

* update rfc

* markdown for names

* record type, app "upgrade" alerts

* update api types

* rfc: change namespace syntax - now it is the usual namespace

* update bot types

* move types to simplexmq

* core: refactore markdown

* update simplexmq

* better names

* new names

* update nix content hashes

* fix

* change valid name function

* update simplexq, update valid name conditions

* fixes

Co-authored-by: simplex-chat-agent[bot] <287173099+simplex-chat-agent[bot]@users.noreply.github.com>

* update simplexmq

* fix localization

* simpler

* refactor

* refactor

* fix

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: simplex-chat-agent[bot] <287173099+simplex-chat-agent[bot]@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-05-28 08:44:43 +01:00
committed by GitHub
parent 12fbf61f32
commit 68abd805d4
24 changed files with 703 additions and 156 deletions
@@ -4680,6 +4680,7 @@ sealed class Format {
val viaHosts: String get() =
"(${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})"
}
@Serializable @SerialName("simplexName") class SimplexName(val nameInfo: SimplexNameInfo): Format()
@Serializable @SerialName("command") class Command(val commandStr: String): Format()
@Serializable @SerialName("mention") class Mention(val memberName: String): Format()
@Serializable @SerialName("email") class Email: Format()
@@ -4697,6 +4698,7 @@ sealed class Format {
is Uri -> linkStyle
is HyperLink -> linkStyle
is SimplexLink -> linkStyle
is SimplexName -> linkStyle
is Command -> SpanStyle(color = MaterialTheme.colors.primary, fontFamily = FontFamily.Monospace)
is Mention -> SpanStyle(fontWeight = FontWeight.Medium)
is Email -> linkStyle
@@ -4728,6 +4730,27 @@ enum class SimplexLinkType(val linkType: String) {
})
}
@Serializable
data class SimplexNameInfo(
val nameType: SimplexNameType,
val nameTLD: SimplexTLD,
val domain: String,
val subDomain: List<String>
)
@Serializable
enum class SimplexTLD {
@SerialName("simplex") simplex,
@SerialName("testing") testing,
@SerialName("web") web
}
@Serializable
enum class SimplexNameType {
@SerialName("publicGroup") publicGroup,
@SerialName("contact") contact
}
@Serializable
enum class FormatColor(val color: String) {
red("red"),
@@ -281,6 +281,13 @@ fun MarkdownText (
}
}
}
is Format.SimplexName -> {
hasLinks = true
val ftStyle = Format.linkStyle
withAnnotation(tag = "SIMPLEX_NAME", annotation = i.toString()) {
withStyle(ftStyle) { append(ft.text) }
}
}
is Format.Email -> {
hasLinks = true
val ftStyle = Format.linkStyle
@@ -329,6 +336,16 @@ fun MarkdownText (
withAnnotation("WEB_URL") { a -> openBrowserAlert(a.item, uriHandler) }
withAnnotation("OTHER_URL") { a -> safeOpenUri(a.item, uriHandler) }
withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) }
withAnnotation("SIMPLEX_NAME") { a ->
val idx = a.item.toIntOrNull()
val nameInfo = (idx?.let { formattedText.getOrNull(it) }?.format as? Format.SimplexName)?.nameInfo
val (title, msg) = if (nameInfo?.nameType == SimplexNameType.contact) {
generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version)
} else {
generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version)
}
AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}")
}
}
if (hasSecrets) {
withAnnotation("SECRET") { a ->
@@ -343,7 +360,7 @@ fun MarkdownText (
onHover = { offset ->
val hasAnnotation: (String) -> Boolean = { tag -> annotatedText.hasStringAnnotations(tag, start = offset, end = offset) }
icon.value =
if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) {
if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SIMPLEX_NAME") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) {
PointerIcon.Hand
} else {
PointerIcon.Text
@@ -791,31 +791,29 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
snapshotFlow { searchText.value.text }
.distinctUntilChanged()
.collect {
val link = strHasSingleSimplexLink(it.trim())
if (link != null) {
// if SimpleX link is pasted, show connection dialogue
hideKeyboard(view)
if (link.format is Format.SimplexLink) {
val linkText = link.format.simplexLinkText
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
when (val target = strConnectTarget(it.trim())) {
is ConnectTarget.Link -> {
hideKeyboard(view)
searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero)
searchShowingSimplexLink.value = true
searchChatFilteredBySimplexLink.value = null
connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
}
searchShowingSimplexLink.value = true
searchChatFilteredBySimplexLink.value = null
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
// if some other text is pasted, enter search mode
focusRequester.requestFocus()
} else {
if (!chatModel.appOpenUrlConnecting.value) {
connectProgressManager.cancelConnectProgress()
}
if (listState.layoutInfo.totalItemsCount > 0) {
listState.scrollToItem(0)
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
focusRequester.requestFocus()
} else {
if (!chatModel.appOpenUrlConnecting.value) {
connectProgressManager.cancelConnectProgress()
}
if (listState.layoutInfo.totalItemsCount > 0) {
listState.scrollToItem(0)
}
}
searchShowingSimplexLink.value = false
searchChatFilteredBySimplexLink.value = null
}
searchShowingSimplexLink.value = false
searchChatFilteredBySimplexLink.value = null
}
}
}
@@ -30,14 +30,23 @@ suspend fun planAndConnect(
filterKnownContact: ((Contact) -> Unit)? = null,
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
): CompletableDeferred<Boolean> {
val link = strHasSingleSimplexLink(shortOrFullLink.trim())
if (link?.format is Format.SimplexLink && (link.format as Format.SimplexLink).linkType == SimplexLinkType.relay) {
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.relay_address_alert_title),
generalGetString(MR.strings.relay_address_alert_message),
)
cleanup?.invoke()
return CompletableDeferred(false)
when (val target = strConnectTarget(shortOrFullLink.trim())) {
is ConnectTarget.Name -> {
showUnsupportedNameAlert(target.nameInfo)
cleanup?.invoke()
return CompletableDeferred(false)
}
is ConnectTarget.Link -> {
if (target.linkType == SimplexLinkType.relay) {
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.relay_address_alert_title),
generalGetString(MR.strings.relay_address_alert_message),
)
cleanup?.invoke()
return CompletableDeferred(false)
}
}
null -> {}
}
connectProgressManager.cancelConnectProgress()
val inProgress = mutableStateOf(true)
@@ -73,11 +82,8 @@ private suspend fun planAndConnectTask(
if (!inProgress.value) { return completable }
if (result != null) {
val (connectionLink, connectionPlan) = result
val link = strHasSingleSimplexLink(shortOrFullLink.trim())
val linkText = if (link?.format is Format.SimplexLink)
"<br><br><u>${link.format.simplexLinkText}</u>"
else
""
val target = strConnectTarget(shortOrFullLink.trim())
val linkText = if (target is ConnectTarget.Link) "<br><br><u>${target.linkText}</u>" else ""
when (connectionPlan) {
is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) {
is InvitationLinkPlan.Ok ->
@@ -523,34 +523,32 @@ private fun ContactsSearchBar(
snapshotFlow { searchText.value.text }
.distinctUntilChanged()
.collect {
val link = strHasSingleSimplexLink(it.trim())
if (link != null) {
// if SimpleX link is pasted, show connection dialogue
hideKeyboard(view)
if (link.format is Format.SimplexLink) {
val linkText = link.format.simplexLinkText
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
when (val target = strConnectTarget(it.trim())) {
is ConnectTarget.Link -> {
hideKeyboard(view)
searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero)
searchShowingSimplexLink.value = true
searchChatFilteredBySimplexLink.value = null
connect(
link = target.text,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
cleanup = { searchText.value = TextFieldValue() }
)
}
searchShowingSimplexLink.value = true
searchChatFilteredBySimplexLink.value = null
connect(
link = link.text,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
cleanup = { searchText.value = TextFieldValue() }
)
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
// if some other text is pasted, enter search mode
focusRequester.requestFocus()
} else {
connectProgressManager.cancelConnectProgress()
if (listState.layoutInfo.totalItemsCount > 0) {
listState.scrollToItem(0)
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
focusRequester.requestFocus()
} else {
connectProgressManager.cancelConnectProgress()
if (listState.layoutInfo.totalItemsCount > 0) {
listState.scrollToItem(0)
}
}
searchShowingSimplexLink.value = false
searchChatFilteredBySimplexLink.value = null
}
searchShowingSimplexLink.value = false
searchChatFilteredBySimplexLink.value = null
}
}
}
@@ -671,13 +671,14 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRC
val clipboard = LocalClipboardManager.current
SectionItemView({
val str = clipboard.getText()?.text ?: return@SectionItemView
val link = strHasSingleSimplexLink(str.trim())
if (link != null) {
pastedLink.value = link.text
showQRCodeScanner.value = false
withBGApi { connect(rhId, link.text, close) { pastedLink.value = "" } }
} else {
AlertManager.shared.showAlertMsg(
when (val target = strConnectTarget(str.trim())) {
is ConnectTarget.Link -> {
pastedLink.value = target.text
showQRCodeScanner.value = false
withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } }
}
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
null -> AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_contact_link),
text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link)
)
@@ -819,12 +820,32 @@ fun strIsSimplexLink(str: String): Boolean {
return parsedMd != null && parsedMd.size == 1 && parsedMd[0].format is Format.SimplexLink
}
fun strHasSingleSimplexLink(str: String): FormattedText? {
val parsedMd = parseToMarkdown(str) ?: return null
val parsedLinks = parsedMd.filter { it.format?.isSimplexLink ?: false }
if (parsedLinks.size != 1) return null
sealed class ConnectTarget {
class Link(val text: String, val linkType: SimplexLinkType, val linkText: String) : ConnectTarget()
class Name(val nameInfo: SimplexNameInfo) : ConnectTarget()
}
return parsedLinks[0]
fun strConnectTarget(str: String): ConnectTarget? {
val parsedMd = parseToMarkdown(str) ?: return null
val links = parsedMd.filter { it.format?.isSimplexLink ?: false }
if (links.size == 1) {
val fmt = links[0].format as Format.SimplexLink
return ConnectTarget.Link(links[0].text, fmt.linkType, fmt.simplexLinkText)
}
if (links.isEmpty()) {
val nameInfo = parsedMd.firstNotNullOfOrNull { (it.format as? Format.SimplexName)?.nameInfo }
if (nameInfo != null) return ConnectTarget.Name(nameInfo)
}
return null
}
fun showUnsupportedNameAlert(nameInfo: SimplexNameInfo) {
val (title, msg) = if (nameInfo.nameType == SimplexNameType.contact) {
generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version)
} else {
generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version)
}
AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}")
}
@Composable
@@ -194,6 +194,11 @@
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
<string name="unsupported_connection_link">Unsupported connection link</string>
<string name="link_requires_newer_app_version_please_upgrade">This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</string>
<string name="unsupported_channel_name">Unsupported channel name</string>
<string name="unsupported_contact_name">Unsupported contact name</string>
<string name="channel_name_requires_newer_app_version">Connecting via channel name requires a newer app version.</string>
<string name="contact_name_requires_newer_app_version">Connecting via contact name requires a newer app version.</string>
<string name="please_upgrade_the_app">Please upgrade the app.</string>
<string name="channel_temporarily_unavailable">Channel temporarily unavailable</string>
<string name="channel_no_active_relays_try_later">Channel has no active relays. Please try to join later.</string>
<string name="app_update_required">App update required</string>