1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3611 Navigation | Fix | Allow restoration of last opened space from loading state on splash screen - Hotfix (#2361)

This commit is contained in:
Evgenii Kozlov 2025-04-28 16:33:21 +02:00
parent 2649a50cfa
commit 11c151828a
4 changed files with 233 additions and 38 deletions

View file

@ -40,13 +40,16 @@ import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate
import com.anytypeio.anytype.presentation.confgs.ChatConfig
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEvent
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
/**
@ -307,7 +310,7 @@ class SplashViewModel(
.observe(SpaceId(space))
.take(1)
.collect { view ->
if (view.isActive) {
if (view.isActive || view.isLoading) {
when (response.obj.layout) {
ObjectType.Layout.SET, ObjectType.Layout.COLLECTION ->
commands.emit(
@ -367,43 +370,50 @@ class SplashViewModel(
Timber.d("proceedWithVaultNavigation deep link: $deeplink")
val space = getLastOpenedSpace.async(Unit).getOrNull()
if (space != null && spaceManager.getState() != SpaceManager.State.NoSpace) {
Timber.d("Got the last opened space from settings: ${space.id}")
spaceManager
.observe()
.take(1)
.flatMapLatest { config ->
spaceViews
.observe(SpaceId(config.space))
.filterNot { view ->
// Wait until the space view is no longer in a fully UNKNOWN state
view.spaceLocalStatus == SpaceStatus.UNKNOWN
&& view.spaceAccountStatus == SpaceStatus.UNKNOWN
}
.take(1)
}
.collect { view ->
if (view.isActive || view.isLoading) {
val chat = view.chatId
if (chat.isNullOrEmpty() || !ChatConfig.isChatAllowed(space.id)) {
commands.emit(
Command.NavigateToWidgets(
space = space.id,
deeplink = deeplink
)
)
} else {
commands.emit(
Command.NavigateToSpaceLevelChat(
space = space.id,
chat = chat,
deeplink = deeplink
)
)
}
} else {
commands.emit(Command.NavigateToVault(deeplink))
val view = withTimeoutOrNull(SPACE_LOADING_TIMEOUT) {
spaceManager
.observe()
.take(1)
.flatMapLatest { config ->
spaceViews
.observe(SpaceId(config.space))
.filterNot { view ->
if (view.isUnknown) {
Timber.w("View is unknown during restoration of the last opened space")
}
view.isUnknown
}
.take(1)
}
.firstOrNull()
}
if (view != null) {
if (view.isActive || view.isLoading) {
val chat = view.chatId
if (chat.isNullOrEmpty() || !ChatConfig.isChatAllowed(space.id)) {
commands.emit(
Command.NavigateToWidgets(
space = space.id,
deeplink = deeplink
)
)
} else {
commands.emit(
Command.NavigateToSpaceLevelChat(
space = space.id,
chat = chat,
deeplink = deeplink
)
)
}
} else {
commands.emit(Command.NavigateToVault(deeplink))
}
} else {
Timber.w("Timeout while waiting for space view. Navigating to vault.")
commands.emit(Command.NavigateToVault(deeplink))
}
} else {
commands.emit(Command.NavigateToVault(deeplink))
}
@ -454,6 +464,7 @@ class SplashViewModel(
const val ERROR_MESSAGE = "An error occurred while starting account"
const val ERROR_NEED_UPDATE = "Unable to retrieve account. Please update Anytype to the latest version."
const val ERROR_CREATE_OBJECT = "Error while creating object: object type not found"
const val SPACE_LOADING_TIMEOUT = 5000L
}
sealed class State {

View file

@ -1,16 +1,21 @@
package com.anytypeio.anytype.presentation.splash
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.cash.turbine.test
import com.anytypeio.anytype.CrashReporter
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.StubConfig
import com.anytypeio.anytype.core_models.StubSpaceView
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.restrictions.SpaceStatus
import com.anytypeio.anytype.domain.auth.interactor.CheckAuthorizationStatus
import com.anytypeio.anytype.domain.auth.interactor.GetLastOpenedObject
import com.anytypeio.anytype.domain.auth.interactor.LaunchAccount
import com.anytypeio.anytype.domain.auth.interactor.LaunchWallet
import com.anytypeio.anytype.domain.auth.model.AuthStatus
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.base.Result
import com.anytypeio.anytype.domain.base.Resultat
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.misc.LocaleProvider
@ -23,7 +28,13 @@ import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import java.util.Locale
import kotlin.test.assertEquals
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -186,6 +197,166 @@ class SplashViewModelTest {
}
}
@Test
fun `should navigate to vault if space view loading times out`() = runTest {
// GIVEN
val deeplink = "test-deeplink"
val status = AuthStatus.AUTHORIZED
val response = Either.Right(status)
val space = defaultSpaceConfig.space
stubCheckAuthStatus(response)
stubLaunchWallet()
stubLaunchAccount()
stubGetLastOpenedObject()
getLastOpenedSpace.stub {
onBlocking {
async(Unit)
} doReturn Resultat.Success(
SpaceId(space)
)
}
initViewModel()
// WHEN
// simulate spaceManager.observe() never emits a valid space view (simulate timeout)
spaceManager.stub {
on { observe() } doReturn flow { /* no emission */ }
}
spaceViewSubscriptionContainer.stub {
on { observe() } doReturn flow { /* no emission */ }
}
vm.commands.test {
// Act: manually trigger proceedWithVaultNavigation
vm.onDeepLinkLaunch(deeplink)
// THEN
// small delay to allow the coroutine to timeout internally
delay(SplashViewModel.SPACE_LOADING_TIMEOUT + 100)
val first = awaitItem()
assertEquals(
expected = SplashViewModel.Command.NavigateToVault(deeplink),
actual = first
)
}
}
@Test
fun `should navigate to space view when space is restored`() = runTest {
// GIVEN
val deeplink = "test-deeplink"
val status = AuthStatus.AUTHORIZED
val response = Either.Right(status)
val space = defaultSpaceConfig.space
val spaceView = StubSpaceView(
targetSpaceId = space,
spaceLocalStatus = SpaceStatus.OK,
spaceAccountStatus = SpaceStatus.OK
)
stubCheckAuthStatus(response)
stubLaunchWallet()
stubLaunchAccount()
stubGetLastOpenedObject()
getLastOpenedSpace.stub {
onBlocking {
async(Unit)
} doReturn Resultat.Success(
SpaceId(space)
)
}
initViewModel()
// WHEN
// simulate spaceManager.observe() never emits a valid space view (simulate timeout)
spaceManager.stub {
on { observe() } doReturn flowOf(defaultSpaceConfig)
}
spaceViewSubscriptionContainer.stub {
on { observe(SpaceId(defaultSpaceConfig.space)) } doReturn flowOf(spaceView)
}
vm.commands.test {
// Act: manually trigger proceedWithVaultNavigation
vm.onDeepLinkLaunch(deeplink)
val first = awaitItem()
assertEquals(
expected = SplashViewModel.Command.NavigateToWidgets(
space = defaultSpaceConfig.space,
deeplink
),
actual = first
)
}
}
@Test
fun `should navigate to space-level chat if chat is available`() = runTest {
// GIVEN
val deeplink = "test-deeplink"
val status = AuthStatus.AUTHORIZED
val response = Either.Right(status)
val space = defaultSpaceConfig.space
val chatId = "chat-id"
val spaceView = StubSpaceView(
targetSpaceId = space,
spaceLocalStatus = SpaceStatus.OK,
spaceAccountStatus = SpaceStatus.OK,
chatId = chatId
)
stubCheckAuthStatus(response)
stubLaunchWallet()
stubLaunchAccount()
stubGetLastOpenedObject()
getLastOpenedSpace.stub {
onBlocking { async(Unit) } doReturn Resultat.Success(SpaceId(space))
}
initViewModel()
// WHEN
spaceManager.stub {
on { observe() } doReturn flowOf(defaultSpaceConfig)
}
spaceViewSubscriptionContainer.stub {
on { observe(SpaceId(defaultSpaceConfig.space)) } doReturn flowOf(spaceView)
}
vm.commands.test {
// Act
vm.onDeepLinkLaunch(deeplink)
val first = awaitItem()
assertEquals(
expected = SplashViewModel.Command.NavigateToSpaceLevelChat(
space = space,
chat = chatId,
deeplink = deeplink
),
actual = first
)
}
}
private fun stubCheckAuthStatus(response: Either.Right<AuthStatus>) {
checkAuthorizationStatus.stub {
onBlocking { invoke(eq(Unit)) } doReturn response