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

DROID-2663 Version history | Ui + logic, part 1 (#1437)

This commit is contained in:
Konstantin Ivanov 2024-07-29 13:47:22 +02:00 committed by GitHub
parent 7bd9f99586
commit 8f3af5035e
Signed by: github
GPG key ID: B5690EEEBB952194
25 changed files with 1327 additions and 22 deletions

View file

@ -50,6 +50,7 @@ import com.anytypeio.anytype.di.feature.ViewerSortModule
import com.anytypeio.anytype.di.feature.auth.DaggerDeletedAccountComponent
import com.anytypeio.anytype.di.feature.cover.UnsplashModule
import com.anytypeio.anytype.di.feature.gallery.DaggerGalleryInstallationComponent
import com.anytypeio.anytype.di.feature.history.DaggerVersionHistoryComponent
import com.anytypeio.anytype.di.feature.home.DaggerHomeScreenComponent
import com.anytypeio.anytype.di.feature.library.DaggerLibraryComponent
import com.anytypeio.anytype.di.feature.membership.DaggerMembershipComponent
@ -100,6 +101,7 @@ import com.anytypeio.anytype.di.feature.widgets.SelectWidgetTypeModule
import com.anytypeio.anytype.di.main.MainComponent
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel
import com.anytypeio.anytype.presentation.library.LibraryViewModel
import com.anytypeio.anytype.presentation.multiplayer.RequestJoinSpaceViewModel
import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel
@ -1033,6 +1035,12 @@ class ComponentManager(
.build()
}
val versionHistoryComponent =
ComponentMapWithParam { vmParams: VersionHistoryViewModel.VmParams ->
DaggerVersionHistoryComponent.factory()
.create(findComponentDependencies(), vmParams)
}
class Component<T>(private val builder: () -> T) {
private var instance: T? = null

View file

@ -0,0 +1,63 @@
package com.anytypeio.anytype.di.feature.history
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
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.presentation.history.VersionHistoryVMFactory
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel
import com.anytypeio.anytype.ui.history.VersionHistoryFragment
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
import dagger.Module
@Component(
dependencies = [VersionHistoryComponentDependencies::class],
modules = [
VersionHistoryModule::class,
VersionHistoryModule.Declarations::class
]
)
@PerScreen
interface VersionHistoryComponent {
@Component.Factory
interface Factory {
fun create(
dependencies: VersionHistoryComponentDependencies,
@BindsInstance vmParams: VersionHistoryViewModel.VmParams
): VersionHistoryComponent
}
fun inject(fragment: VersionHistoryFragment)
}
@Module
object VersionHistoryModule {
@Module
interface Declarations {
@PerScreen
@Binds
fun bindViewModelFactory(
factory: VersionHistoryVMFactory
): ViewModelProvider.Factory
}
}
interface VersionHistoryComponentDependencies : ComponentDependencies {
fun analytics(): Analytics
fun blockRepository(): BlockRepository
fun appCoroutineDispatchers(): AppCoroutineDispatchers
fun dateProvider(): DateProvider
fun localeProvider(): LocaleProvider
fun provideUrlBuilder(): UrlBuilder
}

View file

@ -20,6 +20,7 @@ import com.anytypeio.anytype.di.feature.PersonalizationSettingsSubComponent
import com.anytypeio.anytype.di.feature.SplashDependencies
import com.anytypeio.anytype.di.feature.auth.DeletedAccountDependencies
import com.anytypeio.anytype.di.feature.gallery.GalleryInstallationComponentDependencies
import com.anytypeio.anytype.di.feature.history.VersionHistoryComponentDependencies
import com.anytypeio.anytype.di.feature.home.HomeScreenDependencies
import com.anytypeio.anytype.di.feature.library.LibraryDependencies
import com.anytypeio.anytype.di.feature.multiplayer.RequestJoinSpaceDependencies
@ -124,7 +125,8 @@ interface MainComponent :
GalleryInstallationComponentDependencies,
NotificationDependencies,
GlobalSearchDependencies,
MembershipUpdateComponentDependencies
MembershipUpdateComponentDependencies,
VersionHistoryComponentDependencies
{
fun inject(app: AndroidApplication)
@ -345,4 +347,9 @@ abstract class ComponentDependenciesModule {
@IntoMap
@ComponentDependenciesKey(MembershipUpdateComponentDependencies::class)
abstract fun provideMembershipUpdateComponentDependencies(component: MainComponent): ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(VersionHistoryComponentDependencies::class)
abstract fun provideVersionHistoryComponentDependencies(component: MainComponent): ComponentDependencies
}

View file

@ -33,6 +33,7 @@ import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectFragment
import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectSetFragment
import com.anytypeio.anytype.ui.editor.layout.ObjectLayoutFragment
import com.anytypeio.anytype.ui.editor.modals.IconPickerFragmentBase
import com.anytypeio.anytype.ui.history.VersionHistoryFragment
import com.anytypeio.anytype.ui.linking.BacklinkAction
import com.anytypeio.anytype.ui.linking.BacklinkOrAddToObjectFragment
import com.anytypeio.anytype.ui.moving.OnMoveToAction
@ -80,7 +81,7 @@ abstract class ObjectMenuBaseFragment :
override fun onStart() {
click(binding.objectDiagnostics) { vm.onDiagnosticsClicked(ctx = ctx) }
click(binding.optionHistory) { vm.onHistoryClicked() }
click(binding.optionHistory) { vm.onHistoryClicked(ctx = ctx, space = space) }
click(binding.optionLayout) { vm.onLayoutClicked(ctx = ctx, space = space) }
click(binding.optionIcon) { vm.onIconClicked(ctx = ctx, space = space) }
click(binding.optionRelations) { vm.onRelationsClicked() }
@ -166,6 +167,18 @@ abstract class ObjectMenuBaseFragment :
}
startActivity(Intent.createChooser(intent, null))
}
is ObjectMenuViewModelBase.Command.OpenHistoryScreen -> {
runCatching {
findNavController().navigate(
R.id.versionHistoryScreen,
VersionHistoryFragment.args(
ctx = ctx,
spaceId = space
)
)
}
}
}
}

View file

@ -0,0 +1,82 @@
package com.anytypeio.anytype.ui.history
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.features.history.VersionHistoryScreen
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.history.VersionHistoryVMFactory
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel
import javax.inject.Inject
class VersionHistoryFragment : BaseBottomSheetComposeFragment() {
private val ctx get() = argString(CTX_ARG)
private val spaceId get() = argString(SPACE_ID_ARG)
@Inject
lateinit var factory: VersionHistoryVMFactory
private val vm by viewModels<VersionHistoryViewModel> { factory }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
VersionHistoryScreen(
state = vm.viewState.collectAsStateWithLifecycle().value,
onGroupClick = vm::onGroupClicked,
onItemClick = vm::onGroupItemClicked
)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupBottomSheetBehavior(74)
}
override fun onStart() {
super.onStart()
vm.onStart()
}
override fun injectDependencies() {
val vmParams = VersionHistoryViewModel.VmParams(
objectId = ctx,
spaceId = spaceId
)
componentManager().versionHistoryComponent.get(
param = vmParams,
key = ctx + spaceId
).inject(this)
}
override fun releaseDependencies() {
componentManager().versionHistoryComponent.release(ctx + spaceId)
}
companion object {
const val CTX_ARG = "anytype.ui.history.ctx_arg"
const val SPACE_ID_ARG = "anytype.ui.history.space_id_arg"
fun args(ctx: Id, spaceId: Id) = bundleOf(
CTX_ARG to ctx,
SPACE_ID_ARG to spaceId
)
}
}

View file

@ -68,7 +68,9 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ThemeColor
@ -666,10 +668,11 @@ fun GlobalSearchObjectIcon(
icon: ObjectIcon,
modifier: Modifier,
iconSize: Dp = 48.dp,
onTaskIconClicked: (Boolean) -> Unit = {}
onTaskIconClicked: (Boolean) -> Unit = {},
fontSize: TextUnit = 28.sp
) {
when (icon) {
is ObjectIcon.Profile.Avatar -> DefaultProfileAvatarIcon(modifier, iconSize, icon)
is ObjectIcon.Profile.Avatar -> DefaultProfileAvatarIcon(modifier, iconSize, icon, fontSize)
is ObjectIcon.Profile.Image -> defaultProfileIconImage(icon, modifier, iconSize)
is ObjectIcon.Basic.Emoji -> DefaultEmojiObjectIcon(modifier, iconSize, icon)
is ObjectIcon.Basic.Image -> DefaultObjectImageIcon(icon.hash, modifier, iconSize)

View file

@ -276,6 +276,10 @@
android:id="@+id/galleryInstallationScreen"
android:name="com.anytypeio.anytype.ui.gallery.GalleryInstallationFragment" />
<dialog
android:id="@+id/versionHistoryScreen"
android:name="com.anytypeio.anytype.ui.history.VersionHistoryFragment" />
<fragment
android:id="@+id/splashScreen"
android:name="com.anytypeio.anytype.ui.splash.SplashFragment"

View file

@ -549,7 +549,7 @@ sealed class Command {
sealed class VersionHistory {
data class GetVersions(
val objectId: Id,
val lastVersion: Id,
val lastVersion: Id?,
val limit: Int
) : VersionHistory()

View file

@ -3,13 +3,14 @@ package com.anytypeio.anytype.core_models.history
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.primitives.TimeInSeconds
data class Version(
val id: Id,
val previousIds: List<Id>,
val authorId: Id,
val authorName: String,
val timestamp: Long,
val spaceMember: Id,
val spaceMemberName: String,
val timestamp: TimeInSeconds,
val groupId: Long
)

View file

@ -21,4 +21,9 @@ value class RelationId(val id: String)
@JvmInline
value class RelationKey(val key: String)
@JvmInline
value class TimeInSeconds(val time: Long) {
val inMillis get() = time * 1000
}
typealias Space = SpaceId

View file

@ -0,0 +1,357 @@
package com.anytypeio.anytype.core_ui.features.history
import android.content.res.Configuration
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.Caption1Regular
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.presentation.history.VersionHistoryGroup
import com.anytypeio.anytype.presentation.history.VersionHistoryState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
@Composable
fun VersionHistoryScreen(
state: VersionHistoryState,
onGroupClick: (VersionHistoryGroup) -> Unit,
onItemClick: (VersionHistoryGroup.Item) -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.nestedScroll(rememberNestedScrollInteropConnection())
) {
Dragger(
modifier = Modifier
.padding(vertical = 6.dp)
.align(Alignment.CenterHorizontally)
)
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()
VersionHistoryState.Loading -> VersionHistoryLoading()
is VersionHistoryState.Success -> {
VersionHistorySuccessState(
state = state,
onGroupClick = onGroupClick,
onItemClick = onItemClick
)
}
}
}
}
@Composable
private fun VersionHistoryLoading() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(
modifier = Modifier
.size(24.dp),
color = colorResource(R.color.shape_secondary),
trackColor = colorResource(R.color.shape_primary)
)
}
}
@Composable
private fun VersionHistorySuccessState(
state: VersionHistoryState.Success,
onGroupClick: (VersionHistoryGroup) -> Unit,
onItemClick: (VersionHistoryGroup.Item) -> Unit
) {
val lazyListState = rememberLazyListState()
LazyColumn(
state = lazyListState,
modifier = Modifier
.fillMaxWidth(),
contentPadding = PaddingValues(
start = 20.dp,
top = 0.dp,
end = 20.dp,
bottom = 56.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
count = state.groups.size,
key = { idx -> state.groups[idx].id }
) { idx ->
val group = state.groups[idx]
if (group.isExpanded) {
GroupItemExpanded(
group = group,
onGroupClick = onGroupClick,
onItemClick = onItemClick
)
} else {
GroupItemCollapsed(group = group, onGroupClick = onGroupClick)
}
}
}
}
@Composable
private fun GroupItemExpanded(
group: VersionHistoryGroup,
onGroupClick: (VersionHistoryGroup) -> Unit,
onItemClick: (VersionHistoryGroup.Item) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors().copy(
containerColor = colorResource(id = R.color.background_secondary),
),
border = BorderStroke(
width = 0.5.dp, color = colorResource(id = R.color.shape_primary)
),
onClick = { onGroupClick(group) }
) {
Text(
modifier = Modifier
.padding(start = 16.dp, top = 12.dp, bottom = 4.dp)
.wrapContentSize(),
text = group.title,
style = PreviewTitle2Regular,
color = colorResource(id = R.color.text_secondary)
)
group.items.forEach {
GroupItem(item = it, onItemClick = onItemClick)
}
}
}
@Composable
private fun GroupItem(
item: VersionHistoryGroup.Item,
onItemClick: (VersionHistoryGroup.Item) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(horizontal = 16.dp)
.clickable { onItemClick(item) },
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
Text(
text = item.timeFormatted,
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary)
)
Text(
text = item.spaceMemberName,
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary)
)
}
if (item.icon != null) {
ListWidgetObjectIcon(
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterVertically),
icon = item.icon!!,
iconSize = 24.dp,
fontSize = 16.sp
)
}
}
}
@Composable
private fun GroupItemCollapsed(
group: VersionHistoryGroup,
onGroupClick: (VersionHistoryGroup) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors().copy(
containerColor = colorResource(id = R.color.background_secondary),
),
border = BorderStroke(
width = 0.5.dp, color = colorResource(id = R.color.shape_primary)
),
onClick = { onGroupClick(group) }
) {
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.wrapContentSize(),
text = group.title,
style = PreviewTitle2Regular,
color = colorResource(id = R.color.text_secondary)
)
LazyRow(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
horizontalArrangement = Arrangement.spacedBy(
space = (-4).dp,
alignment = Alignment.End
)
) {
items(
count = group.icons.size,
) { idx ->
val icon = group.icons[idx]
ListWidgetObjectIcon(
modifier = Modifier.size(24.dp),
icon = icon,
iconSize = 24.dp,
fontSize = 16.sp
)
}
}
}
}
}
@Preview(
showBackground = true,
backgroundColor = 0xFFFFFF,
uiMode = Configuration.UI_MODE_NIGHT_NO,
name = "Light Mode"
)
@Preview(
showBackground = true,
backgroundColor = 0x000000,
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark Mode"
)
@Composable
private fun SpaceListScreenPreview() {
VersionHistoryScreen(
state = VersionHistoryState.Success(
groups = listOf(
VersionHistoryGroup(
id = "1",
title = "Today",
isExpanded = true,
items = listOf(
VersionHistoryGroup.Item(
id = "1",
timeFormatted = "12:00 PM",
spaceMemberName = "John Doe",
icon = ObjectIcon.Profile.Avatar("A"),
spaceMember = "1",
timeStamp = TimeInSeconds(23423423L),
versions = emptyList()
),
VersionHistoryGroup.Item(
id = "2",
timeFormatted = "12:10 PM",
spaceMemberName = "Alice Doe",
icon = ObjectIcon.Profile.Avatar("B"),
spaceMember = "1",
timeStamp = TimeInSeconds(23423423L),
versions = emptyList()
),
VersionHistoryGroup.Item(
id = "3",
timeFormatted = "12:20 PM",
spaceMemberName = "Bob Doe",
icon = ObjectIcon.Profile.Avatar("C"),
spaceMember = "1",
timeStamp = TimeInSeconds(23423423L),
versions = emptyList()
),
),
icons = listOf(ObjectIcon.Profile.Avatar("A"), ObjectIcon.Profile.Avatar("B"))
),
VersionHistoryGroup(
id = "2",
title = "Yesterday",
items = emptyList(),
icons = listOf(ObjectIcon.Profile.Avatar("C"), ObjectIcon.Profile.Avatar("D"))
),
VersionHistoryGroup(
id = "3",
title = "Yesterday",
items = emptyList(),
icons = listOf(ObjectIcon.Profile.Avatar("C"), ObjectIcon.Profile.Avatar("D"))
)
)
),
onGroupClick = {},
onItemClick = {}
)
}
@Preview(
showBackground = true,
backgroundColor = 0xFFFFFF,
uiMode = Configuration.UI_MODE_NIGHT_NO,
name = "Light Mode"
)
@Preview(
showBackground = true,
backgroundColor = 0x000000,
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark Mode"
)
@Composable
private fun SpaceListScreenPreviewLoading() {
VersionHistoryScreen(
state = VersionHistoryState.Loading,
onGroupClick = {},
onItemClick = {}
)
}

View file

@ -3,7 +3,6 @@ package com.anytypeio.anytype.core_ui.widgets
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@ -19,6 +18,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
@ -34,10 +34,11 @@ fun ListWidgetObjectIcon(
icon: ObjectIcon,
modifier: Modifier,
iconSize: Dp = 48.dp,
onTaskIconClicked: (Boolean) -> Unit = {}
onTaskIconClicked: (Boolean) -> Unit = {},
fontSize: TextUnit = 28.sp
) {
when (icon) {
is ObjectIcon.Profile.Avatar -> DefaultProfileAvatarIcon(modifier, iconSize, icon)
is ObjectIcon.Profile.Avatar -> DefaultProfileAvatarIcon(modifier, iconSize, icon, fontSize)
is ObjectIcon.Profile.Image -> defaultProfileIconImage(icon, modifier, iconSize)
is ObjectIcon.Basic.Emoji -> DefaultEmojiObjectIcon(modifier, iconSize, icon)
is ObjectIcon.Basic.Image -> DefaultObjectImageIcon(icon.hash, modifier, iconSize)
@ -119,7 +120,8 @@ fun DefaultObjectBookmarkIcon(
fun DefaultProfileAvatarIcon(
modifier: Modifier,
iconSize: Dp,
icon: ObjectIcon.Profile.Avatar
icon: ObjectIcon.Profile.Avatar,
fontSize: TextUnit
) {
Box(
modifier = modifier
@ -137,7 +139,7 @@ fun DefaultProfileAvatarIcon(
.uppercase(),
modifier = Modifier.align(Alignment.Center),
style = TextStyle(
fontSize = 28.sp,
fontSize = fontSize,
fontWeight = FontWeight.SemiBold,
color = colorResource(id = R.color.text_white)
)

View file

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

View file

@ -2,6 +2,7 @@ package com.anytypeio.anytype.domain.misc
import com.anytypeio.anytype.core_models.TimeInMillis
import com.anytypeio.anytype.core_models.TimeInSeconds
import java.text.DateFormat
import java.time.ZoneId
import java.util.Locale
@ -21,6 +22,12 @@ interface DateProvider {
fun adjustToStartOfDayInUserTimeZone(timestamp: TimeInSeconds): TimeInMillis
fun adjustFromStartOfDayInUserTimeZoneToUTC(timestamp: TimeInMillis, zoneId: ZoneId): TimeInSeconds
fun formatToDateString(timestamp: Long, pattern: String, locale: Locale): String
fun formatTimestampToDateAndTime(
timestamp: TimeInMillis,
locale: Locale,
dateStyle: Int = DateFormat.MEDIUM,
timeStyle: Int = DateFormat.SHORT
): Pair<String, String>
}
interface DateTypeNameProvider {

View file

@ -1733,4 +1733,6 @@ Please provide specific details of your needs here.</string>
<string name="sync_status_network_error">No access to the space</string>
<string name="sync_status_unrecognized">Unrecognized error</string>
<string name="version_history_title">Version history</string>
</resources>

View file

@ -2707,7 +2707,7 @@ class Middleware @Inject constructor(
@Throws
fun getVersions(command: Command.VersionHistory.GetVersions): List<Version> {
val request = Rpc.History.GetVersions.Request(
lastVersionId = command.lastVersion,
lastVersionId = command.lastVersion.orEmpty(),
objectId = command.objectId,
limit = command.limit

View file

@ -59,6 +59,7 @@ import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TimeInSeconds
import com.anytypeio.anytype.core_models.restrictions.DataViewRestriction
import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
@ -1105,9 +1106,9 @@ fun Rpc.History.Version.toCoreModel(): Version {
return Version(
id = id,
previousIds = previousIds,
authorId = authorId,
authorName = authorName,
timestamp = time,
spaceMember = authorId,
spaceMemberName = authorName,
timestamp = TimeInSeconds(time),
groupId = groupId
)
}

View file

@ -0,0 +1,36 @@
package com.anytypeio.anytype.presentation.history
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.domain.history.GetVersions
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.search.SearchObjects
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel.VmParams
import javax.inject.Inject
class VersionHistoryVMFactory @Inject constructor(
private val vmParams: VmParams,
private val getVersions: GetVersions,
private val objectSearch: SearchObjects,
private val dateProvider: DateProvider,
private val localeProvider: LocaleProvider,
private val analytics: Analytics,
private val urlBuilder: UrlBuilder
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return VersionHistoryViewModel(
vmParams = vmParams,
getVersions = getVersions,
objectSearch = objectSearch,
dateProvider = dateProvider,
localeProvider = localeProvider,
analytics = analytics,
urlBuilder = urlBuilder
) as T
}
}

View file

@ -0,0 +1,252 @@
package com.anytypeio.anytype.presentation.history
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.history.Version
import com.anytypeio.anytype.core_models.primitives.TimeInSeconds
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.history.GetVersions
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.search.SearchObjects
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class VersionHistoryViewModel(
private val vmParams: VmParams,
private val analytics: Analytics,
private val getVersions: GetVersions,
private val objectSearch: SearchObjects,
private val dateProvider: DateProvider,
private val localeProvider: LocaleProvider,
private val urlBuilder: UrlBuilder
) : ViewModel() {
private val _viewState = MutableStateFlow<VersionHistoryState>(VersionHistoryState.Loading)
val viewState = _viewState
init {
Timber.d("VersionHistoryViewModel created")
getSpaceMembers()
}
fun onStart() {
Timber.d("VersionHistoryViewModel started")
}
fun onGroupClicked(group: VersionHistoryGroup) {
val expanded = group.isExpanded
val newGroup = group.copy(isExpanded = !expanded)
val newGroups = viewState.value.let { state ->
if (state is VersionHistoryState.Success) {
state.groups.map { if (it.id == group.id) newGroup else it }
} else {
emptyList()
}
}
_viewState.value = VersionHistoryState.Success(newGroups)
}
fun onGroupItemClicked(item: VersionHistoryGroup.Item) {
}
private fun getSpaceMembers() {
viewModelScope.launch {
val filters =
ObjectSearchConstants.filterParticipants(spaces = listOf(vmParams.spaceId))
objectSearch(
SearchObjects.Params(
filters = filters,
keys = ObjectSearchConstants.spaceMemberKeys
)
).process(
failure = {
Timber.e(it, "Error while fetching new member")
_viewState.value = VersionHistoryState.Error.SpaceMembers(it.message.orEmpty())
},
success = { members ->
if (members.isEmpty()) {
_viewState.value =
VersionHistoryState.Error.SpaceMembers("No members found")
} else {
getHistoryVersions(vmParams.objectId, members)
}
}
)
}
}
private fun getHistoryVersions(objectId: String, members: List<ObjectWrapper.Basic>) {
viewModelScope.launch {
val params = GetVersions.Params(objectId = objectId)
getVersions.async(params).fold(
onSuccess = { versions ->
if (versions.isEmpty()) {
_viewState.value = VersionHistoryState.Error.NoVersions
} else {
handleVersionsSuccess(versions, members)
}
},
onFailure = {
_viewState.value = VersionHistoryState.Error.GetVersions(it.message.orEmpty())
},
onLoading = {}
)
}
}
private fun handleVersionsSuccess(versions: List<Version>, members: List<ObjectWrapper.Basic>) {
val groupedItems = groupItems(versions, members)
_viewState.value = VersionHistoryState.Success(groups = groupedItems)
}
private fun groupItems(
versions: List<Version>,
spaceMembers: List<ObjectWrapper.Basic>,
): List<VersionHistoryGroup> {
val locale = localeProvider.locale()
// Group by day
val versionsByDay = versions.groupBy { version ->
val formattedDate = dateProvider.formatToDateString(
timestamp = (version.timestamp.inMillis),
pattern = GROUP_BY_DAY_FORMAT,
locale = locale
)
formattedDate
}
// Sort days descending
val sortedDays = versionsByDay.keys.sortedBy { it }
// Within each day, sort all versions by timestamp descending
val sortedVersionsByDay = sortedDays.associateWith { day ->
val versionByDay = versionsByDay[day] ?: return@associateWith emptyList()
versionByDay.sortedByDescending { it.timestamp.time }
}
// Group by space member sequentially within each day
val groupedBySpaceMember = sortedVersionsByDay.mapValues { (_, versions) ->
val grouped = mutableListOf<MutableList<Version>>()
var currentGroup = mutableListOf<Version>()
for (version in versions) {
if (currentGroup.isEmpty() || currentGroup.last().spaceMember == version.spaceMember) {
currentGroup.add(version)
} else {
grouped.add(currentGroup)
currentGroup = mutableListOf(version)
}
}
if (currentGroup.isNotEmpty()) {
grouped.add(currentGroup)
}
grouped
}
val groups = groupedBySpaceMember.mapNotNull { (_, spaceMemberVersions) ->
if (spaceMemberVersions.isEmpty()) {
return emptyList()
}
val spaceMemberLatestVersion =
spaceMemberVersions.firstOrNull()?.firstOrNull() ?: return@mapNotNull null
val (latestVersionDate, latestVersionTime) = dateProvider.formatTimestampToDateAndTime(
timestamp = spaceMemberLatestVersion.timestamp.inMillis,
locale = locale
)
val groupItems = spaceMemberVersions.toGroupItems(
spaceMembers = spaceMembers,
latestVersionTime = latestVersionTime
)
VersionHistoryGroup(
id = spaceMemberLatestVersion.id,
title = latestVersionDate,
icons = groupItems.mapNotNull { it.icon },
items = groupItems
)
}
return groups
}
private fun List<List<Version>>.toGroupItems(
spaceMembers: List<ObjectWrapper.Basic>,
latestVersionTime: String
): List<VersionHistoryGroup.Item> {
return mapNotNull { versions ->
val latestVersion = versions.firstOrNull() ?: return@mapNotNull null
val spaceMemberId = latestVersion.spaceMember
val spaceMember = spaceMembers.find { it.id == spaceMemberId }
?: return@mapNotNull null
val icon = ObjectIcon.from(
obj = spaceMember,
layout = spaceMember.layout,
builder = urlBuilder
)
VersionHistoryGroup.Item(
id = latestVersion.id,
spaceMember = spaceMemberId,
spaceMemberName = spaceMember.name.orEmpty(),
timeStamp = latestVersion.timestamp,
icon = icon,
timeFormatted = latestVersionTime,
versions = versions
)
}
}
data class VmParams(
val objectId: Id,
val spaceId: Id
)
sealed class Command {
data class OpenVersion(val versionId: Id) : Command()
}
companion object {
const val GROUP_BY_DAY_FORMAT = "d MM yyyy"
}
}
sealed class VersionHistoryState {
data object Loading : 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 NoVersions : Error()
}
}
data class VersionHistoryGroup(
val id: String,
val title: String,
val icons: List<ObjectIcon>,
val items: List<Item>,
val isExpanded: Boolean = false
) {
data class Item(
val id: Id,
val spaceMember: Id,
val spaceMemberName: String,
val timeFormatted: String,
val timeStamp: TimeInSeconds,
val icon: ObjectIcon?,
val versions: List<Version>
)
}

View file

@ -87,8 +87,10 @@ abstract class ObjectMenuViewModelBase(
abstract fun onLayoutClicked(ctx: Id, space: Id)
abstract fun onRelationsClicked()
fun onHistoryClicked() {
throw IllegalStateException("History isn't supported yet")
fun onHistoryClicked(ctx: Id, space: Id) {
viewModelScope.launch {
commands.emit(Command.OpenHistoryScreen(ctx, space))
}
}
fun onStop() {
@ -435,6 +437,7 @@ abstract class ObjectMenuViewModelBase(
data class ShareDeeplinkToObject(val link: String): Command()
data class ShareDebugTree(val uri: Uri) : Command()
data class ShareDebugGoroutines(val path: String) : Command()
data class OpenHistoryScreen(val objectId: Id, val spaceId: Id) : Command()
data class OpenSnackbar(
val id: Id,
val space: Id,

View file

@ -1200,6 +1200,7 @@ object ObjectSearchConstants {
Relations.ICON_IMAGE,
Relations.PARTICIPANT_STATUS,
Relations.PARTICIPANT_PERMISSIONS,
Relations.LAYOUT
)
val spaceViewKeys = listOf(

View file

@ -3,8 +3,10 @@ package com.anytypeio.anytype.presentation.widgets.collection
import android.text.format.DateUtils
import com.anytypeio.anytype.core_models.TimeInMillis
import com.anytypeio.anytype.core_models.TimeInSeconds
import com.anytypeio.anytype.core_utils.date.Milliseconds
import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.DateType
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDate
@ -136,6 +138,27 @@ class DateProviderImpl @Inject constructor() : DateProvider {
return ""
}
}
override fun formatTimestampToDateAndTime(
timestamp: TimeInMillis,
locale: Locale,
dateStyle: Int,
timeStyle: Int
): Pair<String, String> {
return try {
val datePattern = (DateFormat.getDateInstance(dateStyle, locale) as SimpleDateFormat).toPattern()
val timePattern = (DateFormat.getTimeInstance(timeStyle, locale) as SimpleDateFormat).toPattern()
val dateFormatter = SimpleDateFormat(datePattern, locale)
val timeFormatter = SimpleDateFormat(timePattern, locale)
val date = Date(timestamp)
val dateString = dateFormatter.format(date)
val timeString = timeFormatter.format(date)
Pair(dateString, timeString)
} catch (e: Exception) {
Timber.e(e, "Error formatting timestamp to date and time string")
Pair("", "")
}
}
}

View file

@ -0,0 +1,51 @@
package com.anytypeio.anytype.presentation.history
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.history.DiffVersionResponse
import com.anytypeio.anytype.core_models.history.ShowVersionResponse
import com.anytypeio.anytype.core_models.history.Version
import com.anytypeio.anytype.core_models.primitives.TimeInSeconds
import kotlin.random.Random
import net.bytebuddy.utility.RandomString
fun StubVersion(
id: Id = "versionId - ${RandomString.make()}",
previousIds: List<Id> = emptyList(),
authorId: Id = "authorId - ${RandomString.make()}",
authorName: String = "",
timestamp: TimeInSeconds,
groupId: Long = Random(100).nextLong()
): Version {
return Version(
id = id,
previousIds = previousIds,
spaceMember = authorId,
spaceMemberName = authorName,
timestamp = timestamp,
groupId = groupId
)
}
fun StubShowVersionResponse(
objectView: ObjectView?,
version: Version?,
traceId: Id
): ShowVersionResponse {
return ShowVersionResponse(
objectView = objectView,
version = version,
traceId = traceId
)
}
fun StubDiffVersionResponse(
historyEvents: List<Event.Command>,
objectView: ObjectView?
): DiffVersionResponse {
return DiffVersionResponse(
historyEvents = historyEvents,
objectView = objectView
)
}

View file

@ -0,0 +1,360 @@
package com.anytypeio.anytype.presentation.history
import android.util.Log
import app.cash.turbine.turbineScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.StubSpaceMember
import com.anytypeio.anytype.core_models.history.Version
import com.anytypeio.anytype.core_models.multiplayer.ParticipantStatus
import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions
import com.anytypeio.anytype.core_models.primitives.TimeInSeconds
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.base.Resultat
import com.anytypeio.anytype.domain.history.GetVersions
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.search.SearchObjects
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import com.anytypeio.anytype.presentation.widgets.collection.DateProviderImpl
import java.util.Locale
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import net.bytebuddy.utility.RandomString
import net.lachlanmckee.timberjunit.TimberTestRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
class VersionHistoryViewModelTest {
private val objectId = "objectId-${RandomString.make()}"
private val spaceId = "spaceId-${RandomString.make()}"
private val vmParams = VersionHistoryViewModel.VmParams(
objectId = objectId,
spaceId = "spaceId-${RandomString.make()}"
)
private val dispatcher = StandardTestDispatcher(TestCoroutineScheduler())
@get:Rule
val timberTestRule: TimberTestRule = TimberTestRule.builder()
.minPriority(Log.DEBUG)
.showThread(true)
.showTimestamp(false)
.onlyLogWhenTestFails(false)
.build()
@OptIn(ExperimentalCoroutinesApi::class)
val dispatchers = AppCoroutineDispatchers(
io = dispatcher,
main = dispatcher,
computation = dispatcher
).also { Dispatchers.setMain(dispatcher) }
@Mock
lateinit var analytics: Analytics
@Mock
lateinit var getVersions: GetVersions
@Mock
lateinit var urlBuilder: UrlBuilder
@Mock
lateinit var objectSearch: SearchObjects
private val dateProvider: DateProvider = DateProviderImpl()
@Mock
lateinit var localeProvider: LocaleProvider
lateinit var spaceManager: SpaceManager
lateinit var vm: VersionHistoryViewModel
private val user1 = StubSpaceMember(
space = spaceId,
name = "user1",
iconEmoji = "👩‍🎤",
identity = "identity1",
memberStatus = ParticipantStatus.ACTIVE,
memberPermissions = listOf(SpaceMemberPermissions.OWNER)
)
private val user2 = StubSpaceMember(
space = spaceId,
name = "user2",
iconEmoji = "👨‍🎤",
identity = "identity2",
memberStatus = ParticipantStatus.ACTIVE,
memberPermissions = listOf(SpaceMemberPermissions.WRITER)
)
private val user3 = StubSpaceMember(
space = spaceId,
name = "user3",
iconEmoji = "👩‍🎤",
identity = "identity3",
memberStatus = ParticipantStatus.ACTIVE,
memberPermissions = listOf(SpaceMemberPermissions.READER)
)
private val user4 = StubSpaceMember(
space = spaceId,
name = "user4",
iconEmoji = "👩‍🎤",
identity = "identity4",
memberStatus = ParticipantStatus.REMOVED,
memberPermissions = listOf(SpaceMemberPermissions.READER)
)
/**
* Timeline, GMT+0200
* Start : Sat Jan 01 2022 00:00:03 GMT+0100 1640991603 User1
* Sat Jan 01 2022 00:00:02 GMT+0100 1640991602 User4
* Sat Jan 01 2022 00:00:01 GMT+0100 1640991601 User3
* Sat Jan 01 2022 00:00:00 GMT+0100 1640991600 User1
* Fri Dec 31 2021 23:59:59 GMT+0100 1640991599 User1
* Fri Dec 31 2021 23:59:58 GMT+0100 1640991598 User1
* Fri Dec 31 2021 23:59:57 GMT+0100 1640991597 User2
*/
private val timestamp0 = TimeInSeconds(1640991603L)
private val timestamp1 = TimeInSeconds(1640991602L)
private val timestamp2 = TimeInSeconds(1640991601L)
private val timestamp3 = TimeInSeconds(1640991600L)
private val timestamp4 = TimeInSeconds(1640991599L)
private val timestamp5 = TimeInSeconds(1640991598L)
private val timestamp6 = TimeInSeconds(1640991597L)
private val versions = listOf(
StubVersion(
authorId = user1.id,
timestamp = timestamp0
),
StubVersion(
authorId = user4.id,
timestamp = timestamp1
),
StubVersion(
authorId = user3.id,
timestamp = timestamp2
),
StubVersion(
authorId = user1.id,
timestamp = timestamp3
),
StubVersion(
authorId = user1.id,
timestamp = timestamp4
),
StubVersion(
authorId = user1.id,
timestamp = timestamp5
),
StubVersion(
authorId = user2.id,
timestamp = timestamp6
)
)
@Before
fun before() {
MockitoAnnotations.openMocks(this)
spaceManager = mock(verboseLogging = true)
stubLocale(locale = Locale.CANADA)
}
@Test
fun `should has proper date`() = runTest {
turbineScope {
stubVersions(stubbedVersions = versions)
stubSpaceMembers()
vm = buildViewModel()
val locale = localeProvider.locale()
val (date0, time0) = dateProvider.formatTimestampToDateAndTime(
timestamp = versions[0].timestamp.inMillis,
locale = locale
)
val (date1, time1) = dateProvider.formatTimestampToDateAndTime(
timestamp = versions[1].timestamp.inMillis,
locale = locale
)
val (date2, time2) = dateProvider.formatTimestampToDateAndTime(
timestamp = versions[2].timestamp.inMillis,
locale = locale
)
val (date3, time3) = dateProvider.formatTimestampToDateAndTime(
timestamp = versions[3].timestamp.inMillis,
locale = locale
)
val (date4, time4) = dateProvider.formatTimestampToDateAndTime(
timestamp = versions[4].timestamp.inMillis,
locale = locale
)
val (date5, time5) = dateProvider.formatTimestampToDateAndTime(
timestamp = versions[5].timestamp.inMillis,
locale = locale
)
val (date6, time6) = dateProvider.formatTimestampToDateAndTime(
timestamp = versions[6].timestamp.inMillis,
locale = locale
)
val viewStateFlow = vm.viewState.testIn(backgroundScope)
assertIs<VersionHistoryState.Loading>(viewStateFlow.awaitItem())
val expected = VersionHistoryState.Success(
groups = buildList {
add(
VersionHistoryGroup(
id = versions[0].id,
title = date0,
icons = listOf(ObjectIcon.None, ObjectIcon.None, ObjectIcon.None, ObjectIcon.None),
items = buildList {
add(
VersionHistoryGroup.Item(
id = versions[0].id,
spaceMember = user1.id,
spaceMemberName = user1.name!!,
timeStamp = versions[0].timestamp,
icon = ObjectIcon.None,
versions = listOf(versions[0]),
timeFormatted = time0
)
)
add(
VersionHistoryGroup.Item(
id = versions[1].id,
spaceMember = user4.id,
spaceMemberName = user4.name!!,
timeStamp = versions[1].timestamp,
icon = ObjectIcon.None,
versions = listOf(versions[1]),
timeFormatted = time1
)
)
add(
VersionHistoryGroup.Item(
id = versions[2].id,
spaceMember = user3.id,
spaceMemberName = user3.name!!,
timeStamp = versions[2].timestamp,
icon = ObjectIcon.None,
versions = listOf(versions[2]),
timeFormatted = time2
)
)
add(
VersionHistoryGroup.Item(
id = versions[3].id,
spaceMember = user1.id,
spaceMemberName = user1.name!!,
timeStamp = versions[3].timestamp,
icon = ObjectIcon.None,
versions = listOf(versions[3]),
timeFormatted = time3
)
)
}
)
)
add(
VersionHistoryGroup(
id = versions[4].id,
title = date4,
icons = listOf(ObjectIcon.None, ObjectIcon.None),
items = buildList {
add(
VersionHistoryGroup.Item(
id = versions[4].id,
spaceMember = user1.id,
spaceMemberName = user1.name!!,
timeStamp = versions[4].timestamp,
icon = ObjectIcon.None,
versions = listOf(versions[4], versions[5]),
timeFormatted = time4
)
)
add(
VersionHistoryGroup.Item(
id = versions[6].id,
spaceMember = user2.id,
spaceMemberName = user2.name!!,
timeStamp = versions[6].timestamp,
icon = ObjectIcon.None,
versions = listOf(versions[6]),
timeFormatted = time6
)
)
}
)
)
}
)
assertEquals(
expected = expected,
actual = viewStateFlow.awaitItem()
)
}
}
private fun stubSpaceMembers() {
val filters =
ObjectSearchConstants.filterParticipants(spaces = listOf(vmParams.spaceId))
val params = SearchObjects.Params(
filters = filters,
keys = ObjectSearchConstants.spaceMemberKeys
)
objectSearch.stub {
onBlocking { invoke(params) } doReturn Either.Right(listOf(user1, user2, user3, user4))
}
}
private fun stubVersions(stubbedVersions: List<Version>) {
val params = GetVersions.Params(
objectId = objectId
)
getVersions.stub {
onBlocking { async(params) } doReturn Resultat.success(stubbedVersions)
}
}
private fun stubLocale(locale: Locale) {
localeProvider.stub {
on { locale() } doReturn locale
}
}
private fun buildViewModel(): VersionHistoryViewModel {
return VersionHistoryViewModel(
analytics = analytics,
getVersions = getVersions,
objectSearch = objectSearch,
dateProvider = dateProvider,
localeProvider = localeProvider,
vmParams = vmParams,
urlBuilder = urlBuilder
)
}
}

View file

@ -1,5 +1,7 @@
package com.anytypeio.anytype.core_models
import com.anytypeio.anytype.core_models.multiplayer.ParticipantStatus
import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions
import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.test_utils.MockDataFactory
@ -19,7 +21,8 @@ fun StubObject(
isReadOnly: Boolean? = null,
isHidden: Boolean? = null,
links: List<Id> = emptyList(),
targetObjectType: Id? = null
targetObjectType: Id? = null,
identity: Id? = null
): ObjectWrapper.Basic = ObjectWrapper.Basic(
map = mapOf(
Relations.ID to id,
@ -37,6 +40,27 @@ fun StubObject(
Relations.LINKS to links,
Relations.TARGET_OBJECT_TYPE to targetObjectType,
Relations.UNIQUE_KEY to uniqueKey,
Relations.IDENTITY to identity
)
)
fun StubSpaceMember(
id: String = MockDataFactory.randomUuid(),
space: Id = MockDataFactory.randomUuid(),
name: String = MockDataFactory.randomString(),
iconEmoji: String? = null,
identity: Id? = null,
memberPermissions: List<SpaceMemberPermissions> = emptyList(),
memberStatus: ParticipantStatus = ParticipantStatus.ACTIVE
): ObjectWrapper.Basic = ObjectWrapper.Basic(
map = mapOf(
Relations.ID to id,
Relations.SPACE_ID to space,
Relations.NAME to name,
Relations.ICON_EMOJI to iconEmoji,
Relations.IDENTITY to identity,
Relations.PARTICIPANT_PERMISSIONS to memberPermissions,
Relations.PARTICIPANT_STATUS to memberStatus.code.toDouble()
)
)