diff --git a/changelog.d/6413.feature b/changelog.d/6413.feature new file mode 100644 index 0000000000..d1dba78fb9 --- /dev/null +++ b/changelog.d/6413.feature @@ -0,0 +1 @@ +Show a loader if all the Room Members are not yet loaded. diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 7ac81b2d86..8be8e83569 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -53,6 +53,13 @@ class FlowRoom(private val room: Room) { } } + fun liveAreAllMembersLoaded(): Flow { + return room.membershipService().areAllMembersLoadedLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.membershipService().areAllMembersLoaded() + } + } + fun liveAnnotationSummary(eventId: String): Flow> { return room.relationService().getEventAnnotationsSummaryLive(eventId).asFlow() .startWith(room.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt index e7ac69be74..144cfeb3b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt @@ -30,6 +30,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 + /** * Return the roomMember with userId or null. * @param userId the userId param to look for diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt new file mode 100644 index 0000000000..bcbc53f95e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt @@ -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 { + val liveData = monarchy.findAllMappedWithChanges( + { + RoomEntity.where(it, roomId) + }, + { + it.membersLoadStatus == RoomMembersLoadStatusType.LOADED + } + ) + + return Transformations.map(liveData) { results -> + results.firstOrNull().orFalse() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt index ef89ca33a7..ec140e7b17 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt @@ -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.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.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.query.QueryStringValueProcessor 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.joining.InviteTask 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 inviteThreePidTask: InviteThreePidTask, private val membershipAdminTask: MembershipAdminTask, + private val roomDataSource: RoomDataSource, @UserId private val userId: String, private val queryStringValueProcessor: QueryStringValueProcessor @@ -62,6 +65,15 @@ internal class DefaultMembershipService @AssistedInject constructor( loadRoomMembersTask.execute(params) } + override suspend fun areAllMembersLoaded(): Boolean { + val status = roomDataSource.getRoomMembersLoadStatus(roomId) + return status == RoomMembersLoadStatusType.LOADED + } + + override fun areAllMembersLoadedLive(): LiveData { + return roomDataSource.getRoomMembersLoadStatusLive(roomId) + } + override fun getRoomMember(userId: String): RoomMemberSummary? { val roomMemberEntity = monarchy.fetchCopied { RoomMemberHelper(it, roomId).getLastRoomMember(userId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index 7052eb23e2..c02049f40d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -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 @@ -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.executeRequest 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.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.Task @@ -58,6 +58,7 @@ internal interface LoadRoomMembersTask : Task internal class DefaultLoadRoomMembersTask @Inject constructor( private val roomAPI: RoomAPI, @SessionDatabase private val monarchy: Monarchy, + private val roomDataSource: RoomDataSource, private val syncTokenStore: SyncTokenStore, private val roomSummaryUpdater: RoomSummaryUpdater, private val roomMemberEventHandler: RoomMemberEventHandler, @@ -68,7 +69,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { - when (getRoomMembersLoadStatus(params.roomId)) { + when (roomDataSource.getRoomMembersLoadStatus(params.roomId)) { RoomMembersLoadStatusType.NONE -> doRequest(params) RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params) 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) { monarchy.awaitTransaction { realm -> val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index 951e3e1dcd..52a2339f13 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 3e6fb7b9d1..915ce51d91 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -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 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() } .unwrap() @@ -94,8 +96,19 @@ class RoomMemberListViewModel @AssistedInject constructor( copy(roomMemberSummaries = async) } + roomFlow.liveAreAllMembersLoaded() + .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 +151,7 @@ class RoomMemberListViewModel @AssistedInject constructor( } private fun observeRoomSummary() { - room.flow().liveRoomSummary() + roomFlow.liveRoomSummary() .unwrap() .execute { async -> copy(roomSummary = async) @@ -146,7 +159,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) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt index 47a89b523a..3cea47e60d 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt @@ -32,6 +32,7 @@ data class RoomMemberListViewState( val roomId: String, val roomSummary: Async = Uninitialized, val roomMemberSummaries: Async = Uninitialized, + val areAllMembersLoaded: Boolean = false, val ignoredUserIds: List = emptyList(), val filter: String = "", val threePidInvites: Async> = Uninitialized, diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt index 3f60166ba3..1181ccfa52 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt @@ -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(), 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) diff --git a/vector/src/main/res/layout/fragment_recyclerview_with_search.xml b/vector/src/main/res/layout/fragment_recyclerview_with_search.xml index 4f7e78971c..673ff98b70 100644 --- a/vector/src/main/res/layout/fragment_recyclerview_with_search.xml +++ b/vector/src/main/res/layout/fragment_recyclerview_with_search.xml @@ -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" /> + + + + + + + - \ No newline at end of file + diff --git a/vector/src/main/res/layout/fragment_room_setting_generic.xml b/vector/src/main/res/layout/fragment_room_setting_generic.xml index 887145faf5..b25313ca2d 100644 --- a/vector/src/main/res/layout/fragment_room_setting_generic.xml +++ b/vector/src/main/res/layout/fragment_room_setting_generic.xml @@ -113,6 +113,25 @@ + + + + + + +