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:
parent
e7a37b4299
commit
622524aac7
6 changed files with 197 additions and 45 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ dependencies {
|
|||
|
||||
implementation libs.lifecycleViewModel
|
||||
implementation libs.lifecycleLiveData
|
||||
implementation libs.compose
|
||||
|
||||
implementation libs.timber
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue