empty state for new invites screen

This commit is contained in:
NIkita Fedrunov 2022-09-01 14:39:13 +02:00
parent 3ef3e3760d
commit 5003459962
8 changed files with 117 additions and 34 deletions

1
changelog.d/6876.feature Normal file
View file

@ -0,0 +1 @@
[App Layout] - Invites now show empty screen after you reject last invite

View file

@ -20,15 +20,19 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.paging.PagedList
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentInvitesBinding import im.vector.app.databinding.FragmentInvitesBinding
import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import javax.inject.Inject import javax.inject.Inject
@ -51,6 +55,8 @@ class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListLi
setupToolbar(views.invitesToolbar) setupToolbar(views.invitesToolbar)
.allowBack() .allowBack()
views.invitesStateView.contentView = views.invitesRecycler
views.invitesRecycler.configureWith(controller) views.invitesRecycler.configureWith(controller)
controller.listener = this controller.listener = this
@ -62,13 +68,32 @@ class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListLi
when (it) { when (it) {
is InvitesViewEvents.Failure -> showFailure(it.throwable) is InvitesViewEvents.Failure -> showFailure(it.throwable)
is InvitesViewEvents.OpenRoom -> handleOpenRoom(it.roomSummary, it.shouldCloseInviteView) is InvitesViewEvents.OpenRoom -> handleOpenRoom(it.roomSummary, it.shouldCloseInviteView)
InvitesViewEvents.Close -> handleClose()
} }
} }
}
private fun handleClose() { viewModel.invites.onEach {
requireActivity().finish() when (it) {
is InvitesContentState.Content -> {
views.invitesStateView.state = StateView.State.Content
Suppress("UNCHECKED_CAST")
controller.submitList(it.content as? PagedList<RoomSummary>)
}
is InvitesContentState.Empty -> {
views.invitesStateView.state = StateView.State.Empty(
title = it.title,
image = it.image,
message = it.message
)
}
is InvitesContentState.Error -> {
when (views.invitesStateView.state) {
StateView.State.Content -> showErrorInSnackbar(it.throwable)
else -> views.invitesStateView.state = StateView.State.Error(it.throwable.message)
}
}
InvitesContentState.Loading -> views.invitesStateView.state = StateView.State.Loading
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
} }
private fun handleOpenRoom(roomSummary: RoomSummary, shouldCloseInviteView: Boolean) { private fun handleOpenRoom(roomSummary: RoomSummary, shouldCloseInviteView: Boolean) {
@ -83,14 +108,6 @@ class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListLi
} }
} }
override fun invalidate(): Unit = withState(viewModel) { state ->
super.invalidate()
state.pagedList?.observe(viewLifecycleOwner) { list ->
controller.submitList(list)
}
}
override fun onRejectRoomInvitation(room: RoomSummary) { override fun onRejectRoomInvitation(room: RoomSummary) {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) } notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
viewModel.handle(InvitesAction.RejectInvitation(room)) viewModel.handle(InvitesAction.RejectInvitation(room))

View file

@ -22,5 +22,4 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
sealed class InvitesViewEvents : VectorViewEvents { sealed class InvitesViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : InvitesViewEvents() data class Failure(val throwable: Throwable) : InvitesViewEvents()
data class OpenRoom(val roomSummary: RoomSummary, val shouldCloseInviteView: Boolean) : InvitesViewEvents() data class OpenRoom(val roomSummary: RoomSummary, val shouldCloseInviteView: Boolean) : InvitesViewEvents()
object Close : InvitesViewEvents()
} }

View file

@ -16,14 +16,25 @@
package im.vector.app.features.home.room.list.home.invites package im.vector.app.features.home.room.list.home.invites
import androidx.lifecycle.asFlow
import androidx.paging.PagedList import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -36,6 +47,8 @@ import timber.log.Timber
class InvitesViewModel @AssistedInject constructor( class InvitesViewModel @AssistedInject constructor(
@Assisted val initialState: InvitesViewState, @Assisted val initialState: InvitesViewState,
private val session: Session, private val session: Session,
private val stringProvider: StringProvider,
private val drawableProvider: DrawableProvider
) : VectorViewModel<InvitesViewState, InvitesAction, InvitesViewEvents>(initialState) { ) : VectorViewModel<InvitesViewState, InvitesAction, InvitesViewEvents>(initialState) {
private val pagedListConfig = PagedList.Config.Builder() private val pagedListConfig = PagedList.Config.Builder()
@ -52,6 +65,11 @@ class InvitesViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<InvitesViewModel, InvitesViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<InvitesViewModel, InvitesViewState> by hiltMavericksViewModelFactory()
private val _invites = MutableSharedFlow<InvitesContentState>(replay = 1)
val invites = _invites.asSharedFlow()
var invitesCount = -1
init { init {
observeInvites() observeInvites()
} }
@ -72,8 +90,6 @@ class InvitesViewModel @AssistedInject constructor(
return@withState return@withState
} }
val shouldCloseInviteView = state.pagedList?.value?.size == 1
viewModelScope.launch { viewModelScope.launch {
try { try {
session.roomService().leaveRoom(roomId) session.roomService().leaveRoom(roomId)
@ -81,9 +97,6 @@ class InvitesViewModel @AssistedInject constructor(
// Instead, we wait for the room to be rejected // Instead, we wait for the room to be rejected
// Known bug: if the user is invited again (after rejecting the first invitation), the loading will be displayed instead of the buttons. // Known bug: if the user is invited again (after rejecting the first invitation), the loading will be displayed instead of the buttons.
// If we update the state, the button will be displayed again, so it's not ideal... // If we update the state, the button will be displayed again, so it's not ideal...
if (shouldCloseInviteView) {
_viewEvents.post(InvitesViewEvents.Close)
}
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Notify the user // Notify the user
_viewEvents.post(InvitesViewEvents.Failure(failure)) _viewEvents.post(InvitesViewEvents.Failure(failure))
@ -101,9 +114,7 @@ class InvitesViewModel @AssistedInject constructor(
} }
// close invites view when navigate to a room from the last one invite // close invites view when navigate to a room from the last one invite
val shouldCloseInviteView = state.pagedList?.value?.size == 1 val shouldCloseInviteView = invitesCount == 1
_viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView))
// quick echo // quick echo
setState { setState {
@ -117,6 +128,8 @@ class InvitesViewModel @AssistedInject constructor(
} }
) )
} }
_viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView))
} }
private fun observeInvites() { private fun observeInvites() {
@ -129,8 +142,26 @@ class InvitesViewModel @AssistedInject constructor(
sortOrder = RoomSortOrder.ACTIVITY sortOrder = RoomSortOrder.ACTIVITY
) )
setState { pagedList.asFlow()
copy(pagedList = pagedList) .map {
} if (it.isEmpty()) {
InvitesContentState.Empty(
title = stringProvider.getString(R.string.invites_empty_title),
image = drawableProvider.getDrawable(R.drawable.ic_invites_empty),
message = stringProvider.getString(R.string.invites_empty_message)
)
} else {
invitesCount = it.loadedCount
InvitesContentState.Content(it)
}
}
.catch {
emit(InvitesContentState.Error(it))
}
.onStart {
emit(InvitesContentState.Loading)
}.onEach {
_invites.emit(it)
}.launchIn(viewModelScope)
} }
} }

View file

@ -16,13 +16,24 @@
package im.vector.app.features.home.room.list.home.invites package im.vector.app.features.home.room.list.home.invites
import androidx.lifecycle.LiveData import android.graphics.drawable.Drawable
import androidx.paging.PagedList import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class InvitesViewState( data class InvitesViewState(
val pagedList: LiveData<PagedList<RoomSummary>>? = null,
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(), val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
) : MavericksState ) : MavericksState
sealed class InvitesContentState {
object Loading : InvitesContentState()
data class Empty(
val title: CharSequence,
val image: Drawable?,
val message: CharSequence
) : InvitesContentState()
data class Content(val content: PagedList<RoomSummary>) : InvitesContentState()
data class Error(val throwable: Throwable) : InvitesContentState()
}

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="60dp"
android:height="60dp"
android:viewportWidth="60"
android:viewportHeight="60">
<path
android:pathData="M30,30m-30,0a30,30 0,1 1,60 0a30,30 0,1 1,-60 0"
android:fillColor="#E3E8F0"/>
<path
android:pathData="M25.665,33.544L15.229,23.209L29.236,13.398C29.993,12.868 31.007,12.868 31.764,13.398L45.771,23.209L35.247,33.631L33.851,32.446C31.93,30.816 29.11,30.778 27.145,32.355L25.665,33.544ZM22.439,36.134L14,42.91V27.777L22.439,36.134ZM47,27.777V43.606L38.393,36.301L47,27.777ZM31.177,35.566L43.47,46H16.714L29.733,35.546C30.156,35.208 30.765,35.216 31.177,35.566Z"
android:strokeWidth="2"
android:fillColor="#737D8C"
android:strokeColor="#737D8C"/>
</vector>

View file

@ -20,17 +20,24 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <im.vector.app.core.platform.StateView
android:id="@+id/invites_recycler" android:id="@+id/invites_state_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" /> app:layout_constraintTop_toBottomOf="@id/appBarLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/invites_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical" />
</im.vector.app.core.platform.StateView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -445,6 +445,9 @@
<!-- Invites fragment --> <!-- Invites fragment -->
<string name="invites_title">Invites</string> <string name="invites_title">Invites</string>
<string name="invites_empty_title">Nothing new.</string>
<string name="invites_empty_message">This is where your new requests and invites will be.</string>
<!-- People fragment --> <!-- People fragment -->
<string name="direct_chats_header">Conversations</string> <string name="direct_chats_header">Conversations</string>
<string name="matrix_only_filter">Matrix contacts only</string> <string name="matrix_only_filter">Matrix contacts only</string>