BillCarsonFr/JsonViewer
+
+ Copyright (C) 2018 stfalcon.com
+
Apache License
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
index ceb276614a..6cf555b32d 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
@@ -48,6 +48,7 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.link.LinkHandlerActivity
import im.vector.riotx.features.login.LoginActivity
+import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.media.BigImageViewerActivity
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity
@@ -135,6 +136,7 @@ interface ScreenComponent {
fun inject(activity: ReviewTermsActivity)
fun inject(activity: WidgetActivity)
fun inject(activity: VectorCallActivity)
+ fun inject(activity: VectorAttachmentViewerActivity)
/* ==========================================================================================
* BottomSheets
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
index d38a26c099..938ae6a1bb 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
@@ -1171,14 +1171,27 @@ class RoomDetailFragment @Inject constructor(
}
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
- navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = roomDetailArgs.roomId,
+ mediaData = mediaData,
+ view = view
+ ) { pairs ->
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
}
}
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
- navigator.openVideoViewer(requireActivity(), mediaData)
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = roomDetailArgs.roomId,
+ mediaData = mediaData,
+ view = view
+ ) { pairs ->
+ pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
+ pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
+ }
}
// override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {
@@ -1196,7 +1209,7 @@ class RoomDetailFragment @Inject constructor(
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
if (allGranted(grantResults)) {
when (requestCode) {
- SAVE_ATTACHEMENT_REQUEST_CODE -> {
+ SAVE_ATTACHEMENT_REQUEST_CODE -> {
sharedActionViewModel.pendingAction?.let {
handleActions(it)
sharedActionViewModel.pendingAction = null
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 2174556098..4f5f34cbf0 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -337,7 +337,7 @@ class MessageItemFactory @Inject constructor(
.playable(true)
.highlighted(highlight)
.mediaData(thumbnailData)
- .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
+ .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
}
private fun buildItemForTextContent(messageContent: MessageTextContent,
diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt
new file mode 100644
index 0000000000..2812b011f9
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.riotx.features.media
+
+import android.content.Context
+import android.graphics.Color
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.Group
+import im.vector.riotx.R
+import im.vector.riotx.attachmentviewer.AttachmentEventListener
+import im.vector.riotx.attachmentviewer.AttachmentEvents
+
+class AttachmentOverlayView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
+
+ var onShareCallback: (() -> Unit)? = null
+ var onBack: (() -> Unit)? = null
+ var onPlayPause: ((play: Boolean) -> Unit)? = null
+ var videoSeekTo: ((progress: Int) -> Unit)? = null
+
+ private val counterTextView: TextView
+ private val infoTextView: TextView
+ private val shareImage: ImageView
+ private val overlayPlayPauseButton: ImageView
+ private val overlaySeekBar: SeekBar
+
+ var isPlaying = false
+
+ val videoControlsGroup: Group
+
+ var suspendSeekBarUpdate = false
+
+ init {
+ View.inflate(context, R.layout.merge_image_attachment_overlay, this)
+ setBackgroundColor(Color.TRANSPARENT)
+ counterTextView = findViewById(R.id.overlayCounterText)
+ infoTextView = findViewById(R.id.overlayInfoText)
+ shareImage = findViewById(R.id.overlayShareButton)
+ videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup)
+ overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton)
+ overlaySeekBar = findViewById(R.id.overlaySeekBar)
+ findViewById(R.id.overlayBackButton).setOnClickListener {
+ onBack?.invoke()
+ }
+ findViewById(R.id.overlayShareButton).setOnClickListener {
+ onShareCallback?.invoke()
+ }
+ findViewById(R.id.overlayPlayPauseButton).setOnClickListener {
+ onPlayPause?.invoke(!isPlaying)
+ }
+
+ overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
+ if (fromUser) {
+ videoSeekTo?.invoke(progress)
+ }
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar?) {
+ suspendSeekBarUpdate = true
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {
+ suspendSeekBarUpdate = false
+ }
+ })
+ }
+
+ fun updateWith(counter: String, senderInfo: String) {
+ counterTextView.text = counter
+ infoTextView.text = senderInfo
+ }
+
+ override fun onEvent(event: AttachmentEvents) {
+ when (event) {
+ is AttachmentEvents.VideoEvent -> {
+ overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause)
+ if (!suspendSeekBarUpdate) {
+ val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat()
+ val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100)
+ isPlaying = event.isPlaying
+ overlaySeekBar.progress = percent
+ }
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt
new file mode 100644
index 0000000000..d4c41c7cb3
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.riotx.features.media
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.ImageView
+import com.bumptech.glide.request.target.CustomViewTarget
+import com.bumptech.glide.request.transition.Transition
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.attachmentviewer.AttachmentSourceProvider
+import im.vector.riotx.attachmentviewer.ImageLoaderTarget
+import im.vector.riotx.attachmentviewer.VideoLoaderTarget
+import java.io.File
+
+abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRenderer, val fileService: FileService) : AttachmentSourceProvider {
+
+ interface InteractionListener {
+ fun onDismissTapped()
+ fun onShareTapped()
+ fun onPlayPause(play: Boolean)
+ fun videoSeekTo(percent: Int)
+ }
+
+ var interactionListener: InteractionListener? = null
+
+ protected var overlayView: AttachmentOverlayView? = null
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ if (position == -1) return null
+ if (overlayView == null) {
+ overlayView = AttachmentOverlayView(context)
+ overlayView?.onBack = {
+ interactionListener?.onDismissTapped()
+ }
+ overlayView?.onShareCallback = {
+ interactionListener?.onShareTapped()
+ }
+ overlayView?.onPlayPause = { play ->
+ interactionListener?.onPlayPause(play)
+ }
+ overlayView?.videoSeekTo = { percent ->
+ interactionListener?.videoSeekTo(percent)
+ }
+ }
+ return overlayView
+ }
+
+ override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) {
+ (info.data as? ImageContentRenderer.Data)?.let {
+ imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onResourceReady(info.uid, resource)
+ }
+ })
+ }
+ }
+
+ override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) {
+ (info.data as? ImageContentRenderer.Data)?.let {
+ imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onResourceReady(info.uid, resource)
+ }
+ })
+ }
+ }
+
+ override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) {
+ val data = info.data as? VideoContentRenderer.Data ?: return
+// videoContentRenderer.render(data,
+// holder.thumbnailImage,
+// holder.loaderProgressBar,
+// holder.videoView,
+// holder.errorTextView)
+ imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onThumbnailLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onThumbnailResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onThumbnailResourceReady(info.uid, resource)
+ }
+ })
+
+ target.onVideoFileLoading(info.uid)
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
+ id = data.eventId,
+ mimeType = data.mimeType,
+ elementToDecrypt = data.elementToDecrypt,
+ fileName = data.filename,
+ url = data.url,
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ target.onVideoFileReady(info.uid, data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ target.onVideoFileLoadFailed(info.uid)
+ }
+ }
+ )
+ }
+
+ override fun clear(id: String) {
+ // TODO("Not yet implemented")
+ }
+
+ abstract fun getFileForSharing(position: Int, callback: ((File?) -> Unit))
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt
new file mode 100644
index 0000000000..cb0039fc7e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.riotx.features.media
+
+import android.content.Context
+import android.view.View
+import androidx.core.view.isVisible
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.events.model.isVideoMessage
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.matrix.android.api.session.room.Room
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.core.date.VectorDateFormatter
+import im.vector.riotx.core.extensions.localDateTime
+import java.io.File
+
+class DataAttachmentRoomProvider(
+ private val attachments: List,
+ private val room: Room?,
+ private val initialIndex: Int,
+ imageContentRenderer: ImageContentRenderer,
+ private val dateFormatter: VectorDateFormatter,
+ fileService: FileService) : BaseAttachmentProvider(imageContentRenderer, fileService) {
+
+ override fun getItemCount(): Int = attachments.size
+
+ override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
+ return attachments[position].let {
+ when (it) {
+ is ImageContentRenderer.Data -> {
+ if (it.mimeType == "image/gif") {
+ AttachmentInfo.AnimatedImage(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it
+ )
+ }
+ }
+ is VideoContentRenderer.Data -> {
+ AttachmentInfo.Video(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it,
+ thumbnail = AttachmentInfo.Image(
+ uid = it.eventId,
+ url = it.thumbnailMediaData.url ?: "",
+ data = it.thumbnailMediaData
+ )
+ )
+ }
+ else -> throw IllegalArgumentException()
+ }
+ }
+ }
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ super.overlayViewAtPosition(context, position)
+ val item = attachments[position]
+ val timeLineEvent = room?.getTimeLineEvent(item.eventId)
+ if (timeLineEvent != null) {
+ val dateString = timeLineEvent.root.localDateTime().let {
+ "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
+ }
+ overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString")
+ overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage()
+ } else {
+ overlayView?.updateWith("", "")
+ }
+ return overlayView
+ }
+
+ override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
+ val item = attachments[position]
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = item.eventId,
+ fileName = item.filename,
+ mimeType = item.mimeType,
+ url = item.url ?: "",
+ elementToDecrypt = item.elementToDecrypt,
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ callback(data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ callback(null)
+ }
+ }
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
index eeeb55ed15..f7613855c5 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
@@ -19,11 +19,13 @@ package im.vector.riotx.features.media
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Parcelable
+import android.view.View
import android.widget.ImageView
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
import com.github.piasy.biv.view.BigImageView
@@ -42,21 +44,29 @@ import java.io.File
import javax.inject.Inject
import kotlin.math.min
+interface AttachmentData : Parcelable {
+ val eventId: String
+ val filename: String
+ val mimeType: String?
+ val url: String?
+ val elementToDecrypt: ElementToDecrypt?
+}
+
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val dimensionConverter: DimensionConverter) {
@Parcelize
data class Data(
- val eventId: String,
- val filename: String,
- val mimeType: String?,
- val url: String?,
- val elementToDecrypt: ElementToDecrypt?,
+ override val eventId: String,
+ override val filename: String,
+ override val mimeType: String?,
+ override val url: String?,
+ override val elementToDecrypt: ElementToDecrypt?,
val height: Int?,
val maxHeight: Int,
val width: Int?,
val maxWidth: Int
- ) : Parcelable {
+ ) : AttachmentData {
fun isLocalFile() = url.isLocalFile()
}
@@ -93,6 +103,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
+ fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) {
+ val req = if (data.elementToDecrypt != null) {
+ // Encrypted image
+ GlideApp
+ .with(contextView)
+ .load(data)
+ } else {
+ // Clear image
+ val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
+ GlideApp
+ .with(contextView)
+ .load(resolvedUrl)
+ }
+
+ req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
+ .fitCenter()
+ .into(target)
+ }
+
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
val size = processSize(data, mode)
@@ -122,6 +151,45 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
+ fun renderThumbnailDontTransform(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
+ // a11y
+ imageView.contentDescription = data.filename
+
+ val req = if (data.elementToDecrypt != null) {
+ // Encrypted image
+ GlideApp
+ .with(imageView)
+ .load(data)
+ } else {
+ // Clear image
+ val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
+ GlideApp
+ .with(imageView)
+ .load(resolvedUrl)
+ }
+
+ req.listener(object : RequestListener {
+ override fun onLoadFailed(e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean): Boolean {
+ callback?.invoke(false)
+ return false
+ }
+
+ override fun onResourceReady(resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean): Boolean {
+ callback?.invoke(true)
+ return false
+ }
+ })
+ .dontTransform()
+ .into(imageView)
+ }
+
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest {
return if (data.elementToDecrypt != null) {
// Encrypted image
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
index 092199759f..8a6c2f7545 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
@@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
encryptedImageView.isVisible = false
// Postpone transaction a bit until thumbnail is loaded
supportPostponeEnterTransition()
+
+ // We are not passing the exact same image that in the
imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) {
// Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView)
diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt
new file mode 100644
index 0000000000..7a7fea6dc4
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.riotx.features.media
+
+import android.content.Context
+import android.view.View
+import androidx.core.view.isVisible
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.events.model.isVideoMessage
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.matrix.android.api.session.room.Room
+import im.vector.matrix.android.api.session.room.model.message.MessageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
+import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
+import im.vector.matrix.android.api.session.room.model.message.getFileUrl
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.core.date.VectorDateFormatter
+import im.vector.riotx.core.extensions.localDateTime
+import java.io.File
+import javax.inject.Inject
+
+class RoomEventsAttachmentProvider(
+ private val attachments: List,
+ private val initialIndex: Int,
+ imageContentRenderer: ImageContentRenderer,
+ private val dateFormatter: VectorDateFormatter,
+ fileService: FileService
+) : BaseAttachmentProvider(imageContentRenderer, fileService) {
+
+ override fun getItemCount(): Int {
+ return attachments.size
+ }
+
+ override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
+ return attachments[position].let {
+ val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent
+ if (content is MessageImageContent) {
+ val data = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ maxHeight = -1,
+ maxWidth = -1,
+ width = null,
+ height = null
+ )
+ if (content.mimeType == "image/gif") {
+ AttachmentInfo.AnimatedImage(
+ uid = it.eventId,
+ url = content.url ?: "",
+ data = data
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = content.url ?: "",
+ data = data
+ )
+ }
+ } else if (content is MessageVideoContent) {
+ val thumbnailData = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.videoInfo?.thumbnailFile?.url
+ ?: content.videoInfo?.thumbnailUrl,
+ elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
+ height = content.videoInfo?.height,
+ maxHeight = -1,
+ width = content.videoInfo?.width,
+ maxWidth = -1
+ )
+ val data = VideoContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ thumbnailMediaData = thumbnailData
+ )
+ AttachmentInfo.Video(
+ uid = it.eventId,
+ url = content.getFileUrl() ?: "",
+ data = data,
+ thumbnail = AttachmentInfo.Image(
+ uid = it.eventId,
+ url = content.videoInfo?.thumbnailFile?.url
+ ?: content.videoInfo?.thumbnailUrl ?: "",
+ data = thumbnailData
+
+ )
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = "",
+ data = null
+ )
+ }
+ }
+ }
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ super.overlayViewAtPosition(context, position)
+ val item = attachments[position]
+ val dateString = item.root.localDateTime().let {
+ "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
+ }
+ overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString")
+ overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()
+ return overlayView
+ }
+
+ override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
+ attachments[position].let { timelineEvent ->
+
+ val messageContent = timelineEvent.root.getClearContent().toModel()
+ as? MessageWithAttachmentContent
+ ?: return@let
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = timelineEvent.eventId,
+ fileName = messageContent.body,
+ mimeType = messageContent.mimeType,
+ url = messageContent.getFileUrl(),
+ elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ callback(data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ callback(null)
+ }
+ }
+ )
+ }
+ }
+}
+
+class AttachmentProviderFactory @Inject constructor(
+ private val imageContentRenderer: ImageContentRenderer,
+ private val vectorDateFormatter: VectorDateFormatter,
+ private val session: Session
+) {
+
+ fun createProvider(attachments: List, initialIndex: Int): RoomEventsAttachmentProvider {
+ return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
+ }
+
+ fun createProvider(attachments: List, room: Room?, initialIndex: Int): DataAttachmentRoomProvider {
+ return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt
new file mode 100644
index 0000000000..38e3ccc69c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.riotx.features.media
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import androidx.core.transition.addListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.transition.Transition
+import im.vector.riotx.R
+import im.vector.riotx.attachmentviewer.AttachmentCommands
+import im.vector.riotx.attachmentviewer.AttachmentViewerActivity
+import im.vector.riotx.core.di.ActiveSessionHolder
+import im.vector.riotx.core.di.DaggerScreenComponent
+import im.vector.riotx.core.di.HasVectorInjector
+import im.vector.riotx.core.di.ScreenComponent
+import im.vector.riotx.core.di.VectorComponent
+import im.vector.riotx.core.intent.getMimeTypeFromUri
+import im.vector.riotx.core.utils.shareMedia
+import im.vector.riotx.features.themes.ActivityOtherThemes
+import im.vector.riotx.features.themes.ThemeUtils
+import kotlinx.android.parcel.Parcelize
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.system.measureTimeMillis
+
+class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener {
+
+ @Parcelize
+ data class Args(
+ val roomId: String?,
+ val eventId: String,
+ val sharedTransitionName: String?
+ ) : Parcelable
+
+ @Inject
+ lateinit var sessionHolder: ActiveSessionHolder
+
+ @Inject
+ lateinit var dataSourceFactory: AttachmentProviderFactory
+
+ @Inject
+ lateinit var imageContentRenderer: ImageContentRenderer
+
+ private lateinit var screenComponent: ScreenComponent
+
+ private var initialIndex = 0
+ private var isAnimatingOut = false
+
+ var currentSourceProvider: BaseAttachmentProvider? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Timber.i("onCreate Activity ${this.javaClass.simpleName}")
+ val vectorComponent = getVectorComponent()
+ screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
+ val timeForInjection = measureTimeMillis {
+ screenComponent.inject(this)
+ }
+ Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms")
+ ThemeUtils.setActivityTheme(this, getOtherThemes())
+
+ val args = args() ?: throw IllegalArgumentException("Missing arguments")
+
+ if (savedInstanceState == null && addTransitionListener()) {
+ args.sharedTransitionName?.let {
+ ViewCompat.setTransitionName(imageTransitionView, it)
+ transitionImageContainer.isVisible = true
+
+ // Postpone transaction a bit until thumbnail is loaded
+ val mediaData: Parcelable? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
+ if (mediaData is ImageContentRenderer.Data) {
+ // will be shown at end of transition
+ pager2.isInvisible = true
+ supportPostponeEnterTransition()
+ imageContentRenderer.renderThumbnailDontTransform(mediaData, imageTransitionView) {
+ // Proceed with transaction
+ scheduleStartPostponedTransition(imageTransitionView)
+ }
+ } else if (mediaData is VideoContentRenderer.Data) {
+ // will be shown at end of transition
+ pager2.isInvisible = true
+ supportPostponeEnterTransition()
+ imageContentRenderer.renderThumbnailDontTransform(mediaData.thumbnailMediaData, imageTransitionView) {
+ // Proceed with transaction
+ scheduleStartPostponedTransition(imageTransitionView)
+ }
+ }
+ }
+ }
+
+ val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
+
+ val room = args.roomId?.let { session.getRoom(it) }
+
+ val inMemoryData = intent.getParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA)
+ if (inMemoryData != null) {
+ val sourceProvider = dataSourceFactory.createProvider(inMemoryData, room, initialIndex)
+ val index = inMemoryData.indexOfFirst { it.eventId == args.eventId }
+ initialIndex = index
+ sourceProvider.interactionListener = this
+ setSourceProvider(sourceProvider)
+ this.currentSourceProvider = sourceProvider
+ if (savedInstanceState == null) {
+ pager2.setCurrentItem(index, false)
+ // The page change listener is not notified of the change...
+ pager2.post {
+ onSelectedPositionChanged(index)
+ }
+ }
+ } else {
+ val events = room?.getAttachmentMessages()
+ ?: emptyList()
+ val index = events.indexOfFirst { it.eventId == args.eventId }
+ initialIndex = index
+
+ val sourceProvider = dataSourceFactory.createProvider(events, index)
+ sourceProvider.interactionListener = this
+ setSourceProvider(sourceProvider)
+ this.currentSourceProvider = sourceProvider
+ if (savedInstanceState == null) {
+ pager2.setCurrentItem(index, false)
+ // The page change listener is not notified of the change...
+ pager2.post {
+ onSelectedPositionChanged(index)
+ }
+ }
+ }
+
+ window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
+ window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
+ }
+
+ private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
+
+ override fun shouldAnimateDismiss(): Boolean {
+ return currentPosition != initialIndex
+ }
+
+ override fun onBackPressed() {
+ if (currentPosition == initialIndex) {
+ // show back the transition view
+ // TODO, we should track and update the mapping
+ transitionImageContainer.isVisible = true
+ }
+ isAnimatingOut = true
+ super.onBackPressed()
+ }
+
+ override fun animateClose() {
+ if (currentPosition == initialIndex) {
+ // show back the transition view
+ // TODO, we should track and update the mapping
+ transitionImageContainer.isVisible = true
+ }
+ isAnimatingOut = true
+ ActivityCompat.finishAfterTransition(this)
+ }
+
+ // ==========================================================================================
+ // PRIVATE METHODS
+ // ==========================================================================================
+
+ /**
+ * Try and add a [Transition.TransitionListener] to the entering shared element
+ * [Transition]. We do this so that we can load the full-size image after the transition
+ * has completed.
+ *
+ * @return true if we were successful in adding a listener to the enter transition
+ */
+ private fun addTransitionListener(): Boolean {
+ val transition = window.sharedElementEnterTransition
+
+ if (transition != null) {
+ // There is an entering shared element transition so add a listener to it
+ transition.addListener(
+ onEnd = {
+ // The listener is also called when we are exiting
+ // so we use a boolean to avoid reshowing pager at end of dismiss transition
+ if (!isAnimatingOut) {
+ transitionImageContainer.isVisible = false
+ pager2.isInvisible = false
+ }
+ },
+ onCancel = {
+ if (!isAnimatingOut) {
+ transitionImageContainer.isVisible = false
+ pager2.isInvisible = false
+ }
+ }
+ )
+ return true
+ }
+
+ // If we reach here then we have not added a listener
+ return false
+ }
+
+ private fun args() = intent.getParcelableExtra(EXTRA_ARGS)
+
+ private fun getVectorComponent(): VectorComponent {
+ return (application as HasVectorInjector).injector()
+ }
+
+ private fun scheduleStartPostponedTransition(sharedElement: View) {
+ sharedElement.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ sharedElement.viewTreeObserver.removeOnPreDrawListener(this)
+ supportStartPostponedEnterTransition()
+ return true
+ }
+ })
+ }
+
+ companion object {
+ const val EXTRA_ARGS = "EXTRA_ARGS"
+ const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
+ const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
+
+ fun newIntent(context: Context,
+ mediaData: AttachmentData,
+ roomId: String?,
+ eventId: String,
+ inMemoryData: List,
+ sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {
+ it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName))
+ it.putExtra(EXTRA_IMAGE_DATA, mediaData)
+ if (inMemoryData.isNotEmpty()) {
+ it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData))
+ }
+ }
+ }
+
+ override fun onDismissTapped() {
+ animateClose()
+ }
+
+ override fun onPlayPause(play: Boolean) {
+ handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
+ }
+
+ override fun videoSeekTo(percent: Int) {
+ handle(AttachmentCommands.SeekTo(percent))
+ }
+
+ override fun onShareTapped() {
+ this.currentSourceProvider?.getFileForSharing(currentPosition) { data ->
+ if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
+ shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri()))
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
index 760d3b12a0..e6dec88349 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
@@ -16,7 +16,6 @@
package im.vector.riotx.features.media
-import android.os.Parcelable
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
@@ -38,13 +37,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
@Parcelize
data class Data(
- val eventId: String,
- val filename: String,
- val mimeType: String?,
- val url: String?,
- val elementToDecrypt: ElementToDecrypt?,
+ override val eventId: String,
+ override val filename: String,
+ override val mimeType: String?,
+ override val url: String?,
+ override val elementToDecrypt: ElementToDecrypt?,
val thumbnailMediaData: ImageContentRenderer.Data
- ) : Parcelable
+ ) : AttachmentData
fun render(data: Data,
thumbnailView: ImageView,
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
index 83f0baa12c..ae73cb8dad 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
@@ -19,7 +19,6 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import android.content.Intent
-import android.os.Build
import android.view.View
import android.view.Window
import androidx.core.app.ActivityOptionsCompat
@@ -49,11 +48,9 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
+import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.media.BigImageViewerActivity
-import im.vector.riotx.features.media.ImageContentRenderer
-import im.vector.riotx.features.media.ImageMediaViewerActivity
-import im.vector.riotx.features.media.VideoContentRenderer
-import im.vector.riotx.features.media.VideoMediaViewerActivity
+import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@@ -89,7 +86,8 @@ class DefaultNavigator @Inject constructor(
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
val session = sessionHolder.getSafeActiveSession() ?: return
- val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return
+ val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId)
+ ?: return
(tx as? IncomingSasVerificationTransaction)?.performAccept()
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
@@ -237,7 +235,8 @@ class DefaultNavigator @Inject constructor(
?.let { avatarUrl ->
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
val options = sharedElement?.let {
- ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
+ ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
+ ?: "")
}
activity.startActivity(intent, options?.toBundle())
}
@@ -265,27 +264,32 @@ class DefaultNavigator @Inject constructor(
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
}
- override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) {
- val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
- val pairs = ArrayList>()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ override fun openMediaViewer(activity: Activity,
+ roomId: String,
+ mediaData: AttachmentData,
+ view: View,
+ inMemory: List,
+ options: ((MutableList>) -> Unit)?) {
+ VectorAttachmentViewerActivity.newIntent(activity,
+ mediaData,
+ roomId,
+ mediaData.eventId,
+ inMemory,
+ ViewCompat.getTransitionName(view)).let { intent ->
+ val pairs = ArrayList>()
activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
+
+ pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
+ options?.invoke(pairs)
+
+ val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
+ activity.startActivity(intent, bundle)
}
- pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
- options?.invoke(pairs)
-
- val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
- activity.startActivity(intent, bundle)
- }
-
- override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) {
- val intent = VideoMediaViewerActivity.newIntent(activity, mediaData)
- activity.startActivity(intent)
}
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
index 3ead483369..56176f819a 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
@@ -23,11 +23,10 @@ import androidx.core.util.Pair
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.terms.TermsService
-import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
-import im.vector.riotx.features.media.ImageContentRenderer
-import im.vector.riotx.features.media.VideoContentRenderer
+import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
@@ -93,7 +92,10 @@ interface Navigator {
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
- fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?)
-
- fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
+ fun openMediaViewer(activity: Activity,
+ roomId: String,
+ mediaData: AttachmentData,
+ view: View,
+ inMemory: List = emptyList(),
+ options: ((MutableList>) -> Unit)?)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
index 78a0cece41..e5b2f34f61 100644
--- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
@@ -26,6 +26,7 @@ import com.tapadoo.alerter.Alerter
import com.tapadoo.alerter.OnHideAlertListener
import dagger.Lazy
import im.vector.riotx.R
+import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.themes.ThemeUtils
import timber.log.Timber
@@ -83,7 +84,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy()
+ ?.firstOrNull()
+ ?.roomUploadsAppBar
}
- override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
- navigator.openVideoViewer(requireActivity(), mediaData)
+ override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) = withState(uploadsViewModel) { state ->
+ val inMemory = getItemsArgs(state)
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = state.roomId,
+ mediaData = mediaData,
+ view = view,
+ inMemory = inMemory
+ ) { pairs ->
+ trickFindAppBar()?.let {
+ pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: ""))
+ }
+ }
+ }
+
+ private fun getItemsArgs(state: RoomUploadsViewState): List {
+ return state.mediaEvents.mapNotNull {
+ when (val content = it.contentWithAttachmentContent) {
+ is MessageImageContent -> {
+ ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ maxHeight = -1,
+ maxWidth = -1,
+ width = null,
+ height = null
+ )
+ }
+ is MessageVideoContent -> {
+ val thumbnailData = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.videoInfo?.thumbnailFile?.url
+ ?: content.videoInfo?.thumbnailUrl,
+ elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
+ height = content.videoInfo?.height,
+ maxHeight = -1,
+ width = content.videoInfo?.width,
+ maxWidth = -1
+ )
+ VideoContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ thumbnailMediaData = thumbnailData
+ )
+ }
+ else -> null
+ }
+ }
+ }
+
+ override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) = withState(uploadsViewModel) { state ->
+ val inMemory = getItemsArgs(state)
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = state.roomId,
+ mediaData = mediaData,
+ view = view,
+ inMemory = inMemory
+ ) { pairs ->
+ trickFindAppBar()?.let {
+ pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: ""))
+ }
+ }
}
override fun loadMore() {
diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
index 98026901cc..3b83e99656 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
@@ -18,11 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View
import android.widget.ImageView
+import androidx.core.view.ViewCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_uploads_image)
@@ -35,8 +37,13 @@ abstract class UploadsImageItem : VectorEpoxyModel() {
override fun bind(holder: Holder) {
super.bind(holder)
- holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
+ holder.view.setOnClickListener(
+ DebouncedClickListener(View.OnClickListener { _ ->
+ listener?.onItemClicked(holder.imageView, data)
+ })
+ )
imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP)
+ ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
}
class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
index 82e33b76da..f20f6ed5b1 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
@@ -18,11 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View
import android.widget.ImageView
+import androidx.core.view.ViewCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
@@ -36,8 +38,13 @@ abstract class UploadsVideoItem : VectorEpoxyModel() {
override fun bind(holder: Holder) {
super.bind(holder)
- holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
+ holder.view.setOnClickListener(
+ DebouncedClickListener(View.OnClickListener { _ ->
+ listener?.onItemClicked(holder.imageView, data)
+ })
+ )
imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP)
+ ViewCompat.setTransitionName(holder.imageView, "videoPreview_${id()}")
}
class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt b/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
index b37c1a4818..b29e60784e 100644
--- a/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
+++ b/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
@@ -38,4 +38,10 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int,
R.style.AppTheme_AttachmentsPreview,
R.style.AppTheme_AttachmentsPreview
)
+
+ object VectorAttachmentsPreview : ActivityOtherThemes(
+ R.style.AppTheme_Transparent,
+ R.style.AppTheme_Transparent,
+ R.style.AppTheme_Transparent
+ )
}
diff --git a/vector/src/main/res/drawable/ic_pause.xml b/vector/src/main/res/drawable/ic_pause.xml
new file mode 100644
index 0000000000..13d6d2ec00
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_pause.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_play_arrow.xml b/vector/src/main/res/drawable/ic_play_arrow.xml
new file mode 100644
index 0000000000..13c137a921
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_play_arrow.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/layout/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml
index 5e289d4724..f5d3658ee5 100644
--- a/vector/src/main/res/layout/fragment_room_uploads.xml
+++ b/vector/src/main/res/layout/fragment_room_uploads.xml
@@ -8,6 +8,8 @@
diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml
new file mode 100644
index 0000000000..b0e769579c
--- /dev/null
+++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml
index a9cb32c3fd..c9d1c2a223 100644
--- a/vector/src/main/res/values/colors_riotx.xml
+++ b/vector/src/main/res/values/colors_riotx.xml
@@ -40,6 +40,7 @@
#FF000000
#FFFFFFFF
+ #55000000
Ongoing conference call.\nJoin as %1$s or %2$s
Voice
diff --git a/vector/src/main/res/values/theme_common.xml b/vector/src/main/res/values/theme_common.xml
index 151d97c097..414d562ff0 100644
--- a/vector/src/main/res/values/theme_common.xml
+++ b/vector/src/main/res/values/theme_common.xml
@@ -10,4 +10,15 @@
+
+
\ No newline at end of file