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

DROID-2797 Date as an object | Support relative dates setting as default in object lists (#1908)

This commit is contained in:
Konstantin Ivanov 2024-12-11 22:03:42 +01:00 committed by Evgenii Kozlov
parent ea4f144f86
commit f9d28cd47c
18 changed files with 171 additions and 80 deletions

View file

@ -34,6 +34,7 @@ import com.anytypeio.anytype.domain.relations.AddRelationToObject
import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations
import com.anytypeio.anytype.domain.relations.DeleteRelationFromObject
import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.editor.Editor
@ -122,6 +123,9 @@ class ObjectRelationListTest {
@Mock
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
@Mock
lateinit var stringResourceProvider: StringResourceProvider
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
@ -130,7 +134,8 @@ class ObjectRelationListTest {
fieldParser = FieldParserImpl(
logger = logger,
dateProvider = dateProvider,
getDateObjectByTimestamp = getDateObjectByTimestamp
getDateObjectByTimestamp = getDateObjectByTimestamp,
stringResourceProvider = stringResourceProvider
)
addToFeaturedRelations = AddToFeaturedRelations(repo)
removeFromFeaturedRelations = RemoveFromFeaturedRelations(repo)

View file

@ -6,7 +6,6 @@ import androidx.preference.PreferenceManager
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.app.DefaultFeatureToggles
import com.anytypeio.anytype.app.TogglePrefs
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.core_utils.tools.AppInfo
import com.anytypeio.anytype.core_utils.tools.DefaultAppInfo
import com.anytypeio.anytype.core_utils.tools.DefaultThreadInfo
@ -28,11 +27,13 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.middleware.interactor.MiddlewareProtobufLogger
import com.anytypeio.anytype.middleware.interactor.ProtobufConverterProvider
import com.anytypeio.anytype.other.BasicLogger
import com.anytypeio.anytype.other.DefaultDateTypeNameProvider
import com.anytypeio.anytype.other.DefaultDebugConfig
import com.anytypeio.anytype.presentation.util.StringResourceProviderImpl
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProviderImpl
import dagger.Binds
@ -88,8 +89,9 @@ object UtilModule {
fun provideFieldsProvider(
dateProvider: DateProvider,
logger: Logger,
getDateObjectByTimestamp: GetDateObjectByTimestamp
): FieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
getDateObjectByTimestamp: GetDateObjectByTimestamp,
stringResourceProvider: StringResourceProvider
): FieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider)
@JvmStatic
@Provides
@ -97,6 +99,12 @@ object UtilModule {
fun provideResourceProvider(context: Context): ResourceProvider =
ResourceProviderImpl(context)
@JvmStatic
@Provides
@Singleton
fun provideStringResourceProvider(context: Context): StringResourceProvider =
StringResourceProviderImpl(context)
@Module
interface Bindings {

View file

@ -19,17 +19,20 @@ import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_ui.extensions.isKeyboardVisible
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.di.feature.discussions.DiscussionFragment
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel
import com.anytypeio.anytype.ui.date.DateObjectFragment
import com.anytypeio.anytype.ui.editor.EditorFragment
import com.anytypeio.anytype.ui.sets.ObjectSetFragment
import com.anytypeio.anytype.ui.settings.typography
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
class GlobalSearchFragment : BaseBottomSheetComposeFragment() {
@ -98,6 +101,25 @@ class GlobalSearchFragment : BaseBottomSheetComposeFragment() {
)
)
}
OpenObjectNavigation.NonValidObject -> {
toast(getString(R.string.error_non_valid_object))
}
is OpenObjectNavigation.OpenDataObject -> {
runCatching {
findNavController().navigate(
R.id.dateObjectScreen,
DateObjectFragment.args(
objectId = nav.target,
space = nav.space
)
)
}.onFailure {
Timber.e(it, "Failed to navigate to date object screen")
}
}
is OpenObjectNavigation.UnexpectedLayoutError -> {
toast(getString(R.string.error_unexpected_layout))
}
else -> {
// Do nothing.
}

View file

@ -79,7 +79,6 @@ fun EmptyWidgetPlaceholderWithCreateButton(
fun WidgetView.Name.getPrettyName(): String {
return when (this) {
is WidgetView.Name.Bundled -> stringResource(id = source.res())
is WidgetView.Name.Date -> relativeDate.getPrettyName()
is WidgetView.Name.Default -> prettyPrintName.ifEmpty { stringResource(id = R.string.untitled) }
}
}

View file

@ -18,19 +18,18 @@ import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.debugging.Logger
import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import javax.inject.Inject
import kotlin.collections.contains
interface FieldParser {
fun toDate(any: Any?): Field.Date?
fun calculateRelativeDate(timeStampInSeconds: TimeInSeconds): RelativeDate
suspend fun getDateObjectByTimeInSeconds(
timeInSeconds: TimeInSeconds,
spaceId: SpaceId,
actionSuccess: suspend (ObjectWrapper.Basic) -> Unit,
actionFailure: suspend (Throwable) -> Unit
)
fun getObjectName(objectWrapper: ObjectWrapper.Basic): String
fun getObjectTypeIdAndName(
objectWrapper: ObjectWrapper.Basic,
@ -41,7 +40,8 @@ interface FieldParser {
class FieldParserImpl @Inject constructor(
private val dateProvider: DateProvider,
private val logger: Logger,
private val getDateObjectByTimestamp: GetDateObjectByTimestamp
private val getDateObjectByTimestamp: GetDateObjectByTimestamp,
private val stringResourceProvider: StringResourceProvider
) : FieldParser {
//region Date field
@ -107,17 +107,41 @@ class FieldParserImpl @Inject constructor(
)
)
}
override fun calculateRelativeDate(timeStampInSeconds: TimeInSeconds): RelativeDate {
return dateProvider.calculateRelativeDates(
dateInSeconds = timeStampInSeconds
)
}
//endregion
//region ObjectWrapper.Basic fields
override fun getObjectName(objectWrapper: ObjectWrapper.Basic): String {
return objectWrapper.getProperObjectName().orEmpty()
val result = when (objectWrapper.layout) {
ObjectType.Layout.DATE -> {
val relativeDate = dateProvider.calculateRelativeDates(
dateInSeconds = objectWrapper.getSingleValue<Double>(Relations.TIMESTAMP)?.toLong()
)
stringResourceProvider.getRelativeDateName(relativeDate)
}
ObjectType.Layout.NOTE -> {
objectWrapper.snippet?.replace("\n", " ")?.take(MAX_SNIPPET_SIZE)
}
in SupportedLayouts.fileLayouts -> {
val fileName = if (objectWrapper.name.isNullOrBlank()) {
stringResourceProvider.getUntitledObjectTitle()
} else {
objectWrapper.name
}
when {
objectWrapper.fileExt.isNullOrBlank() -> fileName
fileName?.endsWith(".${objectWrapper.fileExt}") == true -> fileName
else -> "$fileName.${objectWrapper.fileExt}"
}
}
else -> {
objectWrapper.name
}
}
return if (result.isNullOrBlank()) {
stringResourceProvider.getUntitledObjectTitle()
} else {
result
}
}
override fun getObjectTypeIdAndName(
@ -135,46 +159,5 @@ class FieldParserImpl @Inject constructor(
null to null
}
}
private fun ObjectWrapper.Basic.getProperObjectName(): String? {
return when (layout) {
ObjectType.Layout.DATE -> {
getProperDateName()
}
ObjectType.Layout.NOTE -> {
snippet?.replace("\n", " ")?.take(MAX_SNIPPET_SIZE)
}
in SupportedLayouts.fileLayouts -> {
val fileName = if (name.isNullOrBlank()) "Untitled" else name.orEmpty()
if (fileExt.isNullOrBlank()) {
fileName
} else {
if (fileName.endsWith(".$fileExt")) {
fileName
} else {
"$fileName.$fileExt"
}
}
}
else -> {
name
}
}
}
private fun ObjectWrapper.Basic.getProperDateName(): String {
val timestampInSeconds = getSingleValue<Double>(Relations.TIMESTAMP)?.toLong()
if (timestampInSeconds != null) {
val (formattedDate, _) = dateProvider.formatTimestampToDateAndTime(
timestamp = timestampInSeconds * 1000,
)
return formattedDate
} else {
return ""
}
}
//endregion
}

View file

@ -0,0 +1,9 @@
package com.anytypeio.anytype.domain.resources
import com.anytypeio.anytype.core_models.RelativeDate
interface StringResourceProvider {
fun getRelativeDateName(relativeDate: RelativeDate): String
fun getDeletedObjectTitle(): String
fun getUntitledObjectTitle(): String
}

View file

@ -2497,6 +2497,12 @@ fun ObjectType.Layout.navigation(
space = space
)
}
ObjectType.Layout.DATE -> {
OpenObjectNavigation.OpenDataObject(
target = target,
space = space
)
}
else -> {
OpenObjectNavigation.UnexpectedLayoutError(this)
}

View file

@ -0,0 +1,30 @@
package com.anytypeio.anytype.presentation.util
import android.content.Context
import com.anytypeio.anytype.core_models.RelativeDate
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.presentation.R
import javax.inject.Inject
class StringResourceProviderImpl @Inject constructor(private val context: Context) :
StringResourceProvider {
override fun getRelativeDateName(relativeDate: RelativeDate): String {
return when (relativeDate) {
RelativeDate.Empty -> ""
is RelativeDate.Other -> relativeDate.formattedDate
is RelativeDate.Today -> context.getString(R.string.today)
is RelativeDate.Tomorrow -> context.getString(R.string.tomorrow)
is RelativeDate.Yesterday -> context.getString(R.string.yesterday)
else -> ""
}
}
override fun getDeletedObjectTitle(): String {
return context.getString(R.string.non_existent_object)
}
override fun getUntitledObjectTitle(): String {
return context.getString(R.string.untitled)
}
}

View file

@ -219,18 +219,8 @@ fun buildWidgetName(
obj: ObjectWrapper.Basic,
fieldParser: FieldParser
): Name {
return if (obj.layout == ObjectType.Layout.DATE) {
val timestamp = obj.getSingleValue<Double>(Relations.TIMESTAMP)?.toLong()
if (timestamp != null) {
Name.Date(
relativeDate = fieldParser.calculateRelativeDate(timeStampInSeconds = timestamp)
)
} else {
createDefaultName(obj, fieldParser)
}
} else {
createDefaultName(obj, fieldParser)
}
val prettyPrintName = fieldParser.getObjectName(obj)
return Name.Default(prettyPrintName = prettyPrintName)
}
private fun createDefaultName(

View file

@ -15,7 +15,6 @@ sealed class WidgetView {
sealed interface Name {
data class Bundled(val source: Widget.Source.Bundled): Name
data class Default(val prettyPrintName: String): Name
data class Date(val relativeDate: RelativeDate): Name
}
interface Element {

View file

@ -31,6 +31,7 @@ import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.emojifier.data.DefaultDocumentEmojiIconProvider
import com.anytypeio.anytype.presentation.MockBlockContentFactory.StubLinkContent
import com.anytypeio.anytype.presentation.MockBlockFactory.link
@ -126,10 +127,13 @@ class DefaultBlockViewRendererTest {
@Mock
lateinit var resourceProvider: ResourceProvider
@Mock
lateinit var stringResourceProvider: StringResourceProvider
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider)
renderer = DefaultBlockViewRenderer(
urlBuilder = UrlBuilder(gateway),
toggleStateHolder = toggleStateHolder,

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.presentation.editor.editor
import android.R
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.ObjectType
@ -27,6 +28,9 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import org.mockito.kotlin.whenever
class EditorLockPageTest : EditorPresentationTestSetup() {
@ -37,7 +41,9 @@ class EditorLockPageTest : EditorPresentationTestSetup() {
@get:Rule
val coroutineTestRule = DefaultCoroutineTestRule()
val title = StubTitle()
val title = StubTitle(
id = "titleId-${MockDataFactory.randomUuid()}",
)
val header = Block(
id = MockDataFactory.randomUuid(),
@ -287,6 +293,8 @@ class EditorLockPageTest : EditorPresentationTestSetup() {
)
)
whenever(stringResourceProvider.getUntitledObjectTitle()).thenReturn("Untitled")
val vm = buildViewModel()
// TESTING
@ -311,7 +319,7 @@ class EditorLockPageTest : EditorPresentationTestSetup() {
background = link.parseThemeBackgroundColor()
)
),
text = ""
text = "Untitled"
)
)

View file

@ -83,6 +83,7 @@ import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.relations.AddRelationToObject
import com.anytypeio.anytype.domain.relations.SetRelationKey
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.table.CreateTable
@ -884,8 +885,11 @@ open class EditorPresentationTestSetup {
}
}
@Mock
lateinit var stringResourceProvider: StringResourceProvider
fun proceedWithDefaultBeforeTestStubbing() {
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider)
stubAnalyticSpaceHelperDelegate()
stubSpaceManager()
stubUserPermission()

View file

@ -63,6 +63,7 @@ import com.anytypeio.anytype.domain.page.CloseBlock
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.spaces.ClearLastOpenedSpace
import com.anytypeio.anytype.domain.spaces.GetSpaceView
@ -317,10 +318,13 @@ class HomeScreenViewModelTest {
@Mock
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
@Mock
lateinit var stringResourceProvider: StringResourceProvider
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider)
urlBuilder = UrlBuilder(gateway)
stubSpaceManager()
userPermissionProvider = UserPermissionProviderStub()

View file

@ -18,6 +18,7 @@ import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.objects.ObjectWatcher
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.spaces.GetSpaceView
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule
@ -77,13 +78,16 @@ class TreeWidgetContainerTest {
@Mock
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
@Mock
lateinit var stringResourceProvider: StringResourceProvider
private val config = StubConfig()
private val workspace = config.spaceView
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider)
urlBuilder = UrlBuilder(gateway = gateway)
}

View file

@ -9,6 +9,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.presentation.objects.toViews
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlin.test.assertEquals
@ -34,6 +35,9 @@ class ObjectWrapperExtensionsKtTest {
@Mock
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
@Mock
lateinit var stringResourceProvider: StringResourceProvider
lateinit var fieldParser: FieldParser
val URL = "anytype.io/"
@ -41,7 +45,7 @@ class ObjectWrapperExtensionsKtTest {
@Before
fun before() {
MockitoAnnotations.openMocks(this)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider)
}
@Test

View file

@ -14,6 +14,7 @@ import com.anytypeio.anytype.domain.objects.DefaultObjectStore
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.presentation.mapper.toViewerColumns
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
@ -29,6 +30,7 @@ import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
import org.mockito.kotlin.whenever
class TagAndStatusTests {
@ -48,11 +50,13 @@ class TagAndStatusTests {
@Mock
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
@Mock
lateinit var stringResourceProvider: StringResourceProvider
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider)
}
@Test
@ -96,6 +100,8 @@ class TagAndStatusTests {
)
)
whenever(stringResourceProvider.getUntitledObjectTitle()).thenReturn("Untitled")
val recordId = MockDataFactory.randomUuid()
val records = mapOf<String, Any?>(
ObjectSetConfig.ID_KEY to recordId,
@ -155,7 +161,7 @@ class TagAndStatusTests {
val expected = Viewer.GridView.Row(
id = recordId,
name = "",
name = "Untitled",
type = "Type111",
showIcon = false,
objectIcon = ObjectIcon.Empty.Page,
@ -204,6 +210,8 @@ class TagAndStatusTests {
)
)
whenever(stringResourceProvider.getUntitledObjectTitle()).thenReturn("Untitled")
val selOptions = listOf(
StubRelationOptionObject(
id = MockDataFactory.randomUuid(),
@ -290,7 +298,7 @@ class TagAndStatusTests {
val expected = Viewer.GridView.Row(
id = recordId,
name = "",
name = "Untitled",
type = "Type111",
showIcon = false,
objectIcon = ObjectIcon.Empty.Page,

View file

@ -56,6 +56,7 @@ import com.anytypeio.anytype.domain.page.CloseBlock
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.search.DataViewSubscriptionContainer
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.domain.sets.OpenObjectSet
@ -239,6 +240,9 @@ open class ObjectSetViewModelTestSetup {
@Mock
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
@Mock
lateinit var stringResourceProvider: StringResourceProvider
fun proceedWithDefaultBeforeTestStubbing() = runTest {
repo = mock(verboseLogging = true)
dispatchers = AppCoroutineDispatchers(
@ -262,7 +266,7 @@ open class ObjectSetViewModelTestSetup {
dataViewSubscription = DefaultDataViewSubscription(dataViewSubscriptionContainer)
storeOfObjectTypes = DefaultStoreOfObjectTypes()
stubLocalProvider()
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
fieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp, stringResourceProvider)
stubGetDefaultPageType()
stubObservePermissions()
stubAnalyticSpaceHelperDelegate()