mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-08 16:18:53 +01:00
Linkification: import workaround done on Riot
This commit is contained in:
parent
eaff5ac9f0
commit
347967700b
@ -89,6 +89,4 @@ object MatrixLinkify {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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
|
||||||
|
*
|
||||||
|
* http://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 im.vector.riotredesign.core.linkify
|
||||||
|
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Better support for geo URi
|
||||||
|
*/
|
||||||
|
object VectorAutoLinkPatterns {
|
||||||
|
|
||||||
|
//geo:
|
||||||
|
private const val LAT_OR_LONG_OR_ALT_NUMBER = "-?\\d+(?:\\.\\d+)?"
|
||||||
|
private const val COORDINATE_SYSTEM = ";crs=[\\w-]+"
|
||||||
|
|
||||||
|
val GEO_URI: Pattern = Pattern.compile("(?:geo:)?" +
|
||||||
|
"(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" +
|
||||||
|
"," +
|
||||||
|
"(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" +
|
||||||
|
"(?:" + "," + LAT_OR_LONG_OR_ALT_NUMBER + ")?" + //altitude
|
||||||
|
"(?:" + COORDINATE_SYSTEM + ")?" +
|
||||||
|
"(?:" + ";u=\\d+(?:\\.\\d+)?" + ")?" +//uncertainty in meters
|
||||||
|
"(?:" +
|
||||||
|
";[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+" + //dafuk
|
||||||
|
")*"
|
||||||
|
, Pattern.CASE_INSENSITIVE)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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
|
||||||
|
*
|
||||||
|
* http://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 im.vector.riotredesign.core.linkify
|
||||||
|
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.style.URLSpan
|
||||||
|
import android.text.util.Linkify
|
||||||
|
import androidx.core.text.util.LinkifyCompat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object VectorLinkify {
|
||||||
|
/**
|
||||||
|
* Better support for auto link than the default implementation
|
||||||
|
*/
|
||||||
|
fun addLinks(spannable: Spannable, keepExistingUrlSpan: Boolean = false) {
|
||||||
|
//we might want to modify some matches
|
||||||
|
val createdSpans = ArrayList<LinkSpec>()
|
||||||
|
|
||||||
|
if (keepExistingUrlSpan) {
|
||||||
|
//Keep track of existing URLSpans, and mark them as important
|
||||||
|
spannable.forEachSpanIndexed { _, urlSpan, start, end ->
|
||||||
|
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end, important = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Use the framework first, the found span can then be manipulated if needed
|
||||||
|
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
|
||||||
|
|
||||||
|
//we might want to modify some matches
|
||||||
|
spannable.forEachSpanIndexed { _, urlSpan, start, end ->
|
||||||
|
spannable.removeSpan(urlSpan)
|
||||||
|
|
||||||
|
//remove short PN, too much false positive
|
||||||
|
if (urlSpan.url?.startsWith("tel:") == true) {
|
||||||
|
if (end - start > 6) { //Do not match under 7 digit
|
||||||
|
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
|
||||||
|
}
|
||||||
|
return@forEachSpanIndexed
|
||||||
|
}
|
||||||
|
|
||||||
|
//include mailto: if found before match
|
||||||
|
if (urlSpan.url?.startsWith("mailto:") == true) {
|
||||||
|
val protocolLength = "mailto:".length
|
||||||
|
if (start - protocolLength >= 0 && "mailto:" == spannable.substring(start - protocolLength, start)) {
|
||||||
|
//modify to include the protocol
|
||||||
|
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start - protocolLength, end))
|
||||||
|
} else {
|
||||||
|
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
return@forEachSpanIndexed
|
||||||
|
}
|
||||||
|
|
||||||
|
//Handle url matches
|
||||||
|
|
||||||
|
//check trailing space
|
||||||
|
if (end < spannable.length - 1 && spannable[end] == '/') {
|
||||||
|
//modify the span to include the slash
|
||||||
|
val spec = LinkSpec(URLSpan(urlSpan.url + "/"), start, end + 1)
|
||||||
|
createdSpans.add(spec)
|
||||||
|
return@forEachSpanIndexed
|
||||||
|
}
|
||||||
|
//Try to do something for ending ) issues/3020
|
||||||
|
if (spannable[end - 1] == ')') {
|
||||||
|
var lbehind = end - 2
|
||||||
|
var isFullyContained = 1
|
||||||
|
while (lbehind > start) {
|
||||||
|
val char = spannable[lbehind]
|
||||||
|
if (char == '(') isFullyContained -= 1
|
||||||
|
if (char == ')') isFullyContained += 1
|
||||||
|
lbehind--
|
||||||
|
}
|
||||||
|
if (isFullyContained != 0) {
|
||||||
|
//In this case we will return false to match, and manually add span if we want?
|
||||||
|
val span = URLSpan(spannable.substring(start, end - 1))
|
||||||
|
val spec = LinkSpec(span, start, end - 1)
|
||||||
|
createdSpans.add(spec)
|
||||||
|
return@forEachSpanIndexed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkifyCompat.addLinks(spannable, VectorAutoLinkPatterns.GEO_URI, "geo:", arrayOf("geo:"), geoMatchFilter, null)
|
||||||
|
spannable.forEachSpanIndexed { _, urlSpan, start, end ->
|
||||||
|
spannable.removeSpan(urlSpan)
|
||||||
|
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneOverlaps(createdSpans)
|
||||||
|
for (spec in createdSpans) {
|
||||||
|
spannable.setSpan(spec.span, spec.start, spec.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pruneOverlaps(links: ArrayList<LinkSpec>) {
|
||||||
|
Collections.sort(links, COMPARATOR)
|
||||||
|
var len = links.size
|
||||||
|
var i = 0
|
||||||
|
while (i < len - 1) {
|
||||||
|
val a = links[i]
|
||||||
|
val b = links[i + 1]
|
||||||
|
var remove = -1
|
||||||
|
|
||||||
|
//test if there is an overlap
|
||||||
|
if (b.start in a.start until a.end) {
|
||||||
|
if (a.important != b.important) {
|
||||||
|
remove = if (a.important) i + 1 else i
|
||||||
|
} else {
|
||||||
|
when {
|
||||||
|
b.end <= a.end ->
|
||||||
|
//b is inside a -> b should be removed
|
||||||
|
remove = i + 1
|
||||||
|
a.end - a.start > b.end - b.start ->
|
||||||
|
//overlap and a is bigger -> b should be removed
|
||||||
|
remove = i + 1
|
||||||
|
a.end - a.start < b.end - b.start ->
|
||||||
|
//overlap and a is smaller -> a should be removed
|
||||||
|
remove = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove != -1) {
|
||||||
|
links.removeAt(remove)
|
||||||
|
len--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private data class LinkSpec(val span: URLSpan,
|
||||||
|
val start: Int,
|
||||||
|
val end: Int,
|
||||||
|
val important: Boolean = false)
|
||||||
|
|
||||||
|
private val COMPARATOR = Comparator<LinkSpec> { (_, startA, endA), (_, startB, endB) ->
|
||||||
|
if (startA < startB) {
|
||||||
|
return@Comparator -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startA > startB) {
|
||||||
|
return@Comparator 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endA < endB) {
|
||||||
|
return@Comparator 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endA > endB) {
|
||||||
|
-1
|
||||||
|
} else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//Exclude short match that don't have geo: prefix, e.g do not highlight things like 1,2
|
||||||
|
private val geoMatchFilter = Linkify.MatchFilter { s, start, end ->
|
||||||
|
if (s[start] != 'g') { //doesn't start with geo:
|
||||||
|
return@MatchFilter end - start > 12
|
||||||
|
}
|
||||||
|
return@MatchFilter true
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun Spannable.forEachSpanIndexed(action: (index: Int, urlSpan: URLSpan, start: Int, end: Int) -> Unit) {
|
||||||
|
getSpans(0, length, URLSpan::class.java)
|
||||||
|
.forEachIndexed { index, urlSpan ->
|
||||||
|
val start = getSpanStart(urlSpan)
|
||||||
|
val end = getSpanEnd(urlSpan)
|
||||||
|
action.invoke(index, urlSpan, start, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,6 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
|||||||
|
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.util.Linkify
|
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
@ -28,6 +27,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotredesign.core.extensions.localDateTime
|
import im.vector.riotredesign.core.extensions.localDateTime
|
||||||
|
import im.vector.riotredesign.core.linkify.VectorLinkify
|
||||||
import im.vector.riotredesign.core.resources.ColorProvider
|
import im.vector.riotredesign.core.resources.ColorProvider
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||||
@ -155,7 +155,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
callback?.onUrlClicked(url)
|
callback?.onUrlClicked(url)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Linkify.addLinks(spannable, Linkify.ALL)
|
VectorLinkify.addLinks(spannable, true)
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user