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 🙌: 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 🗣:

View File

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

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.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
) )
} }

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.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://") }

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

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

View File

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

View File

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

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

View File

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

View File

@ -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
@ -103,8 +100,8 @@ class RoomProfileFragment @Inject constructor(
appBarStateChangeListener = MatrixItemAppBarStateChangeListener( appBarStateChangeListener = MatrixItemAppBarStateChangeListener(
headerView, headerView,
listOf(views.matrixProfileToolbarAvatarImageView, listOf(views.matrixProfileToolbarAvatarImageView,
views.matrixProfileToolbarTitleView, views.matrixProfileToolbarTitleView,
views.matrixProfileDecorationToolbarAvatarImageView) views.matrixProfileDecorationToolbarAvatarImageView)
) )
views.matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) views.matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
roomProfileViewModel.observeViewEvents { roomProfileViewModel.observeViewEvents {
@ -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())
}
} }
} }