Merge pull request #6413 from vector-im/feature/bma/room_member_loading

Show a loader if all the Room Member are not yet loaded.
This commit is contained in:
Benoit Marty 2022-06-30 17:13:50 +02:00 committed by GitHub
commit 58580f1e6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 155 additions and 22 deletions

1
changelog.d/6413.feature Normal file
View File

@ -0,0 +1 @@
Show a loader if all the Room Members are not yet loaded.

View File

@ -53,6 +53,13 @@ class FlowRoom(private val room: Room) {
} }
} }
fun liveAreAllMembersLoaded(): Flow<Boolean> {
return room.membershipService().areAllMembersLoadedLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.membershipService().areAllMembersLoaded()
}
}
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

@ -30,6 +30,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<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

@ -0,0 +1,55 @@
/*
* 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
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.extensions.orFalse
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 javax.inject.Inject
internal class RoomDataSource @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
) {
fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType {
var result: RoomMembersLoadStatusType?
Realm.getInstance(monarchy.realmConfiguration).use {
result = RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus
}
return result ?: RoomMembersLoadStatusType.NONE
}
fun getRoomMembersLoadStatusLive(roomId: String): LiveData<Boolean> {
val liveData = monarchy.findAllMappedWithChanges(
{
RoomEntity.where(it, roomId)
},
{
it.membersLoadStatus == RoomMembersLoadStatusType.LOADED
}
)
return Transformations.map(liveData) { results ->
results.firstOrNull().orFalse()
}
}
}

View File

@ -31,10 +31,12 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
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.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.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
import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.query.process
import org.matrix.android.sdk.internal.session.room.RoomDataSource
import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask
import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask
import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask
@ -47,6 +49,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 roomDataSource: RoomDataSource,
@UserId @UserId
private val userId: String, private val userId: String,
private val queryStringValueProcessor: QueryStringValueProcessor private val queryStringValueProcessor: QueryStringValueProcessor
@ -62,6 +65,15 @@ internal class DefaultMembershipService @AssistedInject constructor(
loadRoomMembersTask.execute(params) loadRoomMembersTask.execute(params)
} }
override suspend fun areAllMembersLoaded(): Boolean {
val status = roomDataSource.getRoomMembersLoadStatus(roomId)
return status == RoomMembersLoadStatusType.LOADED
}
override fun areAllMembersLoadedLive(): LiveData<Boolean> {
return roomDataSource.getRoomMembersLoadStatusLive(roomId)
}
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

@ -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
@ -38,6 +37,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.RoomDataSource
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
@ -58,6 +58,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 roomDataSource: RoomDataSource,
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomMemberEventHandler: RoomMemberEventHandler, private val roomMemberEventHandler: RoomMemberEventHandler,
@ -68,7 +69,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 (roomDataSource.getRoomMembersLoadStatus(params.roomId)) {
RoomMembersLoadStatusType.NONE -> doRequest(params) RoomMembersLoadStatusType.NONE -> doRequest(params)
RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params) RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params)
RoomMembersLoadStatusType.LOADED -> Unit RoomMembersLoadStatusType.LOADED -> Unit
@ -142,14 +143,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,19 @@ class RoomMemberListViewModel @AssistedInject constructor(
copy(roomMemberSummaries = async) copy(roomMemberSummaries = async)
} }
roomFlow.liveAreAllMembersLoaded()
.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 +151,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 +159,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"
@ -43,4 +61,4 @@
</com.google.android.material.appbar.AppBarLayout> </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> </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