Restore intentional mentions in the markdown/plain text editor (#3193)

* Restore intentional mentions in the markdown/plain text editor

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-07-15 18:27:59 +02:00 committed by GitHub
parent 5493d6b741
commit 2ff5fa67fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 717 additions and 288 deletions

View file

@ -16,13 +16,16 @@
package io.element.android.libraries.matrix.api.core
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
/**
* This class contains pattern to match the different Matrix ids
* Ref: https://matrix.org/docs/spec/appendices#identifier-grammar
*/
object MatrixPatterns {
// Note: TLD is not mandatory (localhost, IP address...)
private const val DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?"
private const val DOMAIN_REGEX = ":[A-Za-z0-9.-]+(:[0-9]{2,5})?"
// regex pattern to find matrix user ids in a string.
// See https://matrix.org/docs/spec/appendices#historical-user-ids
@ -109,4 +112,56 @@ object MatrixPatterns {
* @return true if the string is a valid thread id.
*/
fun isThreadId(str: String?) = isEventId(str)
/**
* Finds existing ids or aliases in a [CharSequence].
* Note not all cases are implemented.
*/
fun findPatterns(text: CharSequence, permalinkParser: PermalinkParser): List<MatrixPatternResult> {
val rawTextMatches = "\\S+?$DOMAIN_REGEX".toRegex(RegexOption.IGNORE_CASE).findAll(text)
val urlMatches = "\\[\\S+?\\]\\((\\S+?)\\)".toRegex(RegexOption.IGNORE_CASE).findAll(text)
val atRoomMatches = Regex("@room").findAll(text)
return buildList {
for (match in rawTextMatches) {
// Match existing id and alias patterns in the text
val type = when {
isUserId(match.value) -> MatrixPatternType.USER_ID
isRoomId(match.value) -> MatrixPatternType.ROOM_ID
isRoomAlias(match.value) -> MatrixPatternType.ROOM_ALIAS
isEventId(match.value) -> MatrixPatternType.EVENT_ID
else -> null
}
if (type != null) {
add(MatrixPatternResult(type, match.value, match.range.first, match.range.last + 1))
}
}
for (match in urlMatches) {
// Extract the link and check if it's a valid permalink
val urlMatch = match.groupValues[1]
when (val permalink = permalinkParser.parse(urlMatch)) {
is PermalinkData.UserLink -> {
add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.toString(), match.range.first, match.range.last + 1))
}
is PermalinkData.RoomLink -> {
add(MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, permalink.roomIdOrAlias.identifier, match.range.first, match.range.last + 1))
}
else -> Unit
}
}
for (match in atRoomMatches) {
// Special case for `@room` mentions
add(MatrixPatternResult(MatrixPatternType.AT_ROOM, match.value, match.range.first, match.range.last + 1))
}
}
}
}
enum class MatrixPatternType {
USER_ID,
ROOM_ID,
ROOM_ALIAS,
EVENT_ID,
AT_ROOM
}
data class MatrixPatternResult(val type: MatrixPatternType, val value: String, val start: Int, val end: Int)

View file

@ -16,12 +16,15 @@
package io.element.android.libraries.matrix.api.permalink
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.UserId
interface PermalinkBuilder {
fun permalinkForUser(userId: UserId): Result<String>
fun permalinkForRoomAlias(roomAlias: RoomAlias): Result<String>
}
sealed class PermalinkBuilderError : Throwable() {
data object InvalidUserId : PermalinkBuilderError()
data object InvalidRoomAlias : PermalinkBuilderError()
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.core
import android.net.Uri
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import org.junit.Test
class MatrixPatternsTest {
@Test
fun `findPatterns - returns raw user ids`() {
val text = "A @user:server.com and @user2:server.com"
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser())
assertThat(patterns).containsExactly(
MatrixPatternResult(MatrixPatternType.USER_ID, "@user:server.com", 2, 18),
MatrixPatternResult(MatrixPatternType.USER_ID, "@user2:server.com", 23, 40)
)
}
@Test
fun `findPatterns - returns raw room ids`() {
val text = "A !room:server.com and !room2:server.com"
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser())
assertThat(patterns).containsExactly(
MatrixPatternResult(MatrixPatternType.ROOM_ID, "!room:server.com", 2, 18),
MatrixPatternResult(MatrixPatternType.ROOM_ID, "!room2:server.com", 23, 40)
)
}
@Test
fun `findPatterns - returns raw room aliases`() {
val text = "A #room:server.com and #room2:server.com"
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser())
assertThat(patterns).containsExactly(
MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, "#room:server.com", 2, 18),
MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, "#room2:server.com", 23, 40)
)
}
@Test
fun `findPatterns - returns raw room event ids`() {
val text = "A \$event:server.com and \$event2:server.com"
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser())
assertThat(patterns).containsExactly(
MatrixPatternResult(MatrixPatternType.EVENT_ID, "\$event:server.com", 2, 19),
MatrixPatternResult(MatrixPatternType.EVENT_ID, "\$event2:server.com", 24, 42)
)
}
@Test
fun `findPatterns - returns @room mention`() {
val text = "A @room mention"
val patterns = MatrixPatterns.findPatterns(text, aPermalinkParser())
assertThat(patterns).containsExactly(MatrixPatternResult(MatrixPatternType.AT_ROOM, "@room", 2, 7))
}
@Test
fun `findPatterns - returns user ids in permalinks`() {
val text = "A [User](https://matrix.to/#/@user:server.com)"
val permalinkParser = aPermalinkParser { _ ->
PermalinkData.UserLink(UserId("@user:server.com"))
}
val patterns = MatrixPatterns.findPatterns(text, permalinkParser)
assertThat(patterns).containsExactly(MatrixPatternResult(MatrixPatternType.USER_ID, "@user:server.com", 2, 46))
}
@Test
fun `findPatterns - returns room aliases in permalinks`() {
val text = "A [Room](https://matrix.to/#/#room:server.com)"
val permalinkParser = aPermalinkParser { _ ->
PermalinkData.RoomLink(RoomIdOrAlias.Alias(RoomAlias("#room:server.com")))
}
val patterns = MatrixPatterns.findPatterns(text, permalinkParser)
assertThat(patterns).containsExactly(MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, "#room:server.com", 2, 46))
}
private fun aPermalinkParser(block: (String) -> PermalinkData = { PermalinkData.FallbackLink(Uri.EMPTY) }) = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return block(uriString)
}
}
}