diff --git a/CHANGES.md b/CHANGES.md index f11c1ed078..8b9a298d38 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 🗣: diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt index c82a929ee0..1a00b85ff4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt @@ -20,7 +20,7 @@ object ContentUtils { val lines = repliedBody.lines() var wellFormed = repliedBody.startsWith(">") var endOfPreviousFound = false - val usefullines = ArrayList() + val usefulLines = ArrayList() 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 { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt index a218f3f93c..d85e471f1d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -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 ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt index 6137b4152c..d1fb5b98ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt @@ -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://") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Html.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Html.kt new file mode 100644 index 0000000000..329b100497 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Html.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 2d2059377c..aeb1c30f4b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 588b7783e2..7eedd5ca8e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -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()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId if (inReplyTo != null) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index ba3ffe3174..1e108a2062 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt index a36b1281ba..f5d09efd18 100755 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -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() } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index fded8602c4..43a3f748a5 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -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()) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 504fccb63a..dda071795b 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -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, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 58fd78e26a..0cb57fda4f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -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) } } diff --git a/vector/src/main/res/layout/url_preview.xml b/vector/src/main/res/layout/view_url_preview.xml similarity index 100% rename from vector/src/main/res/layout/url_preview.xml rename to vector/src/main/res/layout/view_url_preview.xml