From a95102a78f7a59e5604524f2fdadc5dbf0de6a76 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 13:24:42 +0200 Subject: [PATCH] Uploads: Use StateView for better Loading/Empty rendering --- .../vector/riotx/core/platform/StateView.kt | 29 +++------- .../uploads/files/RoomUploadsFilesFragment.kt | 49 ++++++++++++++--- .../uploads/files/UploadsFileController.kt | 55 +++---------------- .../uploads/media/RoomUploadsMediaFragment.kt | 53 ++++++++++++++---- .../uploads/media/UploadsMediaController.kt | 54 +++--------------- .../fragment_generic_state_view_recycler.xml | 13 +++++ vector/src/main/res/layout/view_state.xml | 1 + vector/src/main/res/values/strings.xml | 4 +- 8 files changed, 125 insertions(+), 133 deletions(-) create mode 100644 vector/src/main/res/layout/fragment_generic_state_view_recycler.xml diff --git a/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt b/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt index 4c5a987b4b..bc24874f9f 100755 --- a/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt @@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View import android.widget.FrameLayout +import androidx.core.view.isVisible import im.vector.riotx.R import kotlinx.android.synthetic.main.view_state.view.* @@ -31,6 +32,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? object Content : State() object Loading : State() data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State() + data class Error(val message: CharSequence? = null) : State() } @@ -59,34 +61,21 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } private fun update(newState: State) { + progressBar.isVisible = newState is State.Loading + errorView.isVisible = newState is State.Error + emptyView.isVisible = newState is State.Empty + contentView?.isVisible = newState is State.Content + when (newState) { - is State.Content -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.INVISIBLE - contentView?.visibility = View.VISIBLE - } - is State.Loading -> { - progressBar.visibility = View.VISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.INVISIBLE - contentView?.visibility = View.INVISIBLE - } + is State.Content -> Unit + is State.Loading -> Unit is State.Empty -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.VISIBLE emptyImageView.setImageDrawable(newState.image) emptyMessageView.text = newState.message emptyTitleView.text = newState.title - contentView?.visibility = View.INVISIBLE } is State.Error -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.VISIBLE - emptyView.visibility = View.INVISIBLE errorMessageView.text = newState.message - contentView?.visibility = View.INVISIBLE } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt index 63f9e5215e..bba7a40440 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -18,6 +18,10 @@ package im.vector.riotx.features.roomprofile.uploads.files import android.os.Bundle import android.view.View +import androidx.core.content.ContextCompat +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.room.uploads.UploadEvent @@ -25,31 +29,37 @@ import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.trackItemsVisibilityChange +import im.vector.riotx.core.platform.StateView 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 kotlinx.android.synthetic.main.fragment_generic_state_view_recycler.* import javax.inject.Inject class RoomUploadsFilesFragment @Inject constructor( private val controller: UploadsFileController -) : VectorBaseFragment(), UploadsFileController.Listener { +) : VectorBaseFragment(), + UploadsFileController.Listener, + StateView.EventCallback { private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) - override fun getLayoutResId() = R.layout.fragment_generic_recycler + override fun getLayoutResId() = R.layout.fragment_generic_state_view_recycler override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.trackItemsVisibilityChange() - recyclerView.configureWith(controller, showDivider = true) + genericStateViewListStateView.contentView = genericStateViewListRecycler + genericStateViewListStateView.eventCallback = this + + genericStateViewListRecycler.trackItemsVisibilityChange() + genericStateViewListRecycler.configureWith(controller, showDivider = true) controller.listener = this } override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericStateViewListRecycler.cleanup() controller.listener = null } @@ -58,7 +68,7 @@ class RoomUploadsFilesFragment @Inject constructor( uploadsViewModel.handle(RoomUploadsAction.Share(uploadEvent)) } - override fun onRetry() { + override fun onRetryClicked() { uploadsViewModel.handle(RoomUploadsAction.Retry) } @@ -75,6 +85,29 @@ class RoomUploadsFilesFragment @Inject constructor( } override fun invalidate() = withState(uploadsViewModel) { state -> - controller.setData(state) + if (state.fileEvents.isEmpty()) { + when (state.asyncEventsRequest) { + is Loading -> { + genericStateViewListStateView.state = StateView.State.Loading + } + is Fail -> { + genericStateViewListStateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncEventsRequest.error)) + } + is Success -> { + if (state.hasMore) { + // We need to load more items + loadMore() + } else { + genericStateViewListStateView.state = StateView.State.Empty( + title = getString(R.string.uploads_files_no_result), + image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_file) + ) + } + } + } + } else { + genericStateViewListStateView.state = StateView.State.Content + controller.setData(state) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt index 8c4dd1f300..2b355af8a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -18,28 +18,20 @@ package im.vector.riotx.features.roomprofile.uploads.files import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.VisibilityState -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.room.uploads.UploadEvent import im.vector.riotx.R import im.vector.riotx.core.date.VectorDateFormatter -import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem -import im.vector.riotx.core.epoxy.noResultItem -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState import javax.inject.Inject class UploadsFileController @Inject constructor( - private val errorFormatter: ErrorFormatter, private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter ) : TypedEpoxyController() { interface Listener { - fun onRetry() fun loadMore() fun onOpenClicked(uploadEvent: UploadEvent) fun onDownloadClicked(uploadEvent: UploadEvent) @@ -57,46 +49,15 @@ class UploadsFileController @Inject constructor( 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() } - } - } - is Success -> { - if (data.hasMore) { - // We need to load more items - listener?.loadMore() - loadingItem { - id("loading") - } - } else { - noResultItem { - id("noResult") - text(stringProvider.getString(R.string.uploads_files_no_result)) - } - } - } - } - } else { - buildFileItems(data.fileEvents) + buildFileItems(data.fileEvents) - if (data.hasMore) { - loadingItem { - // Always use a different id, because we can be notified several times of visibility state changed - id("loadMore${idx++}") - onVisibilityStateChanged { _, _, visibilityState -> - if (visibilityState == VisibilityState.VISIBLE) { - listener?.loadMore() - } + if (data.hasMore) { + loadingItem { + // Always use a different id, because we can be notified several times of visibility state changed + id("loadMore${idx++}") + onVisibilityStateChanged { _, _, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + listener?.loadMore() } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index a722c8281f..a4e6c61238 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -19,37 +19,47 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.os.Bundle import android.util.DisplayMetrics import android.view.View +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.GridLayoutManager +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success 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.extensions.trackItemsVisibilityChange +import im.vector.riotx.core.platform.StateView 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 kotlinx.android.synthetic.main.fragment_generic_state_view_recycler.* import javax.inject.Inject class RoomUploadsMediaFragment @Inject constructor( private val controller: UploadsMediaController, private val dimensionConverter: DimensionConverter -) : VectorBaseFragment(), UploadsMediaController.Listener { +) : VectorBaseFragment(), + UploadsMediaController.Listener, + StateView.EventCallback { private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) - override fun getLayoutResId() = R.layout.fragment_generic_recycler + override fun getLayoutResId() = R.layout.fragment_generic_state_view_recycler override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.trackItemsVisibilityChange() - recyclerView.layoutManager = GridLayoutManager(context, getNumberOfColumns()) - recyclerView.adapter = controller.adapter - recyclerView.setHasFixedSize(true) + genericStateViewListStateView.contentView = genericStateViewListRecycler + genericStateViewListStateView.eventCallback = this + + genericStateViewListRecycler.trackItemsVisibilityChange() + genericStateViewListRecycler.layoutManager = GridLayoutManager(context, getNumberOfColumns()) + genericStateViewListRecycler.adapter = controller.adapter + genericStateViewListRecycler.setHasFixedSize(true) controller.listener = this } @@ -62,7 +72,7 @@ class RoomUploadsMediaFragment @Inject constructor( override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericStateViewListRecycler.cleanup() controller.listener = null } @@ -78,11 +88,34 @@ class RoomUploadsMediaFragment @Inject constructor( uploadsViewModel.handle(RoomUploadsAction.LoadMore) } - override fun onRetry() { + override fun onRetryClicked() { uploadsViewModel.handle(RoomUploadsAction.Retry) } override fun invalidate() = withState(uploadsViewModel) { state -> - controller.setData(state) + if (state.mediaEvents.isEmpty()) { + when (state.asyncEventsRequest) { + is Loading -> { + genericStateViewListStateView.state = StateView.State.Loading + } + is Fail -> { + genericStateViewListStateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncEventsRequest.error)) + } + is Success -> { + if (state.hasMore) { + // We need to load more items + loadMore() + } else { + genericStateViewListStateView.state = StateView.State.Empty( + title = getString(R.string.uploads_media_no_result), + image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_image) + ) + } + } + } + } else { + genericStateViewListStateView.state = StateView.State.Content + controller.setData(state) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt index 1d797bec78..cd3e401dc5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -19,18 +19,12 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.view.View import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.VisibilityState -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageType 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.api.session.room.uploads.UploadEvent 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.noResultItem import im.vector.riotx.core.epoxy.squareLoadingItem import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.StringProvider @@ -48,7 +42,6 @@ class UploadsMediaController @Inject constructor( ) : TypedEpoxyController() { interface Listener { - fun onRetry() fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) fun loadMore() @@ -67,46 +60,15 @@ class UploadsMediaController @Inject constructor( 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() } - } - } - is Success -> { - if (data.hasMore) { - // We need to load more items - listener?.loadMore() - squareLoadingItem { - id("loading") - } - } else { - noResultItem { - id("noResult") - text(stringProvider.getString(R.string.uploads_media_no_result)) - } - } - } - } - } else { - buildMediaItems(data.mediaEvents) + buildMediaItems(data.mediaEvents) - if (data.hasMore) { - squareLoadingItem { - // Always use a different id, because we can be notified several times of visibility state changed - id("loadMore${idx++}") - onVisibilityStateChanged { _, _, visibilityState -> - if (visibilityState == VisibilityState.VISIBLE) { - listener?.loadMore() - } + if (data.hasMore) { + squareLoadingItem { + // Always use a different id, because we can be notified several times of visibility state changed + id("loadMore${idx++}") + onVisibilityStateChanged { _, _, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + listener?.loadMore() } } } diff --git a/vector/src/main/res/layout/fragment_generic_state_view_recycler.xml b/vector/src/main/res/layout/fragment_generic_state_view_recycler.xml new file mode 100644 index 0000000000..410373b97f --- /dev/null +++ b/vector/src/main/res/layout/fragment_generic_state_view_recycler.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/vector/src/main/res/layout/view_state.xml b/vector/src/main/res/layout/view_state.xml index c17e1b216b..082a0bb24c 100644 --- a/vector/src/main/res/layout/view_state.xml +++ b/vector/src/main/res/layout/view_state.xml @@ -73,6 +73,7 @@ android:layout_width="64dp" android:layout_height="64dp" android:layout_gravity="center_horizontal" + android:tint="?riotx_text_primary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b0ddce5ed3..36532f25e3 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1789,11 +1789,11 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Couldn\'t handle share data MEDIA - There is no media in this room + There are no media in this room FILES %1$s at %2$s - There is no files in this room + There are no files in this room "It's spam" "It's inappropriate"