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

DROID-2727 Version history | Enable version history for sets and collections (#1542)

This commit is contained in:
Konstantin Ivanov 2024-09-16 14:29:56 +02:00 committed by GitHub
parent 23706d1fe8
commit 0544fb7cf9
Signed by: github
GPG key ID: B5690EEEBB952194
8 changed files with 348 additions and 52 deletions

View file

@ -4,10 +4,13 @@ import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.core_utils.di.scope.PerModal
import com.anytypeio.anytype.presentation.history.VersionHistoryVMFactory
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel
import com.anytypeio.anytype.presentation.sets.state.DefaultObjectStateReducer
import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer
import com.anytypeio.anytype.ui.history.VersionHistoryFragment
import dagger.Binds
import dagger.BindsInstance
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@Subcomponent(
@ -32,6 +35,11 @@ interface VersionHistoryComponent {
@Module
object VersionHistoryModule {
@JvmStatic
@Provides
@PerModal
fun provideObjectStateReducer(): ObjectStateReducer = DefaultObjectStateReducer()
@Module
interface Declarations {

View file

@ -19,6 +19,7 @@ import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_ui.features.dataview.ViewerGridHeaderAdapter
import com.anytypeio.anytype.core_ui.features.editor.BlockAdapter
import com.anytypeio.anytype.core_ui.features.editor.DragAndDropAdapterDelegate
import com.anytypeio.anytype.core_ui.features.history.VersionHistoryPreviewScreen
@ -71,6 +72,7 @@ class VersionHistoryFragment : BaseBottomSheetComposeFragment() {
vm.proceedWithClick(it)
}
)
private val viewerGridHeaderAdapter by lazy { ViewerGridHeaderAdapter() }
@OptIn(ExperimentalMaterialNavigationApi::class)
override fun onCreateView(
@ -120,7 +122,8 @@ class VersionHistoryFragment : BaseBottomSheetComposeFragment() {
state = vm.previewViewState.collectAsStateWithLifecycle().value,
editorAdapter = editorAdapter,
onDismiss = vm::proceedWithHidePreview,
onRestore = vm::proceedWithRestore
onRestore = vm::proceedWithRestore,
gridAdapter = viewerGridHeaderAdapter
)
}
}

View file

@ -1,5 +1,8 @@
package com.anytypeio.anytype.core_ui.features.history
import android.view.LayoutInflater
import android.widget.HorizontalScrollView
import android.widget.TextView
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -39,6 +42,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.features.dataview.ViewerGridHeaderAdapter
import com.anytypeio.anytype.core_ui.features.editor.BlockAdapter
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
@ -55,7 +59,8 @@ fun VersionHistoryPreviewScreen(
state: VersionHistoryPreviewScreen,
onDismiss: () -> Unit,
onRestore: () -> Unit,
editorAdapter: BlockAdapter
editorAdapter: BlockAdapter,
gridAdapter: ViewerGridHeaderAdapter
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val versionTitle = remember { mutableStateOf("") }
@ -87,21 +92,22 @@ fun VersionHistoryPreviewScreen(
.padding(vertical = 6.dp)
)
Header(title = versionTitle.value, icon = versionIcon.value)
AndroidView(
factory = { context ->
RecyclerView(context).apply {
layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.MATCH_PARENT
)
layoutManager = LinearLayoutManager(context)
adapter = editorAdapter
}
},
update = {
editorAdapter.updateWithDiffUtil(state.blocks)
when (state) {
is VersionHistoryPreviewScreen.Success.Editor -> {
EditorScreen(
editorAdapter = editorAdapter,
state = state
)
}
)
is VersionHistoryPreviewScreen.Success.Set -> {
ObjectSetScreen(
editorAdapter = editorAdapter,
state = state,
gridAdapter = gridAdapter
)
}
}
}
Buttons(onDismiss = onDismiss, onRestore = onRestore)
}
@ -109,6 +115,62 @@ fun VersionHistoryPreviewScreen(
}
}
@Composable
private fun EditorScreen(
editorAdapter: BlockAdapter,
state: VersionHistoryPreviewScreen.Success.Editor
) {
AndroidView(
factory = { context ->
RecyclerView(context).apply {
layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.MATCH_PARENT
)
layoutManager = LinearLayoutManager(context)
adapter = editorAdapter
}
},
update = {
editorAdapter.updateWithDiffUtil(state.blocks)
}
)
}
@Composable
private fun ObjectSetScreen(
editorAdapter: BlockAdapter,
gridAdapter: ViewerGridHeaderAdapter,
state: VersionHistoryPreviewScreen.Success.Set
) {
AndroidView(
factory = { context ->
val view = LayoutInflater.from(context).inflate(
R.layout.layout_version_history_set,
null
).apply {
findViewById<RecyclerView>(R.id.objectHeader).apply {
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
adapter = editorAdapter
}
findViewById<HorizontalScrollView>(R.id.gridContainer).apply {
findViewById<RecyclerView>(R.id.rvHeader).apply {
adapter = gridAdapter
}
}
}
view
},
update = {
it.findViewById<TextView>(R.id.tvCurrentViewerName).apply {
text = state.viewer?.name
}
editorAdapter.updateWithDiffUtil(state.blocks)
gridAdapter.submitList(state.viewer?.columns)
}
)
}
@Composable
private fun Header(title: String, icon: ObjectIcon?) {
Box(
@ -165,14 +227,18 @@ private fun BoxScope.Buttons(
text = stringResource(id = R.string.cancel),
onClick = onDismiss,
size = ButtonSize.LargeSecondary,
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp).weight(1.0f)
modifier = Modifier
.padding(top = 16.dp, bottom = 32.dp)
.weight(1.0f)
)
Spacer(modifier = Modifier.width(9.dp))
ButtonPrimary(
text = stringResource(id = R.string.restore),
onClick = onRestore,
size = ButtonSize.Large,
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp).weight(1.0f)
modifier = Modifier
.padding(top = 16.dp, bottom = 32.dp)
.weight(1.0f)
)
}
}

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/objectHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/default_collection_dv_header_height">
<TextView
android:id="@+id/tvCurrentViewerName"
style="@style/ViewerTitleStyle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:drawablePadding="2dp"
android:gravity="center_vertical"
android:paddingStart="@dimen/dp_20"
app:drawableEndCompat="@drawable/ic_arrow_expand_dv_viewer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="All" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/shape_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/gridContainer"
layout="@layout/item_viewer_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -7,4 +7,8 @@
<item name="menuChangeType" type="id"/>
<item name="menuOpenSet" type="id"/>
<item name="menuCreateSet" type="id"/>
<item name="object_set_header" type="id"/>
<item name="featuredRelationsWidget" type="id"/>
</resources>

View file

@ -9,9 +9,12 @@ import com.anytypeio.anytype.domain.history.ShowVersion
import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel.VmParams
import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer
import javax.inject.Inject
class VersionHistoryVMFactory @Inject constructor(
@ -24,7 +27,10 @@ class VersionHistoryVMFactory @Inject constructor(
private val urlBuilder: UrlBuilder,
private val showVersion: ShowVersion,
private val setVersion: SetVersion,
private val renderer: DefaultBlockViewRenderer
private val renderer: DefaultBlockViewRenderer,
private val setStateReducer: ObjectStateReducer,
private val coverImageHashProvider: CoverImageHashProvider,
private val storeOfRelations: StoreOfRelations
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -39,7 +45,10 @@ class VersionHistoryVMFactory @Inject constructor(
urlBuilder = urlBuilder,
showVersion = showVersion,
renderer = renderer,
setVersion = setVersion
setVersion = setVersion,
setStateReducer = setStateReducer,
coverImageHashProvider = coverImageHashProvider,
storeOfRelations = storeOfRelations
) as T
}
}

View file

@ -6,6 +6,7 @@ import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.ext.asMap
@ -22,9 +23,11 @@ import com.anytypeio.anytype.domain.history.ShowVersion
import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.presentation.editor.Editor.Mode
import com.anytypeio.anytype.presentation.editor.EditorViewModel.Companion.INITIAL_INDENT
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.editor.render.BlockViewRenderer
@ -33,9 +36,15 @@ import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenVersionPr
import com.anytypeio.anytype.presentation.extension.sendAnalyticsShowVersionHistoryScreen
import com.anytypeio.anytype.presentation.extension.sendAnalyticsVersionHistoryRestore
import com.anytypeio.anytype.presentation.history.VersionHistoryGroup.GroupTitle
import com.anytypeio.anytype.presentation.mapper.toViewerColumns
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
import com.anytypeio.anytype.presentation.relations.getRelationFormat
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import com.anytypeio.anytype.presentation.sets.model.Viewer
import com.anytypeio.anytype.presentation.sets.state.ObjectState
import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer
import com.anytypeio.anytype.presentation.sets.viewerByIdOrFirst
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
@ -45,6 +54,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import timber.log.Timber
@ -58,7 +69,10 @@ class VersionHistoryViewModel(
private val urlBuilder: UrlBuilder,
private val showVersion: ShowVersion,
private val setVersion: SetVersion,
private val renderer: DefaultBlockViewRenderer
private val renderer: DefaultBlockViewRenderer,
private val setStateReducer: ObjectStateReducer,
private val coverImageHashProvider: CoverImageHashProvider,
private val storeOfRelations: StoreOfRelations
) : ViewModel(), BlockViewRenderer by renderer {
private val _viewState = MutableStateFlow<VersionHistoryState>(VersionHistoryState.Loading)
@ -76,8 +90,13 @@ class VersionHistoryViewModel(
val latestVisibleVersionId = MutableStateFlow("")
private val _versions = MutableStateFlow<List<Version>>(emptyList())
private val defaultPayloadConsumer: suspend (Payload) -> Unit = { payload ->
setStateReducer.dispatch(payload.events)
}
init {
Timber.d("VersionHistoryViewModel created")
viewModelScope.launch { setStateReducer.run() }
getSpaceMembers()
getHistoryVersions(objectId = vmParams.objectId)
viewModelScope.launch {
@ -95,6 +114,34 @@ class VersionHistoryViewModel(
}
}
}
viewModelScope.launch {
setStateReducer.state
.filterIsInstance<ObjectState.DataView>()
.distinctUntilChanged()
.collectLatest { state ->
val viewer = mapToViewer(state)
when (val currentState = _previewViewState.value) {
VersionHistoryPreviewScreen.Loading -> {
_previewViewState.value = VersionHistoryPreviewScreen.Success.Set(
versionId = "",
blocks = emptyList(),
dateFormatted = "",
timeFormatted = "",
viewer = viewer,
icon = null
)
}
is VersionHistoryPreviewScreen.Success.Set -> {
_previewViewState.value = currentState.copy(
viewer = viewer
)
}
else -> {
Timber.d("Version preview state is not loading or success.set, skipping state update")
}
}
}
}
}
fun onStart() {
@ -142,7 +189,9 @@ class VersionHistoryViewModel(
}
}
else -> {}
else -> {
Timber.d("No interaction allowed with this listener type: $click")
}
}
}
}
@ -153,7 +202,7 @@ class VersionHistoryViewModel(
) {
val currentState =
(_previewViewState.value as? VersionHistoryPreviewScreen.Success) ?: return
val isSet = currentState.isSet
val isSet = currentState is VersionHistoryPreviewScreen.Success.Set
when (relationFormat) {
RelationFormat.SHORT_TEXT,
RelationFormat.LONG_TEXT,
@ -468,27 +517,13 @@ class VersionHistoryViewModel(
.first()
val obj =
ObjectWrapper.Basic(event.details.details[vmParams.objectId]?.map.orEmpty())
val root = event.blocks.first { it.id == vmParams.objectId }
val blocks = event.blocks.asMap().render(
mode = Mode.Read,
root = root,
focus = Editor.Focus.empty(),
anchor = vmParams.objectId,
indent = INITIAL_INDENT,
details = event.details,
relationLinks = event.relationLinks,
restrictions = event.objectRestrictions,
selection = emptySet()
)
val currentState = _previewViewState.value
if (currentState !is VersionHistoryPreviewScreen.Hidden) {
_previewViewState.value = VersionHistoryPreviewScreen.Success(
versionId = item.id,
blocks = blocks,
dateFormatted = item.dateFormatted,
timeFormatted = item.timeFormatted,
icon = item.icon,
isSet = obj.layout.isDataView()
parseObject(
payload = payload,
event = event,
item = item,
obj = obj
)
}
}
@ -496,6 +531,100 @@ class VersionHistoryViewModel(
)
}
private suspend fun parseObject(
payload: Payload,
//TODO: Refactoring: update ShowVersion response to include ObjectView instead of Payload
event: Event.Command.ShowObject,
item: VersionHistoryGroup.Item,
obj: ObjectWrapper.Basic
) {
if (obj.layout.isDataView()) {
defaultPayloadConsumer(payload)
val root = event.blocks.first { it.id == vmParams.objectId }
val blocks = event.blocks.asMap().render(
mode = Mode.Read,
root = root,
focus = Editor.Focus.empty(),
anchor = vmParams.objectId,
indent = INITIAL_INDENT,
details = event.details,
relationLinks = event.relationLinks,
restrictions = event.objectRestrictions,
selection = emptySet()
).filterNot { it is BlockView.DataView }
when (val currentState = _previewViewState.value) {
VersionHistoryPreviewScreen.Loading -> {
_previewViewState.value = VersionHistoryPreviewScreen.Success.Set(
versionId = item.id,
blocks = blocks,
dateFormatted = item.dateFormatted,
timeFormatted = item.timeFormatted,
viewer = null,
icon = item.icon
)
}
is VersionHistoryPreviewScreen.Success.Set -> {
_previewViewState.value = currentState.copy(
versionId = item.id,
dateFormatted = item.dateFormatted,
timeFormatted = item.timeFormatted,
icon = item.icon,
blocks = blocks
)
}
else -> {
Timber.d("Version preview state is not loading or success.set, skipping state update")
}
}
} else {
val root = event.blocks.first { it.id == vmParams.objectId }
val blocks = event.blocks.asMap().render(
mode = Mode.Read,
root = root,
focus = Editor.Focus.empty(),
anchor = vmParams.objectId,
indent = INITIAL_INDENT,
details = event.details,
relationLinks = event.relationLinks,
restrictions = event.objectRestrictions,
selection = emptySet()
)
_previewViewState.value = VersionHistoryPreviewScreen.Success.Editor(
versionId = item.id,
blocks = blocks,
dateFormatted = item.dateFormatted,
timeFormatted = item.timeFormatted,
icon = item.icon
)
}
}
private suspend fun mapToViewer(objectState: ObjectState.DataView): Viewer.GridView? {
val dvViewer = objectState.viewerByIdOrFirst(null)
val viewerRelations = dvViewer?.viewerRelations ?: return null
val vmap = viewerRelations.associateBy { it.key }
val dataViewRelations = objectState.dataViewContent.relationLinks.mapNotNull {
storeOfRelations.getByKey(it.key)
}
val visibleRelations = dataViewRelations.filter { relation ->
val vr = vmap[relation.key]
vr?.isVisible ?: false
}
val columns = viewerRelations.toViewerColumns(
relations = visibleRelations,
filterBy = listOf(ObjectSetConfig.NAME_KEY)
)
return Viewer.GridView(
id = dvViewer.id,
name = dvViewer.name,
columns = columns,
rows = emptyList()
)
}
data class VmParams(
val objectId: Id,
val spaceId: SpaceId
@ -536,15 +665,30 @@ sealed class VersionHistoryState {
sealed class VersionHistoryPreviewScreen {
data object Hidden : VersionHistoryPreviewScreen()
data object Loading : VersionHistoryPreviewScreen()
data class Success(
val versionId: Id,
val blocks: List<BlockView>,
val dateFormatted: String,
val timeFormatted: String,
val icon: ObjectIcon?,
val isSet: Boolean
) :
VersionHistoryPreviewScreen()
sealed class Success : VersionHistoryPreviewScreen() {
abstract val versionId: Id
abstract val icon: ObjectIcon?
abstract val dateFormatted: String
abstract val timeFormatted: String
data class Editor(
override val versionId: Id,
override val dateFormatted: String,
override val timeFormatted: String,
override val icon: ObjectIcon?,
val blocks: List<BlockView>
) : Success()
data class Set(
override val versionId: Id,
override val dateFormatted: String,
override val timeFormatted: String,
override val icon: ObjectIcon?,
val viewer: Viewer.GridView?,
val blocks: List<BlockView>
) : Success()
}
data class Error(val message: String) : VersionHistoryPreviewScreen()
}

View file

@ -18,13 +18,16 @@ import com.anytypeio.anytype.domain.history.ShowVersion
import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel.Companion.GROUP_DATE_FORMAT_OTHER_YEAR
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel.Companion.VERSIONS_MAX_LIMIT
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import com.anytypeio.anytype.presentation.sets.state.DefaultObjectStateReducer
import com.anytypeio.anytype.presentation.widgets.collection.DateProviderImpl
import java.time.ZoneId
import java.util.Locale
@ -99,6 +102,12 @@ class VersionHistoryViewModelTest {
@Mock
lateinit var renderer: DefaultBlockViewRenderer
@Mock
lateinit var storeOfRelations: StoreOfRelations
@Mock
lateinit var coverImageHashProvider: CoverImageHashProvider
lateinit var spaceManager: SpaceManager
lateinit var vm: VersionHistoryViewModel
@ -544,7 +553,10 @@ class VersionHistoryViewModelTest {
urlBuilder = urlBuilder,
renderer = renderer,
setVersion = setVersion,
showVersion = showVersion
showVersion = showVersion,
setStateReducer = DefaultObjectStateReducer(),
storeOfRelations = storeOfRelations,
coverImageHashProvider = coverImageHashProvider
)
}
}