Merge pull request #6598 from vector-im/task/eric/space-switching-unit-tests

Space Switching Refactoring and Unit Tests
This commit is contained in:
Eric Decanini 2022-07-29 16:21:33 +02:00 committed by GitHub
commit ddaf1128ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 395 additions and 108 deletions

1
changelog.d/6598.misc Normal file
View file

@ -0,0 +1 @@
Refactors SpaceStateHandler (previously AppStateHandler) and adds unit tests for it

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 im.vector.app
import androidx.lifecycle.DefaultLifecycleObserver
import arrow.core.Option
import kotlinx.coroutines.flow.Flow
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.RoomSummary
/**
* Gets info about the current space the user has navigated to, any space backstack they may have
* and handles switching to different spaces
*/
interface SpaceStateHandler : DefaultLifecycleObserver {
/**
* Gets the current space the current user has navigated to
*
* @return null if the user is not in
*/
fun getCurrentSpace(): RoomSummary?
/**
* Sets the new space the current user is navigating to
*
* @param spaceId the id of the space being navigated to
* @param session the current active session
* @param persistNow if true, the current space will immediately be persisted in shared prefs
* @param isForwardNavigation whether this navigation is a forward action to properly handle backstack
*/
fun setCurrentSpace(
spaceId: String?,
session: Session? = null,
persistNow: Boolean = false,
isForwardNavigation: Boolean = true,
)
/**
* Gets the current backstack of spaces (via their id)
*
* null may be an entry in the ArrayDeque to indicate the root space (All Chats)
*/
fun getSpaceBackstack(): ArrayDeque<String?>
/**
* Gets a flow of the selected space for clients to react immediately to space changes
*/
fun getSelectedSpaceFlow(): Flow<Option<RoomSummary>>
/**
* Gets the id of the active space, or null if there is none
*/
fun getSafeActiveSpaceId(): String?
}

View file

@ -16,7 +16,6 @@
package im.vector.app
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import arrow.core.Option
import im.vector.app.core.di.ActiveSessionHolder
@ -46,54 +45,60 @@ import javax.inject.Singleton
/**
* This class handles the global app state.
* It requires to be added to ProcessLifecycleOwner.get().lifecycle
* It is required that this class is added as an observer to ProcessLifecycleOwner.get().lifecycle in [VectorApplication]
*/
// TODO Keep this class for now, will maybe be used fro Space
@Singleton
class AppStateHandler @Inject constructor(
class SpaceStateHandlerImpl @Inject constructor(
private val sessionDataSource: ActiveSessionDataSource,
private val uiStateRepository: UiStateRepository,
private val activeSessionHolder: ActiveSessionHolder,
private val analyticsTracker: AnalyticsTracker
) : DefaultLifecycleObserver {
) : SpaceStateHandler {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomSummary>>(Option.empty())
val selectedSpaceFlow = selectedSpaceDataSource.stream()
private val selectedSpaceFlow = selectedSpaceDataSource.stream()
private val spaceBackstack = ArrayDeque<String?>()
fun getCurrentSpace(): RoomSummary? {
override fun getCurrentSpace(): RoomSummary? {
return selectedSpaceDataSource.currentValue?.orNull()?.let { spaceSummary ->
activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(spaceSummary.roomId)
}
}
fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false, isForwardNavigation: Boolean = true) {
override fun setCurrentSpace(
spaceId: String?,
session: Session?,
persistNow: Boolean,
isForwardNavigation: Boolean,
) {
val activeSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
val currentSpace = selectedSpaceDataSource.currentValue?.orNull()
val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
if (currentSpace != null && spaceId == currentSpace.roomId) return
val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) }
val spaceSummary = spaceId?.let { activeSession.getRoomSummary(spaceId) }
val sameSpaceSelected = currentSpace != null && spaceId == currentSpace.roomId
if (sameSpaceSelected) {
return
}
if (isForwardNavigation) {
spaceBackstack.addLast(currentSpace?.roomId)
}
if (persistNow) {
uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId)
uiStateRepository.storeSelectedSpace(spaceSummary?.roomId, activeSession.sessionId)
}
if (spaceSum == null) {
if (spaceSummary == null) {
selectedSpaceDataSource.post(Option.empty())
} else {
selectedSpaceDataSource.post(Option.just(spaceSum))
selectedSpaceDataSource.post(Option.just(spaceSummary))
}
if (spaceId != null) {
uSession.coroutineScope.launch(Dispatchers.IO) {
activeSession.coroutineScope.launch(Dispatchers.IO) {
tryOrNull {
uSession.getRoom(spaceId)?.membershipService()?.loadRoomMembersIfNeeded()
activeSession.getRoom(spaceId)?.membershipService()?.loadRoomMembersIfNeeded()
}
}
}
@ -122,9 +127,11 @@ class AppStateHandler @Inject constructor(
}.launchIn(session.coroutineScope)
}
fun getSpaceBackstack() = spaceBackstack
override fun getSpaceBackstack() = spaceBackstack
fun safeActiveSpaceId(): String? {
override fun getSelectedSpaceFlow() = selectedSpaceFlow
override fun getSafeActiveSpaceId(): String? {
return selectedSpaceDataSource.currentValue?.orNull()?.roomId
}

View file

@ -88,7 +88,7 @@ class VectorApplication :
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var pinLocker: PinLocker
@Inject lateinit var callManager: WebRtcCallManager
@ -177,7 +177,7 @@ class VectorApplication :
fcmHelper.onEnterBackground(activeSessionHolder)
}
})
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
ProcessLifecycleOwner.get().lifecycle.addObserver(spaceStateHandler)
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
ProcessLifecycleOwner.get().lifecycle.addObserver(callManager)
// This should be done as early as possible

View file

@ -31,6 +31,8 @@ import dagger.hilt.components.SingletonComponent
import im.vector.app.BuildConfig
import im.vector.app.EmojiCompatWrapper
import im.vector.app.EmojiSpanify
import im.vector.app.SpaceStateHandler
import im.vector.app.SpaceStateHandlerImpl
import im.vector.app.config.analyticsConfig
import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.error.DefaultErrorFormatter
@ -108,6 +110,9 @@ abstract class VectorBindModule {
@Binds
abstract fun bindSystemSettingsProvide(provider: AndroidSystemSettingsProvider): SystemSettingsProvider
@Binds
abstract fun bindSpaceStateHandler(spaceStateHandlerImpl: SpaceStateHandlerImpl): SpaceStateHandler
}
@InstallIn(SingletonComponent::class)

View file

@ -35,8 +35,8 @@ import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult
@ -130,7 +130,7 @@ class HomeActivity :
@Inject lateinit var permalinkHandler: PermalinkHandler
@Inject lateinit var avatarRenderer: AvatarRenderer
@Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter
@Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy

View file

@ -28,8 +28,8 @@ import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.badge.BadgeDrawable
import im.vector.app.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.platform.OnBackPressed
@ -68,7 +68,7 @@ class HomeDetailFragment @Inject constructor(
private val alertManager: PopupAlertManager,
private val callManager: WebRtcCallManager,
private val vectorPreferences: VectorPreferences,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val vectorFeatures: VectorFeatures,
) : VectorBaseFragment<FragmentHomeDetailBinding>(),
KeysBackupBanner.Delegate,
@ -186,13 +186,13 @@ class HomeDetailFragment @Inject constructor(
}
private fun navigateBack() {
val previousSpaceId = appStateHandler.getSpaceBackstack().removeLastOrNull()
val parentSpaceId = appStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
val previousSpaceId = spaceStateHandler.getSpaceBackstack().removeLastOrNull()
val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
setCurrentSpace(previousSpaceId ?: parentSpaceId)
}
private fun setCurrentSpace(spaceId: String?) {
appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
spaceStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace)
}
@ -215,7 +215,7 @@ class HomeDetailFragment @Inject constructor(
}
private fun refreshSpaceState() {
appStateHandler.getCurrentSpace()?.let {
spaceStateHandler.getCurrentSpace()?.let {
onSpaceChange(it)
}
}
@ -473,7 +473,7 @@ class HomeDetailFragment @Inject constructor(
return this
}
override fun onBackPressed(toolbarButton: Boolean) = if (appStateHandler.getCurrentSpace() != null) {
override fun onBackPressed(toolbarButton: Boolean) = if (spaceStateHandler.getCurrentSpace() != null) {
navigateBack()
true
} else {

View file

@ -22,7 +22,7 @@ import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.singletonEntryPoint
@ -68,7 +68,7 @@ class HomeDetailViewModel @AssistedInject constructor(
private val vectorDataStore: VectorDataStore,
private val callManager: WebRtcCallManager,
private val directRoomHelper: DirectRoomHelper,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val autoAcceptInvites: AutoAcceptInvites,
private val vectorOverrides: VectorOverrides
) : VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState),
@ -206,7 +206,7 @@ class HomeDetailViewModel @AssistedInject constructor(
}
private fun observeRoomGroupingMethod() {
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.setOnEach {
copy(
selectedSpace = it.orNull()
@ -215,7 +215,7 @@ class HomeDetailViewModel @AssistedInject constructor(
}
private fun observeRoomSummaries() {
appStateHandler.selectedSpaceFlow.distinctUntilChanged().flatMapLatest {
spaceStateHandler.getSelectedSpaceFlow().distinctUntilChanged().flatMapLatest {
// we use it as a trigger to all changes in room, but do not really load
// the actual models
session.roomService().getPagedRoomSummariesLive(
@ -227,7 +227,7 @@ class HomeDetailViewModel @AssistedInject constructor(
}
.throttleFirst(300)
.onEach {
val activeSpaceRoomId = appStateHandler.getCurrentSpace()?.roomId
val activeSpaceRoomId = spaceStateHandler.getCurrentSpace()?.roomId
var dmInvites = 0
var roomsInvite = 0
if (autoAcceptInvites.showInvites()) {

View file

@ -22,7 +22,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction
@ -58,7 +58,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
@Assisted initialState: UnreadMessagesState,
session: Session,
private val vectorPreferences: VectorPreferences,
appStateHandler: AppStateHandler,
spaceStateHandler: SpaceStateHandler,
private val autoAcceptInvites: AutoAcceptInvites
) :
VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) {
@ -109,8 +109,8 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
}
combine(
appStateHandler.selectedSpaceFlow.distinctUntilChanged(),
appStateHandler.selectedSpaceFlow.flatMapLatest {
spaceStateHandler.getSelectedSpaceFlow().distinctUntilChanged(),
spaceStateHandler.getSelectedSpaceFlow().flatMapLatest {
roomService.getPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = Membership.activeMemberships()
@ -162,10 +162,10 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
CountInfo(
homeCount = counts,
otherCount = RoomAggregateNotificationCount(
notificationCount = rootCounts.fold(0, { acc, rs -> acc + rs.notificationCount }) +
notificationCount = rootCounts.fold(0) { acc, rs -> acc + rs.notificationCount } +
(counts.notificationCount.takeIf { selectedSpace != null } ?: 0) +
spaceInviteCount,
highlightCount = rootCounts.fold(0, { acc, rs -> acc + rs.highlightCount }) +
highlightCount = rootCounts.fold(0) { acc, rs -> acc + rs.highlightCount } +
(counts.highlightCount.takeIf { selectedSpace != null } ?: 0) +
spaceInviteCount
)

View file

@ -27,8 +27,8 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.mvrx.runCatchingToAsync
@ -138,7 +138,7 @@ class TimelineViewModel @AssistedInject constructor(
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase,
timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler,
spaceStateHandler: SpaceStateHandler,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
@ -220,16 +220,16 @@ class TimelineViewModel @AssistedInject constructor(
if (initialState.switchToParentSpace) {
// We are coming from a notification, try to switch to the most relevant space
// so that when hitting back the room will appear in the list
appStateHandler.getCurrentSpace().let { currentSpace ->
spaceStateHandler.getCurrentSpace().let { currentSpace ->
val currentRoomSummary = room.roomSummary() ?: return@let
// nothing we are good
if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) ||
(currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) {
// take first one or switch to home
appStateHandler.setCurrentSpace(
spaceStateHandler.setCurrentSpace(
currentRoomSummary
.flattenParentIds.firstOrNull { it.isNotBlank() },
// force persist, because if not on resume the AppStateHandler will resume
// force persist, because if not on resume the SpaceStateHandler will resume
// the current space from what was persisted on enter background
persistNow = true
)

View file

@ -23,8 +23,8 @@ import androidx.lifecycle.asFlow
import androidx.lifecycle.liveData
import androidx.paging.PagedList
import com.airbnb.mvrx.Async
import im.vector.app.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.invite.AutoAcceptInvites
@ -59,7 +59,7 @@ import timber.log.Timber
class RoomListSectionBuilder(
private val session: Session,
private val stringProvider: StringProvider,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val viewModelScope: CoroutineScope,
private val autoAcceptInvites: AutoAcceptInvites,
private val onUpdatable: (UpdatableLivePageResult) -> Unit,
@ -95,7 +95,7 @@ class RoomListSectionBuilder(
}
}
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.onEach { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull()
@ -150,7 +150,7 @@ class RoomListSectionBuilder(
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, false)
it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = false, isServerNotice = false)
}
addSection(
@ -187,7 +187,7 @@ class RoomListSectionBuilder(
// add suggested rooms
val suggestedRoomsFlow = // MutableLiveData<List<SpaceChildInfo>>()
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.flatMapLatest { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull()
@ -271,7 +271,7 @@ class RoomListSectionBuilder(
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, null)
it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = false, isServerNotice = null)
}
addSection(
@ -283,7 +283,7 @@ class RoomListSectionBuilder(
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, true, null)
it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = true, isServerNotice = null)
}
}
@ -360,7 +360,7 @@ class RoomListSectionBuilder(
query: (RoomSummaryQueryParams.Builder) -> Unit
) {
withQueryParams(query) { roomQueryParams ->
val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.getSafeActiveSpaceId())
val liveQueryParams = MutableStateFlow(updatedQueryParams)
val itemCountFlow = liveQueryParams
.flatMapLatest {
@ -371,7 +371,7 @@ class RoomListSectionBuilder(
val name = stringProvider.getString(nameRes)
val filteredPagedRoomSummariesLive = session.roomService().getFilteredPagedRoomSummariesLive(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.getSafeActiveSpaceId()),
pagedListConfig
)
when (spaceFilterStrategy) {
@ -418,7 +418,7 @@ class RoomListSectionBuilder(
RoomAggregateNotificationCount(it.size, it.size)
} else {
session.roomService().getNotificationCountForRooms(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.getSafeActiveSpaceId())
)
}
)

View file

@ -25,7 +25,7 @@ import com.airbnb.mvrx.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
@ -60,7 +60,7 @@ class RoomListViewModel @AssistedInject constructor(
@Assisted initialState: RoomListViewState,
private val session: Session,
stringProvider: StringProvider,
appStateHandler: AppStateHandler,
spaceStateHandler: SpaceStateHandler,
vectorPreferences: VectorPreferences,
autoAcceptInvites: AutoAcceptInvites,
private val analyticsTracker: AnalyticsTracker
@ -100,7 +100,7 @@ class RoomListViewModel @AssistedInject constructor(
observeMembershipChanges()
observeLocalRooms()
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.execute {
copy(
@ -148,7 +148,7 @@ class RoomListViewModel @AssistedInject constructor(
private val roomListSectionBuilder = RoomListSectionBuilder(
session,
stringProvider,
appStateHandler,
spaceStateHandler,
viewModelScope,
autoAcceptInvites,
{

View file

@ -22,7 +22,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.StateView
@ -50,7 +50,7 @@ import org.matrix.android.sdk.api.session.room.state.isPublic
class HomeRoomListViewModel @AssistedInject constructor(
@Assisted initialState: HomeRoomListViewState,
private val session: Session,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val vectorPreferences: VectorPreferences,
) : VectorViewModel<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) {
@ -99,10 +99,10 @@ class HomeRoomListViewModel @AssistedInject constructor(
pagedListConfig
)
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.onStart {
emit(appStateHandler.getCurrentSpace().toOption())
emit(spaceStateHandler.getCurrentSpace().toOption())
}
.onEach { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull()

View file

@ -31,8 +31,8 @@ import androidx.core.app.TaskStackBuilder
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.error.fatalError
import im.vector.app.features.VectorFeatures
@ -120,7 +120,7 @@ class DefaultNavigator @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val vectorPreferences: VectorPreferences,
private val widgetArgsBuilder: WidgetArgsBuilder,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val features: VectorFeatures,
private val analyticsTracker: AnalyticsTracker
@ -167,7 +167,7 @@ class DefaultNavigator @Inject constructor(
analyticsTracker.capture(
sessionHolder.getActiveSession().getRoomSummary(roomId).toAnalyticsViewRoom(
trigger = trigger,
selectedSpace = appStateHandler.getCurrentSpace()
selectedSpace = spaceStateHandler.getCurrentSpace()
)
)
}
@ -182,7 +182,7 @@ class DefaultNavigator @Inject constructor(
fatalError("Trying to open an unknown space $spaceId", vectorPreferences.failFast())
return
}
appStateHandler.setCurrentSpace(spaceId)
spaceStateHandler.setCurrentSpace(spaceId)
when (postSwitchSpaceAction) {
Navigator.PostSwitchSpaceAction.None -> {
// go back to home if we are showing room details?
@ -318,7 +318,7 @@ class DefaultNavigator @Inject constructor(
}
override fun openRoomDirectory(context: Context, initialFilter: String) {
when (val currentSpace = appStateHandler.getCurrentSpace()) {
when (val currentSpace = spaceStateHandler.getCurrentSpace()) {
null -> RoomDirectoryActivity.getIntent(context, initialFilter)
else -> SpaceExploreActivity.newIntent(context, currentSpace.roomId)
}.start(context)
@ -330,14 +330,14 @@ class DefaultNavigator @Inject constructor(
}
override fun openCreateDirectRoom(context: Context) {
when (val currentSpace = appStateHandler.getCurrentSpace()) {
when (val currentSpace = spaceStateHandler.getCurrentSpace()) {
null -> CreateDirectRoomActivity.getIntent(context)
else -> SpacePeopleActivity.newIntent(context, currentSpace.roomId)
}.start(context)
}
override fun openInviteUsersToRoom(context: Context, roomId: String) {
when (val currentSpace = appStateHandler.getCurrentSpace()) {
when (val currentSpace = spaceStateHandler.getCurrentSpace()) {
null -> InviteUsersToRoomActivity.getIntent(context, roomId).start(context)
else -> showInviteToDialog(context, currentSpace, roomId)
}

View file

@ -25,7 +25,7 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
@ -58,7 +58,7 @@ class CreateRoomViewModel @AssistedInject constructor(
@Assisted private val initialState: CreateRoomViewState,
private val session: Session,
private val rawService: RawService,
appStateHandler: AppStateHandler,
spaceStateHandler: SpaceStateHandler,
private val analyticsTracker: AnalyticsTracker
) : VectorViewModel<CreateRoomViewState, CreateRoomAction, CreateRoomViewEvents>(initialState) {
@ -73,7 +73,7 @@ class CreateRoomViewModel @AssistedInject constructor(
initHomeServerName()
initAdminE2eByDefault()
val parentSpaceId = initialState.parentSpaceId ?: appStateHandler.safeActiveSpaceId()
val parentSpaceId = initialState.parentSpaceId ?: spaceStateHandler.getSafeActiveSpaceId()
val restrictedSupport = session.homeServerCapabilitiesService().getHomeServerCapabilities()
.isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED)

View file

@ -23,7 +23,7 @@ import com.airbnb.mvrx.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
@ -61,7 +61,7 @@ import org.matrix.android.sdk.flow.flow
class SpaceListViewModel @AssistedInject constructor(
@Assisted initialState: SpaceListViewState,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val session: Session,
private val vectorPreferences: VectorPreferences,
private val autoAcceptInvites: AutoAcceptInvites,
@ -85,8 +85,7 @@ class SpaceListViewModel @AssistedInject constructor(
}
observeSpaceSummaries()
// observeSelectionState()
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.setOnEach { selectedSpaceOption ->
copy(
@ -219,7 +218,7 @@ class SpaceListViewModel @AssistedInject constructor(
if (state.selectedSpace?.roomId != action.spaceSummary?.roomId) {
analyticsTracker.capture(Interaction(null, null, Interaction.Name.SpacePanelSwitchSpace))
setState { copy(selectedSpace = action.spaceSummary) }
appStateHandler.setCurrentSpace(action.spaceSummary?.roomId)
spaceStateHandler.setCurrentSpace(action.spaceSummary?.roomId)
_viewEvents.post(SpaceListViewEvents.CloseDrawer)
} else {
analyticsTracker.capture(Interaction(null, null, Interaction.Name.SpacePanelSelectedSpace))

View file

@ -24,7 +24,7 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
@ -50,7 +50,7 @@ import timber.log.Timber
class SpaceMenuViewModel @AssistedInject constructor(
@Assisted val initialState: SpaceMenuState,
val session: Session,
val appStateHandler: AppStateHandler
val spaceStateHandler: SpaceStateHandler
) : VectorViewModel<SpaceMenuState, SpaceLeaveViewAction, EmptyViewEvents>(initialState) {
@AssistedFactory
@ -73,9 +73,9 @@ class SpaceMenuViewModel @AssistedInject constructor(
it.getOrNull()?.let {
if (it.membership == Membership.LEAVE) {
setState { copy(leavingState = Success(Unit)) }
if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) {
if (spaceStateHandler.getSafeActiveSpaceId() == initialState.spaceId) {
// switch to home?
appStateHandler.setCurrentSpace(null, session)
spaceStateHandler.setCurrentSpace(null, session)
}
}
}

View file

@ -24,7 +24,7 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
@ -53,7 +53,7 @@ import timber.log.Timber
class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
@Assisted val initialState: SpaceLeaveAdvanceViewState,
private val session: Session,
private val appStateHandler: AppStateHandler
private val spaceStateHandler: SpaceStateHandler
) : VectorViewModel<SpaceLeaveAdvanceViewState, SpaceLeaveAdvanceViewAction, EmptyViewEvents>(initialState) {
init {
@ -75,19 +75,19 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
}
setState { copy(spaceSummary = spaceSummary) }
session.getRoom(initialState.spaceId)?.let { room ->
room.flow().liveRoomSummary()
.unwrap()
.onEach {
session.getRoom(initialState.spaceId)
?.flow()
?.liveRoomSummary()
?.unwrap()
?.onEach {
if (it.membership == Membership.LEAVE) {
setState { copy(leaveState = Success(Unit)) }
if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) {
if (spaceStateHandler.getSafeActiveSpaceId() == initialState.spaceId) {
// switch to home?
appStateHandler.setCurrentSpace(null, session)
spaceStateHandler.setCurrentSpace(null, session)
}
}
}.launchIn(viewModelScope)
}
}?.launchIn(viewModelScope)
viewModelScope.launch {
val children = session.roomService().getRoomSummaries(

View file

@ -0,0 +1,133 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 im.vector.app
import im.vector.app.test.fakes.FakeActiveSessionDataSource
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeAnalyticsTracker
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeUiStateRepository
import im.vector.app.test.fixtures.RoomSummaryFixture.aRoomSummary
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
internal class SpaceStateHandlerImplTest {
private val spaceId = "spaceId"
private val spaceSummary = aRoomSummary(spaceId)
private val session = FakeSession.withRoomSummary(spaceSummary)
private val sessionDataSource = FakeActiveSessionDataSource()
private val uiStateRepository = FakeUiStateRepository()
private val activeSessionHolder = FakeActiveSessionHolder(session)
private val analyticsTracker = FakeAnalyticsTracker()
private val spaceStateHandler = SpaceStateHandlerImpl(
sessionDataSource.instance,
uiStateRepository,
activeSessionHolder.instance,
analyticsTracker,
)
@Test
fun `given selected space doesn't exist, when getCurrentSpace, then return null`() {
val currentSpace = spaceStateHandler.getCurrentSpace()
currentSpace shouldBe null
}
@Test
fun `given selected space exists, when getCurrentSpace, then return selected space`() {
spaceStateHandler.setCurrentSpace(spaceId, session)
val currentSpace = spaceStateHandler.getCurrentSpace()
currentSpace shouldBe spaceSummary
}
@Test
fun `given persistNow is true, when setCurrentSpace, then immediately persist to ui state`() {
spaceStateHandler.setCurrentSpace(spaceId, session, persistNow = true)
uiStateRepository.verifyStoreSelectedSpace(spaceId, session)
}
@Test
fun `given persistNow is false, when setCurrentSpace, then don't immediately persist to ui state`() {
spaceStateHandler.setCurrentSpace(spaceId, session, persistNow = false)
uiStateRepository.verifyStoreSelectedSpace(spaceId, session, inverse = true)
}
@Test
fun `given is forward navigation and no current space, when setCurrentSpace, then null added to backstack`() {
spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = true)
val backstack = spaceStateHandler.getSpaceBackstack()
backstack.size shouldBe 1
backstack.first() shouldBe null
}
@Test
fun `given is forward navigation and is in space, when setCurrentSpace, then previous space added to backstack`() {
spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = true)
spaceStateHandler.setCurrentSpace("secondSpaceId", session, isForwardNavigation = true)
val backstack = spaceStateHandler.getSpaceBackstack()
backstack.size shouldBe 2
backstack shouldBeEqualTo listOf(null, spaceId)
}
@Test
fun `given is not forward navigation, when setCurrentSpace, then previous space not added to backstack`() {
spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = false)
val backstack = spaceStateHandler.getSpaceBackstack()
backstack.size shouldBe 0
}
@Test
fun `when setCurrentSpace, then space is emitted to selectedSpaceFlow`() = runTest {
spaceStateHandler.setCurrentSpace(spaceId, session)
val currentSpace = spaceStateHandler.getSelectedSpaceFlow().first().orNull()
currentSpace shouldBeEqualTo spaceSummary
}
@Test
fun `given current space exists, when getSafeActiveSpaceId, then return current space id`() {
spaceStateHandler.setCurrentSpace(spaceId, session)
val activeSpaceId = spaceStateHandler.getSafeActiveSpaceId()
activeSpaceId shouldBeEqualTo spaceId
}
@Test
fun `given current space doesn't exist, when getSafeActiveSpaceId, then return current null`() {
val activeSpaceId = spaceStateHandler.getSafeActiveSpaceId()
activeSpaceId shouldBe null
}
}

View file

@ -23,11 +23,14 @@ import im.vector.app.features.session.VectorSessionStore
import im.vector.app.test.testCoroutineDispatchers
import io.mockk.coEvery
import io.mockk.coJustRun
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class FakeSession(
val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
@ -67,4 +70,11 @@ class FakeSession(
this@FakeSession.startSyncing(any())
}
}
companion object {
fun withRoomSummary(roomSummary: RoomSummary) = FakeSession().apply {
every { getRoomSummary(any()) } returns roomSummary
}
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 im.vector.app.test.fakes
import im.vector.app.features.ui.UiStateRepository
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.verify
import org.matrix.android.sdk.api.session.Session
class FakeUiStateRepository : UiStateRepository by mockk() {
init {
justRun { storeSelectedSpace(any(), any()) }
}
fun verifyStoreSelectedSpace(roomId: String, session: Session, inverse: Boolean = false) {
verify(inverse = inverse) { storeSelectedSpace(roomId, session.sessionId) }
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 im.vector.app.test.fixtures
import org.matrix.android.sdk.api.session.room.model.RoomSummary
object RoomSummaryFixture {
fun aRoomSummary(roomId: String) = RoomSummary(
roomId,
isEncrypted = false,
encryptionEventTs = 0,
typingUsers = emptyList(),
)
}