Pillify permalinks (#8242)

This commit is contained in:
Yoan Pintas 2023-03-21 22:31:45 +01:00 committed by GitHub
parent b85a06422c
commit 9fd1a22e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 178 additions and 62 deletions

1
changelog.d/8219.feature Normal file
View File

@ -0,0 +1 @@
Permalinks to a room/space are pillified

1
changelog.d/8220.feature Normal file
View File

@ -0,0 +1 @@
Permalinks to a matrix user are pillified

1
changelog.d/8221.feature Normal file
View File

@ -0,0 +1 @@
Permalinks to messages are pillified

View File

@ -3539,4 +3539,11 @@
<string name="settings_access_token">Access Token</string> <string name="settings_access_token">Access Token</string>
<string name="settings_access_token_summary">Your access token gives full access to your account. Do not share it with anyone.</string> <string name="settings_access_token_summary">Your access token gives full access to your account. Do not share it with anyone.</string>
<!-- Pills -->
<string name="pill_message_from_user">Message from %s</string>
<string name="pill_message_from_unknown_user">Message</string>
<string name="pill_message_in_room">Message in %s</string>
<string name="pill_message_in_unknown_room">Message in room</string>
<string name="pill_message_unknown_room_or_space">Room/Space</string>
</resources> </resources>

View File

@ -18,8 +18,8 @@
<item name="dialog_width_ratio" format="float" type="dimen">0.75</item> <item name="dialog_width_ratio" format="float" type="dimen">0.75</item>
<dimen name="pill_avatar_size">16dp</dimen> <dimen name="pill_avatar_size">20sp</dimen>
<dimen name="pill_min_height">20dp</dimen> <dimen name="pill_min_height">26sp</dimen>
<dimen name="pill_text_padding">4dp</dimen> <dimen name="pill_text_padding">4dp</dimen>
<dimen name="call_pip_height">128dp</dimen> <dimen name="call_pip_height">128dp</dimen>

View File

@ -65,27 +65,14 @@ object MatrixPatterns {
private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/" private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/"
const val SEP_REGEX = "/" const val SEP_REGEX = "/"
private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE) private val PATTERN_CONTAIN_APP_PERMALINK = APP_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = LINK_TO_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = LINK_TO_APP_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
// ascii characters in the range \x20 (space) to \x7E (~) // ascii characters in the range \x20 (space) to \x7E (~)
val ORDER_STRING_REGEX = "[ -~]+".toRegex() val ORDER_STRING_REGEX = "[ -~]+".toRegex()
// list of patterns to find some matrix item. // list of patterns to find some matrix item.
val MATRIX_PATTERNS = listOf( val MATRIX_PATTERNS = listOf(
PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID,
PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS,
PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID,
PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS,
PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER, PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER,
PATTERN_CONTAIN_MATRIX_ALIAS, PATTERN_CONTAIN_MATRIX_ALIAS,
PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER, PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER,
@ -146,6 +133,12 @@ object MatrixPatterns {
return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
} }
fun isPermalink(str: String?): Boolean {
return str != null &&
(PATTERN_CONTAIN_MATRIX_TO_PERMALINK.containsMatchIn(str) ||
PATTERN_CONTAIN_APP_PERMALINK.containsMatchIn(str))
}
/** /**
* Extract server name from a matrix id. * Extract server name from a matrix id.
* *

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.permalinks package org.matrix.android.sdk.api.session.permalinks
import android.text.Spannable import android.text.Spannable
import android.util.Patterns
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
/** /**
@ -44,22 +45,26 @@ object MatrixLinkify {
} }
val text = spannable.toString() val text = spannable.toString()
var hasMatch = false var hasMatch = false
for (pattern in MatrixPatterns.MATRIX_PATTERNS) { for (pattern in listOf(Patterns.WEB_URL.toRegex()).plus(MatrixPatterns.MATRIX_PATTERNS)) {
for (match in pattern.findAll(spannable)) { for (match in pattern.findAll(spannable)) {
hasMatch = true hasMatch = true
val startPos = match.range.first val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') { if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = match.range.last + 1 val endPos = match.range.last + 1
var url = text.substring(match.range) var url = text.substring(match.range)
if (MatrixPatterns.isUserId(url) || val isPermalink = MatrixPatterns.isPermalink(url)
if (isPermalink ||
MatrixPatterns.isUserId(url) ||
MatrixPatterns.isRoomAlias(url) || MatrixPatterns.isRoomAlias(url) ||
MatrixPatterns.isRoomId(url) || MatrixPatterns.isRoomId(url) ||
MatrixPatterns.isGroupId(url) || MatrixPatterns.isGroupId(url) ||
MatrixPatterns.isEventId(url)) { MatrixPatterns.isEventId(url)) {
url = PermalinkService.MATRIX_TO_URL_BASE + url if (!isPermalink) {
url = PermalinkService.MATRIX_TO_URL_BASE + url
}
val span = MatrixPermalinkSpan(url, callback)
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
val span = MatrixPermalinkSpan(url, callback)
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }
} }

View File

@ -76,7 +76,8 @@ sealed class MatrixItem(
data class RoomItem( data class RoomItem(
override val id: String, override val id: String,
override val displayName: String? = null, override val displayName: String? = null,
override val avatarUrl: String? = null override val avatarUrl: String? = null,
val roomDisplayName: String? = null
) : ) :
MatrixItem(id, displayName, avatarUrl) { MatrixItem(id, displayName, avatarUrl) {
init { init {
@ -102,7 +103,8 @@ sealed class MatrixItem(
data class RoomAliasItem( data class RoomAliasItem(
override val id: String, override val id: String,
override val displayName: String? = null, override val displayName: String? = null,
override val avatarUrl: String? = null override val avatarUrl: String? = null,
val roomDisplayName: String? = null
) : ) :
MatrixItem(id, displayName, avatarUrl) { MatrixItem(id, displayName, avatarUrl) {
init { init {
@ -136,6 +138,8 @@ sealed class MatrixItem(
val displayName = when (this) { val displayName = when (this) {
// use the room display name for the notify everyone item // use the room display name for the notify everyone item
is EveryoneInRoomItem -> roomDisplayName is EveryoneInRoomItem -> roomDisplayName
is RoomItem -> roomDisplayName ?: displayName
is RoomAliasItem -> roomDisplayName ?: displayName
else -> displayName else -> displayName
} }
return (displayName?.takeIf { it.isNotBlank() } ?: id) return (displayName?.takeIf { it.isNotBlank() } ?: id)

View File

@ -20,7 +20,7 @@ import org.matrix.android.sdk.api.util.MatrixItem
fun MatrixItem.getBestName(): String { fun MatrixItem.getBestName(): String {
// Note: this code is copied from [DisplayNameResolver] in the SDK // Note: this code is copied from [DisplayNameResolver] in the SDK
return if (this is MatrixItem.RoomAliasItem) { return if (this is MatrixItem.RoomAliasItem && displayName.isNullOrBlank()) {
// Best name is the id, and we keep the displayName of the room for the case we need the first letter // Best name is the id, and we keep the displayName of the room for the case we need the first letter
id id
} else { } else {

View File

@ -163,7 +163,6 @@ import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.app.features.home.room.threads.ThreadsManager import im.vector.app.features.home.room.threads.ThreadsManager
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.invite.VectorInviteView import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.location.LocationSharingMode
import im.vector.app.features.location.toLocationData import im.vector.app.features.location.toLocationData
@ -247,7 +246,6 @@ class TimelineFragment :
@Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider @Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider
@Inject lateinit var imageContentRenderer: ImageContentRenderer @Inject lateinit var imageContentRenderer: ImageContentRenderer
@Inject lateinit var roomDetailPendingActionStore: RoomDetailPendingActionStore @Inject lateinit var roomDetailPendingActionStore: RoomDetailPendingActionStore
@Inject lateinit var pillsPostProcessorFactory: PillsPostProcessor.Factory
@Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker @Inject lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
@Inject lateinit var shareIntentHandler: ShareIntentHandler @Inject lateinit var shareIntentHandler: ShareIntentHandler

View File

@ -20,21 +20,30 @@ import android.content.Context
import android.text.Spannable import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.util.Patterns
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillImageSpan
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
class EventTextRenderer @AssistedInject constructor( class EventTextRenderer @AssistedInject constructor(
@Assisted private val roomId: String?, @Assisted private val roomId: String?,
private val context: Context, private val context: Context,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val activeSessionHolder: ActiveSessionHolder, private val sessionHolder: ActiveSessionHolder,
) { ) {
@AssistedFactory @AssistedFactory
@ -46,7 +55,8 @@ class EventTextRenderer @AssistedInject constructor(
* @param text the text to be rendered * @param text the text to be rendered
*/ */
fun render(text: CharSequence): CharSequence { fun render(text: CharSequence): CharSequence {
return renderNotifyEveryone(text) val formattedText = renderPermalinks(text)
return renderNotifyEveryone(formattedText)
} }
private fun renderNotifyEveryone(text: CharSequence): CharSequence { private fun renderNotifyEveryone(text: CharSequence): CharSequence {
@ -59,8 +69,18 @@ class EventTextRenderer @AssistedInject constructor(
} }
} }
private fun renderPermalinks(text: CharSequence): CharSequence {
return if (roomId != null) {
SpannableStringBuilder(text).apply {
addPermalinksSpans(this)
}
} else {
text
}
}
private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) { private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) {
val room: RoomSummary? = activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId) val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId)
val matrixItem = MatrixItem.EveryoneInRoomItem( val matrixItem = MatrixItem.EveryoneInRoomItem(
id = roomId, id = roomId,
avatarUrl = room?.avatarUrl, avatarUrl = room?.avatarUrl,
@ -76,6 +96,23 @@ class EventTextRenderer @AssistedInject constructor(
} }
} }
private fun addPermalinksSpans(text: Spannable) {
for (match in Patterns.WEB_URL.toRegex().findAll(text)) {
val url = text.substring(match.range)
val matrixItem = if (MatrixPatterns.isPermalink(url)) {
when (val permalinkData = PermalinkParser.parse(url)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
else -> null
}
} else null
if (matrixItem != null) {
addPillSpan(text, createPillImageSpan(matrixItem), match.range.first, match.range.last + 1)
}
}
}
private fun createPillImageSpan(matrixItem: MatrixItem) = private fun createPillImageSpan(matrixItem: MatrixItem) =
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem) PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
@ -87,4 +124,46 @@ class EventTextRenderer @AssistedInject constructor(
) { ) {
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =
roomId?.let { sessionHolder.getSafeActiveSession()?.roomService()?.getRoomMember(userId, it)?.toMatrixItem() }
?: sessionHolder.getSafeActiveSession()?.getUserOrDefault(userId)?.toMatrixItem()
private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem =
if (eventId.isNullOrEmpty()) {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
when {
isRoomAlias -> MatrixItem.RoomAliasItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
room == null -> MatrixItem.RoomItem(roomIdOrAlias, context.getString(R.string.pill_message_unknown_room_or_space))
room.roomType == RoomType.SPACE -> MatrixItem.SpaceItem(roomIdOrAlias, room.displayName, room.avatarUrl)
else -> MatrixItem.RoomItem(roomIdOrAlias, room.displayName, room.avatarUrl)
}
} else {
if (roomIdOrAlias == roomId) {
val session = sessionHolder.getSafeActiveSession()
val event = session?.eventService()?.getEventFromCache(roomId, eventId!!)
val user = event?.senderId?.let { session.roomService().getRoomMember(it, roomId) }
val text = user?.let {
context.getString(R.string.pill_message_from_user, user.displayName)
} ?: context.getString(R.string.pill_message_from_unknown_user)
MatrixItem.RoomItem(roomIdOrAlias, text, user?.avatarUrl, user?.displayName)
} else {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
when {
isRoomAlias -> MatrixItem.RoomAliasItem(
roomIdOrAlias,
context.getString(R.string.pill_message_in_room, room?.displayName ?: roomIdOrAlias),
room?.avatarUrl,
room?.displayName
)
room != null -> MatrixItem.RoomItem(
roomIdOrAlias,
context.getString(R.string.pill_message_in_room, room.displayName),
room.avatarUrl,
room.displayName
)
else -> MatrixItem.RoomItem(roomIdOrAlias, context.getString(R.string.pill_message_in_unknown_room))
}
}
}
} }

View File

@ -44,12 +44,11 @@ fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillI
} }
fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence { fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence {
val text = this.toString()
// SpannableStringBuilder is used to avoid Epoxy throwing ImmutableModelException // SpannableStringBuilder is used to avoid Epoxy throwing ImmutableModelException
val spannable = SpannableStringBuilder(this) val spannable = SpannableStringBuilder(this)
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
override fun onUrlClicked(url: String) { override fun onUrlClicked(url: String) {
callback?.onUrlClicked(url, text) callback?.onUrlClicked(url, this.toString())
} }
}) })
VectorLinkify.addLinks(spannable, true) VectorLinkify.addLinks(spannable, true)

View File

@ -26,14 +26,17 @@ import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan import android.text.style.ReplacementSpan
import android.widget.TextView import android.widget.TextView
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.core.content.ContextCompat
import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipDrawable
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.isMatrixId
import im.vector.app.core.glide.GlideRequests import im.vector.app.core.glide.GlideRequests
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -111,10 +114,28 @@ class PillImageSpan(
private fun createChipDrawable(): ChipDrawable { private fun createChipDrawable(): ChipDrawable {
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding) val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
val icon = try { val icon = when {
avatarRenderer.getCachedDrawable(glideRequests, matrixItem) matrixItem is MatrixItem.RoomAliasItem && matrixItem.avatarUrl.isNullOrEmpty() &&
} catch (exception: Exception) { matrixItem.displayName == context.getString(R.string.pill_message_in_room, matrixItem.id) -> {
avatarRenderer.getPlaceholderDrawable(matrixItem) ContextCompat.getDrawable(context, R.drawable.ic_permalink_round)
}
matrixItem is MatrixItem.RoomItem && matrixItem.avatarUrl.isNullOrEmpty() && (
matrixItem.displayName == context.getString(R.string.pill_message_in_unknown_room) ||
matrixItem.displayName == context.getString(R.string.pill_message_unknown_room_or_space) ||
matrixItem.displayName == context.getString(R.string.pill_message_from_unknown_user)
) -> {
ContextCompat.getDrawable(context, R.drawable.ic_permalink_round)
}
matrixItem is MatrixItem.UserItem && matrixItem.avatarUrl.isNullOrEmpty() && matrixItem.displayName?.isMatrixId().orTrue() -> {
ContextCompat.getDrawable(context, R.drawable.ic_user_round)
}
else -> {
try {
avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
} catch (exception: Exception) {
avatarRenderer.getPlaceholderDrawable(matrixItem)
}
}
} }
return ChipDrawable.createFromResource(context, R.xml.pill_view).apply { return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {

View File

@ -27,7 +27,7 @@ import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.core.spans.LinkSpan import io.noties.markwon.core.spans.LinkSpan
import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.getUser
import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -56,15 +56,15 @@ class PillsPostProcessor @AssistedInject constructor(
* ========================================================================================== */ * ========================================================================================== */
override fun afterRender(renderedText: Spannable) { override fun afterRender(renderedText: Spannable) {
addPillSpans(renderedText, roomId) addPillSpans(renderedText)
} }
/* ========================================================================================== /* ==========================================================================================
* Helper methods * Helper methods
* ========================================================================================== */ * ========================================================================================== */
private fun addPillSpans(renderedText: Spannable, roomId: String?) { private fun addPillSpans(renderedText: Spannable) {
addLinkSpans(renderedText, roomId) addLinkSpans(renderedText)
} }
private fun addPillSpan( private fun addPillSpan(
@ -76,11 +76,11 @@ class PillsPostProcessor @AssistedInject constructor(
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
private fun addLinkSpans(renderedText: Spannable, roomId: String?) { private fun addLinkSpans(renderedText: Spannable) {
// We let markdown handle links and then we add PillImageSpan if needed. // We let markdown handle links and then we add PillImageSpan if needed.
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java) val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
linkSpans.forEach { linkSpan -> linkSpans.forEach { linkSpan ->
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach val pillSpan = linkSpan.createPillSpan() ?: return@forEach
val startSpan = renderedText.getSpanStart(linkSpan) val startSpan = renderedText.getSpanStart(linkSpan)
val endSpan = renderedText.getSpanEnd(linkSpan) val endSpan = renderedText.getSpanEnd(linkSpan)
// GlideImagesPlugin causes duplicated pills if we have a nested spans in the pill span, // GlideImagesPlugin causes duplicated pills if we have a nested spans in the pill span,
@ -104,21 +104,18 @@ class PillsPostProcessor @AssistedInject constructor(
private fun createPillImageSpan(matrixItem: MatrixItem) = private fun createPillImageSpan(matrixItem: MatrixItem) =
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem) PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? { private fun LinkSpan.createPillSpan(): PillImageSpan? {
val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) { val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem(roomId) is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem() is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
else -> null else -> null
} ?: return null } ?: return null
return createPillImageSpan(matrixItem) return createPillImageSpan(matrixItem)
} }
private fun PermalinkData.UserLink.toMatrixItem(roomId: String?): MatrixItem? = private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =
if (roomId == null) { roomId?.let { sessionHolder.getSafeActiveSession()?.roomService()?.getRoomMember(userId, it)?.toMatrixItem() }
sessionHolder.getSafeActiveSession()?.getUserOrDefault(userId)?.toMatrixItem() ?: sessionHolder.getSafeActiveSession()?.getUser(userId)?.toMatrixItem()
} else {
sessionHolder.getSafeActiveSession()?.roomService()?.getRoomMember(userId, roomId)?.toMatrixItem()
}
private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? = private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? =
if (eventId == null) { if (eventId == null) {

View File

@ -110,13 +110,8 @@ class PermalinkHandler @Inject constructor(
val rootThreadEventId = permalinkData.eventId?.let { eventId -> val rootThreadEventId = permalinkData.eventId?.let { eventId ->
val room = roomId?.let { session?.getRoom(it) } val room = roomId?.let { session?.getRoom(it) }
val event = room?.getTimelineEvent(eventId)
val rootThreadEventId = room?.getTimelineEvent(eventId)?.root?.getRootThreadEventId() event?.root?.getRootThreadEventId() ?: eventId.takeIf { event?.isRootThread() == true }
rootThreadEventId ?: if (room?.getTimelineEvent(eventId)?.isRootThread() == true) {
eventId
} else {
null
}
} }
openRoom( openRoom(
navigationInterceptor, navigationInterceptor,

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"
android:fillColor="@color/element_name_01"/>
<path
android:pathData="m12.378,8.101 l0.356,-0.356c0.984,-0.984 2.57,-0.994 3.543,-0.021 0.973,0.972 0.963,2.559 -0.021,3.543l-1.693,1.693c-0.984,0.984 -2.57,0.994 -3.543,0.021m0.603,2.919 l-0.356,0.356c-0.984,0.984 -2.57,0.994 -3.543,0.021 -0.973,-0.973 -0.963,-2.559 0.021,-3.543l1.693,-1.693c0.984,-0.984 2.57,-0.994 3.543,-0.021"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/palette_white"
android:strokeLineCap="round"/>
</vector>

View File

@ -4,15 +4,15 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:pathData="M17.5911,20.2922C15.9951,21.3704 14.0711,22 12,22C9.7488,22 7.6713,21.2561 6,20.0007C3.5711,18.1763 2,15.2716 2,12C2,6.4771 6.4771,2 12,2C17.5228,2 22,6.4771 22,12C22,15.4518 20.2511,18.4951 17.5911,20.2922ZM12,12.5C13.6569,12.5 15,11.0449 15,9.25C15,7.4551 13.6569,6 12,6C10.3431,6 9,7.4551 9,9.25C9,11.0449 10.3431,12.5 12,12.5ZM12,20C14.162,20 16.1236,19.1424 17.5634,17.7488C16.673,15.5506 14.5176,14 12,14C9.4824,14 7.327,15.5506 6.4366,17.7488C7.8763,19.1424 9.838,20 12,20Z" android:pathData="M18.709,21.951C16.794,23.244 14.485,24 12,24C9.299,24 6.806,23.107 4.8,21.601C1.885,19.412 0,15.926 0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,16.142 21.901,19.794 18.709,21.951ZM12,12.6C13.988,12.6 15.6,10.854 15.6,8.7C15.6,6.546 13.988,4.8 12,4.8C10.012,4.8 8.4,6.546 8.4,8.7C8.4,10.854 10.012,12.6 12,12.6ZM12,21.6C14.594,21.6 16.948,20.571 18.676,18.899C17.608,16.261 15.021,14.4 12,14.4C8.979,14.4 6.392,16.261 5.324,18.899C7.052,20.571 9.406,21.6 12,21.6Z"
android:fillColor="#C1C6CD" android:fillColor="?vctr_content_secondary"
android:fillType="evenOdd"/> android:fillType="evenOdd"/>
<group> <group>
<clip-path <clip-path
android:pathData="M17.5911,20.2922C15.9951,21.3704 14.0711,22 12,22C9.7488,22 7.6713,21.2561 6,20.0007C3.5711,18.1763 2,15.2716 2,12C2,6.4771 6.4771,2 12,2C17.5228,2 22,6.4771 22,12C22,15.4518 20.2511,18.4951 17.5911,20.2922ZM12,12.5C13.6569,12.5 15,11.0449 15,9.25C15,7.4551 13.6569,6 12,6C10.3431,6 9,7.4551 9,9.25C9,11.0449 10.3431,12.5 12,12.5ZM12,20C14.162,20 16.1236,19.1424 17.5634,17.7488C16.673,15.5506 14.5176,14 12,14C9.4824,14 7.327,15.5506 6.4366,17.7488C7.8763,19.1424 9.838,20 12,20Z" android:pathData="M18.709,21.951C16.794,23.244 14.485,24 12,24C9.299,24 6.806,23.107 4.8,21.601C1.885,19.412 0,15.926 0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,16.142 21.901,19.794 18.709,21.951ZM12,12.6C13.988,12.6 15.6,10.854 15.6,8.7C15.6,6.546 13.988,4.8 12,4.8C10.012,4.8 8.4,6.546 8.4,8.7C8.4,10.854 10.012,12.6 12,12.6ZM12,21.6C14.594,21.6 16.948,20.571 18.676,18.899C17.608,16.261 15.021,14.4 12,14.4C8.979,14.4 6.392,16.261 5.324,18.899C7.052,20.571 9.406,21.6 12,21.6Z"
android:fillType="evenOdd"/> android:fillType="evenOdd"/>
<path <path
android:pathData="M17.5911,20.2922L16.4715,18.6349L17.5911,20.2922ZM6,20.0007L4.7989,21.5999L4.7989,21.5999L6,20.0007ZM17.5634,17.7488L18.9544,19.1859L19.9234,18.2479L19.4171,16.998L17.5634,17.7488ZM6.4366,17.7488L4.5829,16.998L4.0766,18.2479L5.0456,19.1859L6.4366,17.7488ZM12,24C14.4825,24 16.7945,23.244 18.7107,21.9494L16.4715,18.6349C15.1957,19.4968 13.6596,20 12,20V24ZM4.7989,21.5999C6.8046,23.1065 9.3008,24 12,24V20C10.1967,20 8.538,19.4058 7.2011,18.4016L4.7989,21.5999ZM0,12C0,15.9273 1.8887,19.414 4.7989,21.5999L7.2011,18.4016C5.2535,16.9387 4,14.616 4,12H0ZM12,0C5.3726,0 0,5.3726 0,12H4C4,7.5817 7.5817,4 12,4V0ZM24,12C24,5.3726 18.6274,0 12,0V4C16.4183,4 20,7.5817 20,12H24ZM18.7107,21.9494C21.8977,19.7963 24,16.144 24,12H20C20,14.7596 18.6045,17.1939 16.4715,18.6349L18.7107,21.9494ZM13,9.25C13,10.0941 12.4046,10.5 12,10.5V14.5C14.9091,14.5 17,11.9958 17,9.25H13ZM12,8C12.4046,8 13,8.4059 13,9.25H17C17,6.5043 14.9091,4 12,4V8ZM11,9.25C11,8.4059 11.5954,8 12,8V4C9.0909,4 7,6.5043 7,9.25H11ZM12,10.5C11.5954,10.5 11,10.0941 11,9.25H7C7,11.9958 9.0909,14.5 12,14.5V10.5ZM16.1724,16.3118C15.0906,17.3588 13.6223,18 12,18V22C14.7017,22 17.1567,20.926 18.9544,19.1859L16.1724,16.3118ZM12,16C13.6752,16 15.1146,17.0305 15.7097,18.4996L19.4171,16.998C18.2314,14.0707 15.3599,12 12,12V16ZM8.2903,18.4996C8.8854,17.0305 10.3248,16 12,16V12C8.6401,12 5.7686,14.0707 4.5829,16.998L8.2903,18.4996ZM12,18C10.3777,18 8.9094,17.3588 7.8276,16.3118L5.0456,19.1859C6.8433,20.926 9.2983,22 12,22V18Z" android:pathData="M18.709,21.951L19.564,23.216L18.709,21.951ZM4.8,21.601L3.883,22.822H3.883L4.8,21.601ZM18.676,18.899L19.738,19.996L20.478,19.28L20.092,18.325L18.676,18.899ZM5.324,18.899L3.908,18.325L3.522,19.28L4.262,19.996L5.324,18.899ZM12,25.527C14.8,25.527 17.404,24.675 19.564,23.216L17.854,20.685C16.184,21.814 14.171,22.473 12,22.473V25.527ZM3.883,22.822C6.144,24.52 8.956,25.527 12,25.527V22.473C9.641,22.473 7.467,21.694 5.717,20.38L3.883,22.822ZM-1.527,12C-1.527,16.427 0.601,20.357 3.883,22.822L5.717,20.38C3.17,18.466 1.527,15.425 1.527,12H-1.527ZM12,-1.527C4.529,-1.527 -1.527,4.529 -1.527,12H1.527C1.527,6.216 6.216,1.527 12,1.527V-1.527ZM25.527,12C25.527,4.529 19.471,-1.527 12,-1.527V1.527C17.784,1.527 22.473,6.216 22.473,12H25.527ZM19.564,23.216C23.159,20.788 25.527,16.671 25.527,12H22.473C22.473,15.613 20.644,18.8 17.854,20.685L19.564,23.216ZM14.073,8.7C14.073,10.128 13.032,11.073 12,11.073V14.127C14.944,14.127 17.127,11.58 17.127,8.7H14.073ZM12,6.327C13.032,6.327 14.073,7.272 14.073,8.7H17.127C17.127,5.82 14.944,3.273 12,3.273V6.327ZM9.927,8.7C9.927,7.272 10.968,6.327 12,6.327V3.273C9.055,3.273 6.873,5.82 6.873,8.7H9.927ZM12,11.073C10.968,11.073 9.927,10.128 9.927,8.7H6.873C6.873,11.58 9.055,14.127 12,14.127V11.073ZM17.614,17.801C16.16,19.209 14.182,20.073 12,20.073V23.127C15.007,23.127 17.737,21.933 19.738,19.996L17.614,17.801ZM12,15.927C14.378,15.927 16.417,17.391 17.26,19.472L20.092,18.325C18.798,15.131 15.664,12.873 12,12.873V15.927ZM6.74,19.472C7.582,17.391 9.622,15.927 12,15.927V12.873C8.336,12.873 5.202,15.131 3.908,18.325L6.74,19.472ZM12,20.073C9.818,20.073 7.84,19.209 6.386,17.801L4.262,19.996C6.263,21.933 8.993,23.127 12,23.127V20.073Z"
android:fillColor="#C1C6CD"/> android:fillColor="?vctr_content_secondary"/>
</group> </group>
</vector> </vector>