mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Merge branch 'develop' into fix/6371-save-file-name-conflicts
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.media
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.util.Rational
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
/**
|
||||
* Set the aspect ratio of the Picture-in-Picture mode based on the current orientation of the other user's device.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun PictureInPictureParams.Builder.setAspectRatioFromOrientation(orientation: Int): PictureInPictureParams.Builder {
|
||||
val aspectRatio = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// If landscape orientation, invert the aspect ratio
|
||||
Rational(5, 3)
|
||||
} else {
|
||||
// In any other case, assume portrait orientation
|
||||
Rational(3, 5)
|
||||
}
|
||||
return setAspectRatio(aspectRatio)
|
||||
}
|
||||
+35
@@ -16,6 +16,7 @@ import androidx.core.text.toSpannable
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import timber.log.Timber
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Helper class to linkify text while preserving existing URL spans.
|
||||
@@ -23,6 +24,8 @@ import timber.log.Timber
|
||||
* It also checks the linkified results to make sure URLs spans are not including trailing punctuation.
|
||||
*/
|
||||
object LinkifyHelper {
|
||||
private val BRACKETED_AUTHORITY_URL_REGEX = Regex("""(?<![a-zA-Z0-9+.-])[a-zA-Z][a-zA-Z0-9+.-]*://\[\S*""")
|
||||
|
||||
fun linkify(
|
||||
text: CharSequence,
|
||||
@LinkifyCompat.LinkifyMask linkifyMask: Int = Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES,
|
||||
@@ -47,6 +50,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 {
|
||||
@@ -67,6 +75,24 @@ object LinkifyHelper {
|
||||
}
|
||||
}
|
||||
|
||||
for (match in BRACKETED_AUTHORITY_URL_REGEX.findAll(spannable)) {
|
||||
val start = match.range.first
|
||||
val end = match.range.last + 1
|
||||
val newEnd = runCatchingExceptions {
|
||||
adjustLinkifiedUrlSpanEndIndex(spannable, start, end)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to adjust end index for link span")
|
||||
}.getOrNull() ?: end
|
||||
|
||||
val url = spannable.substring(start, newEnd)
|
||||
if (!url.hasBracketedHost()) continue
|
||||
|
||||
for (span in spannable.getSpans<URLSpan>(start, newEnd)) {
|
||||
spannable.removeSpan(span)
|
||||
}
|
||||
spannable.setSpan(URLSpan(url), start, newEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
// Restore old spans, remove new ones if there is a conflict
|
||||
for ((urlSpan, location) in oldURLSpans) {
|
||||
val (start, end) = location
|
||||
@@ -82,6 +108,15 @@ object LinkifyHelper {
|
||||
return spannable
|
||||
}
|
||||
|
||||
private fun String.hasBracketedHost(): Boolean {
|
||||
val host = runCatchingExceptions { URI(this) }.getOrNull()?.host ?: return false
|
||||
return host.startsWith('[') && host.endsWith(']')
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
+2
@@ -13,3 +13,5 @@ import java.nio.charset.Charset
|
||||
|
||||
fun String.urlEncoded(charset: Charset = Charsets.UTF_8): String = URLEncoder.encode(this, charset.name())
|
||||
fun String.urlDecoded(charset: Charset = Charsets.UTF_8): String = URLDecoder.decode(this, charset.name())
|
||||
|
||||
fun String.takeIfNotBlank(): String? = ifBlank { null }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="error_no_compatible_app_found">"لم يُعثر على تطبيق ليتعامل مع هذا الحدث."</string>
|
||||
</resources>
|
||||
+93
@@ -36,6 +36,60 @@ class LinkifierHelperTest : RobolectricTest() {
|
||||
assertThat(urlSpans.first().url).isEqualTo("https://matrix.org")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linkification finds ipv6 URL`() {
|
||||
val text = "A url http://[2001:db8::1]:8008/path"
|
||||
val result = LinkifyHelper.linkify(text)
|
||||
val urlSpans = result.toSpannable().getSpans<URLSpan>()
|
||||
assertThat(urlSpans.size).isEqualTo(1)
|
||||
assertThat(urlSpans.first().url).isEqualTo("http://[2001:db8::1]:8008/path")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [30])
|
||||
fun `linkification of an ipv6 URL does not leave a phone number span`() {
|
||||
shadowOf(newInstanceOf(TelephonyManager::class.java)).setSimCountryIso("DE")
|
||||
val text = "A url http://[2001:db8::1]:8008/path"
|
||||
val result = LinkifyHelper.linkify(text)
|
||||
val urlSpans = result.toSpannable().getSpans<URLSpan>()
|
||||
assertThat(urlSpans.size).isEqualTo(1)
|
||||
assertThat(urlSpans.first().url).isEqualTo("http://[2001:db8::1]:8008/path")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linkification of an ipv6 URL trims trailing punctuation`() {
|
||||
val text = "A url http://[::1]/x."
|
||||
val result = LinkifyHelper.linkify(text)
|
||||
val urlSpans = result.toSpannable().getSpans<URLSpan>()
|
||||
assertThat(urlSpans.size).isEqualTo(1)
|
||||
assertThat(urlSpans.first().url).isEqualTo("http://[::1]/x")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linkification finds an ipv6 URL of any scheme`() {
|
||||
val text = "A url ssh://[2001:db8::1]/path"
|
||||
val result = LinkifyHelper.linkify(text)
|
||||
val urlSpans = result.toSpannable().getSpans<URLSpan>()
|
||||
assertThat(urlSpans.size).isEqualTo(1)
|
||||
assertThat(urlSpans.first().url).isEqualTo("ssh://[2001:db8::1]/path")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linkification ignores a bracketed authority which is not an address`() {
|
||||
val text = "A url http://[not-an-address]/path"
|
||||
val result = LinkifyHelper.linkify(text)
|
||||
val urlSpans = result.toSpannable().getSpans<URLSpan>()
|
||||
assertThat(urlSpans).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linkification ignores bracketed text which is not a URL`() {
|
||||
val text = "An array values[0]:1 and a bare [::1]"
|
||||
val result = LinkifyHelper.linkify(text)
|
||||
val urlSpans = result.toSpannable().getSpans<URLSpan>()
|
||||
assertThat(urlSpans).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linkification finds partial URL`() {
|
||||
val text = "A partial url matrix.org/test"
|
||||
@@ -87,6 +141,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<URLSpan>()
|
||||
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<URLSpan>()
|
||||
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<URLSpan>()
|
||||
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<URLSpan>()
|
||||
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."
|
||||
|
||||
Reference in New Issue
Block a user