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:
parent
2649a50cfa
commit
11c151828a
4 changed files with 233 additions and 38 deletions
|
@ -354,6 +354,12 @@ sealed class ObjectWrapper {
|
|||
&& spaceAccountStatus != SpaceStatus.SPACE_REMOVING
|
||||
&& spaceAccountStatus != SpaceStatus.SPACE_DELETED
|
||||
}
|
||||
|
||||
val isUnknown: Boolean
|
||||
get() {
|
||||
return spaceLocalStatus == SpaceStatus.UNKNOWN
|
||||
&& spaceAccountStatus == SpaceStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> getValue(relation: Key): T? {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.anytypeio.anytype.core_models
|
||||
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType
|
||||
import com.anytypeio.anytype.core_models.restrictions.SpaceStatus
|
||||
import com.anytypeio.anytype.test_utils.MockDataFactory
|
||||
|
||||
fun StubDataView(
|
||||
|
@ -100,13 +101,19 @@ fun StubSpaceView(
|
|||
id: Id = MockDataFactory.randomUuid(),
|
||||
targetSpaceId: Id = MockDataFactory.randomUuid(),
|
||||
spaceAccessType: SpaceAccessType = SpaceAccessType.DEFAULT,
|
||||
sharedSpaceLimit: Int? = null
|
||||
sharedSpaceLimit: Int? = null,
|
||||
spaceAccountStatus: SpaceStatus? = null,
|
||||
spaceLocalStatus: SpaceStatus? = null,
|
||||
chatId: Id? = null
|
||||
|
||||
) = ObjectWrapper.SpaceView(
|
||||
map = mapOf(
|
||||
Relations.ID to id,
|
||||
Relations.CHAT_ID to chatId,
|
||||
Relations.TARGET_SPACE_ID to targetSpaceId,
|
||||
Relations.SPACE_ACCESS_TYPE to spaceAccessType.code.toDouble(),
|
||||
Relations.SHARED_SPACES_LIMIT to sharedSpaceLimit?.toDouble()
|
||||
Relations.SHARED_SPACES_LIMIT to sharedSpaceLimit?.toDouble(),
|
||||
Relations.SPACE_ACCOUNT_STATUS to spaceAccountStatus?.code?.toDouble(),
|
||||
Relations.SPACE_LOCAL_STATUS to spaceLocalStatus?.code?.toDouble()
|
||||
)
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue