From a8e28b4c10a59ac93b6d87e8821e6b03ccdfa76c Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:36:44 +0100 Subject: [PATCH] DROID-2793 Date as an Object | Tech | Refactoring main state change (#1933) --- .../viewmodel/DateObjectViewModel.kt | 83 +++-- .../feature_date/DateObjectViewModelTest.kt | 300 ++++++++++++++++++ 2 files changed, 356 insertions(+), 27 deletions(-) diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt index 8a837b2227..40fb383128 100644 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt +++ b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt @@ -65,6 +65,7 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart @@ -150,7 +151,8 @@ class DateObjectViewModel( uiObjectsListState.value = UiObjectsListState.LoadingState proceedWithObservingRelationListWithValue() proceedWithObservingPermissions() - proceedWithObservingDateId() + observeDateIdForObject() + observeDateIdForRelations() proceedWithObservingSyncStatus() setupSearchStateFlow() _dateId.value = vmParams.objectId @@ -223,6 +225,9 @@ class DateObjectViewModel( uiFieldsState.value = UiFieldsState.Empty uiObjectsListState.value = UiObjectsListState.Empty uiFieldsSheetState.value = UiFieldsSheetState.Hidden + _dateObjectFieldIds.value = emptyList() + _dateId.value = null + _dateTimestamp.value = null _activeField.value = null _dateId.value = dateObjectId } @@ -286,27 +291,35 @@ class DateObjectViewModel( viewModelScope.launch { combine( _dateObjectFieldIds, - storeOfRelations.observe().filter { it.isNotEmpty() } - ) { relationIds, _ -> - relationIds - }.collect { relationIds -> + storeOfRelations.observe() + ) { relationIds, store -> + relationIds to store + }.collect { (relationIds, store) -> Timber.d("RelationListWithValue: $relationIds") - initFieldsState( - items = relationIds.toUiFieldsItem(storeOfRelations = storeOfRelations) - ) + if (store.isEmpty()) { + handleEmptyFieldsState() + } else { + initFieldsState( + items = relationIds.toUiFieldsItem(storeOfRelations = storeOfRelations) + ) + } } } } - private fun proceedWithObservingDateId() { + private fun observeDateIdForObject() { viewModelScope.launch { - _dateId - .filterNotNull() - .collect { id -> - Timber.d("Getting date object with id: $id") - proceedWithGettingDateObject(id) - proceedWithGettingDateObjectRelationList(id) - } + _dateId.filterNotNull().collect { id -> + proceedWithGettingDateObject(id) + } + } + } + + private fun observeDateIdForRelations() { + viewModelScope.launch { + _dateId.filterNotNull().collect { id -> + proceedWithGettingDateObjectRelationList(id) + } } } @@ -317,6 +330,7 @@ class DateObjectViewModel( ) getObjectRelationListById.async(params).fold( onSuccess = { result -> + Timber.d("RelationListWithValue success: $result") _dateObjectFieldIds.value = result }, onFailure = { e -> @@ -382,21 +396,35 @@ class DateObjectViewModel( private fun setupUiStateFlow() { viewModelScope.launch { combine( - _dateId.filterNotNull(), - _dateTimestamp.filterNotNull(), - _activeField.filterNotNull(), + _dateId, + _dateTimestamp, + _activeField, restartSubscription ) { dateId, timestamp, activeField, _ -> - createSearchParams( - dateId = dateId, - timestamp = timestamp, - space = vmParams.spaceId, - itemsLimit = _itemsLimit, - field = activeField - ) + Timber.d("setupUiStateFlow, Combine: dateId: $dateId, timestamp: $timestamp, activeField: $activeField") + + // If any of these are null, we return null to indicate we should skip loading data. + if (dateId == null || timestamp == null || activeField == null) { + null + } else { + createSearchParams( + dateId = dateId, + timestamp = timestamp, + space = vmParams.spaceId, + itemsLimit = _itemsLimit, + field = activeField + ) + } } .flatMapLatest { searchParams -> - loadData(searchParams) + if (searchParams == null) { + Timber.d("Search params are null, skipping loadData") + // If searchParams is null, we skip loadData and emit an empty list. + flowOf(emptyList()) + } else { + Timber.d("Search params are not null, loading data") + loadData(searchParams) + } } .catch { errorState.value = UiErrorState.Show( @@ -670,6 +698,7 @@ class DateObjectViewModel( } fun onDateEvent(event: DateEvent) { + Timber.d("onDateEvent: $event") when (event) { is DateEvent.Calendar -> onCalendarEvent(event) is DateEvent.TopToolbar -> onTopToolbarEvent(event) diff --git a/feature-date/src/test/java/com/anytypeio/anytype/feature_date/DateObjectViewModelTest.kt b/feature-date/src/test/java/com/anytypeio/anytype/feature_date/DateObjectViewModelTest.kt index 83fdde34b4..6926183713 100644 --- a/feature-date/src/test/java/com/anytypeio/anytype/feature_date/DateObjectViewModelTest.kt +++ b/feature-date/src/test/java/com/anytypeio/anytype/feature_date/DateObjectViewModelTest.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.feature_date +import app.cash.turbine.test import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.* import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions @@ -25,8 +26,11 @@ import com.anytypeio.anytype.feature_date.viewmodel.ActiveField import com.anytypeio.anytype.feature_date.viewmodel.DateObjectViewModel import com.anytypeio.anytype.feature_date.viewmodel.DateObjectViewModel.Companion.DEFAULT_SEARCH_LIMIT import com.anytypeio.anytype.feature_date.viewmodel.DateObjectVmParams +import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem +import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsState import com.anytypeio.anytype.feature_date.viewmodel.createSearchParams import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate +import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf @@ -622,6 +626,302 @@ class DateObjectViewModelTest { } } + @Test + fun `should restart subscription with updated params after next day clicked`() = + runTest { + + // Arrange + val firstDayTimestamp = 101.0 + val firstDayObjectId = "firstDayId-${RandomString.make()}" + val stubFirstDayObjectView = StubObjectView( + root = firstDayObjectId, + details = mapOf( + firstDayObjectId to mapOf( + Relations.TIMESTAMP to firstDayTimestamp + ) + ) + ) + val firstDayRelation = RelationKey("firstDayRelation-${RandomString.make()}") + val firstDayRelationsListWithValues = listOf( + RelationListWithValueItem( + key = firstDayRelation, + counter = 5L + ) + ) + + val nextDayTimestamp = 102.0 + val nextDayObjectId = "nextDayId-${RandomString.make()}" + val stubNextDayObjectView = StubObjectView( + root = nextDayObjectId, + details = mapOf( + nextDayObjectId to mapOf( + Relations.TIMESTAMP to nextDayTimestamp + ) + ) + ) + val nextDayRelation = RelationKey("nextDayRelation-${RandomString.make()}") + val nextDayRelationsListWithValues = listOf( + RelationListWithValueItem( + key = nextDayRelation, + counter = 5L + ) + ) + val relationObjects = listOf( + StubRelationObject( + key = firstDayRelation.key, + name = "First day date relation", + format = RelationFormat.DATE + ), + StubRelationObject( + key = nextDayRelation.key, + name = "Next day date relation", + format = RelationFormat.DATE + ) + ) + storeOfRelations.merge(relationObjects) + + mockGetObjectSuccess(firstDayObjectId, stubFirstDayObjectView) + mockRelationListWithValueSuccess(firstDayObjectId, firstDayRelationsListWithValues) + + whenever(dateProvider.formatTimestampToDateAndTime(firstDayTimestamp.toLong() * 1000)) + .thenReturn("01-01-2024" to "12:00") + whenever(dateProvider.calculateRelativeDates(firstDayTimestamp.toLong())) + .thenReturn( + RelativeDate.Other( + initialTimeInMillis = firstDayTimestamp.toLong() * 1000, + dayOfWeek = DayOfWeekCustom.MONDAY, + formattedDate = "01-01-2024", + formattedTime = "12:00" + ) + ) + + val vm = getViewModel(objectId = firstDayObjectId, spaceId = spaceId) + + val subscribeParams = vm.createSearchParams( + dateId = firstDayObjectId, + timestamp = firstDayTimestamp.toLong(), + space = spaceId, + itemsLimit = DEFAULT_SEARCH_LIMIT, + field = ActiveField( + key = firstDayRelation, + format = RelationFormat.DATE + ) + ) + + whenever(storelessSubscriptionContainer.subscribe(subscribeParams)) + .thenReturn(emptyFlow>()) + + vm.onStart() + advanceUntilIdle() + + // Assert + verifyBlocking(storelessSubscriptionContainer, times(1)) { + subscribe(subscribeParams) + } + + // Arrange, next day clicked, imitate the next day has no relations + whenever(dateProvider.getTimestampForTomorrowAtStartOfDay()) + .thenReturn(nextDayTimestamp.toLong()) + val params = GetDateObjectByTimestamp.Params( + timestampInSeconds = nextDayTimestamp.toLong(), + space = spaceId + ) + whenever(getDateObjectByTimestamp.async(params)).thenReturn( + Resultat.success( + mapOf( + Relations.ID to nextDayObjectId + ) + ) + ) + mockGetObjectSuccess(nextDayObjectId, stubNextDayObjectView) + mockRelationListWithValueSuccess(nextDayObjectId, nextDayRelationsListWithValues) + val subscribeParamsNextDay = vm.createSearchParams( + dateId = nextDayObjectId, + timestamp = nextDayTimestamp.toLong(), + space = spaceId, + itemsLimit = DEFAULT_SEARCH_LIMIT, + field = ActiveField( + key = nextDayRelation, + format = RelationFormat.DATE + ) + ) + whenever(dateProvider.formatTimestampToDateAndTime(nextDayTimestamp.toLong() * 1000)) + .thenReturn("02-01-2024" to "12:00") + whenever(dateProvider.calculateRelativeDates(nextDayTimestamp.toLong())) + .thenReturn( + RelativeDate.Other( + initialTimeInMillis = nextDayTimestamp.toLong() * 1000, + dayOfWeek = DayOfWeekCustom.THURSDAY, + formattedDate = "02-01-2024", + formattedTime = "12:00" + ) + ) + + // Act, next day clicked + vm.onDateEvent(DateEvent.Calendar.OnTomorrowClick) + advanceUntilIdle() + + // Assert, new subscription started + vm.uiFieldsState.test{ + val state = expectMostRecentItem() + assertEquals( + listOf( + UiFieldsItem.Settings(), + UiFieldsItem.Item.Default( + key = nextDayRelation, + title = "Next day date relation", + relationFormat = RelationFormat.DATE, + id = nextDayRelation.key + ) + ), + state.items + ) + } + verifyBlocking(storelessSubscriptionContainer, times(1)) { + subscribe(subscribeParamsNextDay) + } + } + + @Test + fun `should not restart subscription with updated params when new date has no active fields`() = + runTest { + + // Arrange + val firstDayTimestamp = 101.0 + val firstDayObjectId = "firstDayId-${RandomString.make()}" + val stubFirstDayObjectView = StubObjectView( + root = firstDayObjectId, + details = mapOf( + firstDayObjectId to mapOf( + Relations.TIMESTAMP to firstDayTimestamp + ) + ) + ) + val firstDayRelation = RelationKey("firstDayRelation-${RandomString.make()}") + val firstDayRelationsListWithValues = listOf( + RelationListWithValueItem( + key = firstDayRelation, + counter = 5L + ) + ) + + val nextDayTimestamp = 102.0 + val nextDayObjectId = "nextDayId-${RandomString.make()}" + val stubNextDayObjectView = StubObjectView( + root = nextDayObjectId, + details = mapOf( + nextDayObjectId to mapOf( + Relations.TIMESTAMP to nextDayTimestamp + ) + ) + ) + val nextDayRelation = RelationKey("nextDayRelation-${RandomString.make()}") + val nextDayRelationsListWithValues = listOf( + RelationListWithValueItem( + key = nextDayRelation, + counter = 5L + ) + ) + val relationObjects = listOf( + StubRelationObject( + key = firstDayRelation.key, + name = "First day date relation", + format = RelationFormat.DATE + ), + StubRelationObject( + key = nextDayRelation.key, + name = "Next day date relation", + format = RelationFormat.DATE, + isHidden = true + ) + ) + storeOfRelations.merge(relationObjects) + + mockGetObjectSuccess(firstDayObjectId, stubFirstDayObjectView) + mockRelationListWithValueSuccess(firstDayObjectId, firstDayRelationsListWithValues) + + whenever(dateProvider.formatTimestampToDateAndTime(firstDayTimestamp.toLong() * 1000)) + .thenReturn("01-01-2024" to "12:00") + whenever(dateProvider.calculateRelativeDates(firstDayTimestamp.toLong())) + .thenReturn( + RelativeDate.Other( + initialTimeInMillis = firstDayTimestamp.toLong() * 1000, + dayOfWeek = DayOfWeekCustom.MONDAY, + formattedDate = "01-01-2024", + formattedTime = "12:00" + ) + ) + + val vm = getViewModel(objectId = firstDayObjectId, spaceId = spaceId) + + val subscribeParams = vm.createSearchParams( + dateId = firstDayObjectId, + timestamp = firstDayTimestamp.toLong(), + space = spaceId, + itemsLimit = DEFAULT_SEARCH_LIMIT, + field = ActiveField( + key = firstDayRelation, + format = RelationFormat.DATE + ) + ) + + whenever(storelessSubscriptionContainer.subscribe(subscribeParams)) + .thenReturn(emptyFlow>()) + + vm.onStart() + advanceUntilIdle() + + // Assert + verifyBlocking(storelessSubscriptionContainer, times(1)) { + subscribe(subscribeParams) + } + + // Arrange, next day clicked, imitate the next day has no relations + whenever(dateProvider.getTimestampForTomorrowAtStartOfDay()) + .thenReturn(nextDayTimestamp.toLong()) + val params = GetDateObjectByTimestamp.Params( + timestampInSeconds = nextDayTimestamp.toLong(), + space = spaceId + ) + whenever(getDateObjectByTimestamp.async(params)).thenReturn( + Resultat.success( + mapOf( + Relations.ID to nextDayObjectId + ) + ) + ) + mockGetObjectSuccess(nextDayObjectId, stubNextDayObjectView) + mockRelationListWithValueSuccess(nextDayObjectId, nextDayRelationsListWithValues) + val subscribeParamsNextDay = vm.createSearchParams( + dateId = nextDayObjectId, + timestamp = nextDayTimestamp.toLong(), + space = spaceId, + itemsLimit = DEFAULT_SEARCH_LIMIT, + field = ActiveField( + key = nextDayRelation, + format = RelationFormat.DATE + ) + ) + whenever(dateProvider.formatTimestampToDateAndTime(nextDayTimestamp.toLong() * 1000)) + .thenReturn("02-01-2024" to "12:00") + whenever(dateProvider.calculateRelativeDates(nextDayTimestamp.toLong())) + .thenReturn( + RelativeDate.Other( + initialTimeInMillis = nextDayTimestamp.toLong() * 1000, + dayOfWeek = DayOfWeekCustom.THURSDAY, + formattedDate = "02-01-2024", + formattedTime = "12:00" + ) + ) + + // Act, next day clicked + vm.onDateEvent(DateEvent.Calendar.OnTomorrowClick) + advanceUntilIdle() + + // Assert, new subscription not started + verifyNoMoreInteractions(storelessSubscriptionContainer) + } + private fun getViewModel(objectId: Id, spaceId: SpaceId): DateObjectViewModel { val vmParams = DateObjectVmParams( objectId = objectId,