Uploads: add screen - WIP

This commit is contained in:
Benoit Marty 2020-05-19 17:39:10 +02:00
parent 0992e76800
commit e9ca876444
28 changed files with 868 additions and 109 deletions

View File

@ -220,3 +220,20 @@ fun Event.isImageMessage(): Boolean {
else -> false
}
}
fun Event.isVideoMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}
fun Event.isPreviewableMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.uploads
import im.vector.matrix.android.api.session.events.model.Event
data class GetUploadsResult(
// List of fetched Events
// List of fetched Events, most recent first
val events: List<Event>,
// token to get more events, or null if there is no more event to fetch
val nextToken: String?

View File

@ -38,7 +38,9 @@ internal class DefaultUploadsService @AssistedInject constructor(
override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback<GetUploadsResult>): Cancelable {
return getUploadsTask
.configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since))
.configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
}

View File

@ -51,7 +51,7 @@ internal class DefaultGetUploadsTask @Inject constructor(
}
return GetUploadsResult(
events = chunk.events, // reverse?
events = chunk.events,
nextToken = chunk.end?.takeIf { it != chunk.start }
)
}

View File

@ -77,8 +77,8 @@ import im.vector.riotx.features.roomprofile.RoomProfileFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsFilesFragment
import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsMediaFragment
import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsLabsFragment

View File

@ -14,20 +14,13 @@
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.child
package im.vector.riotx.core.epoxy
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
import javax.inject.Inject
/**
* A placeholder fragment containing a simple view.
*/
class RoomUploadsFilesFragment @Inject constructor() : VectorBaseFragment() {
@EpoxyModelClass(layout = R.layout.item_loading_square)
abstract class SquareLoadingItem : VectorEpoxyModel<SquareLoadingItem.Holder>() {
private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class)
override fun getLayoutResId() = R.layout.fragment_generic_recycler
class Holder : VectorEpoxyHolder()
}

View File

@ -36,4 +36,8 @@ class DimensionConverter @Inject constructor(val resources: Resources) {
resources.displayMetrics
).toInt()
}
fun pdToDp(px: Int): Int {
return (px.toFloat() / resources.displayMetrics.density).toInt()
}
}

View File

@ -22,7 +22,6 @@ import android.content.DialogInterface
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.text.Spannable
@ -30,12 +29,10 @@ import android.view.HapticFeedbackConstants
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.Window
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.text.buildSpannedString
@ -150,7 +147,6 @@ import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView
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.notifications.NotificationDrawerManager
@ -998,31 +994,14 @@ class RoomDetailFragment @Inject constructor(
}
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
val pairs = ArrayList<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
requireActivity().window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
requireActivity().window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
}
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(), *pairs.toTypedArray()).toBundle()
startActivity(intent, bundle)
}
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
// TODO Use navigator
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
navigator.openVideoViewer(requireActivity(), mediaData)
}
override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {

View File

@ -65,6 +65,17 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
STICKER
}
/**
* For gallery
*/
fun render(data: Data, imageView: ImageView, size: Int) {
// a11y
imageView.contentDescription = data.filename
createGlideRequest(data, Mode.THUMBNAIL, imageView, Size(size, size))
.into(imageView)
}
fun render(data: Data, mode: Mode, imageView: ImageView) {
val size = processSize(data, mode)
imageView.layoutParams.width = size.width

View File

@ -19,9 +19,12 @@ 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
import androidx.core.app.TaskStackBuilder
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
@ -45,6 +48,10 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
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.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@ -215,6 +222,29 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, requestCode)
}
override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?) {
val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
val pairs = ArrayList<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
activity.window.decorView.findViewById<View>(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)
}
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) {
if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context)

View File

@ -19,10 +19,13 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import android.view.View
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.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
@ -76,4 +79,8 @@ interface Navigator {
baseUrl: String,
token: String?,
requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE)
fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?)
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
}

View File

@ -16,6 +16,12 @@
package im.vector.riotx.features.roomprofile.uploads
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomUploadsAction : VectorViewModelAction
sealed class RoomUploadsAction : VectorViewModelAction {
data class Download(val event: Event) : RoomUploadsAction()
data class Share(val event: Event) : RoomUploadsAction()
object Retry : RoomUploadsAction()
}

View File

@ -21,6 +21,7 @@ import android.view.View
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.tabs.TabLayoutMediator
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
@ -45,11 +46,17 @@ class RoomUploadsFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val sectionsPagerAdapter = RoomUploadsPagerAdapter(childFragmentManager, stringProvider)
view_pager.adapter = sectionsPagerAdapter
tabs.setupWithViewPager(view_pager)
val sectionsPagerAdapter = RoomUploadsPagerAdapter(this)
roomUploadsViewPager.adapter = sectionsPagerAdapter
setupToolbar(matrixProfileToolbar)
TabLayoutMediator(roomUploadsTabs, roomUploadsViewPager) { tab, position ->
when (position) {
0 -> tab.text = stringProvider.getString(R.string.uploads_media_title)
1 -> tab.text = stringProvider.getString(R.string.uploads_files_title)
}
}.attach()
setupToolbar(roomUploadsToolbar)
// Initialize your view, subscribe to viewModel...
}
@ -68,8 +75,8 @@ class RoomUploadsFragment @Inject constructor(
private fun renderRoomSummary(state: RoomUploadsViewState) {
state.roomSummary()?.let {
matrixProfileToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), matrixProfileToolbarAvatarImageView)
roomUploadsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), roomUploadsToolbarAvatarImageView)
}
}
}

View File

@ -17,43 +17,21 @@
package im.vector.riotx.features.roomprofile.uploads
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsFilesFragment
import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsMediaFragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment
private val TAB_TITLES = arrayOf(
R.string.uploads_title_media,
R.string.uploads_title_files
)
/**
* A [FragmentPagerAdapter] that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
class RoomUploadsPagerAdapter(
fm: FragmentManager,
private val stringProvider: StringProvider
) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val fragment: Fragment
) : FragmentStateAdapter(fragment) {
override fun getItem(position: Int): Fragment {
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below).
override fun getItemCount() = 2
override fun createFragment(position: Int): Fragment {
return if (position == 0) {
RoomUploadsMediaFragment()
fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, RoomUploadsMediaFragment::class.java.name)
} else {
RoomUploadsFilesFragment()
fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, RoomUploadsFilesFragment::class.java.name)
}
}
override fun getPageTitle(position: Int): CharSequence? {
return stringProvider.getString(TAB_TITLES[position])
}
override fun getCount(): Int {
// Show 2 total pages.
return 2
}
}

View File

@ -27,6 +27,10 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isPreviewableMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.rx
@ -90,10 +94,15 @@ class RoomUploadsViewModel @AssistedInject constructor(
token = result.nextToken
val groupedEvents = result.events
.filter { it.getClearType() == EventType.MESSAGE && it.getClearContent()?.toModel<MessageContent>() != null }
.groupBy { it.isPreviewableMessage() }
setState {
copy(
asyncEventsRequest = Uninitialized,
events = this.events + result.events,
mediaEvents = this.mediaEvents + groupedEvents[true].orEmpty(),
fileEvents = this.fileEvents + groupedEvents[false].orEmpty(),
hasMore = result.nextToken != null
)
}

View File

@ -0,0 +1,73 @@
/*
* 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.roomprofile.uploads.files
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class RoomUploadsFilesFragment @Inject constructor(
private val controller: UploadsFileController
) : VectorBaseFragment(), UploadsFileController.Listener {
private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class)
override fun getLayoutResId() = R.layout.fragment_generic_recycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(controller, showDivider = true)
controller.listener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
controller.listener = null
}
override fun onOpenClicked(event: Event) {
// TODO
}
override fun onRetry() {
uploadsViewModel.handle(RoomUploadsAction.Retry)
}
override fun onDownloadClicked(event: Event) {
uploadsViewModel.handle(RoomUploadsAction.Download(event))
}
override fun onShareClicked(event: Event) {
uploadsViewModel.handle(RoomUploadsAction.Share(event))
}
override fun invalidate() = withState(uploadsViewModel) { state ->
controller.setData(state)
}
}

View File

@ -0,0 +1,102 @@
/*
* 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.roomprofile.uploads.files
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState
import javax.inject.Inject
class UploadsFileController @Inject constructor(
private val errorFormatter: ErrorFormatter,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomUploadsViewState>() {
interface Listener {
fun onRetry()
fun onOpenClicked(event: Event)
fun onDownloadClicked(event: Event)
fun onShareClicked(event: Event)
}
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
var listener: Listener? = null
init {
setData(null)
}
override fun buildModels(data: RoomUploadsViewState?) {
data ?: return
if (data.fileEvents.isEmpty()) {
when (data.asyncEventsRequest) {
is Loading -> {
loadingItem {
id("loading")
}
}
is Fail -> {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(data.asyncEventsRequest.error))
listener { listener?.onRetry() }
}
}
}
} else {
buildFileItems(data.fileEvents)
if (data.hasMore) {
loadingItem {
id("loadMore")
}
}
}
}
private fun buildFileItems(fileEvents: List<Event>) {
fileEvents.forEach {
uploadsFileItem {
id(it.eventId ?: "")
title(it.getClearType())
subtitle(it.getSenderKey())
listener(object : UploadsFileItem.Listener {
override fun onItemClicked() {
listener?.onOpenClicked(it)
}
override fun onDownloadClicked() {
listener?.onDownloadClicked(it)
}
override fun onShareClicked() {
listener?.onShareClicked(it)
}
})
}
}
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.roomprofile.uploads.files
import android.view.View
import android.widget.TextView
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.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_uploads_file)
abstract class UploadsFileItem : VectorEpoxyModel<UploadsFileItem.Holder>() {
@EpoxyAttribute var title: String? = null
@EpoxyAttribute var subtitle: String? = null
@EpoxyAttribute var listener: Listener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener { listener?.onItemClicked() }
holder.titleView.text = title
holder.subtitleView.setTextOrHide(subtitle)
holder.downloadView.setOnClickListener { listener?.onDownloadClicked() }
holder.shareView.setOnClickListener { listener?.onShareClicked() }
}
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.uploadsFileTitle)
val subtitleView by bind<TextView>(R.id.uploadsFileSubtitle)
val downloadView by bind<View>(R.id.uploadsFileActionDownload)
val shareView by bind<View>(R.id.uploadsFileActionShare)
}
interface Listener {
fun onItemClicked()
fun onDownloadClicked()
fun onShareClicked()
}
}

View File

@ -14,20 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.child
package im.vector.riotx.features.roomprofile.uploads.media
import com.airbnb.mvrx.parentFragmentViewModel
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
import javax.inject.Inject
/**
* A placeholder fragment containing a simple view.
*/
class RoomUploadsMediaFragment @Inject constructor() : VectorBaseFragment() {
private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class)
override fun getLayoutResId() = R.layout.fragment_generic_recycler
}
// Min image size. Size will be adjusted at runtime
const val IMAGE_SIZE_DP = 120

View File

@ -0,0 +1,82 @@
/*
* 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.roomprofile.uploads.media
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class RoomUploadsMediaFragment @Inject constructor(
private val controller: UploadsMediaController,
private val dimensionConverter: DimensionConverter
) : VectorBaseFragment(), UploadsMediaController.Listener {
private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class)
override fun getLayoutResId() = R.layout.fragment_generic_recycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.layoutManager = GridLayoutManager(context, getNumberOfColumns())
recyclerView.adapter = controller.adapter
recyclerView.setHasFixedSize(true)
controller.listener = this
}
private fun getNumberOfColumns(): Int {
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
return dimensionConverter.pdToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
controller.listener = null
}
override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) {
navigator.openImageViewer(requireActivity(), mediaData, view, null)
}
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
navigator.openVideoViewer(requireActivity(), mediaData)
}
override fun onRetry() {
uploadsViewModel.handle(RoomUploadsAction.Retry)
}
override fun invalidate() = withState(uploadsViewModel) { state ->
controller.setData(state)
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.roomprofile.uploads.media
import android.view.View
import android.widget.ImageView
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.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_uploads_image)
abstract class UploadsImageItem : VectorEpoxyModel<UploadsImageItem.Holder>() {
@EpoxyAttribute lateinit var imageContentRenderer: ImageContentRenderer
@EpoxyAttribute lateinit var data: ImageContentRenderer.Data
@EpoxyAttribute var listener: Listener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP)
}
class Holder : VectorEpoxyHolder() {
val imageView by bind<ImageView>(R.id.uploadsImagePreview)
}
interface Listener {
fun onItemClicked(view: View, data: ImageContentRenderer.Data)
}
}

View File

@ -0,0 +1,165 @@
/*
* 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.roomprofile.uploads.media
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.isImageMessage
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.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.squareLoadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState
import javax.inject.Inject
class UploadsMediaController @Inject constructor(
private val errorFormatter: ErrorFormatter,
private val imageContentRenderer: ImageContentRenderer,
private val dimensionConverter: DimensionConverter,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomUploadsViewState>() {
interface Listener {
fun onRetry()
fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data)
fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data)
}
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
var listener: Listener? = null
private val itemSize = dimensionConverter.dpToPx(64)
init {
setData(null)
}
override fun buildModels(data: RoomUploadsViewState?) {
data ?: return
if (data.mediaEvents.isEmpty()) {
when (data.asyncEventsRequest) {
is Loading -> {
squareLoadingItem {
id("loading")
}
}
is Fail -> {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(data.asyncEventsRequest.error))
listener { listener?.onRetry() }
}
}
}
} else {
buildMediaItems(data.mediaEvents)
if (data.hasMore) {
squareLoadingItem {
id("loadMore")
}
}
}
}
private fun buildMediaItems(mediaEvents: List<Event>) {
mediaEvents.forEach { event ->
when {
event.isImageMessage() -> {
val data = event.toImageContentRendererData() ?: return@forEach
uploadsImageItem {
id(event.eventId ?: "")
imageContentRenderer(imageContentRenderer)
data(data)
listener(object : UploadsImageItem.Listener {
override fun onItemClicked(view: View, data: ImageContentRenderer.Data) {
listener?.onOpenImageClicked(view, data)
}
})
}
}
event.isVideoMessage() -> {
val data = event.toVideoContentRendererData() ?: return@forEach
uploadsVideoItem {
id(event.eventId ?: "")
imageContentRenderer(imageContentRenderer)
data(data)
listener(object : UploadsVideoItem.Listener {
override fun onItemClicked(view: View, data: VideoContentRenderer.Data) {
listener?.onOpenVideoClicked(view, data)
}
})
}
}
}
}
}
private fun Event.toImageContentRendererData(): ImageContentRenderer.Data? {
val messageContent = getClearContent()?.toModel<MessageImageContent>() ?: return null
return ImageContentRenderer.Data(
eventId = eventId ?: "",
filename = messageContent.body,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height,
maxHeight = itemSize,
width = messageContent.info?.width,
maxWidth = itemSize
)
}
private fun Event.toVideoContentRendererData(): VideoContentRenderer.Data? {
val messageContent = getClearContent()?.toModel<MessageVideoContent>() ?: return null
val thumbnailData = ImageContentRenderer.Data(
eventId = eventId ?: "",
filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = itemSize,
width = messageContent.videoInfo?.width,
maxWidth = itemSize
)
return VideoContentRenderer.Data(
eventId = eventId ?: "",
filename = messageContent.body,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData
)
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.roomprofile.uploads.media
import android.view.View
import android.widget.ImageView
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.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
@EpoxyModelClass(layout = R.layout.item_uploads_video)
abstract class UploadsVideoItem : VectorEpoxyModel<UploadsVideoItem.Holder>() {
@EpoxyAttribute lateinit var imageContentRenderer: ImageContentRenderer
@EpoxyAttribute lateinit var data: VideoContentRenderer.Data
@EpoxyAttribute var listener: Listener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP)
}
class Holder : VectorEpoxyHolder() {
val imageView by bind<ImageView>(R.id.uploadsVideoPreview)
}
interface Listener {
fun onItemClicked(view: View, data: VideoContentRenderer.Data)
}
}

View File

@ -12,7 +12,7 @@
android:elevation="4dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/matrixProfileToolbar"
android:id="@+id/roomUploadsToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
@ -20,12 +20,12 @@
app:layout_collapseMode="pin">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/matrixProfileToolbarContainer"
android:id="@+id/roomUploadsToolbarContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/matrixProfileToolbarAvatarImageView"
android:id="@+id/roomUploadsToolbarAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
@ -36,17 +36,17 @@
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/matrixProfileDecorationToolbarAvatarImageView"
android:id="@+id/roomUploadsDecorationToolbarAvatarImageView"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintCircle="@+id/matrixProfileToolbarAvatarImageView"
app:layout_constraintCircle="@+id/roomUploadsToolbarAvatarImageView"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp"
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/matrixProfileToolbarTitleView"
android:id="@+id/roomUploadsToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
@ -57,7 +57,7 @@
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/matrixProfileToolbarAvatarImageView"
app:layout_constraintStart_toEndOf="@+id/roomUploadsToolbarAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/roomName" />
@ -66,18 +66,18 @@
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:id="@+id/roomUploadsTabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/matrixProfileToolbarAvatarImageView"
app:layout_constraintTop_toBottomOf="@+id/roomUploadsToolbarAvatarImageView"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabMode="fixed" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/roomUploadsViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground"
android:minHeight="64dp">
<ImageView
android:id="@+id/uploadsFileIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:src="@drawable/ic_file"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/uploadsFileTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/uploadsFileSubtitle"
app:layout_constraintEnd_toStartOf="@+id/uploadsFileActionDownload"
app:layout_constraintStart_toEndOf="@+id/uploadsFileIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Filename.file" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/uploadsFileSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/uploadsFileTitle"
app:layout_constraintStart_toStartOf="@+id/uploadsFileTitle"
app:layout_constraintTop_toBottomOf="@+id/uploadsFileTitle"
tools:text="Username at 12:00 on 01/01/01" />
<ImageView
android:id="@+id/uploadsFileActionDownload"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:scaleType="center"
android:src="@drawable/ic_download"
android:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/uploadsFileActionShare"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/uploadsFileActionShare"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:scaleType="center"
android:src="@drawable/ic_material_share"
android:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground">
<ImageView
android:id="@+id/uploadsImagePreview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="2dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground">
<ImageView
android:id="@+id/uploadsVideoPreview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="2dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_material_play_circle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>