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>> {
return room.relationService().getEventAnnotationsSummaryLive(eventId).asFlow()
.startWith(room.coroutineDispatchers.io) {

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.room.members
import androidx.lifecycle.LiveData
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.util.Optional
/**
* This interface defines methods to handling membership. It's implemented at the room level.
@ -30,6 +31,20 @@ interface MembershipService {
*/
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.
* @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.StartLiveLocationShareTask
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.GetRoomMembersLoadStatusTask
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.MembershipAdminTask
@ -227,6 +229,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask
@Binds
abstract fun bindGetRoomMembersLoadStatusTask(task: DefaultGetRoomMembersLoadStatusTask): GetRoomMembersLoadStatusTask
@Binds
abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.membership
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
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.model.Membership
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.model.RoomEntity
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.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.UserId
import org.matrix.android.sdk.internal.query.QueryStringValueProcessor
@ -47,6 +53,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
private val inviteTask: InviteTask,
private val inviteThreePidTask: InviteThreePidTask,
private val membershipAdminTask: MembershipAdminTask,
private val getRoomMembersLoadStatusTask: GetRoomMembersLoadStatusTask,
@UserId
private val userId: String,
private val queryStringValueProcessor: QueryStringValueProcessor
@ -62,6 +69,26 @@ internal class DefaultMembershipService @AssistedInject constructor(
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? {
val roomMemberEntity = monarchy.fetchCopied {
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
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.kotlin.createObject
import kotlinx.coroutines.TimeoutCancellationException
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(
private val roomAPI: RoomAPI,
@SessionDatabase private val monarchy: Monarchy,
private val getRoomMembersLoadStatusTask: GetRoomMembersLoadStatusTask,
private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomMemberEventHandler: RoomMemberEventHandler,
@ -67,7 +67,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
) : LoadRoomMembersTask {
override suspend fun execute(params: LoadRoomMembersTask.Params) {
when (getRoomMembersLoadStatus(params.roomId)) {
when (getRoomMembersLoadStatusTask.execute(GetRoomMembersLoadStatusTask.Params(params.roomId))) {
RoomMembersLoadStatusType.NONE -> doRequest(params)
RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params)
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) {
monarchy.awaitTransaction { realm ->
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.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.args
@ -114,6 +115,7 @@ class RoomMemberListFragment @Inject constructor(
}
override fun invalidate() = withState(viewModel) { viewState ->
views.roomSettingGeneric.progressBar.isGone = viewState.areAllMembersLoaded
roomMemberListController.setData(viewState)
renderRoomSummary(viewState)
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 kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@ -66,6 +67,7 @@ class RoomMemberListViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<RoomMemberListViewModel, RoomMemberListViewState> by hiltMavericksViewModelFactory()
private val room = session.getRoom(initialState.roomId)!!
private val roomFlow = room.flow()
init {
observeRoomMemberSummaries()
@ -82,8 +84,8 @@ class RoomMemberListViewModel @AssistedInject constructor(
}
combine(
room.flow().liveRoomMembers(roomMemberQueryParams),
room.flow()
roomFlow.liveRoomMembers(roomMemberQueryParams),
roomFlow
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap()
@ -94,8 +96,20 @@ class RoomMemberListViewModel @AssistedInject constructor(
copy(roomMemberSummaries = async)
}
roomFlow.liveAreAllMembersLoaded()
.unwrap()
.distinctUntilChanged()
.onEach {
setState {
copy(
areAllMembersLoaded = it
)
}
}
.launchIn(viewModelScope)
if (room.roomCryptoService().isEncrypted()) {
room.flow().liveRoomMembers(roomMemberQueryParams)
roomFlow.liveRoomMembers(roomMemberQueryParams)
.flatMapLatest { membersSummary ->
session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId })
.asFlow()
@ -138,7 +152,7 @@ class RoomMemberListViewModel @AssistedInject constructor(
}
private fun observeRoomSummary() {
room.flow().liveRoomSummary()
roomFlow.liveRoomSummary()
.unwrap()
.execute { async ->
copy(roomSummary = async)
@ -146,7 +160,7 @@ class RoomMemberListViewModel @AssistedInject constructor(
}
private fun observeThirdPartyInvites() {
room.flow()
roomFlow
.liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE), QueryStringValue.IsNotNull)
.execute { async ->
copy(threePidInvites = async)

View File

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

View File

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

View File

@ -27,8 +27,26 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
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
android:id="@+id/memberNameFilter"
@ -43,4 +61,4 @@
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -113,6 +113,25 @@
</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>
<include