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

DROID-2728 Version history | Pagination (#1486)

This commit is contained in:
Konstantin Ivanov 2024-08-22 12:17:08 +02:00 committed by GitHub
parent e7a37b4299
commit 622524aac7
Signed by: github
GPG key ID: B5690EEEBB952194
6 changed files with 197 additions and 45 deletions

View file

@ -104,15 +104,18 @@ class VersionHistoryFragment : BaseBottomSheetComposeFragment() {
private fun NavigationGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Command.Main.route
startDestination = NAVIGATION_MAIN
) {
composable(Command.Main.route) {
composable(NAVIGATION_MAIN) {
VersionHistoryScreen(
state = vm.viewState.collectAsStateWithLifecycle().value,
onItemClick = vm::onGroupItemClicked
state = vm.viewState.collectAsStateWithLifecycle(),
listState = vm.listState.collectAsStateWithLifecycle(),
latestVisibleVersionId = vm.latestVisibleVersionId.collectAsStateWithLifecycle(),
onGroupItemClicked = vm::onGroupItemClicked,
onLastItemScrolled = vm::startPaging
)
}
bottomSheet(Command.VersionPreview.route) {
bottomSheet(NAVIGATION_VERSION_PREVIEW) {
VersionHistoryPreviewScreen(
state = vm.previewViewState.collectAsStateWithLifecycle().value,
editorAdapter = editorAdapter,
@ -140,7 +143,7 @@ class VersionHistoryFragment : BaseBottomSheetComposeFragment() {
}
private fun navigateToVersionPreview() {
navComposeController.navigate(Command.VersionPreview.route)
navComposeController.navigate(NAVIGATION_VERSION_PREVIEW)
}
private fun exitToObjectMenu() {
@ -231,6 +234,9 @@ class VersionHistoryFragment : BaseBottomSheetComposeFragment() {
const val CTX_ARG = "anytype.ui.history.ctx_arg"
const val SPACE_ID_ARG = "anytype.ui.history.space_id_arg"
const val NAVIGATION_MAIN = "main"
const val NAVIGATION_VERSION_PREVIEW = "preview"
fun args(ctx: Id, spaceId: Id) = bundleOf(
CTX_ARG to ctx,
SPACE_ID_ARG to spaceId

View file

@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
@ -28,10 +29,14 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -40,6 +45,7 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -47,18 +53,59 @@ import com.anytypeio.anytype.core_models.primitives.TimeInSeconds
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.Header
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
import com.anytypeio.anytype.core_ui.views.fontInterRegular
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.presentation.history.ListState
import com.anytypeio.anytype.presentation.history.VersionHistoryGroup
import com.anytypeio.anytype.presentation.history.VersionHistoryState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@Composable
fun VersionHistoryScreen(
state: State<VersionHistoryState>,
listState: State<ListState>,
latestVisibleVersionId: State<String>,
onLastItemScrolled: (String) -> Unit,
onGroupItemClicked: (VersionHistoryGroup.Item) -> Unit,
) {
val lazyListState = rememberLazyListState()
val shouldStartPaging = remember {
derivedStateOf {
val lastItem = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()
lastItem?.key == latestVisibleVersionId.value
}
}
LaunchedEffect(key1 = shouldStartPaging) {
snapshotFlow { shouldStartPaging.value }
.distinctUntilChanged()
.filter { it }
.collect {
if (listState.value == ListState.IDLE) {
onLastItemScrolled(latestVisibleVersionId.value)
}
}
}
VersionHistoryMainScreen(
state = state.value,
lazyListState = lazyListState,
onItemClick = onGroupItemClicked
)
}
@Composable
private fun VersionHistoryMainScreen(
state: VersionHistoryState,
lazyListState: LazyListState,
onItemClick: (VersionHistoryGroup.Item) -> Unit
) {
Column(
@ -73,18 +120,29 @@ fun VersionHistoryScreen(
)
Header(text = stringResource(id = R.string.version_history_title))
when (state) {
is VersionHistoryState.Error.GetVersions -> TODO()
VersionHistoryState.Error.NoVersions -> TODO()
is VersionHistoryState.Error.SpaceMembers -> TODO()
is VersionHistoryState.Error.GetVersions -> VersionHistoryError(
error = stringResource(
id = R.string.version_history_error_get_version
)
)
VersionHistoryState.Error.NoVersions -> VersionHistoryError(
error = stringResource(id = R.string.version_history_error_no_versions)
)
is VersionHistoryState.Error.SpaceMembers -> VersionHistoryError(
error = stringResource(id = R.string.version_history_error_get_members)
)
VersionHistoryState.Loading -> VersionHistoryLoading()
is VersionHistoryState.Success -> {
VersionHistorySuccessState(
state = state,
onItemClick = onItemClick
onItemClick = onItemClick,
lazyListState = lazyListState,
)
}
}
}
}
@ -100,18 +158,32 @@ private fun VersionHistoryLoading() {
}
}
@Composable
private fun VersionHistoryError(error: String) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 20.dp),
text = error,
style = Caption1Medium,
color = colorResource(id = R.color.palette_dark_red),
textAlign = TextAlign.Center
)
}
}
@Composable
private fun VersionHistorySuccessState(
state: VersionHistoryState.Success,
onItemClick: (VersionHistoryGroup.Item) -> Unit
onItemClick: (VersionHistoryGroup.Item) -> Unit,
lazyListState: LazyListState
) {
var expandedGroupId by remember {
mutableStateOf<String?>(state.groups.firstOrNull { it.isExpanded }?.id)
mutableStateOf(state.groups.firstOrNull { it.isExpanded }?.id)
}
val lazyListState = rememberLazyListState()
LazyColumn(
state = lazyListState,
modifier = Modifier
@ -304,7 +376,7 @@ fun VersionHistoryAvatarTextStyle() = TextStyle(
)
@Composable
private fun SpaceListScreenPreview() {
VersionHistoryScreen(
VersionHistoryMainScreen(
state = VersionHistoryState.Success(
groups = listOf(
VersionHistoryGroup(
@ -384,7 +456,8 @@ private fun SpaceListScreenPreview() {
)
)
),
onItemClick = {}
onItemClick = {},
lazyListState = rememberLazyListState()
)
}
@ -404,8 +477,9 @@ const val MEMBERS_ICONS_MAX_SIZE = 3
)
@Composable
private fun SpaceListScreenPreviewLoading() {
VersionHistoryScreen(
state = VersionHistoryState.Loading,
onItemClick = {}
VersionHistoryMainScreen(
state = VersionHistoryState.Error.NoVersions,
onItemClick = {},
lazyListState = rememberLazyListState()
)
}

View file

@ -25,6 +25,6 @@ class GetVersions @Inject constructor(
data class Params(
val objectId: Id,
val lastVersion: Id? = null,
val limit: Int = 200
val limit: Int = 300
)
}

View file

@ -1741,6 +1741,9 @@ Please provide specific details of your needs here.</string>
<string name="sync_status_unrecognized">Unrecognized error</string>
<string name="version_history_title">Version history</string>
<string name="version_history_error_no_versions">No versions found</string>
<string name="version_history_error_get_version">Error getting version history</string>
<string name="version_history_error_get_members">Error getting members</string>
<string name="new_object">New object</string>
<string name="vault_my_spaces">My spaces</string>

View file

@ -39,6 +39,7 @@ dependencies {
implementation libs.lifecycleViewModel
implementation libs.lifecycleLiveData
implementation libs.compose
implementation libs.timber

View file

@ -42,6 +42,9 @@ import java.time.ZoneId
import java.util.Locale
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import timber.log.Timber
@ -59,24 +62,55 @@ class VersionHistoryViewModel(
) : ViewModel(), BlockViewRenderer by renderer {
private val _viewState = MutableStateFlow<VersionHistoryState>(VersionHistoryState.Loading)
val viewState = _viewState
val viewState = _viewState.asStateFlow()
private val _previewViewState =
MutableStateFlow<VersionHistoryPreviewScreen>(VersionHistoryPreviewScreen.Hidden)
val previewViewState = _previewViewState
val navigation = MutableSharedFlow<Command>(0)
private val _members = MutableStateFlow<List<ObjectWrapper.Basic>>(emptyList())
//Paging
private val canPaginate = MutableStateFlow(false)
val listState = MutableStateFlow(ListState.IDLE)
val latestVisibleVersionId = MutableStateFlow("")
private val _versions = MutableStateFlow<List<Version>>(emptyList())
init {
Timber.d("VersionHistoryViewModel created")
getSpaceMembers()
getHistoryVersions(objectId = vmParams.objectId)
viewModelScope.launch {
sendAnalyticsShowVersionHistoryScreen(analytics)
}
viewModelScope.launch {
combine(
_members,
_versions
) { members, versions ->
members to versions
}.collectLatest { (members, versions) ->
if (members.isNotEmpty() && versions.isNotEmpty()) {
handleVersionsSuccess(versions, members)
}
}
}
}
fun onStart() {
Timber.d("VersionHistoryViewModel started")
}
fun startPaging(latestVersionId: String) {
Timber.d("Start paging, latestVersionId: $latestVersionId, canPaginate: ${canPaginate.value}")
if (canPaginate.value) {
getHistoryVersions(
objectId = vmParams.objectId,
latestVersionId = latestVersionId
)
}
}
fun onGroupItemClicked(item: VersionHistoryGroup.Item) {
viewModelScope.launch {
_previewViewState.value = VersionHistoryPreviewScreen.Loading
@ -107,6 +141,7 @@ class VersionHistoryViewModel(
)
}
}
else -> {}
}
}
@ -116,7 +151,8 @@ class VersionHistoryViewModel(
relation: RelationKey,
relationFormat: Relation.Format
) {
val currentState = (_previewViewState.value as? VersionHistoryPreviewScreen.Success) ?: return
val currentState =
(_previewViewState.value as? VersionHistoryPreviewScreen.Success) ?: return
val isSet = currentState.isSet
when (relationFormat) {
RelationFormat.SHORT_TEXT,
@ -180,33 +216,48 @@ class VersionHistoryViewModel(
).process(
failure = {
Timber.e(it, "Error while fetching new member")
_viewState.value = VersionHistoryState.Error.SpaceMembers(it.message.orEmpty())
_viewState.value = VersionHistoryState.Error.SpaceMembers
},
success = { members ->
if (members.isEmpty()) {
_viewState.value =
VersionHistoryState.Error.SpaceMembers("No members found")
VersionHistoryState.Error.SpaceMembers
} else {
getHistoryVersions(vmParams.objectId, members)
_members.value = members
}
}
)
}
}
private fun getHistoryVersions(objectId: String, members: List<ObjectWrapper.Basic>) {
private fun getHistoryVersions(objectId: String, latestVersionId: String = "") {
viewModelScope.launch {
val params = GetVersions.Params(objectId = objectId)
val params = GetVersions.Params(
objectId = objectId,
lastVersion = latestVersionId,
limit = VERSIONS_MAX_LIMIT
)
getVersions.async(params).fold(
onSuccess = { versions ->
if (versions.isEmpty()) {
_viewState.value = VersionHistoryState.Error.NoVersions
canPaginate.value = versions.size == VERSIONS_MAX_LIMIT
if (latestVersionId.isEmpty()) {
if (versions.isEmpty()) {
_viewState.value = VersionHistoryState.Error.NoVersions
} else {
_versions.value = versions
}
} else {
handleVersionsSuccess(versions, members)
_versions.value += versions
}
listState.value = ListState.IDLE
if (canPaginate.value) {
latestVisibleVersionId.value = versions.lastOrNull()?.id ?: ""
}
},
onFailure = {
_viewState.value = VersionHistoryState.Error.GetVersions(it.message.orEmpty())
_viewState.value = VersionHistoryState.Error.GetVersions
listState.value =
if (latestVersionId.isEmpty()) ListState.ERROR else ListState.PAGINATION_EXHAUST
},
onLoading = {}
)
@ -254,13 +305,16 @@ class VersionHistoryViewModel(
val spaceMemberLatestVersion =
spaceMemberVersions.firstOrNull()?.firstOrNull() ?: return@mapNotNull null
val spaceMemberOldestVersion =
spaceMemberVersions.lastOrNull()?.lastOrNull() ?: return@mapNotNull null
val groupItems = spaceMemberVersions.toGroupItems(
spaceMembers = spaceMembers,
locale = locale
)
VersionHistoryGroup(
id = spaceMemberLatestVersion.id,
id = spaceMemberOldestVersion.id,
title = getGroupTitle(spaceMemberLatestVersion.timestamp, locale),
icons = groupItems.distinctBy { it.spaceMember }.mapNotNull { it.icon },
items = groupItems
@ -412,7 +466,8 @@ class VersionHistoryViewModel(
val event = payload.events
.filterIsInstance<Event.Command.ShowObject>()
.first()
val obj = ObjectWrapper.Basic(event.details.details[vmParams.objectId]?.map.orEmpty())
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,
@ -446,29 +501,34 @@ class VersionHistoryViewModel(
val spaceId: SpaceId
)
sealed class Command(val route: String) {
data object Main : Command("main")
data object VersionPreview : Command("version preview")
data object ExitToObject : Command("")
data class RelationMultiSelect(val relationKey: RelationKey, val isSet: Boolean) : Command("relation_select")
data class RelationObject(val relationKey: RelationKey, val isSet: Boolean) : Command("relation_object")
data class RelationDate(val relationKey: RelationKey, val isSet: Boolean) : Command("relation_date")
data class RelationText(val relationKey: RelationKey, val isSet: Boolean) : Command("relation_text")
sealed class Command {
data object Main : Command()
data object VersionPreview : Command()
data object ExitToObject : Command()
data class RelationMultiSelect(val relationKey: RelationKey, val isSet: Boolean) : Command()
data class RelationObject(val relationKey: RelationKey, val isSet: Boolean) : Command()
data class RelationDate(val relationKey: RelationKey, val isSet: Boolean) : Command()
data class RelationText(val relationKey: RelationKey, val isSet: Boolean) : Command()
}
companion object {
const val GROUP_BY_DAY_FORMAT = "d MM yyyy"
const val GROUP_DATE_FORMAT_CURRENT_YEAR = "MMMM d"
const val GROUP_DATE_FORMAT_OTHER_YEAR = "MMMM d, yyyy"
const val VERSIONS_MAX_LIMIT = 200
}
}
sealed class VersionHistoryState {
data object Loading : VersionHistoryState()
data class Success(val groups: List<VersionHistoryGroup>) : VersionHistoryState()
data class Success(
val groups: List<VersionHistoryGroup>
) : VersionHistoryState()
sealed class Error : VersionHistoryState() {
data class SpaceMembers(val message: String) : Error()
data class GetVersions(val message: String) : Error()
data object SpaceMembers : Error()
data object GetVersions : Error()
data object NoVersions : Error()
}
}
@ -512,4 +572,12 @@ data class VersionHistoryGroup(
data object Yesterday : GroupTitle()
data class Date(val date: String) : GroupTitle()
}
}
enum class ListState {
IDLE,
LOADING,
PAGINATING,
ERROR,
PAGINATION_EXHAUST,
}