Merge pull request #2793 from vector-im/feature/bma/url_preview_fixes

Url preview fixes
This commit is contained in:
Benoit Marty 2021-02-08 14:33:05 +01:00 committed by GitHub
commit 28fad01be7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 81 additions and 35 deletions

View File

@ -5,12 +5,14 @@ Features ✨:
-
Improvements 🙌:
-
- Open image from URL Preview (#2705)
Bugfix 🐛:
- Bug in WidgetContent.computeURL() (#2767)
- Duplicate thumbs | Mobile reactions for 👍 and 👎 are not the same as web (#2776)
- 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)
Translations 🗣:

View File

@ -20,7 +20,7 @@ object ContentUtils {
val lines = repliedBody.lines()
var wellFormed = repliedBody.startsWith(">")
var endOfPreviousFound = false
val usefullines = ArrayList<String>()
val usefulLines = ArrayList<String>()
lines.forEach {
if (it == "") {
endOfPreviousFound = true
@ -29,10 +29,10 @@ object ContentUtils {
if (!endOfPreviousFound) {
wellFormed = wellFormed && it.startsWith(">")
} 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 {

View File

@ -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.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.unescapeHtml
import java.util.Date
import javax.inject.Inject
@ -73,9 +74,9 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData {
return PreviewUrlData(
url = (get("og:url") as? String) ?: url,
siteName = get("og:site_name") as? String,
title = get("og:title") as? String,
description = get("og:description") as? String,
siteName = (get("og:site_name") as? String)?.unescapeHtml(),
title = (get("og:title") as? String)?.unescapeHtml(),
description = (get("og:description") as? String)?.unescapeHtml(),
mxcUrl = get("og:image") as? String
)
}

View File

@ -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.timeline.TimelineEvent
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
internal class UrlsExtractor @Inject constructor() {
@ -35,7 +37,14 @@ internal class UrlsExtractor @Inject constructor() {
|| it.msgType == MessageType.MSGTYPE_NOTICE
|| 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) }
?.map { it.value }
?.filter { it.startsWith("https://") || it.startsWith("http://") }

View File

@ -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()
}

View File

@ -1687,6 +1687,10 @@ class RoomDetailFragment @Inject constructor(
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) {
if (action.messageContent is MessageTextContent) {
shareText(requireContext(), action.messageContent.body)

View File

@ -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.timeline.Timeline
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.widgets.model.Widget
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.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.util.awaitCallback
import org.matrix.android.sdk.rx.rx
@ -754,8 +754,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
is SendMode.EDIT -> {
// is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
if (inReplyTo != null) {
// TODO check if same content?
room.getTimeLineEvent(inReplyTo)?.let {

View File

@ -130,6 +130,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
interface PreviewUrlCallback {
fun onPreviewUrlClicked(url: String)
fun onPreviewUrlCloseClicked(eventId: String, url: String)
fun onPreviewUrlImageClicked(sharedView: View?, mxcUrl: String?, title: String?)
}
// Map eventId to adapter position

View File

@ -23,7 +23,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import im.vector.app.R
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.media.ImageContentRenderer
@ -39,7 +39,7 @@ class PreviewUrlView @JvmOverloads constructor(
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
private lateinit var views: UrlPreviewBinding
private lateinit var views: ViewUrlPreviewBinding
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() {
when (val finalState = state) {
is PreviewUrlUiState.Data -> delegate?.onPreviewUrlCloseClicked(finalState.eventId, finalState.url)
@ -90,10 +103,11 @@ class PreviewUrlView @JvmOverloads constructor(
// PRIVATE METHODS ****************************************************************************************************************************************
private fun setupView() {
inflate(context, R.layout.url_preview, this)
views = UrlPreviewBinding.bind(this)
inflate(context, R.layout.view_url_preview, this)
views = ViewUrlPreviewBinding.bind(this)
setOnClickListener(this)
views.urlPreviewImage.setOnClickListener { onImageClick() }
views.urlPreviewClose.setOnClickListener { onCloseClick() }
}

View File

@ -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.widgets.model.Widget
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.Singleton
@ -256,14 +255,13 @@ class DefaultNavigator @Inject constructor(
context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess))
}
override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) {
matrixItem.avatarUrl
override fun openBigImageViewer(activity: Activity, sharedElement: View?, mxcUrl: String?, title: String?) {
mxcUrl
?.takeIf { it.isNotBlank() }
?.let { avatarUrl ->
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
val intent = BigImageViewerActivity.newIntent(activity, title, avatarUrl)
val options = sharedElement?.let {
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
?: "")
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
}
activity.startActivity(intent, options?.toBundle())
}

View File

@ -80,7 +80,11 @@ interface Navigator {
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,
activityResultLauncher: ActivityResultLauncher<Intent>,

View File

@ -25,9 +25,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
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.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.media.BigImageViewerActivity
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.util.MatrixItem
@ -103,8 +100,8 @@ class RoomProfileFragment @Inject constructor(
appBarStateChangeListener = MatrixItemAppBarStateChangeListener(
headerView,
listOf(views.matrixProfileToolbarAvatarImageView,
views.matrixProfileToolbarTitleView,
views.matrixProfileDecorationToolbarAvatarImageView)
views.matrixProfileToolbarTitleView,
views.matrixProfileDecorationToolbarAvatarImageView)
)
views.matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
roomProfileViewModel.observeViewEvents {
@ -289,13 +286,7 @@ class RoomProfileFragment @Inject constructor(
)
}
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) {
matrixItem.avatarUrl
?.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())
}
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) {
navigator.openBigImageViewer(requireActivity(), view, matrixItem)
}
}