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..2b6a2fea50 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().toOptional() + } + } + 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..2e158352ec 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 @@ -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> + /** * 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/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 271e82a1e0..d7ae610c3c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -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 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 005d7f26db..650d6c1d65 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 @@ -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> { + 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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/GetRoomMembersLoadStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/GetRoomMembersLoadStatusTask.kt new file mode 100644 index 0000000000..24db0ad21b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/GetRoomMembersLoadStatusTask.kt @@ -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 { + 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 + } +} 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 ee8a2170bb..0ad7c56d1b 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 @@ -57,6 +56,7 @@ internal interface LoadRoomMembersTask : Task 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) 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..7a81066482 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,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) 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 @@ + + + + + + +