From f5a2d5bf884ab06e771bd80ade4df97e09225902 Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:16:04 +0300 Subject: [PATCH] Do not linkify the server part of a fediverse handle as an email address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fediverse handle such as `@name@server.tld` contains a substring that `Linkify.EMAIL_ADDRESSES` recognises as a valid address, so the timeline renders `name@server.tld` as a tappable `mailto:` link. Tapping it opens a mail composer addressed to an account that does not accept mail. Drop a newly added `mailto:` span when the character immediately before it is `@`. This mirrors what Android already does for web URLs, where `Linkify.sUrlMatchFilter` rejects a match preceded by `@` — which is why the handle currently produces exactly one stray mailto span and no partial link is left behind once it is removed. Author-supplied links are unaffected: spans that were already present before linkification are excluded from the check and restored as before. A genuine address written directly after an `@` — "mail me @support@element.io" — is no longer linkified, which is the same trade-off Android already makes for web URLs. --- .../androidutils/text/LinkifyHelper.kt | 9 +++++ .../androidutils/text/LinkifierHelperTest.kt | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt index 441f819546..7cfcb5d6e6 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt @@ -47,6 +47,11 @@ object LinkifyHelper { val start = spannable.getSpanStart(urlSpan) val end = spannable.getSpanEnd(urlSpan) + if (urlSpan !in oldURLSpans && spannable.isEmailInsideFediverseHandle(urlSpan, start)) { + spannable.removeSpan(urlSpan) + continue + } + // Try to avoid including trailing punctuation in the link. // Since this might fail in some edge cases, we catch the exception and just use the original end index. val newEnd = runCatchingExceptions { @@ -82,6 +87,10 @@ object LinkifyHelper { return spannable } + private fun Spannable.isEmailInsideFediverseHandle(urlSpan: URLSpan, start: Int): Boolean { + return urlSpan.url.startsWith("mailto:") && start > 0 && this[start - 1] == '@' + } + private fun adjustLinkifiedUrlSpanEndIndex(spannable: Spannable, start: Int, end: Int): Int { var end = end diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt index bb7a272908..df21d266e4 100644 --- a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt @@ -87,6 +87,45 @@ class LinkifierHelperTest : RobolectricTest() { assertThat(urlSpans.first().url).isEqualTo("tel:1234") } + @Test + fun `linkification ignores fediverse handle`() { + val text = "Follow me at @name@server.tld" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans).isEmpty() + } + + @Test + fun `linkification ignores fediverse handle at the start of the text`() { + val text = "@name@server.tld posted this" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans).isEmpty() + } + + @Test + fun `linkification finds email next to a fediverse handle`() { + val text = "@name@server.tld and john@doe.com" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("mailto:john@doe.com") + } + + @Test + fun `linkification keeps an existing mailto span preceded by an at sign`() { + val text = buildSpannedString { + append("Mail me @") + inSpans(URLSpan("mailto:john@doe.com")) { + append("john@doe.com") + } + } + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("mailto:john@doe.com") + } + @Test fun `linkification handles trailing dot`() { val text = "A url https://matrix.org."