Skip to content

Commit c55ab68

Browse files
authored
Merge pull request #53 from element-hq/bma/linkClick
Update API to be able to check links
2 parents 1f38fec + 84e50fd commit c55ab68

File tree

7 files changed

+148
-26
lines changed

7 files changed

+148
-26
lines changed

platforms/android/example-compose/src/main/java/io/element/wysiwyg/compose/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ class MainActivity : ComponentActivity() {
157157
.padding(16.dp),
158158
resolveMentionDisplay = { _,_ -> TextDisplay.Pill },
159159
resolveRoomMentionDisplay = { TextDisplay.Pill },
160-
onLinkClickedListener = { url ->
161-
Toast.makeText(this@MainActivity, "Clicked: $url", Toast.LENGTH_SHORT).show()
160+
onLinkClickedListener = { link ->
161+
Toast.makeText(this@MainActivity, "Clicked: $link", Toast.LENGTH_SHORT).show()
162162
}
163163
)
164164

platforms/android/library-compose/src/main/java/io/element/android/wysiwyg/compose/EditorStyledText.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import io.element.android.wysiwyg.compose.internal.rememberTypeface
2323
import io.element.android.wysiwyg.compose.internal.toStyleConfig
2424
import io.element.android.wysiwyg.display.MentionDisplayHandler
2525
import io.element.android.wysiwyg.display.TextDisplay
26+
import io.element.android.wysiwyg.link.Link
2627

2728
/**
2829
* A composable EditorStyledText.
@@ -44,8 +45,8 @@ fun EditorStyledText(
4445
modifier: Modifier = Modifier,
4546
resolveMentionDisplay: (text: String, url: String) -> TextDisplay = RichTextEditorDefaults.MentionDisplay,
4647
resolveRoomMentionDisplay: () -> TextDisplay = RichTextEditorDefaults.RoomMentionDisplay,
47-
onLinkClickedListener: ((String) -> Unit)? = null,
48-
onLinkLongClickedListener: ((String) -> Unit)? = null,
48+
onLinkClickedListener: ((Link) -> Unit)? = null,
49+
onLinkLongClickedListener: ((Link) -> Unit)? = null,
4950
onTextLayout: (Layout) -> Unit = {},
5051
style: RichTextEditorStyle = RichTextEditorDefaults.style(),
5152
releaseOnDetach: Boolean = true,

platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/EditorStyledTextViewTest.kt

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
2020
import androidx.test.espresso.matcher.ViewMatchers
2121
import androidx.test.espresso.matcher.ViewMatchers.withText
2222
import androidx.test.ext.junit.rules.ActivityScenarioRule
23+
import io.element.android.wysiwyg.link.Link
2324
import io.element.android.wysiwyg.test.R
2425
import io.element.android.wysiwyg.test.utils.FakeLinkClickedListener
2526
import io.element.android.wysiwyg.test.utils.TestActivity
@@ -46,6 +47,7 @@ internal class EditorStyledTextViewTest {
4647
const val MENTION_URI = "https://matrix.to/#/@alice:matrix.org"
4748
const val MENTION_HTML = "<p><a href='$MENTION_URI'>$MENTION_TEXT</a></p>"
4849
const val URL = "https://matrix.org"
50+
const val EVIL_URL = "https://evil.org"
4951
}
5052

5153
@Test
@@ -82,7 +84,60 @@ internal class EditorStyledTextViewTest {
8284
.check(matches(withText(HELLO_WORLD)))
8385
.perform(clickXY(0f, 0f))
8486

85-
fakeLinkClickedListener.assertLinkClicked(url = URL)
87+
fakeLinkClickedListener.assertLinkClicked(Link(url = URL, text = HELLO_WORLD))
88+
}
89+
90+
@Test
91+
fun testUrlClicksEvil() {
92+
val urlSpanText = buildSpannedString {
93+
inSpans(URLSpan(EVIL_URL)) {
94+
append(URL)
95+
}
96+
}
97+
onView(ViewMatchers.withId(R.id.styledTextView))
98+
.perform(TextViewActions.setText(urlSpanText, TextView.BufferType.SPANNABLE))
99+
.perform(TextViewActions.setOnLinkClickedListener(fakeLinkClickedListener))
100+
.check(matches(withText(URL)))
101+
.perform(clickXY(0f, 0f))
102+
103+
fakeLinkClickedListener.assertLinkClicked(Link(url = EVIL_URL, text = URL))
104+
}
105+
106+
private val helloOffset = 120f
107+
108+
@Test
109+
fun testUrlClicksWord() {
110+
val urlSpanText = buildSpannedString {
111+
append("Hello, ")
112+
inSpans(URLSpan(URL)) {
113+
append("world")
114+
}
115+
}
116+
onView(ViewMatchers.withId(R.id.styledTextView))
117+
.perform(TextViewActions.setText(urlSpanText, TextView.BufferType.SPANNABLE))
118+
.perform(TextViewActions.setOnLinkClickedListener(fakeLinkClickedListener))
119+
.check(matches(withText(HELLO_WORLD)))
120+
.perform(clickXY(helloOffset, 0f))
121+
122+
fakeLinkClickedListener.assertLinkClicked(Link(url = URL, text = "world"))
123+
}
124+
125+
@Test
126+
fun testUrlClicksWordEvil() {
127+
val urlSpanText = buildSpannedString {
128+
append("Hello, ")
129+
inSpans(URLSpan(EVIL_URL)) {
130+
append(URL)
131+
}
132+
append("!")
133+
}
134+
onView(ViewMatchers.withId(R.id.styledTextView))
135+
.perform(TextViewActions.setText(urlSpanText, TextView.BufferType.SPANNABLE))
136+
.perform(TextViewActions.setOnLinkClickedListener(fakeLinkClickedListener))
137+
.check(matches(withText("Hello, $URL!")))
138+
.perform(clickXY(helloOffset, 0f))
139+
140+
fakeLinkClickedListener.assertLinkClicked(Link(url = EVIL_URL, text = URL))
86141
}
87142

88143
@Test
@@ -98,7 +153,7 @@ internal class EditorStyledTextViewTest {
98153
.check(matches(withText(HELLO_WORLD)))
99154
.perform(clickXY(0f, 0f))
100155

101-
fakeLinkClickedListener.assertLinkClicked(url = URL)
156+
fakeLinkClickedListener.assertLinkClicked(Link(url = URL, text = HELLO_WORLD))
102157
}
103158

104159
@Test
@@ -114,7 +169,7 @@ internal class EditorStyledTextViewTest {
114169
.check(matches(withText(HELLO_WORLD)))
115170
.perform(clickXY(0f, 0f))
116171

117-
fakeLinkClickedListener.assertLinkClicked(url = URL)
172+
fakeLinkClickedListener.assertLinkClicked(Link(url = URL, text = HELLO_WORLD))
118173
}
119174

120175
@Test
@@ -130,7 +185,7 @@ internal class EditorStyledTextViewTest {
130185
.check(matches(withText(HELLO_WORLD)))
131186
.perform(clickXY(0f, 0f))
132187

133-
fakeLinkClickedListener.assertLinkClicked(url = URL)
188+
fakeLinkClickedListener.assertLinkClicked(Link(url = URL, text = HELLO_WORLD))
134189
}
135190

136191
@Test
@@ -141,14 +196,30 @@ internal class EditorStyledTextViewTest {
141196
.check(matches(withText(MENTION_TEXT)))
142197
.perform(clickXY(0f, 0f))
143198

144-
fakeLinkClickedListener.assertLinkClicked(MENTION_URI)
199+
fakeLinkClickedListener.assertLinkClicked(Link(url = MENTION_URI, text = MENTION_TEXT))
145200
}
146201
}
147202

148203
object DummyReplacementSpan : ReplacementSpan() {
149-
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int = 100
150-
151-
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) = Unit
204+
override fun getSize(
205+
paint: Paint,
206+
text: CharSequence?,
207+
start: Int,
208+
end: Int,
209+
fm: Paint.FontMetricsInt?,
210+
): Int = 100
211+
212+
override fun draw(
213+
canvas: Canvas,
214+
text: CharSequence?,
215+
start: Int,
216+
end: Int,
217+
x: Float,
218+
top: Int,
219+
y: Int,
220+
bottom: Int,
221+
paint: Paint,
222+
) = Unit
152223

153224
}
154225

platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/FakeLinkClickedListener.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88

99
package io.element.android.wysiwyg.test.utils
1010

11+
import io.element.android.wysiwyg.link.Link
1112
import org.junit.Assert
1213

13-
class FakeLinkClickedListener: (String) -> Unit {
14-
private val clickedLinks: MutableList<String> = mutableListOf()
14+
class FakeLinkClickedListener: (Link) -> Unit {
15+
private val clickedLinks: MutableList<Link> = mutableListOf()
1516

16-
override fun invoke(link: String) {
17+
override fun invoke(link: Link) {
1718
clickedLinks.add(link)
1819
}
1920

20-
fun assertLinkClicked(url: String) {
21+
fun assertLinkClicked(link: Link) {
2122
Assert.assertTrue(clickedLinks.size == 1)
22-
Assert.assertTrue(clickedLinks.contains(url))
23+
Assert.assertTrue(clickedLinks.contains(link))
2324
}
2425
}

platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/TextViewActions.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.test.espresso.UiController
1515
import androidx.test.espresso.ViewAction
1616
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
1717
import io.element.android.wysiwyg.EditorStyledTextView
18+
import io.element.android.wysiwyg.link.Link
1819
import org.hamcrest.Matcher
1920

2021
object TextViewAction {
@@ -46,7 +47,7 @@ object TextViewAction {
4647
}
4748

4849
class SetOnLinkClickedListener(
49-
private val listener: (String) -> Unit,
50+
private val listener: (Link) -> Unit,
5051
) : ViewAction {
5152
override fun getConstraints(): Matcher<View> = isDisplayed()
5253

@@ -62,5 +63,5 @@ object TextViewAction {
6263
object TextViewActions {
6364
fun setText(text: CharSequence, type: BufferType = BufferType.NORMAL) = TextViewAction.SetText(text, type)
6465
fun setHtml(html: String) = TextViewAction.SetHtml(html)
65-
fun setOnLinkClickedListener(listener: (String) -> Unit) = TextViewAction.SetOnLinkClickedListener(listener)
66+
fun setOnLinkClickedListener(listener: (Link) -> Unit) = TextViewAction.SetOnLinkClickedListener(listener)
6667
}

platforms/android/library/src/main/java/io/element/android/wysiwyg/EditorStyledTextView.kt

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.core.text.getSpans
2222
import com.sun.jna.internal.Cleaner
2323
import io.element.android.wysiwyg.display.MentionDisplayHandler
2424
import io.element.android.wysiwyg.internal.view.EditorEditTextAttributeReader
25+
import io.element.android.wysiwyg.link.Link
2526
import io.element.android.wysiwyg.utils.HtmlConverter
2627
import io.element.android.wysiwyg.utils.RustCleanerTask
2728
import io.element.android.wysiwyg.view.StyleConfig
@@ -67,8 +68,8 @@ open class EditorStyledTextView : AppCompatTextView {
6768
var mentionDisplayHandler: MentionDisplayHandler? = null
6869
private var htmlConverter: HtmlConverter? = null
6970

70-
var onLinkClickedListener: ((String) -> Unit)? = null
71-
var onLinkLongClickedListener: ((String) -> Unit)? = null
71+
var onLinkClickedListener: ((Link) -> Unit)? = null
72+
var onLinkLongClickedListener: ((Link) -> Unit)? = null
7273

7374
var onTextLayout: ((Layout) -> Unit)? = null
7475

@@ -86,23 +87,45 @@ open class EditorStyledTextView : AppCompatTextView {
8687
onLinkClickedListener != null || onLinkLongClickedListener != null
8788

8889
private fun handleLinkClicks(
89-
motionEvent: MotionEvent, listener: (String) -> Unit
90+
motionEvent: MotionEvent, listener: (Link) -> Unit,
9091
): Boolean {
9192
val spans = findSpansForTouchEvent(motionEvent)
9293
for (span in spans) {
9394
when (span) {
9495
is URLSpan -> {
95-
listener(span.url)
96+
val text = getTextForSpan(span) ?: span.url
97+
listener(
98+
Link(
99+
url = span.url,
100+
text = text,
101+
)
102+
)
96103
return true
97104
}
98105

99106
is PillSpan -> {
100-
span.url?.let(listener)
107+
span.url?.let { url ->
108+
val text = getTextForSpan(span) ?: url
109+
listener(
110+
Link(
111+
url = url,
112+
text = text,
113+
)
114+
)
115+
}
101116
return true
102117
}
103118

104119
is CustomMentionSpan -> {
105-
span.url?.let(listener)
120+
span.url?.let { url ->
121+
val text = getTextForSpan(span) ?: url
122+
listener(
123+
Link(
124+
url = url,
125+
text = text,
126+
)
127+
)
128+
}
106129
return true
107130
}
108131

@@ -220,7 +243,7 @@ open class EditorStyledTextView : AppCompatTextView {
220243
}
221244

222245
private fun createHtmlConverter(
223-
styleConfig: StyleConfig, mentionDisplayHandler: MentionDisplayHandler?
246+
styleConfig: StyleConfig, mentionDisplayHandler: MentionDisplayHandler?,
224247
): HtmlConverter {
225248
return HtmlConverter.Factory.create(context = context,
226249
styleConfig = styleConfig,
@@ -258,4 +281,10 @@ open class EditorStyledTextView : AppCompatTextView {
258281
emptyArray()
259282
}
260283
}
284+
285+
private fun getTextForSpan(span: Any): String? {
286+
val start = (text as? Spanned)?.getSpanStart(span).takeIf { it != -1 } ?: return null
287+
val end = (text as? Spanned)?.getSpanEnd(span).takeIf { it != -1 } ?: return null
288+
return text?.subSequence(start, end)?.toString()
289+
}
261290
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.wysiwyg.link
9+
10+
/**
11+
* Data class defining a link, i.e. a target url and a text.
12+
* @property url The url of the string
13+
* @property text The text of the link. If not provided, the url will be used, but in this case, no
14+
* validation will be performed.
15+
*/
16+
data class Link(
17+
val url: String,
18+
val text: String = url,
19+
)

0 commit comments

Comments
 (0)