Show a loader if all the Room Member are not yet loaded.

This commit is contained in:
Benoit Marty 2022-06-29 12:19:17 +02:00
parent 1a986e7437
commit e91be2b599
12 changed files with 165 additions and 22 deletions

View File

@ -53,6 +53,13 @@ class FlowRoom(private val room: Room) {
} }
} }
fun liveAreAllMembersLoaded(): Flow<Optional<Boolean>> {
return room.membershipService().areAllMembersLoadedLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.membershipService().areAllMembersLoaded().toOptional()
}
}
fun liveAnnotationSummary(eventId: String): Flow<Optional<EventAnnotationsSummary>> { fun liveAnnotationSummary(eventId: String): Flow<Optional<EventAnnotationsSummary>> {
return room.relationService().getEventAnnotationsSummaryLive(eventId).asFlow() return room.relationService().getEventAnnotationsSummaryLive(eventId).asFlow()
.startWith(room.coroutineDispatchers.io) { .startWith(room.coroutineDispatchers.io) {

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.room.members
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.util.Optional
/** /**
* This interface defines methods to handling membership. It's implemented at the room level. * This interface defines methods to handling membership. It's implemented at the room level.
@ -30,6 +31,20 @@ interface MembershipService {
*/ */
suspend fun loadRoomMembersIfNeeded() suspend fun loadRoomMembersIfNeeded()
/**
* All the room members can be not loaded, for instance after an initial sync.
* All the members will be loaded when calling [loadRoomMembersIfNeeded], or when sending an encrypted
* event to the room.
* The fun let the app know if all the members have been loaded for this room.
* @return true if all the members are loaded, or false elsewhere.
*/
suspend fun areAllMembersLoaded(): Boolean
/**
* Live version for [areAllMembersLoaded]
*/
fun areAllMembersLoadedLive(): LiveData<Optional<Boolean>>
/** /**
* Return the roomMember with userId or null. * Return the roomMember with userId or null.
* @param userId the userId param to look for * @param userId the userId param to look for

View File

@ -59,7 +59,9 @@ import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTas
import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.StopLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.StopLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.membership.DefaultGetRoomMembersLoadStatusTask
import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.membership.GetRoomMembersLoadStatusTask
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask
import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask
@ -227,6 +229,9 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask
@Binds
abstract fun bindGetRoomMembersLoadStatusTask(task: DefaultGetRoomMembersLoadStatusTask): GetRoomMembersLoadStatusTask
@Binds @Binds
abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.membership package org.matrix.android.sdk.internal.session.room.membership
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -28,9 +29,14 @@ import org.matrix.android.sdk.api.session.room.members.MembershipService
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.query.QueryStringValueProcessor import org.matrix.android.sdk.internal.query.QueryStringValueProcessor
@ -47,6 +53,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
private val inviteTask: InviteTask, private val inviteTask: InviteTask,
private val inviteThreePidTask: InviteThreePidTask, private val inviteThreePidTask: InviteThreePidTask,
private val membershipAdminTask: MembershipAdminTask, private val membershipAdminTask: MembershipAdminTask,
private val getRoomMembersLoadStatusTask: GetRoomMembersLoadStatusTask,
@UserId @UserId
private val userId: String, private val userId: String,
private val queryStringValueProcessor: QueryStringValueProcessor private val queryStringValueProcessor: QueryStringValueProcessor
@ -62,6 +69,26 @@ internal class DefaultMembershipService @AssistedInject constructor(
loadRoomMembersTask.execute(params) loadRoomMembersTask.execute(params)
} }
override suspend fun areAllMembersLoaded(): Boolean {
val status = getRoomMembersLoadStatusTask.execute(GetRoomMembersLoadStatusTask.Params(roomId))
return status == RoomMembersLoadStatusType.LOADED
}
override fun areAllMembersLoadedLive(): LiveData<Optional<Boolean>> {
val liveData = monarchy.findAllMappedWithChanges(
{
RoomEntity.where(it, roomId)
},
{
it.membersLoadStatus == RoomMembersLoadStatusType.LOADED
}
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
}
override fun getRoomMember(userId: String): RoomMemberSummary? { override fun getRoomMember(userId: String): RoomMemberSummary? {
val roomMemberEntity = monarchy.fetchCopied { val roomMemberEntity = monarchy.fetchCopied {
RoomMemberHelper(it, roomId).getLastRoomMember(userId) RoomMemberHelper(it, roomId).getLastRoomMember(userId)

View File

@ -0,0 +1,45 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.membership
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface GetRoomMembersLoadStatusTask : Task<GetRoomMembersLoadStatusTask.Params, RoomMembersLoadStatusType> {
data class Params(
val roomId: String,
)
}
internal class DefaultGetRoomMembersLoadStatusTask @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
) : GetRoomMembersLoadStatusTask {
override suspend fun execute(params: GetRoomMembersLoadStatusTask.Params): RoomMembersLoadStatusType {
var result: RoomMembersLoadStatusType?
Realm.getInstance(monarchy.realmConfiguration).use {
result = RoomEntity.where(it, params.roomId).findFirst()?.membersLoadStatus
}
return result ?: RoomMembersLoadStatusType.NONE
}
}

View File

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.session.room.membership package org.matrix.android.sdk.internal.session.room.membership
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
@ -57,6 +56,7 @@ internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Unit>
internal class DefaultLoadRoomMembersTask @Inject constructor( internal class DefaultLoadRoomMembersTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val getRoomMembersLoadStatusTask: GetRoomMembersLoadStatusTask,
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomMemberEventHandler: RoomMemberEventHandler, private val roomMemberEventHandler: RoomMemberEventHandler,
@ -67,7 +67,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
) : LoadRoomMembersTask { ) : LoadRoomMembersTask {
override suspend fun execute(params: LoadRoomMembersTask.Params) { override suspend fun execute(params: LoadRoomMembersTask.Params) {
when (getRoomMembersLoadStatus(params.roomId)) { when (getRoomMembersLoadStatusTask.execute(GetRoomMembersLoadStatusTask.Params(params.roomId))) {
RoomMembersLoadStatusType.NONE -> doRequest(params) RoomMembersLoadStatusType.NONE -> doRequest(params)
RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params) RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params)
RoomMembersLoadStatusType.LOADED -> Unit RoomMembersLoadStatusType.LOADED -> Unit
@ -136,14 +136,6 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
} }
} }
private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType {
var result: RoomMembersLoadStatusType?
Realm.getInstance(monarchy.realmConfiguration).use {
result = RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus
}
return result ?: RoomMembersLoadStatusType.NONE
}
private suspend fun setRoomMembersLoadStatus(roomId: String, status: RoomMembersLoadStatusType) { private suspend fun setRoomMembersLoadStatus(roomId: String, status: RoomMembersLoadStatusType) {
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)

View File

@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
@ -114,6 +115,7 @@ class RoomMemberListFragment @Inject constructor(
} }
override fun invalidate() = withState(viewModel) { viewState -> override fun invalidate() = withState(viewModel) { viewState ->
views.roomSettingGeneric.progressBar.isGone = viewState.areAllMembersLoaded
roomMemberListController.setData(viewState) roomMemberListController.setData(viewState)
renderRoomSummary(viewState) renderRoomSummary(viewState)
views.inviteUsersButton.isVisible = viewState.actionsPermissions.canInvite views.inviteUsersButton.isVisible = viewState.actionsPermissions.canInvite

View File

@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -66,6 +67,7 @@ class RoomMemberListViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<RoomMemberListViewModel, RoomMemberListViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<RoomMemberListViewModel, RoomMemberListViewState> by hiltMavericksViewModelFactory()
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
private val roomFlow = room.flow()
init { init {
observeRoomMemberSummaries() observeRoomMemberSummaries()
@ -82,8 +84,8 @@ class RoomMemberListViewModel @AssistedInject constructor(
} }
combine( combine(
room.flow().liveRoomMembers(roomMemberQueryParams), roomFlow.liveRoomMembers(roomMemberQueryParams),
room.flow() roomFlow
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
.mapOptional { it.content.toModel<PowerLevelsContent>() } .mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap() .unwrap()
@ -94,8 +96,20 @@ class RoomMemberListViewModel @AssistedInject constructor(
copy(roomMemberSummaries = async) copy(roomMemberSummaries = async)
} }
roomFlow.liveAreAllMembersLoaded()
.unwrap()
.distinctUntilChanged()
.onEach {
setState {
copy(
areAllMembersLoaded = it
)
}
}
.launchIn(viewModelScope)
if (room.roomCryptoService().isEncrypted()) { if (room.roomCryptoService().isEncrypted()) {
room.flow().liveRoomMembers(roomMemberQueryParams) roomFlow.liveRoomMembers(roomMemberQueryParams)
.flatMapLatest { membersSummary -> .flatMapLatest { membersSummary ->
session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId }) session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId })
.asFlow() .asFlow()
@ -138,7 +152,7 @@ class RoomMemberListViewModel @AssistedInject constructor(
} }
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.flow().liveRoomSummary() roomFlow.liveRoomSummary()
.unwrap() .unwrap()
.execute { async -> .execute { async ->
copy(roomSummary = async) copy(roomSummary = async)
@ -146,7 +160,7 @@ class RoomMemberListViewModel @AssistedInject constructor(
} }
private fun observeThirdPartyInvites() { private fun observeThirdPartyInvites() {
room.flow() roomFlow
.liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE), QueryStringValue.IsNotNull) .liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE), QueryStringValue.IsNotNull)
.execute { async -> .execute { async ->
copy(threePidInvites = async) copy(threePidInvites = async)

View File

@ -32,6 +32,7 @@ data class RoomMemberListViewState(
val roomId: String, val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized, val roomSummary: Async<RoomSummary> = Uninitialized,
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized, val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
val areAllMembersLoaded: Boolean = false,
val ignoredUserIds: List<String> = emptyList(), val ignoredUserIds: List<String> = emptyList(),
val filter: String = "", val filter: String = "",
val threePidInvites: Async<List<Event>> = Uninitialized, val threePidInvites: Async<List<Event>> = Uninitialized,

View File

@ -20,6 +20,7 @@ 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.core.view.isGone
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
@ -32,8 +33,6 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.databinding.FragmentRecyclerviewWithSearchBinding import im.vector.app.databinding.FragmentRecyclerviewWithSearchBinding
import im.vector.app.features.roomprofile.members.RoomMemberListAction import im.vector.app.features.roomprofile.members.RoomMemberListAction
import im.vector.app.features.roomprofile.members.RoomMemberListViewModel import im.vector.app.features.roomprofile.members.RoomMemberListViewModel
@ -45,8 +44,6 @@ import reactivecircus.flowbinding.appcompat.queryTextChanges
import javax.inject.Inject import javax.inject.Inject
class SpacePeopleFragment @Inject constructor( class SpacePeopleFragment @Inject constructor(
private val drawableProvider: DrawableProvider,
private val colorProvider: ColorProvider,
private val epoxyController: SpacePeopleListController private val epoxyController: SpacePeopleListController
) : VectorBaseFragment<FragmentRecyclerviewWithSearchBinding>(), ) : VectorBaseFragment<FragmentRecyclerviewWithSearchBinding>(),
OnBackPressed, SpacePeopleListController.InteractionListener { OnBackPressed, SpacePeopleListController.InteractionListener {
@ -64,6 +61,7 @@ class SpacePeopleFragment @Inject constructor(
} }
override fun invalidate() = withState(membersViewModel) { memberListState -> override fun invalidate() = withState(membersViewModel) { memberListState ->
views.progressBar.isGone = memberListState.areAllMembersLoaded
val memberCount = (memberListState.roomSummary.invoke()?.otherMemberIds?.size ?: 0) + 1 val memberCount = (memberListState.roomSummary.invoke()?.otherMemberIds?.size ?: 0) + 1
toolbar?.subtitle = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) toolbar?.subtitle = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)

View File

@ -27,8 +27,26 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:minHeight="0dp" android:minHeight="0dp"
app:title="@string/bottom_action_people" app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"/> app:title="@string/bottom_action_people" />
<!-- Trick to remove surrounding padding (clip from wrapping frame) -->
<FrameLayout
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="3dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
tools:visibility="visible">
<ProgressBar
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
<androidx.appcompat.widget.SearchView <androidx.appcompat.widget.SearchView
android:id="@+id/memberNameFilter" android:id="@+id/memberNameFilter"

View File

@ -113,6 +113,25 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<!-- Trick to remove surrounding padding (clip from wrapping frame) -->
<FrameLayout
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="3dp"
android:elevation="8dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
tools:visibility="visible">
<ProgressBar
style="@style/Widget.Vector.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<include <include