Merge pull request #6877 from vector-im/feature/eric/new-layout-navigation

Space Switching Back Navigation
This commit is contained in:
Eric Decanini 2022-08-26 17:40:18 +02:00 committed by GitHub
commit b5debe92c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 333 additions and 79 deletions

1
changelog.d/6871.feature Normal file
View file

@ -0,0 +1 @@
Improves Developer Mode Debug Button UX and adds it to New App Layout

1
changelog.d/6877.wip Normal file
View file

@ -0,0 +1 @@
[New Layout] Adds back navigation through spaces

View file

@ -51,11 +51,13 @@ interface SpaceStateHandler : DefaultLifecycleObserver {
) )
/** /**
* Gets the current backstack of spaces (via their id). * Gets the Space ID of the space on top of the backstack.
* *
* null may be an entry in the ArrayDeque to indicate the root space (All Chats) * May return null to indicate the All Chats space.
*/ */
fun getSpaceBackstack(): ArrayDeque<String?> fun popSpaceBackstack(): String?
fun getSpaceBackstack(): List<String?>
/** /**
* Gets a flow of the selected space for clients to react immediately to space changes. * Gets a flow of the selected space for clients to react immediately to space changes.

View file

@ -23,6 +23,7 @@ import im.vector.app.core.utils.BehaviorDataSource
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -52,13 +53,13 @@ class SpaceStateHandlerImpl @Inject constructor(
private val sessionDataSource: ActiveSessionDataSource, private val sessionDataSource: ActiveSessionDataSource,
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val analyticsTracker: AnalyticsTracker private val analyticsTracker: AnalyticsTracker,
private val vectorPreferences: VectorPreferences,
) : SpaceStateHandler { ) : SpaceStateHandler {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomSummary>>(Option.empty()) private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomSummary>>(Option.empty())
private val selectedSpaceFlow = selectedSpaceDataSource.stream() private val selectedSpaceFlow = selectedSpaceDataSource.stream()
private val spaceBackstack = ArrayDeque<String?>()
override fun getCurrentSpace(): RoomSummary? { override fun getCurrentSpace(): RoomSummary? {
return selectedSpaceDataSource.currentValue?.orNull()?.let { spaceSummary -> return selectedSpaceDataSource.currentValue?.orNull()?.let { spaceSummary ->
@ -73,26 +74,26 @@ class SpaceStateHandlerImpl @Inject constructor(
isForwardNavigation: Boolean, isForwardNavigation: Boolean,
) { ) {
val activeSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return val activeSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
val currentSpace = selectedSpaceDataSource.currentValue?.orNull() val spaceToLeave = selectedSpaceDataSource.currentValue?.orNull()
val spaceSummary = spaceId?.let { activeSession.getRoomSummary(spaceId) } val spaceToSet = spaceId?.let { activeSession.getRoomSummary(spaceId) }
val sameSpaceSelected = currentSpace != null && spaceId == currentSpace.roomId val sameSpaceSelected = spaceId == spaceToLeave?.roomId
if (sameSpaceSelected) { if (sameSpaceSelected) {
return return
} }
if (isForwardNavigation) { if (isForwardNavigation) {
spaceBackstack.addLast(currentSpace?.roomId) addToBackstack(spaceToLeave, spaceToSet)
} }
if (persistNow) { if (persistNow) {
uiStateRepository.storeSelectedSpace(spaceSummary?.roomId, activeSession.sessionId) uiStateRepository.storeSelectedSpace(spaceToSet?.roomId, activeSession.sessionId)
} }
if (spaceSummary == null) { if (spaceToSet == null) {
selectedSpaceDataSource.post(Option.empty()) selectedSpaceDataSource.post(Option.empty())
} else { } else {
selectedSpaceDataSource.post(Option.just(spaceSummary)) selectedSpaceDataSource.post(Option.just(spaceToSet))
} }
if (spaceId != null) { if (spaceId != null) {
@ -104,6 +105,17 @@ class SpaceStateHandlerImpl @Inject constructor(
} }
} }
private fun addToBackstack(spaceToLeave: RoomSummary?, spaceToSet: RoomSummary?) {
// Only add to the backstack if the space to set is not All Chats, else clear the backstack
if (spaceToSet != null) {
val currentPersistedBackstack = vectorPreferences.getSpaceBackstack().toMutableList()
currentPersistedBackstack.add(spaceToLeave?.roomId)
vectorPreferences.setSpaceBackstack(currentPersistedBackstack)
} else {
vectorPreferences.setSpaceBackstack(emptyList())
}
}
private fun observeActiveSession() { private fun observeActiveSession() {
sessionDataSource.stream() sessionDataSource.stream()
.distinctUntilChanged() .distinctUntilChanged()
@ -127,7 +139,15 @@ class SpaceStateHandlerImpl @Inject constructor(
}.launchIn(session.coroutineScope) }.launchIn(session.coroutineScope)
} }
override fun getSpaceBackstack() = spaceBackstack override fun popSpaceBackstack(): String? {
vectorPreferences.getSpaceBackstack().toMutableList().apply {
val poppedSpaceId = removeLast()
vectorPreferences.setSpaceBackstack(this)
return poppedSpaceId
}
}
override fun getSpaceBackstack() = vectorPreferences.getSpaceBackstack()
override fun getSelectedSpaceFlow() = selectedSpaceFlow override fun getSelectedSpaceFlow() = selectedSpaceFlow

View file

@ -41,6 +41,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.extensions.restart
import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.extensions.validateBackPressed
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.platform.VectorMenuProvider
@ -56,6 +57,8 @@ import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.home.room.list.actions.RoomListSharedAction
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.matrixto.OriginOfMatrixTo import im.vector.app.features.matrixto.OriginOfMatrixTo
@ -110,6 +113,7 @@ class HomeActivity :
VectorMenuProvider { VectorMenuProvider {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private lateinit var roomListSharedActionViewModel: RoomListSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel() private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@ -136,6 +140,8 @@ class HomeActivity :
@Inject lateinit var fcmHelper: FcmHelper @Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy @Inject lateinit var nightlyProxy: NightlyProxy
private var isNewAppLayoutEnabled: Boolean = false // delete once old app layout is removed
private val createSpaceResultLauncher = registerStartForActivityResult { activityResult -> private val createSpaceResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) { if (activityResult.resultCode == Activity.RESULT_OK) {
val spaceId = SpaceCreationActivity.getCreatedSpaceId(activityResult.data) val spaceId = SpaceCreationActivity.getCreatedSpaceId(activityResult.data)
@ -155,8 +161,9 @@ class HomeActivity :
navigator.switchToSpace( navigator.switchToSpace(
context = this, context = this,
spaceId = spaceId, spaceId = spaceId,
postSwitchOption postSwitchOption,
) )
roomListSharedActionViewModel.post(RoomListSharedAction.CloseBottomSheet)
} }
} }
} }
@ -193,6 +200,7 @@ class HomeActivity :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
isNewAppLayoutEnabled = vectorFeatures.isNewAppLayoutEnabled()
analyticsScreenName = MobileScreen.ScreenName.Home analyticsScreenName = MobileScreen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
unifiedPushHelper.register(this) { unifiedPushHelper.register(this) {
@ -205,6 +213,7 @@ class HomeActivity :
} }
} }
sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java] sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java]
roomListSharedActionViewModel = viewModelProvider[RoomListSharedActionViewModel::class.java]
views.drawerLayout.addDrawerListener(drawerListener) views.drawerLayout.addDrawerListener(drawerListener)
if (isFirstCreation()) { if (isFirstCreation()) {
if (vectorFeatures.isNewAppLayoutEnabled()) { if (vectorFeatures.isNewAppLayoutEnabled()) {
@ -561,6 +570,14 @@ class HomeActivity :
// Check nightly // Check nightly
nightlyProxy.onHomeResumed() nightlyProxy.onHomeResumed()
checkNewAppLayoutFlagChange()
}
private fun checkNewAppLayoutFlagChange() {
if (buildMeta.isDebug && vectorFeatures.isNewAppLayoutEnabled() != isNewAppLayoutEnabled) {
restart()
}
} }
override fun getMenuRes() = if (vectorFeatures.isNewAppLayoutEnabled()) R.menu.menu_new_home else R.menu.menu_home override fun getMenuRes() = if (vectorFeatures.isNewAppLayoutEnabled()) R.menu.menu_new_home else R.menu.menu_home

View file

@ -186,7 +186,7 @@ class HomeDetailFragment :
} }
private fun navigateBack() { private fun navigateBack() {
val previousSpaceId = spaceStateHandler.getSpaceBackstack().removeLastOrNull() val previousSpaceId = spaceStateHandler.popSpaceBackstack()
val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull() val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
setCurrentSpace(previousSpaceId ?: parentSpaceId) setCurrentSpace(previousSpaceId ?: parentSpaceId)
} }

View file

@ -116,10 +116,14 @@ class HomeDrawerFragment :
} }
// Debug menu // Debug menu
views.homeDrawerHeaderDebugView.isVisible = buildMeta.isDebug && vectorPreferences.developerMode()
views.homeDrawerHeaderDebugView.debouncedClicks { views.homeDrawerHeaderDebugView.debouncedClicks {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openDebug(requireActivity()) navigator.openDebug(requireActivity())
} }
} }
override fun onResume() {
super.onResume()
views.homeDrawerHeaderDebugView.isVisible = buildMeta.isDebug && vectorPreferences.developerMode()
}
} }

View file

@ -23,10 +23,12 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.SpaceStateHandler import im.vector.app.SpaceStateHandler
@ -35,6 +37,7 @@ import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.CurrentCallsViewPresenter import im.vector.app.core.ui.views.CurrentCallsViewPresenter
@ -74,6 +77,7 @@ class NewHomeDetailFragment :
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var spaceStateHandler: SpaceStateHandler @Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var session: Session @Inject lateinit var session: Session
@Inject lateinit var buildMeta: BuildMeta
private val viewModel: HomeDetailViewModel by fragmentViewModel() private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
@ -127,6 +131,7 @@ class NewHomeDetailFragment :
setupToolbar() setupToolbar()
setupKeysBackupBanner() setupKeysBackupBanner()
setupActiveCallView() setupActiveCallView()
setupDebugButton()
childFragmentManager.commitTransaction { childFragmentManager.commitTransaction {
add(R.id.roomListContainer, HomeRoomListFragment::class.java, null, HOME_ROOM_LIST_FRAGMENT_TAG) add(R.id.roomListContainer, HomeRoomListFragment::class.java, null, HOME_ROOM_LIST_FRAGMENT_TAG)
@ -169,12 +174,6 @@ class NewHomeDetailFragment :
} }
} }
private fun navigateBack() {
val previousSpaceId = spaceStateHandler.getSpaceBackstack().removeLastOrNull()
val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
setCurrentSpace(previousSpaceId ?: parentSpaceId)
}
private fun setCurrentSpace(spaceId: String?) { private fun setCurrentSpace(spaceId: String?) {
spaceStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false) spaceStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace) sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace)
@ -189,6 +188,7 @@ class NewHomeDetailFragment :
super.onResume() super.onResume()
callManager.checkForProtocolsSupportIfNeeded() callManager.checkForProtocolsSupportIfNeeded()
refreshSpaceState() refreshSpaceState()
refreshDebugButtonState()
} }
private fun refreshSpaceState() { private fun refreshSpaceState() {
@ -298,6 +298,21 @@ class NewHomeDetailFragment :
} }
} }
private fun setupDebugButton() {
views.debugButton.debouncedClicks {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openDebug(requireActivity())
}
views.appBarLayout.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
views.debugButton.isVisible = verticalOffset == 0
})
}
private fun refreshDebugButtonState() {
views.debugButton.isVisible = buildMeta.isDebug && vectorPreferences.developerMode()
}
/* ========================================================================================== /* ==========================================================================================
* KeysBackupBanner Listener * KeysBackupBanner Listener
* ========================================================================================== */ * ========================================================================================== */
@ -337,13 +352,16 @@ class NewHomeDetailFragment :
} }
} }
override fun onBackPressed(toolbarButton: Boolean) = if (spaceStateHandler.getCurrentSpace() != null) { override fun onBackPressed(toolbarButton: Boolean) = if (spaceStateHandler.isRoot()) {
navigateBack()
true
} else {
false false
} else {
val lastSpace = spaceStateHandler.popSpaceBackstack()
spaceStateHandler.setCurrentSpace(lastSpace, isForwardNavigation = false)
true
} }
private fun SpaceStateHandler.isRoot() = getSpaceBackstack().isEmpty()
companion object { companion object {
private const val HOME_ROOM_LIST_FRAGMENT_TAG = "TAG_HOME_ROOM_LIST" private const val HOME_ROOM_LIST_FRAGMENT_TAG = "TAG_HOME_ROOM_LIST"
} }

View file

@ -176,13 +176,25 @@ class DefaultNavigator @Inject constructor(
startActivity(context, intent, buildTask) startActivity(context, intent, buildTask)
} }
override fun switchToSpace(context: Context, spaceId: String, postSwitchSpaceAction: Navigator.PostSwitchSpaceAction) { override fun switchToSpace(
context: Context,
spaceId: String,
postSwitchSpaceAction: Navigator.PostSwitchSpaceAction,
) {
if (sessionHolder.getSafeActiveSession()?.getRoomSummary(spaceId) == null) { if (sessionHolder.getSafeActiveSession()?.getRoomSummary(spaceId) == null) {
fatalError("Trying to open an unknown space $spaceId", vectorPreferences.failFast()) fatalError("Trying to open an unknown space $spaceId", vectorPreferences.failFast())
return return
} }
spaceStateHandler.setCurrentSpace(spaceId) spaceStateHandler.setCurrentSpace(spaceId)
when (postSwitchSpaceAction) { handlePostSwitchAction(context, spaceId, postSwitchSpaceAction)
}
private fun handlePostSwitchAction(
context: Context,
spaceId: String,
action: Navigator.PostSwitchSpaceAction,
) {
when (action) {
Navigator.PostSwitchSpaceAction.None -> { Navigator.PostSwitchSpaceAction.None -> {
// go back to home if we are showing room details? // go back to home if we are showing room details?
// This is a bit ugly, but the navigator is supposed to know about the activity stack // This is a bit ugly, but the navigator is supposed to know about the activity stack
@ -198,9 +210,9 @@ class DefaultNavigator @Inject constructor(
} }
is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> { is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> {
val args = TimelineArgs( val args = TimelineArgs(
postSwitchSpaceAction.roomId, action.roomId,
eventId = null, eventId = null,
openShareSpaceForId = spaceId.takeIf { postSwitchSpaceAction.showShareSheet } openShareSpaceForId = spaceId.takeIf { action.showShareSheet }
) )
val intent = RoomDetailActivity.newIntent(context, args, false) val intent = RoomDetailActivity.newIntent(context, args, false)
startActivity(context, intent, false) startActivity(context, intent, false)

View file

@ -68,7 +68,11 @@ interface Navigator {
data class OpenDefaultRoom(val roomId: String, val showShareSheet: Boolean) : PostSwitchSpaceAction() data class OpenDefaultRoom(val roomId: String, val showShareSheet: Boolean) : PostSwitchSpaceAction()
} }
fun switchToSpace(context: Context, spaceId: String, postSwitchSpaceAction: PostSwitchSpaceAction) fun switchToSpace(
context: Context,
spaceId: String,
postSwitchSpaceAction: PostSwitchSpaceAction,
)
fun openSpacePreview(context: Context, spaceId: String) fun openSpacePreview(context: Context, spaceId: String)

View file

@ -78,6 +78,7 @@ class VectorPreferences @Inject constructor(
const val SETTINGS_ALLOW_INTEGRATIONS_KEY = "SETTINGS_ALLOW_INTEGRATIONS_KEY" const val SETTINGS_ALLOW_INTEGRATIONS_KEY = "SETTINGS_ALLOW_INTEGRATIONS_KEY"
const val SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY = "SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY" const val SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY = "SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY"
const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY" const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY"
const val SETTINGS_PERSISTED_SPACE_BACKSTACK = "SETTINGS_PERSISTED_SPACE_BACKSTACK"
const val SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT = "SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT" const val SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT = "SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT"
// const val SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY = "SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY" // const val SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY = "SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY"
@ -1126,6 +1127,25 @@ class VectorPreferences @Inject constructor(
.apply() .apply()
} }
/**
* Sets the space backstack that is used for up navigation.
* This needs to be persisted because navigating up through spaces should work across sessions.
*
* Only the IDs of the spaces are stored.
*/
fun setSpaceBackstack(spaceBackstack: List<String?>) {
val spaceIdsJoined = spaceBackstack.takeIf { it.isNotEmpty() }?.joinToString(",")
defaultPrefs.edit().putString(SETTINGS_PERSISTED_SPACE_BACKSTACK, spaceIdsJoined).apply()
}
/**
* Gets the space backstack used for up navigation.
*/
fun getSpaceBackstack(): List<String?> {
val spaceIdsJoined = defaultPrefs.getString(SETTINGS_PERSISTED_SPACE_BACKSTACK, null)
return spaceIdsJoined?.takeIf { it.isNotEmpty() }?.split(",").orEmpty()
}
fun showLiveSenderInfo(): Boolean { fun showLiveSenderInfo(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default)) return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default))
} }

View file

@ -63,10 +63,6 @@ class NewSpaceSummaryController @Inject constructor(
homeCount: RoomAggregateNotificationCount, homeCount: RoomAggregateNotificationCount,
expandedStates: Map<String, Boolean>, expandedStates: Map<String, Boolean>,
) { ) {
newSpaceListHeaderItem {
id("space_list_header")
}
addHomeItem(selectedSpace == null, homeCount) addHomeItem(selectedSpace == null, homeCount)
addSpaces(spaceSummaries, selectedSpace, rootSpaces, expandedStates) addSpaces(spaceSummaries, selectedSpace, rootSpaces, expandedStates)
addCreateItem() addCreateItem()

View file

@ -65,7 +65,7 @@ class SpaceListViewModel @AssistedInject constructor(
private val session: Session, private val session: Session,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val autoAcceptInvites: AutoAcceptInvites, private val autoAcceptInvites: AutoAcceptInvites,
private val analyticsTracker: AnalyticsTracker private val analyticsTracker: AnalyticsTracker,
) : VectorViewModel<SpaceListViewState, SpaceListAction, SpaceListViewEvents>(initialState) { ) : VectorViewModel<SpaceListViewState, SpaceListAction, SpaceListViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -88,9 +88,7 @@ class SpaceListViewModel @AssistedInject constructor(
spaceStateHandler.getSelectedSpaceFlow() spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged() .distinctUntilChanged()
.setOnEach { selectedSpaceOption -> .setOnEach { selectedSpaceOption ->
copy( copy(selectedSpace = selectedSpaceOption.orNull())
selectedSpace = selectedSpaceOption.orNull()
)
} }
// XXX there should be a way to refactor this and share it // XXX there should be a way to refactor this and share it

View file

@ -74,6 +74,19 @@
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>
<ImageView
android:id="@+id/debug_button"
style="@style/VectorDebug"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="bottom|end"
android:layout_marginStart="12dp"
android:importantForAccessibility="no"
android:scaleType="center"
android:src="@drawable/ic_settings_x"
app:tint="?colorPrimary"
tools:ignore="MissingPrefix" />
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/TextAppearance.Vector.Body.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_space_item"
android:ellipsize="middle"
android:orientation="vertical"
android:padding="16dp"
android:singleLine="true"
android:text="@string/change_space"
android:textAllCaps="true"
android:textColor="?vctr_content_tertiary"
android:textSize="14sp"
tools:viewBindingIgnore="true" />

View file

@ -139,7 +139,6 @@
<string name="all_chats">All Chats</string> <string name="all_chats">All Chats</string>
<string name="start_chat">Start Chat</string> <string name="start_chat">Start Chat</string>
<string name="create_room">Create Room</string> <string name="create_room">Create Room</string>
<string name="change_space">Change Space</string>
<string name="explore_rooms">Explore Rooms</string> <string name="explore_rooms">Explore Rooms</string>
<string name="a11y_expand_space_children">Expand space children</string> <string name="a11y_expand_space_children">Expand space children</string>
<string name="a11y_collapse_space_children">Collapse space children</string> <string name="a11y_collapse_space_children">Collapse space children</string>

View file

@ -21,11 +21,13 @@ import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeAnalyticsTracker import im.vector.app.test.fakes.FakeAnalyticsTracker
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeUiStateRepository import im.vector.app.test.fakes.FakeUiStateRepository
import im.vector.app.test.fakes.FakeVectorPreferences
import im.vector.app.test.fixtures.RoomSummaryFixture.aRoomSummary import im.vector.app.test.fixtures.RoomSummaryFixture.aRoomSummary
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Before
import org.junit.Test import org.junit.Test
internal class SpaceStateHandlerImplTest { internal class SpaceStateHandlerImplTest {
@ -38,14 +40,21 @@ internal class SpaceStateHandlerImplTest {
private val uiStateRepository = FakeUiStateRepository() private val uiStateRepository = FakeUiStateRepository()
private val activeSessionHolder = FakeActiveSessionHolder(session) private val activeSessionHolder = FakeActiveSessionHolder(session)
private val analyticsTracker = FakeAnalyticsTracker() private val analyticsTracker = FakeAnalyticsTracker()
private val vectorPreferences = FakeVectorPreferences()
private val spaceStateHandler = SpaceStateHandlerImpl( private val spaceStateHandler = SpaceStateHandlerImpl(
sessionDataSource.instance, sessionDataSource.instance,
uiStateRepository, uiStateRepository,
activeSessionHolder.instance, activeSessionHolder.instance,
analyticsTracker, analyticsTracker,
vectorPreferences.instance,
) )
@Before
fun setup() {
vectorPreferences.givenSpaceBackstack(emptyList())
}
@Test @Test
fun `given selected space doesn't exist, when getCurrentSpace, then return null`() { fun `given selected space doesn't exist, when getCurrentSpace, then return null`() {
val currentSpace = spaceStateHandler.getCurrentSpace() val currentSpace = spaceStateHandler.getCurrentSpace()
@ -77,33 +86,33 @@ internal class SpaceStateHandlerImplTest {
} }
@Test @Test
fun `given is forward navigation and no current space, when setCurrentSpace, then null added to backstack`() { fun `given not in space and is forward navigation, when setCurrentSpace, then null added to backstack`() {
spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = true) spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = true)
val backstack = spaceStateHandler.getSpaceBackstack() vectorPreferences.verifySetSpaceBackstack(listOf(null))
backstack.size shouldBe 1
backstack.first() shouldBe null
} }
@Test @Test
fun `given is forward navigation and is in space, when setCurrentSpace, then previous space added to backstack`() { fun `given in space and is forward navigation, when setCurrentSpace, then previous space added to backstack`() {
spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = true) spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = true)
spaceStateHandler.setCurrentSpace("secondSpaceId", session, isForwardNavigation = true) spaceStateHandler.setCurrentSpace("secondSpaceId", session, isForwardNavigation = true)
val backstack = spaceStateHandler.getSpaceBackstack() vectorPreferences.verifySetSpaceBackstack(listOf(spaceId))
backstack.size shouldBe 2
backstack shouldBeEqualTo listOf(null, spaceId)
} }
@Test @Test
fun `given is not forward navigation, when setCurrentSpace, then previous space not added to backstack`() { fun `given is not forward navigation, when setCurrentSpace, then previous space not added to backstack`() {
spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = false) spaceStateHandler.setCurrentSpace(spaceId, session, isForwardNavigation = false)
val backstack = spaceStateHandler.getSpaceBackstack() vectorPreferences.verifySetSpaceBackstack(listOf(spaceId), inverse = true)
}
backstack.size shouldBe 0 @Test
fun `given navigating to all chats, when setCurrentSpace, then backstack cleared`() {
spaceStateHandler.setCurrentSpace(spaceId, session)
spaceStateHandler.setCurrentSpace(null, session)
vectorPreferences.verifySetSpaceBackstack(emptyList())
} }
@Test @Test

View file

@ -0,0 +1,68 @@
/*
* 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.features.navigation
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeAnalyticsTracker
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeDebugNavigator
import im.vector.app.test.fakes.FakeSpaceStateHandler
import im.vector.app.test.fakes.FakeSupportedVerificationMethodsProvider
import im.vector.app.test.fakes.FakeVectorFeatures
import im.vector.app.test.fakes.FakeVectorPreferences
import im.vector.app.test.fakes.FakeWidgetArgsBuilder
import im.vector.app.test.fixtures.RoomSummaryFixture.aRoomSummary
import org.junit.Test
internal class DefaultNavigatorTest {
private val sessionHolder = FakeActiveSessionHolder()
private val vectorPreferences = FakeVectorPreferences()
private val widgetArgsBuilder = FakeWidgetArgsBuilder()
private val spaceStateHandler = FakeSpaceStateHandler()
private val supportedVerificationMethodsProvider = FakeSupportedVerificationMethodsProvider()
private val features = FakeVectorFeatures()
private val analyticsTracker = FakeAnalyticsTracker()
private val debugNavigator = FakeDebugNavigator()
private val navigator = DefaultNavigator(
sessionHolder.instance,
vectorPreferences.instance,
widgetArgsBuilder.instance,
spaceStateHandler,
supportedVerificationMethodsProvider.instance,
features,
analyticsTracker,
debugNavigator,
)
/**
* The below test is by no means all that we want to test in [DefaultNavigator].
* Please add relevant tests as you make changes to or related to other functions in the class.
*/
@Test
fun `when switchToSpace, then current space set`() {
val spaceId = "space-id"
val spaceSummary = aRoomSummary(spaceId)
sessionHolder.fakeSession.fakeRoomService.getRoomSummaryReturns(spaceSummary)
navigator.switchToSpace(FakeContext().instance, spaceId, Navigator.PostSwitchSpaceAction.None)
spaceStateHandler.verifySetCurrentSpace(spaceId)
}
}

View file

@ -0,0 +1,22 @@
/*
* 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.core.debug.DebugNavigator
import io.mockk.mockk
class FakeDebugNavigator : DebugNavigator by mockk()

View file

@ -16,12 +16,18 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class FakeRoomService( class FakeRoomService(
private val fakeRoom: FakeRoom = FakeRoom() private val fakeRoom: FakeRoom = FakeRoom()
) : RoomService by mockk() { ) : RoomService by mockk() {
override fun getRoom(roomId: String) = fakeRoom override fun getRoom(roomId: String) = fakeRoom
fun getRoomSummaryReturns(roomSummary: RoomSummary?) {
every { getRoomSummary(any()) } returns roomSummary
}
} }

View file

@ -37,7 +37,7 @@ class FakeSession(
val fakeProfileService: FakeProfileService = FakeProfileService(), val fakeProfileService: FakeProfileService = FakeProfileService(),
val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(), val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(),
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(), val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(),
private val fakeRoomService: FakeRoomService = FakeRoomService(), val fakeRoomService: FakeRoomService = FakeRoomService(),
private val fakeEventService: FakeEventService = FakeEventService(), private val fakeEventService: FakeEventService = FakeEventService(),
) : Session by mockk(relaxed = true) { ) : Session by mockk(relaxed = true) {

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2021 New Vector Ltd * Copyright (c) 2022 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,14 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.spaces package im.vector.app.test.fakes
import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.SpaceStateHandler
import im.vector.app.R import io.mockk.mockk
import im.vector.app.core.epoxy.VectorEpoxyHolder import io.mockk.verify
import im.vector.app.core.epoxy.VectorEpoxyModel
@EpoxyModelClass class FakeSpaceStateHandler : SpaceStateHandler by mockk(relaxUnitFun = true) {
abstract class NewSpaceListHeaderItem : VectorEpoxyModel<NewSpaceListHeaderItem.Holder>(R.layout.item_new_space_list_header) {
class Holder : VectorEpoxyHolder() fun verifySetCurrentSpace(spaceId: String) {
verify { setCurrentSpace(spaceId) }
}
} }

View file

@ -0,0 +1,25 @@
/*
* 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.crypto.verification.SupportedVerificationMethodsProvider
import io.mockk.mockk
class FakeSupportedVerificationMethodsProvider {
val instance = mockk<SupportedVerificationMethodsProvider>()
}

View file

@ -19,12 +19,21 @@ package im.vector.app.test.fakes
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify
class FakeVectorPreferences { class FakeVectorPreferences {
val instance = mockk<VectorPreferences>() val instance = mockk<VectorPreferences>(relaxUnitFun = true)
fun givenUseCompleteNotificationFormat(value: Boolean) { fun givenUseCompleteNotificationFormat(value: Boolean) {
every { instance.useCompleteNotificationFormat() } returns value every { instance.useCompleteNotificationFormat() } returns value
} }
fun givenSpaceBackstack(value: List<String?>) {
every { instance.getSpaceBackstack() } returns value
}
fun verifySetSpaceBackstack(value: List<String?>, inverse: Boolean = false) {
verify(inverse = inverse) { instance.setSpaceBackstack(value) }
}
} }

View file

@ -0,0 +1,25 @@
/*
* 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.widgets.WidgetArgsBuilder
import io.mockk.mockk
class FakeWidgetArgsBuilder {
val instance = mockk<WidgetArgsBuilder>()
}