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 mentions (#1898)

This commit is contained in:
Konstantin Ivanov 2024-12-10 11:08:31 +01:00 committed by konstantiniiv
parent 8d3854072b
commit a03f37129b
16 changed files with 796 additions and 47 deletions

View file

@ -114,6 +114,7 @@ import com.anytypeio.anytype.presentation.templates.ObjectTypeTemplatesContainer
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.downloader.DocumentFileShareDownloader
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.emptyFlow
@ -298,6 +299,9 @@ open class EditorTestSetup {
@Mock
lateinit var fieldParser: FieldParser
@Mock
lateinit var resourceProvider: ResourceProvider
lateinit var interceptFileLimitEvents: InterceptFileLimitEvents
lateinit var addRelationToObject: AddRelationToObject
@ -415,7 +419,8 @@ open class EditorTestSetup {
coverImageHashProvider = coverImageHashProvider,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
),
orchestrator = Orchestrator(
createBlock = createBlock,

View file

@ -131,6 +131,7 @@ import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.downloader.DebugTreeShareDownloader
import com.anytypeio.anytype.presentation.util.downloader.DocumentFileShareDownloader
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.providers.DefaultCoverImageHashProvider
import com.anytypeio.anytype.ui.editor.EditorFragment
import dagger.BindsInstance
@ -380,14 +381,16 @@ object EditorSessionModule {
coverImageHashProvider: CoverImageHashProvider,
storeOfRelations: StoreOfRelations,
storeOfObjectTypes: StoreOfObjectTypes,
fieldParser: FieldParser
fieldParser: FieldParser,
resourceProvider: ResourceProvider
): DefaultBlockViewRenderer = DefaultBlockViewRenderer(
urlBuilder = urlBuilder,
toggleStateHolder = toggleStateHolder,
coverImageHashProvider = coverImageHashProvider,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
@JvmStatic

View file

@ -11,6 +11,7 @@ import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.templates.TemplateBlankViewModelFactory
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.providers.DefaultCoverImageHashProvider
import com.anytypeio.anytype.ui.templates.TemplateBlankFragment
import dagger.Binds
@ -73,6 +74,7 @@ interface TemplateBlankDependencies : ComponentDependencies {
fun storeOfObjectTypes(): StoreOfObjectTypes
fun dateProvider(): DateProvider
fun fieldsProvider(): FieldParser
fun resourceProvider(): ResourceProvider
}
@Scope

View file

@ -33,6 +33,8 @@ 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.widgets.collection.ResourceProvider
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProviderImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -89,6 +91,12 @@ object UtilModule {
getDateObjectByTimestamp: GetDateObjectByTimestamp
): FieldParser = FieldParserImpl(dateProvider, logger, getDateObjectByTimestamp)
@JvmStatic
@Provides
@Singleton
fun provideResourceProvider(context: Context): ResourceProvider =
ResourceProviderImpl(context)
@Module
interface Bindings {

View file

@ -36,6 +36,7 @@ import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.widgets.WidgetDispatchEvent
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.presentation.widgets.collection.CollectionViewModel
import com.anytypeio.anytype.ui.settings.RemoteFilesManageFragment
import dagger.Binds
@ -209,4 +210,5 @@ interface CollectionDependencies : ComponentDependencies {
fun analyticsHelperDelegate(): AnalyticSpaceHelperDelegate
fun provideUserPermissionProvider(): UserPermissionProvider
fun fieldParser(): FieldParser
fun resourceProvider(): ResourceProvider
}

View file

@ -58,6 +58,13 @@ data class Block(
else -> null
}
val timestamp: Double?
get() = when (val value = map[Relations.TIMESTAMP]) {
is Double -> value
is Int -> value.toDouble()
else -> null
}
companion object {
fun empty(): Fields = Fields(emptyMap())
const val NAME_KEY = "name"

View file

@ -17,6 +17,7 @@ android {
buildConfigField "boolean", "ENABLE_LINK_APPERANCE_MENU", "true"
buildConfigField "boolean", "USE_SIMPLE_TABLES_IN_EDITOR_EDDITING", "true"
buildConfigField "boolean", "ENABLE_VIEWS_MENU", "true"
buildConfigField "boolean", "ENABLE_RELATIVE_DATES_IN_MENTIONS", "true"
}
namespace 'com.anytypeio.anytype.presentation'

View file

@ -4,23 +4,23 @@ import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.MAX_SNIPPET_SIZE
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.ext.replaceRangeWithWord
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.presentation.BuildConfig
import com.anytypeio.anytype.presentation.editor.editor.Markup
import com.anytypeio.anytype.presentation.editor.editor.Markup.Companion.NON_EXISTENT_OBJECT_MENTION_NAME
import com.anytypeio.anytype.presentation.extension.shift
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import timber.log.Timber
fun Block.Content.Text.getTextAndMarks(
details: Block.Details,
marks: List<Markup.Mark>,
fieldParser: FieldParser
fieldParser: FieldParser,
resourceProvider: ResourceProvider
): Pair<String, List<Markup.Mark>> {
if (details.details.isEmpty() ||
marks.none { it is Markup.Mark.Mention }
) {
return Pair(text, marks)
if (details.details.isEmpty() || marks.none { it is Markup.Mark.Mention }) {
return text to marks
}
var updatedText = text
val updatedMarks = marks.toMutableList()
@ -28,31 +28,54 @@ fun Block.Content.Text.getTextAndMarks(
try {
updatedMarks.forEach { mark ->
if (mark !is Markup.Mark.Mention || mark.param.isBlank()) return@forEach
var newName = if (mark is Markup.Mark.Mention.Deleted) {
NON_EXISTENT_OBJECT_MENTION_NAME
} else {
details.details.getProperObjectName(id = mark.param) ?: return@forEach
val newName = when (mark) {
is Markup.Mark.Mention.Date -> getFormattedDateMention(
mark = mark,
details = details,
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
is Markup.Mark.Mention.Deleted -> resourceProvider.getNonExistentObjectTitle()
else -> details.details.getProperObjectName(id = mark.param) ?: return@forEach
}
val oldName = updatedText.substring(mark.from, mark.to)
if (newName != oldName) {
if (newName.isEmpty()) newName = Relations.RELATION_NAME_EMPTY
val d = newName.length - oldName.length
val finalName =
if (newName.isNullOrBlank()) resourceProvider.getUntitledTitle() else newName
if (finalName != oldName) {
val lengthDifference = finalName.length - oldName.length
updatedText = updatedText.replaceRangeWithWord(
replace = newName,
replace = finalName,
from = mark.from,
to = mark.to
)
updatedMarks.shift(
start = mark.from,
length = d
length = lengthDifference
)
}
}
} catch (e: Exception) {
Timber.e(e, "Error while update mention markups")
return Pair(text, marks)
return text to marks
}
return updatedText to updatedMarks
}
private fun Block.Content.Text.getFormattedDateMention(
mark: Markup.Mark.Mention.Date,
details: Block.Details,
fieldParser: FieldParser,
resourceProvider: ResourceProvider
): String? {
return if (BuildConfig.ENABLE_RELATIVE_DATES_IN_MENTIONS) {
val dateObject = details.details[mark.param] ?: return null
val timestamp = dateObject.timestamp ?: return null
val relativeDate = fieldParser.toDate(timestamp)?.relativeDate
resourceProvider.toFormattedString(relativeDate = relativeDate).takeIf { it.isNotEmpty() }
} else {
details.details[mark.param]?.name
}
return Pair(updatedText, updatedMarks)
}
private fun Map<Id, Block.Fields>.getProperObjectName(id: Id?): String? {

View file

@ -41,6 +41,7 @@ import com.anytypeio.anytype.presentation.relations.getCover
import com.anytypeio.anytype.presentation.relations.linksFeaturedRelation
import com.anytypeio.anytype.presentation.relations.objectTypeRelation
import com.anytypeio.anytype.presentation.relations.view
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import javax.inject.Inject
import timber.log.Timber
import com.anytypeio.anytype.presentation.editor.Editor.Mode as EditorMode
@ -51,7 +52,8 @@ class DefaultBlockViewRenderer @Inject constructor(
private val coverImageHashProvider: CoverImageHashProvider,
private val storeOfRelations: StoreOfRelations,
private val storeOfObjectTypes: StoreOfObjectTypes,
private val fieldParser: FieldParser
private val fieldParser: FieldParser,
private val resourceProvider: ResourceProvider
) : BlockViewRenderer, ToggleStateHolder by toggleStateHolder {
override suspend fun Map<Id, List<Block>>.render(
@ -802,7 +804,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
val isFocused = resolveIsFocused(focus, block)
@ -864,7 +867,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
return BlockView.Text.Header.Three(
mode = if (mode == EditorMode.Edit) BlockView.Mode.EDIT else BlockView.Mode.READ,
@ -900,7 +904,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
return BlockView.Text.Header.Two(
mode = if (mode == EditorMode.Edit) BlockView.Mode.EDIT else BlockView.Mode.READ,
@ -936,7 +941,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
return BlockView.Text.Header.One(
mode = if (mode == EditorMode.Edit) BlockView.Mode.EDIT else BlockView.Mode.READ,
@ -972,7 +978,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
return BlockView.Text.Checkbox(
mode = if (mode == EditorMode.Edit) BlockView.Mode.EDIT else BlockView.Mode.READ,
@ -1008,7 +1015,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
return BlockView.Text.Bulleted(
mode = if (mode == EditorMode.Edit) BlockView.Mode.EDIT else BlockView.Mode.READ,
@ -1068,7 +1076,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
val current = listOf(
BlockView.Decoration(
@ -1111,7 +1120,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
val iconImage = content.iconImage
val iconEmoji = content.iconEmoji
@ -1165,7 +1175,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
return BlockView.Text.Toggle(
mode = if (mode == EditorMode.Edit) BlockView.Mode.EDIT else BlockView.Mode.READ,
@ -1203,7 +1214,8 @@ class DefaultBlockViewRenderer @Inject constructor(
val (normalizedText, normalizedMarks) = content.getTextAndMarks(
details = details,
marks = marks,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
return BlockView.Text.Numbered(
mode = if (mode == EditorMode.Edit) BlockView.Mode.EDIT else BlockView.Mode.READ,

View file

@ -95,7 +95,7 @@ class CollectionViewModel(
private val setObjectListIsArchived: SetObjectListIsArchived,
private val setObjectListIsFavorite: SetObjectListIsFavorite,
private val deleteObjects: DeleteObjects,
private val resourceProvider: CollectionResourceProvider,
private val resourceProvider: ResourceProvider,
private val openObject: OpenObject,
private val createObject: CreateObject,
interceptEvents: InterceptEvents,
@ -988,7 +988,7 @@ class CollectionViewModel(
private val setObjectListIsArchived: SetObjectListIsArchived,
private val setObjectListIsFavorite: SetObjectListIsFavorite,
private val deleteObjects: DeleteObjects,
private val resourceProvider: CollectionResourceProvider,
private val resourceProvider: ResourceProvider,
private val openObject: OpenObject,
private val createObject: CreateObject,
private val interceptEvents: InterceptEvents,

View file

@ -1,15 +1,31 @@
package com.anytypeio.anytype.presentation.widgets.collection
import android.content.Context
import com.anytypeio.anytype.core_models.RelativeDate
import com.anytypeio.anytype.presentation.R
import javax.inject.Inject
class CollectionResourceProvider @Inject constructor(
private val context: Context
) {
interface ResourceProvider {
fun actionModeName(
actionMode: ActionMode,
isResultEmpty: Boolean
): String
fun subscriptionName(subscription: Subscription): String
fun toFormattedString(relativeDate: RelativeDate?): String
fun getNonExistentObjectTitle(): String
fun getUntitledTitle(): String
}
class ResourceProviderImpl @Inject constructor(
private val context: Context
) : ResourceProvider {
override fun actionModeName(
actionMode: ActionMode,
isResultEmpty: Boolean
): String {
return when (actionMode) {
ActionMode.SelectAll -> {
@ -18,6 +34,7 @@ class CollectionResourceProvider @Inject constructor(
else
context.getString(R.string.select_all)
}
ActionMode.UnselectAll -> {
if (isResultEmpty) {
""
@ -25,12 +42,13 @@ class CollectionResourceProvider @Inject constructor(
context.getString(R.string.unselect_all)
}
}
ActionMode.Edit -> context.getString(R.string.edit)
ActionMode.Done -> context.getString(R.string.done)
}
}
fun subscriptionName(subscription: Subscription): String {
override fun subscriptionName(subscription: Subscription): String {
return when (subscription) {
Subscription.Recent -> context.getString(R.string.recent)
Subscription.RecentLocal -> context.getString(R.string.recently_opened)
@ -42,4 +60,23 @@ class CollectionResourceProvider @Inject constructor(
Subscription.Files -> context.getString(R.string.synced_files)
}
}
override fun toFormattedString(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 getNonExistentObjectTitle(): String {
return context.getString(R.string.non_existent_object)
}
override fun getUntitledTitle(): String {
return context.getString(R.string.untitled)
}
}

View file

@ -3,9 +3,12 @@ package com.anytypeio.anytype.presentation.editor
import android.util.Log
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Block.Content.Link
import com.anytypeio.anytype.core_models.DayOfWeekCustom
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectType.Layout
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.RelativeDate
import com.anytypeio.anytype.core_models.StubBookmark
import com.anytypeio.anytype.core_models.StubCallout
import com.anytypeio.anytype.core_models.StubFile
@ -44,6 +47,7 @@ import com.anytypeio.anytype.presentation.editor.render.parseThemeBackgroundColo
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.util.TXT
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlin.test.assertEquals
import kotlinx.coroutines.runBlocking
@ -119,6 +123,9 @@ class DefaultBlockViewRendererTest {
@Mock
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
@Mock
lateinit var resourceProvider: ResourceProvider
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
@ -129,7 +136,8 @@ class DefaultBlockViewRendererTest {
coverImageHashProvider = coverImageHashProvider,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
}
@ -2373,6 +2381,10 @@ class DefaultBlockViewRendererTest {
val title = MockTypicalDocumentFactory.title
val header = MockTypicalDocumentFactory.header
resourceProvider.stub {
onBlocking { getNonExistentObjectTitle() } doReturn NON_EXISTENT_OBJECT_MENTION_NAME
}
val mentionText1 = "Foobar"
val mentionTextUpdated1 = NON_EXISTENT_OBJECT_MENTION_NAME //Non-existent object
val mentionText2 = "Anytype"
@ -5452,4 +5464,288 @@ class DefaultBlockViewRendererTest {
}
//endregion
//region Date mention
/**
* ENABLE_RELATIVE_DATES_IN_MENTIONS should be enabled
*/
@Test
fun `should set date mention in text and shift all markups when relative date is shorter then mention text`() {
val title = MockTypicalDocumentFactory.title
val header = MockTypicalDocumentFactory.header
val timestamp = 1733775232L
val relativeDateTomorrow = "Tomorrow"
val relativeDate = RelativeDate.Tomorrow(
initialTimeInMillis = timestamp,
dayOfWeek = DayOfWeekCustom.MONDAY
)
dateProvider.stub {
onBlocking { calculateRelativeDates(dateInSeconds = timestamp) } doReturn relativeDate
}
resourceProvider.stub {
onBlocking { toFormattedString(relativeDate) } doReturn relativeDateTomorrow
}
val mentionTextUpdated1 = "07-12-2024"
val source = "Start 07-12-2024 middle"
val sourceUpdated = "Start $relativeDateTomorrow middle"
val textColor = "F0So"
val mentionTarget1 = "_date_07_12_2024"
val marks: List<Block.Content.Text.Mark> = listOf(
Block.Content.Text.Mark(
range = 0..5,
type = Block.Content.Text.Mark.Type.TEXT_COLOR,
param = textColor
),
Block.Content.Text.Mark(
range = 6..16,
type = Block.Content.Text.Mark.Type.MENTION,
param = mentionTarget1
),
Block.Content.Text.Mark(
range = 17..22,
type = Block.Content.Text.Mark.Type.BOLD
)
)
val a = Block(
id = MockDataFactory.randomUuid(),
children = listOf(),
content = Block.Content.Text(
text = source,
style = Block.Content.Text.Style.P,
marks = marks,
align = Block.Align.AlignLeft
),
fields = Block.Fields.empty()
)
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(header.id, a.id),
fields = Block.Fields.empty(),
content = Block.Content.Smart
)
val fieldsUpdated1 = Block.Fields(
mapOf(
Relations.ID to mentionTarget1,
Relations.NAME to mentionTextUpdated1,
Relations.TIMESTAMP to 1733775232,
Relations.LAYOUT to Layout.DATE.code.toDouble()
)
)
val detailsAmend = mapOf(
mentionTarget1 to fieldsUpdated1,
)
val blocks = listOf(page, header, title, a)
val map = blocks.asMap()
wrapper = BlockViewRenderWrapper(
blocks = map,
renderer = renderer
)
val result = runBlocking {
wrapper.render(
root = page,
anchor = page.id,
focus = Editor.Focus.id(a.id),
indent = 0,
details = Block.Details(detailsAmend)
)
}
val expected = listOf(
BlockView.Title.Basic(
id = title.id,
isFocused = false,
text = title.content<Block.Content.Text>().text,
image = null,
mode = BlockView.Mode.EDIT
),
BlockView.Text.Paragraph(
id = a.id,
text = sourceUpdated,
marks = listOf(
Markup.Mark.TextColor(
from = 0,
to = 5,
color = textColor
),
Markup.Mark.Mention.Date(
from = 6,
to = 14,
param = mentionTarget1
),
Markup.Mark.Bold(
from = 15,
to = 20
)
),
isFocused = true,
alignment = Alignment.START,
decorations = listOf(
BlockView.Decoration(
background = a.parseThemeBackgroundColor()
)
)
)
)
assertEquals(expected = expected, actual = result)
}
/**
* ENABLE_RELATIVE_DATES_IN_MENTIONS should be enabled
*/
@Test
fun `should set date mention in text and shift all markups when relative date is longer then mention text`() {
val title = MockTypicalDocumentFactory.title
val header = MockTypicalDocumentFactory.header
val timestamp = 1733775232L
val relativeDateTomorrow = "TomorrowTomorrowTomorrow"
val relativeDate = RelativeDate.Tomorrow(
initialTimeInMillis = timestamp,
dayOfWeek = DayOfWeekCustom.MONDAY
)
dateProvider.stub {
onBlocking { calculateRelativeDates(dateInSeconds = timestamp) } doReturn relativeDate
}
resourceProvider.stub {
onBlocking { toFormattedString(relativeDate) } doReturn relativeDateTomorrow
}
val mentionTextUpdated1 = "07-12-2024"
val source = "Start 07-12-2024 middle"
val sourceUpdated = "Start $relativeDateTomorrow middle"
val textColor = "F0So"
val mentionTarget1 = "_date_07_12_2024"
val marks: List<Block.Content.Text.Mark> = listOf(
Block.Content.Text.Mark(
range = 0..5,
type = Block.Content.Text.Mark.Type.TEXT_COLOR,
param = textColor
),
Block.Content.Text.Mark(
range = 6..16,
type = Block.Content.Text.Mark.Type.MENTION,
param = mentionTarget1
),
Block.Content.Text.Mark(
range = 17..22,
type = Block.Content.Text.Mark.Type.BOLD
)
)
val a = Block(
id = MockDataFactory.randomUuid(),
children = listOf(),
content = Block.Content.Text(
text = source,
style = Block.Content.Text.Style.P,
marks = marks,
align = Block.Align.AlignLeft
),
fields = Block.Fields.empty()
)
val page = Block(
id = MockDataFactory.randomUuid(),
children = listOf(header.id, a.id),
fields = Block.Fields.empty(),
content = Block.Content.Smart
)
val fieldsUpdated1 = Block.Fields(
mapOf(
Relations.ID to mentionTarget1,
Relations.NAME to mentionTextUpdated1,
Relations.TIMESTAMP to 1733775232,
Relations.LAYOUT to Layout.DATE.code.toDouble()
)
)
val detailsAmend = mapOf(
mentionTarget1 to fieldsUpdated1,
)
val blocks = listOf(page, header, title, a)
val map = blocks.asMap()
wrapper = BlockViewRenderWrapper(
blocks = map,
renderer = renderer
)
val result = runBlocking {
wrapper.render(
root = page,
anchor = page.id,
focus = Editor.Focus.id(a.id),
indent = 0,
details = Block.Details(detailsAmend)
)
}
val expected = listOf(
BlockView.Title.Basic(
id = title.id,
isFocused = false,
text = title.content<Block.Content.Text>().text,
image = null,
mode = BlockView.Mode.EDIT
),
BlockView.Text.Paragraph(
id = a.id,
text = sourceUpdated,
marks = listOf(
Markup.Mark.TextColor(
from = 0,
to = 5,
color = textColor
),
Markup.Mark.Mention.Date(
from = 6,
to = 30,
param = mentionTarget1
),
Markup.Mark.Bold(
from = 31,
to = 36
)
),
isFocused = true,
alignment = Alignment.START,
decorations = listOf(
BlockView.Decoration(
background = a.parseThemeBackgroundColor()
)
)
)
)
assertEquals(expected = expected, actual = result)
}
//endregion
}

View file

@ -131,6 +131,7 @@ import com.anytypeio.anytype.presentation.util.TXT
import com.anytypeio.anytype.presentation.util.dispatchers
import com.anytypeio.anytype.presentation.util.downloader.DocumentFileShareDownloader
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.jraska.livedata.test
import kotlin.test.assertEquals
@ -373,6 +374,9 @@ open class EditorViewModelTest {
@Mock
lateinit var fieldParser: FieldParser
@Mock
lateinit var resourceProvider: ResourceProvider
lateinit var vm: EditorViewModel
lateinit var orchestrator: Orchestrator
@ -3942,7 +3946,8 @@ open class EditorViewModelTest {
coverImageHashProvider = coverImageHashProvider,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider,
),
orchestrator = orchestrator,
analytics = analytics,

View file

@ -5,8 +5,10 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.cash.turbine.test
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.ObjectType.Layout
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.ext.content
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.base.Result
@ -972,10 +974,26 @@ class EditorMentionTest : EditorPresentationTestSetup() {
children = listOf(header.id, a.id)
)
val fieldsUpdated1 = Block.Fields(
mapOf(
Relations.ID to mentionTarget,
Relations.NAME to ""
)
)
val detailsAmend = mapOf(
mentionTarget to fieldsUpdated1,
)
val document = listOf(page, header, title, a)
val params = InterceptEvents.Params(context = root)
val untitled = "UntitledWord"
resourceProvider.stub {
onBlocking { getUntitledTitle() } doReturn untitled
}
openPage.stub {
onBlocking { async(any()) } doReturn Resultat.success(
Result.Success(
@ -985,7 +1003,7 @@ class EditorMentionTest : EditorPresentationTestSetup() {
Event.Command.ShowObject(
context = root,
root = root,
details = Block.Details(),
details = Block.Details(detailsAmend),
relations = emptyList(),
blocks = document,
objectRestrictions = emptyList()
@ -1010,6 +1028,7 @@ class EditorMentionTest : EditorPresentationTestSetup() {
vm.onStart(id = root, space = defaultSpace)
val actual = vm.state.value
val expected = ViewState.Success(
blocks = listOf(
BlockView.Title.Basic(
@ -1030,17 +1049,17 @@ class EditorMentionTest : EditorPresentationTestSetup() {
),
Markup.Mark.Mention.Base(
from = 6,
to = 14,
to = 18,
param = mentionTarget,
isArchived = false
),
Markup.Mark.Strikethrough(
from = 15,
to = 18
from = 19,
to = 22
)
),
indent = 0,
text = "Start Untitled end",
text = "Start $untitled end",
mode = BlockView.Mode.EDIT,
decorations = listOf(
BlockView.Decoration(
@ -1184,6 +1203,325 @@ class EditorMentionTest : EditorPresentationTestSetup() {
clearPendingCoroutines()
}
@Test
fun `should update mention text with details amend event when new text is blank`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val mentionTarget = MockDataFactory.randomUuid()
val givenText = "Start F end"
val a = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = givenText,
marks = listOf(
Block.Content.Text.Mark(
range = IntRange(0, 5),
type = Block.Content.Text.Mark.Type.BOLD
),
Block.Content.Text.Mark(
range = IntRange(6, 7),
type = Block.Content.Text.Mark.Type.MENTION,
param = mentionTarget
),
Block.Content.Text.Mark(
range = IntRange(8, 11),
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
)
),
style = Block.Content.Text.Style.P
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart,
children = listOf(header.id, a.id)
)
val fieldsUpdated1 = Block.Fields(
mapOf(
Relations.ID to mentionTarget,
Relations.NAME to " "
)
)
val detailsAmend = mapOf(
mentionTarget to fieldsUpdated1,
)
val document = listOf(page, header, title, a)
val params = InterceptEvents.Params(context = root)
val untitled = "UntitledWord"
resourceProvider.stub {
onBlocking { getUntitledTitle() } doReturn untitled
}
openPage.stub {
onBlocking { async(any()) } doReturn Resultat.success(
Result.Success(
Payload(
context = root,
events = listOf(
Event.Command.ShowObject(
context = root,
root = root,
details = Block.Details(detailsAmend),
relations = emptyList(),
blocks = document,
objectRestrictions = emptyList()
),
Event.Command.Details.Amend(
context = root,
target = mentionTarget,
details = mapOf(Block.Fields.NAME_KEY to "")
)
)
)
)
)
}
stubInterceptEvents()
stubSearchObjects()
val vm = buildViewModel()
verifyNoInteractions(interceptEvents)
vm.onStart(id = root, space = defaultSpace)
val actual = vm.state.value
val expected = ViewState.Success(
blocks = listOf(
BlockView.Title.Basic(
id = title.id,
isFocused = false,
text = title.content<TXT>().text,
mode = BlockView.Mode.EDIT
),
BlockView.Text.Paragraph(
id = a.id,
cursor = null,
isSelected = false,
isFocused = false,
marks = listOf(
Markup.Mark.Bold(
from = 0,
to = 5
),
Markup.Mark.Mention.Base(
from = 6,
to = 18,
param = mentionTarget,
isArchived = false
),
Markup.Mark.Strikethrough(
from = 19,
to = 22
)
),
indent = 0,
text = "Start $untitled end",
mode = BlockView.Mode.EDIT,
decorations = listOf(
BlockView.Decoration(
background = a.parseThemeBackgroundColor()
)
)
)
)
)
verify(interceptEvents, times(1)).build(params = params)
assertEquals(expected = expected, actual = actual)
clearPendingCoroutines()
}
@Test
fun `should update mention text with details amend event when new text is null`() {
val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
val mentionTarget = MockDataFactory.randomUuid()
val givenText = "Start F end"
val a = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = givenText,
marks = listOf(
Block.Content.Text.Mark(
range = IntRange(0, 5),
type = Block.Content.Text.Mark.Type.BOLD
),
Block.Content.Text.Mark(
range = IntRange(6, 7),
type = Block.Content.Text.Mark.Type.MENTION,
param = mentionTarget
),
Block.Content.Text.Mark(
range = IntRange(8, 11),
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
)
),
style = Block.Content.Text.Style.P
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart,
children = listOf(header.id, a.id)
)
val fieldsUpdated1 = Block.Fields(
mapOf(
Relations.ID to mentionTarget
)
)
val detailsAmend = mapOf(
mentionTarget to fieldsUpdated1,
)
val document = listOf(page, header, title, a)
val params = InterceptEvents.Params(context = root)
val untitled = "UntitledWord"
resourceProvider.stub {
onBlocking { getUntitledTitle() } doReturn untitled
}
openPage.stub {
onBlocking { async(any()) } doReturn Resultat.success(
Result.Success(
Payload(
context = root,
events = listOf(
Event.Command.ShowObject(
context = root,
root = root,
details = Block.Details(detailsAmend),
relations = emptyList(),
blocks = document,
objectRestrictions = emptyList()
),
Event.Command.Details.Amend(
context = root,
target = mentionTarget,
details = mapOf(Block.Fields.NAME_KEY to "")
)
)
)
)
)
}
stubInterceptEvents()
stubSearchObjects()
val vm = buildViewModel()
verifyNoInteractions(interceptEvents)
vm.onStart(id = root, space = defaultSpace)
val actual = vm.state.value
val expected = ViewState.Success(
blocks = listOf(
BlockView.Title.Basic(
id = title.id,
isFocused = false,
text = title.content<TXT>().text,
mode = BlockView.Mode.EDIT
),
BlockView.Text.Paragraph(
id = a.id,
cursor = null,
isSelected = false,
isFocused = false,
marks = listOf(
Markup.Mark.Bold(
from = 0,
to = 5
),
Markup.Mark.Mention.Base(
from = 6,
to = 18,
param = mentionTarget,
isArchived = false
),
Markup.Mark.Strikethrough(
from = 19,
to = 22
)
),
indent = 0,
text = "Start $untitled end",
mode = BlockView.Mode.EDIT,
decorations = listOf(
BlockView.Decoration(
background = a.parseThemeBackgroundColor()
)
)
)
)
)
verify(interceptEvents, times(1)).build(params = params)
assertEquals(expected = expected, actual = actual)
clearPendingCoroutines()
}
private fun clearPendingCoroutines() {
coroutineTestRule.advanceTime(EditorViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
}

View file

@ -122,6 +122,7 @@ import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.dispatchers
import com.anytypeio.anytype.presentation.util.downloader.DocumentFileShareDownloader
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
@ -231,6 +232,9 @@ open class EditorPresentationTestSetup {
@Mock
lateinit var setupBookmark: SetupBookmark
@Mock
lateinit var resourceProvider: ResourceProvider
@Mock
lateinit var createBookmarkBlock: CreateBookmarkBlock
@ -477,7 +481,8 @@ open class EditorPresentationTestSetup {
coverImageHashProvider = coverImageHashProvider,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
),
orchestrator = orchestrator,
analytics = analytics,

View file

@ -30,6 +30,7 @@ import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.render.parseThemeBackgroundColor
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.util.TXT
import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider
import kotlin.test.assertEquals
import kotlinx.coroutines.runBlocking
import net.lachlanmckee.timberjunit.TimberTestRule
@ -90,6 +91,9 @@ class TableBlockRendererTest {
private val storeOfObjectTypes = DefaultStoreOfObjectTypes()
@Mock
lateinit var resourceProvider: ResourceProvider
@Mock
lateinit var fieldParser: FieldParser
@ -102,7 +106,8 @@ class TableBlockRendererTest {
coverImageHashProvider = coverImageHashProvider,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
fieldParser = fieldParser
fieldParser = fieldParser,
resourceProvider = resourceProvider
)
}