mirror of
https://github.com/element-hq/element-android
synced 2024-12-18 15:24:33 +03:00
Merge pull request #6986 from vector-im/feature/nfe/invites_empty_state
empty state for new invites screen
This commit is contained in:
commit
1c35e5ae9c
8 changed files with 115 additions and 34 deletions
1
changelog.d/6876.feature
Normal file
1
changelog.d/6876.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[App Layout] - Invites now show empty screen after you reject last invite
|
|
@ -451,6 +451,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>
|
||||||
|
|
|
@ -20,15 +20,18 @@ 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 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 +54,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 +67,31 @@ 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
|
||||||
|
controller.submitList(it.content)
|
||||||
|
}
|
||||||
|
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 +106,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))
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
private 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 interface 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
|
||||||
|
}
|
||||||
|
|
14
vector/src/main/res/drawable/ic_invites_empty.xml
Normal file
14
vector/src/main/res/drawable/ic_invites_empty.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue