Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pillify permalinks #8242

Merged
merged 12 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8219.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Permalinks to a room/space are pillified
1 change: 1 addition & 0 deletions changelog.d/8220.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Permalinks to a matrix user are pillified
1 change: 1 addition & 0 deletions changelog.d/8221.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Permalinks to messages are pillified
7 changes: 7 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3539,4 +3539,11 @@

<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>

<!-- 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>
4 changes: 2 additions & 2 deletions library/ui-styles/src/main/res/values/dimens.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

<item name="dialog_width_ratio" format="float" type="dimen">0.75</item>

<dimen name="pill_avatar_size">16dp</dimen>
<dimen name="pill_min_height">20dp</dimen>
<dimen name="pill_avatar_size">20sp</dimen>
<dimen name="pill_min_height">26sp</dimen>
<dimen name="pill_text_padding">4dp</dimen>

<dimen name="call_pip_height">128dp</dimen>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,14 @@ object MatrixPatterns {
private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/"
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_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.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)
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)
private val PATTERN_CONTAIN_APP_PERMALINK = APP_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)

// ascii characters in the range \x20 (space) to \x7E (~)
val ORDER_STRING_REGEX = "[ -~]+".toRegex()

// list of patterns to find some matrix item.
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_ALIAS,
PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER,
Expand Down Expand Up @@ -146,6 +133,12 @@ object MatrixPatterns {
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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.permalinks

import android.text.Spannable
import android.util.Patterns
import org.matrix.android.sdk.api.MatrixPatterns

/**
Expand Down Expand Up @@ -44,22 +45,26 @@ object MatrixLinkify {
}
val text = spannable.toString()
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)) {
hasMatch = true
val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = match.range.last + 1
var url = text.substring(match.range)
if (MatrixPatterns.isUserId(url) ||
val isPermalink = MatrixPatterns.isPermalink(url)
if (isPermalink ||
MatrixPatterns.isUserId(url) ||
MatrixPatterns.isRoomAlias(url) ||
MatrixPatterns.isRoomId(url) ||
MatrixPatterns.isGroupId(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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ sealed class MatrixItem(
data class RoomItem(
override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null
override val avatarUrl: String? = null,
val roomDisplayName: String? = null
) :
MatrixItem(id, displayName, avatarUrl) {
init {
Expand All @@ -102,7 +103,8 @@ sealed class MatrixItem(
data class RoomAliasItem(
override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null
override val avatarUrl: String? = null,
val roomDisplayName: String? = null
) :
MatrixItem(id, displayName, avatarUrl) {
init {
Expand Down Expand Up @@ -136,6 +138,8 @@ sealed class MatrixItem(
val displayName = when (this) {
// use the room display name for the notify everyone item
is EveryoneInRoomItem -> roomDisplayName
is RoomItem -> roomDisplayName ?: displayName
is RoomAliasItem -> roomDisplayName ?: displayName
else -> displayName
}
return (displayName?.takeIf { it.isNotBlank() } ?: id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import org.matrix.android.sdk.api.util.MatrixItem

fun MatrixItem.getBestName(): String {
// 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
id
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.arguments.ThreadTimelineArgs
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.location.LocationSharingMode
import im.vector.app.features.location.toLocationData
Expand Down Expand Up @@ -247,7 +246,6 @@ class TimelineFragment :
@Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider
@Inject lateinit var imageContentRenderer: ImageContentRenderer
@Inject lateinit var roomDetailPendingActionStore: RoomDetailPendingActionStore
@Inject lateinit var pillsPostProcessorFactory: PillsPostProcessor.Factory
@Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
@Inject lateinit var shareIntentHandler: ShareIntentHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,30 @@ import android.content.Context
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.util.Patterns
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer
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.RoomType
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem

class EventTextRenderer @AssistedInject constructor(
@Assisted private val roomId: String?,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val activeSessionHolder: ActiveSessionHolder,
private val sessionHolder: ActiveSessionHolder,
) {

@AssistedFactory
Expand All @@ -46,7 +55,8 @@ class EventTextRenderer @AssistedInject constructor(
* @param text the text to be rendered
*/
fun render(text: CharSequence): CharSequence {
return renderNotifyEveryone(text)
val formattedText = renderPermalinks(text)
return renderNotifyEveryone(formattedText)
}

private fun renderNotifyEveryone(text: CharSequence): CharSequence {
Expand All @@ -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) {
val room: RoomSummary? = activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId)
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId)
val matrixItem = MatrixItem.EveryoneInRoomItem(
id = roomId,
avatarUrl = room?.avatarUrl,
Expand All @@ -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) =
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)

Expand All @@ -87,4 +124,46 @@ class EventTextRenderer @AssistedInject constructor(
) {
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))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,11 @@ fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillI
}

fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence {
val text = this.toString()
// SpannableStringBuilder is used to avoid Epoxy throwing ImmutableModelException
val spannable = SpannableStringBuilder(this)
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
override fun onUrlClicked(url: String) {
callback?.onUrlClicked(url, text)
callback?.onUrlClicked(url, this.toString())
}
})
VectorLinkify.addLinks(spannable, true)
Expand Down
29 changes: 25 additions & 4 deletions vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan
import android.widget.TextView
import androidx.annotation.UiThread
import androidx.core.content.ContextCompat
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.ChipDrawable
import im.vector.app.R
import im.vector.app.core.extensions.isMatrixId
import im.vector.app.core.glide.GlideRequests
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
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.util.MatrixItem
import java.lang.ref.WeakReference
Expand Down Expand Up @@ -111,10 +114,28 @@ class PillImageSpan(

private fun createChipDrawable(): ChipDrawable {
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
val icon = try {
avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
} catch (exception: Exception) {
avatarRenderer.getPlaceholderDrawable(matrixItem)
val icon = when {
matrixItem is MatrixItem.RoomAliasItem && matrixItem.avatarUrl.isNullOrEmpty() &&
matrixItem.displayName == context.getString(R.string.pill_message_in_room, matrixItem.id) -> {
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 {
Expand Down
Loading