Merge pull request #2793 from vector-im/feature/bma/url_preview_fixes
Url preview fixes
This commit is contained in:
commit
28fad01be7
@ -5,12 +5,14 @@ Features ✨:
|
|||||||
-
|
-
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
- Open image from URL Preview (#2705)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Bug in WidgetContent.computeURL() (#2767)
|
- Bug in WidgetContent.computeURL() (#2767)
|
||||||
- Duplicate thumbs | Mobile reactions for 👍 and 👎 are not the same as web (#2776)
|
- Duplicate thumbs | Mobile reactions for 👍 and 👎 are not the same as web (#2776)
|
||||||
- Join room by alias other federation error (#2778)
|
- Join room by alias other federation error (#2778)
|
||||||
|
- HTML unescaping for URL preview (#2766)
|
||||||
|
- URL preview on reply fallback (#2756)
|
||||||
- RTL: some arrows should be rotated in RTL (#2757)
|
- RTL: some arrows should be rotated in RTL (#2757)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
|
@ -20,7 +20,7 @@ object ContentUtils {
|
|||||||
val lines = repliedBody.lines()
|
val lines = repliedBody.lines()
|
||||||
var wellFormed = repliedBody.startsWith(">")
|
var wellFormed = repliedBody.startsWith(">")
|
||||||
var endOfPreviousFound = false
|
var endOfPreviousFound = false
|
||||||
val usefullines = ArrayList<String>()
|
val usefulLines = ArrayList<String>()
|
||||||
lines.forEach {
|
lines.forEach {
|
||||||
if (it == "") {
|
if (it == "") {
|
||||||
endOfPreviousFound = true
|
endOfPreviousFound = true
|
||||||
@ -29,10 +29,10 @@ object ContentUtils {
|
|||||||
if (!endOfPreviousFound) {
|
if (!endOfPreviousFound) {
|
||||||
wellFormed = wellFormed && it.startsWith(">")
|
wellFormed = wellFormed && it.startsWith(">")
|
||||||
} else {
|
} else {
|
||||||
usefullines.add(it)
|
usefulLines.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
|
return usefulLines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
|
||||||
}
|
}
|
||||||
|
|
||||||
fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
|
fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
|
||||||
|
@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
|||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
|
import org.matrix.android.sdk.internal.util.unescapeHtml
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -73,9 +74,9 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||||||
private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData {
|
private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData {
|
||||||
return PreviewUrlData(
|
return PreviewUrlData(
|
||||||
url = (get("og:url") as? String) ?: url,
|
url = (get("og:url") as? String) ?: url,
|
||||||
siteName = get("og:site_name") as? String,
|
siteName = (get("og:site_name") as? String)?.unescapeHtml(),
|
||||||
title = get("og:title") as? String,
|
title = (get("og:title") as? String)?.unescapeHtml(),
|
||||||
description = get("og:description") as? String,
|
description = (get("og:description") as? String)?.unescapeHtml(),
|
||||||
mxcUrl = get("og:image") as? String
|
mxcUrl = get("og:image") as? String
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
|||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.isReply
|
||||||
|
import org.matrix.android.sdk.api.util.ContentUtils
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class UrlsExtractor @Inject constructor() {
|
internal class UrlsExtractor @Inject constructor() {
|
||||||
@ -35,7 +37,14 @@ internal class UrlsExtractor @Inject constructor() {
|
|||||||
|| it.msgType == MessageType.MSGTYPE_NOTICE
|
|| it.msgType == MessageType.MSGTYPE_NOTICE
|
||||||
|| it.msgType == MessageType.MSGTYPE_EMOTE
|
|| it.msgType == MessageType.MSGTYPE_EMOTE
|
||||||
}
|
}
|
||||||
?.body
|
?.let { messageContent ->
|
||||||
|
if (event.isReply()) {
|
||||||
|
// This is a reply, strip the reply fallback
|
||||||
|
ContentUtils.extractUsefulTextFromReply(messageContent.body)
|
||||||
|
} else {
|
||||||
|
messageContent.body
|
||||||
|
}
|
||||||
|
}
|
||||||
?.let { urlRegex.findAll(it) }
|
?.let { urlRegex.findAll(it) }
|
||||||
?.map { it.value }
|
?.map { it.value }
|
||||||
?.filter { it.startsWith("https://") || it.startsWith("http://") }
|
?.filter { it.startsWith("https://") || it.startsWith("http://") }
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.util
|
||||||
|
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
|
||||||
|
internal fun String.unescapeHtml(): String {
|
||||||
|
return HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
|
||||||
|
}
|
@ -1687,6 +1687,10 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
roomDetailViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url))
|
roomDetailViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPreviewUrlImageClicked(sharedView: View?, mxcUrl: String?, title: String?) {
|
||||||
|
navigator.openBigImageViewer(requireActivity(), sharedView, mxcUrl, title)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
||||||
if (action.messageContent is MessageTextContent) {
|
if (action.messageContent is MessageTextContent) {
|
||||||
shareText(requireContext(), action.messageContent.body)
|
shareText(requireContext(), action.messageContent.body)
|
||||||
|
@ -87,12 +87,12 @@ import org.matrix.android.sdk.api.session.room.read.ReadService
|
|||||||
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||||
import org.matrix.android.sdk.api.util.appendParamToUrl
|
import org.matrix.android.sdk.api.util.appendParamToUrl
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
@ -754,8 +754,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
is SendMode.EDIT -> {
|
is SendMode.EDIT -> {
|
||||||
// is original event a reply?
|
// is original event a reply?
|
||||||
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
||||||
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
|
||||||
if (inReplyTo != null) {
|
if (inReplyTo != null) {
|
||||||
// TODO check if same content?
|
// TODO check if same content?
|
||||||
room.getTimeLineEvent(inReplyTo)?.let {
|
room.getTimeLineEvent(inReplyTo)?.let {
|
||||||
|
@ -130,6 +130,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
interface PreviewUrlCallback {
|
interface PreviewUrlCallback {
|
||||||
fun onPreviewUrlClicked(url: String)
|
fun onPreviewUrlClicked(url: String)
|
||||||
fun onPreviewUrlCloseClicked(eventId: String, url: String)
|
fun onPreviewUrlCloseClicked(eventId: String, url: String)
|
||||||
|
fun onPreviewUrlImageClicked(sharedView: View?, mxcUrl: String?, title: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map eventId to adapter position
|
// Map eventId to adapter position
|
||||||
|
@ -23,7 +23,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.setTextOrHide
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
import im.vector.app.databinding.UrlPreviewBinding
|
import im.vector.app.databinding.ViewUrlPreviewBinding
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
|
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
|
||||||
|
|
||||||
private lateinit var views: UrlPreviewBinding
|
private lateinit var views: ViewUrlPreviewBinding
|
||||||
|
|
||||||
var delegate: TimelineEventController.PreviewUrlCallback? = null
|
var delegate: TimelineEventController.PreviewUrlCallback? = null
|
||||||
|
|
||||||
@ -80,6 +80,19 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onImageClick() {
|
||||||
|
when (val finalState = state) {
|
||||||
|
is PreviewUrlUiState.Data -> {
|
||||||
|
delegate?.onPreviewUrlImageClicked(
|
||||||
|
sharedView = views.urlPreviewImage,
|
||||||
|
mxcUrl = finalState.previewUrlData.mxcUrl,
|
||||||
|
title = finalState.previewUrlData.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onCloseClick() {
|
private fun onCloseClick() {
|
||||||
when (val finalState = state) {
|
when (val finalState = state) {
|
||||||
is PreviewUrlUiState.Data -> delegate?.onPreviewUrlCloseClicked(finalState.eventId, finalState.url)
|
is PreviewUrlUiState.Data -> delegate?.onPreviewUrlCloseClicked(finalState.eventId, finalState.url)
|
||||||
@ -90,10 +103,11 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||||||
// PRIVATE METHODS ****************************************************************************************************************************************
|
// PRIVATE METHODS ****************************************************************************************************************************************
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
inflate(context, R.layout.url_preview, this)
|
inflate(context, R.layout.view_url_preview, this)
|
||||||
views = UrlPreviewBinding.bind(this)
|
views = ViewUrlPreviewBinding.bind(this)
|
||||||
|
|
||||||
setOnClickListener(this)
|
setOnClickListener(this)
|
||||||
|
views.urlPreviewImage.setOnClickListener { onImageClick() }
|
||||||
views.urlPreviewClose.setOnClickListener { onCloseClick() }
|
views.urlPreviewClose.setOnClickListener { onCloseClick() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,6 @@ import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryDat
|
|||||||
import org.matrix.android.sdk.api.session.terms.TermsService
|
import org.matrix.android.sdk.api.session.terms.TermsService
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ -256,14 +255,13 @@ class DefaultNavigator @Inject constructor(
|
|||||||
context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess))
|
context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) {
|
override fun openBigImageViewer(activity: Activity, sharedElement: View?, mxcUrl: String?, title: String?) {
|
||||||
matrixItem.avatarUrl
|
mxcUrl
|
||||||
?.takeIf { it.isNotBlank() }
|
?.takeIf { it.isNotBlank() }
|
||||||
?.let { avatarUrl ->
|
?.let { avatarUrl ->
|
||||||
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
|
val intent = BigImageViewerActivity.newIntent(activity, title, avatarUrl)
|
||||||
val options = sharedElement?.let {
|
val options = sharedElement?.let {
|
||||||
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
|
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
|
||||||
?: "")
|
|
||||||
}
|
}
|
||||||
activity.startActivity(intent, options?.toBundle())
|
activity.startActivity(intent, options?.toBundle())
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,11 @@ interface Navigator {
|
|||||||
|
|
||||||
fun openRoomProfile(context: Context, roomId: String, directAccess: Int? = null)
|
fun openRoomProfile(context: Context, roomId: String, directAccess: Int? = null)
|
||||||
|
|
||||||
fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem)
|
fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) {
|
||||||
|
openBigImageViewer(activity, sharedElement, matrixItem.avatarUrl, matrixItem.getBestName())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openBigImageViewer(activity: Activity, sharedElement: View?, mxcUrl: String?, title: String?)
|
||||||
|
|
||||||
fun openPinCode(context: Context,
|
fun openPinCode(context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||||
|
@ -25,9 +25,7 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
@ -52,7 +50,6 @@ import im.vector.app.features.home.room.list.actions.RoomListActionsArgs
|
|||||||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
|
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
|
||||||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
|
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
|
||||||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
|
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
|
||||||
import im.vector.app.features.media.BigImageViewerActivity
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
@ -289,13 +286,7 @@ class RoomProfileFragment @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) {
|
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) {
|
||||||
matrixItem.avatarUrl
|
navigator.openBigImageViewer(requireActivity(), view, matrixItem)
|
||||||
?.takeIf { it.isNotEmpty() }
|
|
||||||
?.let { avatarUrl ->
|
|
||||||
val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), avatarUrl)
|
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "")
|
|
||||||
startActivity(intent, options.toBundle())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user