Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/6876.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[App Layout] - Invites now show empty screen after you reject last invite
3 changes: 3 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,9 @@
<!-- Invites fragment -->
<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 -->
<string name="direct_chats_header">Conversations</string>
<string name="matrix_only_filter">Matrix contacts only</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
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.databinding.FragmentInvitesBinding
import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.room.list.RoomListListener
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.SpaceChildInfo
import javax.inject.Inject
Expand All @@ -51,6 +54,8 @@ class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListLi
setupToolbar(views.invitesToolbar)
.allowBack()

views.invitesStateView.contentView = views.invitesRecycler

views.invitesRecycler.configureWith(controller)
controller.listener = this

Expand All @@ -62,13 +67,31 @@ class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListLi
when (it) {
is InvitesViewEvents.Failure -> showFailure(it.throwable)
is InvitesViewEvents.OpenRoom -> handleOpenRoom(it.roomSummary, it.shouldCloseInviteView)
InvitesViewEvents.Close -> handleClose()
}
}
}

private fun handleClose() {
requireActivity().finish()
viewModel.invites.onEach {
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) {
Expand All @@ -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) {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
viewModel.handle(InvitesAction.RejectInvitation(room))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,4 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
sealed class InvitesViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : InvitesViewEvents()
data class OpenRoom(val roomSummary: RoomSummary, val shouldCloseInviteView: Boolean) : InvitesViewEvents()
object Close : InvitesViewEvents()
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,25 @@

package im.vector.app.features.home.room.list.home.invites

import androidx.lifecycle.asFlow
import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
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 org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
Expand All @@ -36,6 +47,8 @@ import timber.log.Timber
class InvitesViewModel @AssistedInject constructor(
@Assisted val initialState: InvitesViewState,
private val session: Session,
private val stringProvider: StringProvider,
private val drawableProvider: DrawableProvider
) : VectorViewModel<InvitesViewState, InvitesAction, InvitesViewEvents>(initialState) {

private val pagedListConfig = PagedList.Config.Builder()
Expand All @@ -52,6 +65,11 @@ class InvitesViewModel @AssistedInject constructor(

companion object : MavericksViewModelFactory<InvitesViewModel, InvitesViewState> by hiltMavericksViewModelFactory()

private val _invites = MutableSharedFlow<InvitesContentState>(replay = 1)
val invites = _invites.asSharedFlow()

private var invitesCount = -1

init {
observeInvites()
}
Expand All @@ -72,18 +90,13 @@ class InvitesViewModel @AssistedInject constructor(
return@withState
}

val shouldCloseInviteView = state.pagedList?.value?.size == 1

viewModelScope.launch {
try {
session.roomService().leaveRoom(roomId)
// We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data.
// 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.
// 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) {
// Notify the user
_viewEvents.post(InvitesViewEvents.Failure(failure))
Expand All @@ -101,9 +114,7 @@ class InvitesViewModel @AssistedInject constructor(
}
// close invites view when navigate to a room from the last one invite

val shouldCloseInviteView = state.pagedList?.value?.size == 1

_viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView))
val shouldCloseInviteView = invitesCount == 1

// quick echo
setState {
Expand All @@ -117,6 +128,8 @@ class InvitesViewModel @AssistedInject constructor(
}
)
}

_viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView))
}

private fun observeInvites() {
Expand All @@ -129,8 +142,26 @@ class InvitesViewModel @AssistedInject constructor(
sortOrder = RoomSortOrder.ACTIVITY
)

setState {
copy(pagedList = pagedList)
}
pagedList.asFlow()
.map {
if (it.isEmpty()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of interest, is an empty result only returned after the pagedList has finished loading?

or is it possible for it to emit an empty state instantly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

up to my tests it emits empty list only when there are no invites

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will work since we will test only if this is equal to 1, but I think it's more correct to use it.size()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it.size() will also return placeholders (if there are any), while here we are interested in exact count without them

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from my understanding, the exact count is returned by size(). loadedCount does not include placeholders (= not loaded data, but which exist). If we have for instance 120 invites, there is maybe a risk that loadedCount return 0, or 1 (and so we have a bug), while size() returns 120, during the time the data are not yet loaded.
Anyway the risk is very low I think, and probably temporary, since more data will be loaded to be displayed.

InvitesContentState.Content(it)
}
}
.catch {
emit(InvitesContentState.Error(it))
}
.onStart {
emit(InvitesContentState.Loading)
}.onEach {
_invites.emit(it)
}.launchIn(viewModelScope)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@

package im.vector.app.features.home.room.list.home.invites

import androidx.lifecycle.LiveData
import android.graphics.drawable.Drawable
import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksState
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary

data class InvitesViewState(
val pagedList: LiveData<PagedList<RoomSummary>>? = null,
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
) : 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 changes: 14 additions & 0 deletions vector/src/main/res/drawable/ic_invites_empty.xml
Original file line number Diff line number Diff line change
@@ -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>
21 changes: 14 additions & 7 deletions vector/src/main/res/layout/fragment_invites.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,24 @@

</com.google.android.material.appbar.AppBarLayout>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/invites_recycler"
<im.vector.app.core.platform.StateView
android:id="@+id/invites_state_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To double check, is it OK to remove this layout_behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we don't need it for state view

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="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>