@ -20,9 +20,11 @@ import android.net.Uri
@@ -20,9 +20,11 @@ import android.net.Uri
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.SpannedString
import android.text.style.URLSpan
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.text.toSpannable
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
@ -74,6 +76,7 @@ import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorW
@@ -74,6 +76,7 @@ import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorW
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.test.runTest
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@ -195,7 +198,7 @@ class TimelineItemContentMessageFactoryTest {
@@ -195,7 +198,7 @@ class TimelineItemContentMessageFactoryTest {
inSpans ( URLSpan ( " https://matrix.org " ) ) {
append ( " and manually added link " )
}
}
} . toSpannable ( )
val sut = createTimelineItemContentMessageFactory (
htmlConverterTransform = { expected }
)
@ -610,7 +613,7 @@ class TimelineItemContentMessageFactoryTest {
@@ -610,7 +613,7 @@ class TimelineItemContentMessageFactoryTest {
senderDisambiguatedDisplayName = " Bob " ,
eventId = AN _EVENT _ID ,
)
assertThat ( ( result as TimelineItemNoticeContent ) . formattedBody ) . isEqualTo ( " formatted " )
( result as TimelineItemNoticeContent ) . formattedBody . assertSpannedEquals ( SpannedString ( " formatted " ) )
}
@Test
@ -644,7 +647,8 @@ class TimelineItemContentMessageFactoryTest {
@@ -644,7 +647,8 @@ class TimelineItemContentMessageFactoryTest {
senderDisambiguatedDisplayName = " Bob " ,
eventId = AN _EVENT _ID ,
)
assertThat ( ( result as TimelineItemEmoteContent ) . formattedBody ) . isEqualTo ( SpannableString ( " * Bob formatted " ) )
( result as TimelineItemEmoteContent ) . formattedBody . assertSpannedEquals ( SpannableString ( " * Bob formatted " ) )
}
@Test
@ -654,7 +658,7 @@ class TimelineItemContentMessageFactoryTest {
@@ -654,7 +658,7 @@ class TimelineItemContentMessageFactoryTest {
inSpans ( URLSpan ( " https://www.example.org " ) ) {
append ( " me@matrix.org " )
}
}
} . toSpannable ( )
val sut = createTimelineItemContentMessageFactory (
htmlConverterTransform = { expectedSpanned } ,
permalinkParser = FakePermalinkParser { PermalinkData . FallbackLink ( Uri . EMPTY ) }
@ -669,7 +673,59 @@ class TimelineItemContentMessageFactoryTest {
@@ -669,7 +673,59 @@ class TimelineItemContentMessageFactoryTest {
senderDisambiguatedDisplayName = " Bob " ,
eventId = AN _EVENT _ID ,
)
assertThat ( ( result as TimelineItemTextContent ) . formattedBody ) . isEqualTo ( expectedSpanned )
( result as TimelineItemTextContent ) . formattedBody . assertSpannedEquals ( expectedSpanned )
}
@Test
fun `a message with plain URL in a formatted body Spanned format gets linkified too` ( ) = runTest {
val expectedSpanned = buildSpannedString {
append ( " Test " )
inSpansWithFlags ( URLSpan ( " https://www.example.org " ) , flags = Spanned . SPAN _EXCLUSIVE _EXCLUSIVE ) {
append ( " https://www.example.org " )
}
}
val sut = createTimelineItemContentMessageFactory (
htmlConverterTransform = { expectedSpanned } ,
permalinkParser = FakePermalinkParser { PermalinkData . FallbackLink ( Uri . EMPTY ) }
)
val result = sut . create (
content = createMessageContent (
type = TextMessageType (
body = " Test [me@matrix.org](https://www.example.org) " ,
formatted = FormattedBody ( MessageFormat . HTML , " Test https://www.example.org " )
)
) ,
senderDisambiguatedDisplayName = " Bob " ,
eventId = AN _EVENT _ID ,
)
( result as TimelineItemTextContent ) . formattedBody . assertSpannedEquals ( expectedSpanned )
}
@Test
fun `a message with plain URL in a formatted body with plain text format gets linkified too` ( ) = runTest {
val resultString = " Test https://www.example.org "
val expectedSpanned = buildSpannedString {
append ( " Test " )
inSpansWithFlags ( URLSpan ( " https://www.example.org " ) , flags = Spanned . SPAN _EXCLUSIVE _EXCLUSIVE ) {
append ( " https://www.example.org " )
}
} . toSpannable ( )
val sut = createTimelineItemContentMessageFactory (
htmlConverterTransform = { resultString } ,
permalinkParser = FakePermalinkParser { PermalinkData . FallbackLink ( Uri . EMPTY ) }
)
val result = sut . create (
content = createMessageContent (
type = TextMessageType (
body = " Test [me@matrix.org](https://www.example.org) " ,
formatted = FormattedBody ( MessageFormat . HTML , " Test https://www.example.org " )
)
) ,
senderDisambiguatedDisplayName = " Bob " ,
eventId = AN _EVENT _ID ,
)
( result as TimelineItemTextContent ) . formattedBody . assertSpannedEquals ( expectedSpanned )
}
private fun createMessageContent (
@ -718,3 +774,40 @@ class TimelineItemContentMessageFactoryTest {
@@ -718,3 +774,40 @@ class TimelineItemContentMessageFactoryTest {
fileExtensionExtractor = FileExtensionExtractorWithoutValidation ( )
)
}
private inline fun SpannableStringBuilder . inSpansWithFlags ( span : Any , flags : Int , action : SpannableStringBuilder . ( ) -> Unit ) {
val start = this . length
action ( )
val end = this . length
setSpan ( span , start , end , flags )
}
fun CharSequence ?. assertSpannedEquals ( other : CharSequence ? ) {
if ( this == null && other == null ) {
return
} else if ( this is Spanned && other is Spanned ) {
assertThat ( this . toString ( ) ) . isEqualTo ( other . toString ( ) )
assertThat ( this . length ) . isEqualTo ( other . length )
val thisSpans = this . getSpans ( 0 , this . length , Any :: class . java )
val otherSpans = other . getSpans ( 0 , other . length , Any :: class . java )
if ( thisSpans . size != otherSpans . size ) {
fail ( " Expected ${thisSpans.size} spans, got ${otherSpans.size} " )
}
thisSpans . forEachIndexed { index , span ->
val otherSpan = otherSpans [ index ]
// URLSpans don't have a proper `equals` implementation, so we compare the URL instead
if ( span is URLSpan && otherSpan is URLSpan ) {
assertThat ( span . url ) . isEqualTo ( otherSpan . url )
} else {
assertThat ( span ) . isEqualTo ( otherSpan )
}
assertThat ( this . getSpanStart ( span ) ) . isEqualTo ( other . getSpanStart ( otherSpan ) )
assertThat ( this . getSpanEnd ( span ) ) . isEqualTo ( other . getSpanEnd ( otherSpan ) )
assertThat ( this . getSpanFlags ( span ) ) . isEqualTo ( other . getSpanFlags ( otherSpan ) )
}
} else {
val thisString = this ?. toString ( ) ?: " null "
val otherString = other ?. toString ( ) ?: " null "
fail ( " Expected Spanned, got $thisString and $otherString " )
}
}