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 8 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 @@ -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,34 @@ 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 {
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)
} else {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
val text = room?.displayName?.let {
context.getString(R.string.pill_message_in_room, it)
} ?: context.getString(R.string.pill_message_in_unknown_room)
MatrixItem.RoomItem(roomIdOrAlias, text, room?.avatarUrl)
}
}
}
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
20 changes: 16 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,6 +26,7 @@ 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
Expand Down Expand Up @@ -111,10 +112,21 @@ 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 hasRoomAvatar = matrixItem is MatrixItem.RoomItem && matrixItem.avatarUrl.isNullOrEmpty()
val icon = when {
hasRoomAvatar && matrixItem.displayName == context.getString(R.string.pill_message_from_unknown_user) -> null
hasRoomAvatar && (matrixItem.displayName == context.getString(R.string.pill_message_in_unknown_room) || matrixItem.displayName == context.getString(
R.string.pill_message_unknown_room_or_space
)) -> {
ContextCompat.getDrawable(context, R.drawable.ic_permalink_round)
}
else -> {
try {
avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
} catch (exception: Exception) {
avatarRenderer.getPlaceholderDrawable(matrixItem)
}
}
}

return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.core.spans.LinkSpan
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.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.RoomSummary
Expand Down Expand Up @@ -56,15 +56,15 @@ class PillsPostProcessor @AssistedInject constructor(
* ========================================================================================== */

override fun afterRender(renderedText: Spannable) {
addPillSpans(renderedText, roomId)
addPillSpans(renderedText)
}

/* ==========================================================================================
* Helper methods
* ========================================================================================== */

private fun addPillSpans(renderedText: Spannable, roomId: String?) {
addLinkSpans(renderedText, roomId)
private fun addPillSpans(renderedText: Spannable) {
addLinkSpans(renderedText)
}

private fun addPillSpan(
Expand All @@ -76,11 +76,11 @@ class PillsPostProcessor @AssistedInject constructor(
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.
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
linkSpans.forEach { linkSpan ->
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
val pillSpan = linkSpan.createPillSpan() ?: return@forEach
val startSpan = renderedText.getSpanStart(linkSpan)
val endSpan = renderedText.getSpanEnd(linkSpan)
// GlideImagesPlugin causes duplicated pills if we have a nested spans in the pill span,
Expand All @@ -104,21 +104,18 @@ class PillsPostProcessor @AssistedInject constructor(
private fun createPillImageSpan(matrixItem: 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)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem(roomId)
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
else -> null
} ?: return null
return createPillImageSpan(matrixItem)
}

private fun PermalinkData.UserLink.toMatrixItem(roomId: String?): MatrixItem? =
if (roomId == null) {
sessionHolder.getSafeActiveSession()?.getUserOrDefault(userId)?.toMatrixItem()
} else {
sessionHolder.getSafeActiveSession()?.roomService()?.getRoomMember(userId, roomId)?.toMatrixItem()
}
private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =
roomId?.let { sessionHolder.getSafeActiveSession()?.roomService()?.getRoomMember(userId, it)?.toMatrixItem() }
?: sessionHolder.getSafeActiveSession()?.getUser(userId)?.toMatrixItem()

private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? =
if (eventId == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,8 @@ class PermalinkHandler @Inject constructor(

val rootThreadEventId = permalinkData.eventId?.let { eventId ->
val room = roomId?.let { session?.getRoom(it) }

val rootThreadEventId = room?.getTimelineEvent(eventId)?.root?.getRootThreadEventId()
rootThreadEventId ?: if (room?.getTimelineEvent(eventId)?.isRootThread() == true) {
eventId
} else {
null
}
val event = room?.getTimelineEvent(eventId)
event?.root?.getRootThreadEventId() ?: eventId.takeIf { event?.isRootThread() == true }
}
openRoom(
navigationInterceptor,
Expand Down
15 changes: 15 additions & 0 deletions vector/src/main/res/drawable/ic_permalink_round.xml
Original file line number Diff line number Diff line change
@@ -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"
yostyle marked this conversation as resolved.
Show resolved Hide resolved
android:strokeLineCap="round"/>
</vector>