mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 01:15:54 +03:00
Uploads: Use StateView for better Loading/Empty rendering
This commit is contained in:
parent
2adafbeb03
commit
a95102a78f
8 changed files with 125 additions and 133 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<RoomUploadsViewState>() {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<RoomUploadsViewState>() {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<im.vector.riotx.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/genericStateViewListStateView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/genericStateViewListRecycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:overScrollMode="always" />
|
||||
|
||||
</im.vector.riotx.core.platform.StateView>
|
|
@ -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"
|
||||
|
|
|
@ -1789,11 +1789,11 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
<string name="error_handling_incoming_share">Couldn\'t handle share data</string>
|
||||
|
||||
<string name="uploads_media_title">MEDIA</string>
|
||||
<string name="uploads_media_no_result">There is no media in this room</string>
|
||||
<string name="uploads_media_no_result">There are no media in this room</string>
|
||||
<string name="uploads_files_title">FILES</string>
|
||||
<!-- First parameter is a username and second is a date Example: "Matthew at 12:00 on 01/01/01" -->
|
||||
<string name="uploads_files_subtitle">%1$s at %2$s</string>
|
||||
<string name="uploads_files_no_result">There is no files in this room</string>
|
||||
<string name="uploads_files_no_result">There are no files in this room</string>
|
||||
|
||||
<string name="report_content_spam">"It's spam"</string>
|
||||
<string name="report_content_inappropriate">"It's inappropriate"</string>
|
||||
|
|
Loading…
Reference in a new issue