DROID-2146 Objects | Enhancement | Allow pinning and unpinning types in the new object-creation panel (#771)

This commit is contained in:
Evgenii Kozlov 2024-01-16 16:10:52 +01:00 committed by GitHub
parent 60415edafd
commit e6acc965ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 461 additions and 37 deletions

View File

@ -6,6 +6,7 @@ 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.config.UserSettingsRepository
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.objects.SelectObjectTypeViewModel
import com.anytypeio.anytype.ui.objects.creation.SelectObjectTypeFragment
@ -48,4 +49,5 @@ interface SelectObjectTypeDependencies : ComponentDependencies {
fun analytics(): Analytics
fun dispatchers(): AppCoroutineDispatchers
fun spaceManager(): SpaceManager
fun userSettingsRepo(): UserSettingsRepository
}

View File

@ -277,7 +277,11 @@ object DataModule {
@Singleton
fun provideUserSettingsCache(
@Named("default") prefs: SharedPreferences,
): UserSettingsCache = DefaultUserSettingsCache(prefs)
context: Context
): UserSettingsCache = DefaultUserSettingsCache(
prefs = prefs,
context = context
)
@JvmStatic
@Provides

View File

@ -11,6 +11,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ObjectWrapper
@ -23,6 +24,8 @@ import com.anytypeio.anytype.presentation.objects.Command
import com.anytypeio.anytype.presentation.objects.SelectObjectTypeViewModel
import com.anytypeio.anytype.ui.settings.typography
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
@ -48,6 +51,20 @@ class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
SelectObjectTypeScreen(
state = vm.viewState.collectAsStateWithLifecycle().value,
onTypeClicked = vm::onTypeClicked,
onPinOnTopClicked = {
lifecycleScope.launch {
// Workaround to prevent dropdown-menu flickering
delay(DROP_DOWN_MENU_ACTION_DELAY)
vm.onPinTypeClicked(it)
}
},
onUnpinTypeClicked = {
lifecycleScope.launch {
// Workaround to prevent dropdown-menu flickering
delay(DROP_DOWN_MENU_ACTION_DELAY)
vm.onUnpinTypeClicked(it)
}
},
onQueryChanged = vm::onQueryChanged,
onFocused = {
skipCollapsed()
@ -89,6 +106,7 @@ class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
companion object {
const val EXCLUDED_TYPE_KEYS_ARG_KEY = "arg.create-object-of-type.excluded-type-keys"
const val DROP_DOWN_MENU_ACTION_DELAY = 100L
fun newInstance(
excludedTypeKeys: List<Key>,

View File

@ -8,7 +8,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -32,6 +32,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
@ -55,6 +57,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import com.anytypeio.anytype.R
@ -70,6 +73,7 @@ import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.emojifier.Emojifier
import com.anytypeio.anytype.presentation.objects.SelectTypeView
import com.anytypeio.anytype.presentation.objects.SelectTypeViewState
import kotlinx.coroutines.delay
@Preview
@Composable
@ -78,13 +82,17 @@ fun PreviewScreen() {
onTypeClicked = {},
state = SelectTypeViewState.Loading,
onQueryChanged = {},
onFocused = {}
onFocused = {},
onUnpinTypeClicked = {},
onPinOnTopClicked = {}
)
}
@Composable
fun SelectObjectTypeScreen(
onTypeClicked: (SelectTypeView.Type) -> Unit,
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
onPinOnTopClicked: (SelectTypeView.Type) -> Unit,
onQueryChanged: (String) -> Unit,
onFocused: () -> Unit,
state: SelectTypeViewState
@ -104,18 +112,30 @@ fun SelectObjectTypeScreen(
onFocused = onFocused
)
Spacer(modifier = Modifier.height(8.dp))
ScreenContent(state, onTypeClicked)
ScreenContent(
state = state,
onTypeClicked = onTypeClicked,
onPinOnTopClicked = onPinOnTopClicked,
onUnpinTypeClicked = onUnpinTypeClicked
)
}
}
@Composable
private fun ScreenContent(
state: SelectTypeViewState,
onTypeClicked: (SelectTypeView.Type) -> Unit
onTypeClicked: (SelectTypeView.Type) -> Unit,
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
onPinOnTopClicked: (SelectTypeView.Type) -> Unit
) {
when (state) {
is SelectTypeViewState.Content -> {
FlowRowContent(state.views, onTypeClicked)
FlowRowContent(
views = state.views,
onTypeClicked = onTypeClicked,
onPinOnTopClicked = onPinOnTopClicked,
onUnpinTypeClicked = onUnpinTypeClicked
)
}
SelectTypeViewState.Empty -> {
AnimatedVisibility(
@ -149,7 +169,9 @@ private fun ScreenContent(
@OptIn(ExperimentalLayoutApi::class)
private fun FlowRowContent(
views: List<SelectTypeView>,
onTypeClicked: (SelectTypeView.Type) -> Unit
onTypeClicked: (SelectTypeView.Type) -> Unit,
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
onPinOnTopClicked: (SelectTypeView.Type) -> Unit
) {
FlowRow(
modifier = Modifier
@ -162,13 +184,62 @@ private fun FlowRowContent(
views.forEach { view ->
when (view) {
is SelectTypeView.Type -> {
ObjectTypeItem(
name = view.name,
emoji = view.icon,
onItemClicked = throttledClick(
onClick = { onTypeClicked(view) }
),
modifier = Modifier
val isMenuExpanded = remember {
mutableStateOf(false)
}
Box {
ObjectTypeItem(
name = view.name,
emoji = view.icon,
onItemClicked = throttledClick(
onClick = { onTypeClicked(view) }
),
onItemLongClicked = {
isMenuExpanded.value = !isMenuExpanded.value
},
modifier = Modifier
)
if (view.isPinnable) {
DropdownMenu(
expanded = isMenuExpanded.value,
onDismissRequest = { isMenuExpanded.value = false },
offset = DpOffset(x = 0.dp, y = 6.dp)
) {
if (!view.isPinned || !view.isFirstInSection) {
DropdownMenuItem(
onClick = {
isMenuExpanded.value = false
onPinOnTopClicked(view)
}
) {
Text(
text = stringResource(R.string.any_object_creation_menu_pin_on_top),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
if (view.isPinned) {
DropdownMenuItem(
onClick = {
isMenuExpanded.value = false
onUnpinTypeClicked(view)
}
) {
Text(
text = stringResource(R.string.any_object_creation_menu_unpin),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
}
}
}
}
is SelectTypeView.Section.Pinned -> {
Section(
title = stringResource(id = R.string.create_object_section_pinned),
)
}
is SelectTypeView.Section.Groups -> {
@ -230,6 +301,16 @@ private fun LazyColumnContent(
)
}
}
is SelectTypeView.Section.Pinned -> {
item(
key = view.javaClass.name,
span = { GridItemSpan(maxLineSpan) }
) {
Section(
title = stringResource(id = R.string.create_object_section_pinned)
)
}
}
is SelectTypeView.Section.Library -> {
item(
key = view.javaClass.name,
@ -252,6 +333,9 @@ private fun LazyColumnContent(
onTypeClicked(view)
}
),
onItemLongClicked = {
},
modifier = Modifier.animateItemPlacement()
)
}
@ -262,12 +346,14 @@ private fun LazyColumnContent(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ObjectTypeItem(
modifier: Modifier,
name: String,
emoji: String,
onItemClicked: () -> Unit
onItemClicked: () -> Unit,
onItemLongClicked: () -> Unit
) {
Row(
modifier = modifier
@ -278,7 +364,14 @@ fun ObjectTypeItem(
shape = RoundedCornerShape(12.dp)
)
.clip(RoundedCornerShape(12.dp))
.clickable { onItemClicked() },
.combinedClickable(
onClick = {
onItemClicked()
},
onLongClick = {
onItemLongClicked()
}
),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(

View File

@ -1,4 +1,7 @@
<resources>
<string name="amplitude_api_key">1ba981d1a9afb8af8c81847ef3383a20</string>
<string name="amplitude_api_key_debug">b9791dd64a1e9f07a330a4ac9feb1f10</string>
<string name="create_object_section_pinned">Pinned</string>
<string name="any_object_creation_menu_pin_on_top">Pin on top</string>
<string name="any_object_creation_menu_unpin">Unpin</string>
</resources>

View File

@ -6,12 +6,15 @@ import com.anytypeio.anytype.core_models.Wallpaper
import com.anytypeio.anytype.core_models.WidgetSession
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeId
import kotlinx.coroutines.flow.Flow
interface UserSettingsCache {
suspend fun setCurrentSpace(space: SpaceId)
suspend fun getCurrentSpace(): SpaceId?
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
suspend fun getDefaultObjectType(space: SpaceId): TypeId?
suspend fun setPinnedObjectTypes(space: SpaceId, types: List<TypeId>)
fun getPinnedObjectTypes(space: SpaceId) : Flow<List<TypeId>>
suspend fun setWallpaper(space: Id, wallpaper: Wallpaper)
suspend fun getWallpaper(space: Id) : Wallpaper
suspend fun setThemeMode(mode: ThemeMode)

View File

@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_models.WidgetSession
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import kotlinx.coroutines.flow.Flow
class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSettingsRepository {
@ -28,6 +29,17 @@ class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSet
space: SpaceId
): TypeId? = cache.getDefaultObjectType(space = space)
override suspend fun setPinnedObjectTypes(space: SpaceId, types: List<TypeId>) {
cache.setPinnedObjectTypes(
space = space,
types = types
)
}
override fun getPinnedObjectTypes(space: SpaceId): Flow<List<TypeId>> {
return cache.getPinnedObjectTypes(space = space)
}
override suspend fun setThemeMode(mode: ThemeMode) {
cache.setThemeMode(mode)
}

View File

@ -73,7 +73,9 @@ abstract class FlowInteractor<in P, R>(
private val context: CoroutineContext
) {
protected abstract fun build() : Flow<R>
protected abstract fun build(params: P) : Flow<R>
fun flow() : Flow<R> = build().flowOn(context)
fun flow(params: P) : Flow<R> = build(params).flowOn(context)
}

View File

@ -6,6 +6,7 @@ import com.anytypeio.anytype.core_models.Wallpaper
import com.anytypeio.anytype.core_models.WidgetSession
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeId
import kotlinx.coroutines.flow.Flow
interface UserSettingsRepository {
@ -18,6 +19,9 @@ interface UserSettingsRepository {
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
suspend fun getDefaultObjectType(space: SpaceId): TypeId?
suspend fun setPinnedObjectTypes(space: SpaceId, types: List<TypeId>)
fun getPinnedObjectTypes(space: SpaceId) : Flow<List<TypeId>>
suspend fun setThemeMode(mode: ThemeMode)
suspend fun getThemeMode(): ThemeMode

View File

@ -0,0 +1,27 @@
package com.anytypeio.anytype.domain.types
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.FlowInteractor
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emptyFlow
class GetPinnedObjectTypes @Inject constructor(
private val repo: UserSettingsRepository,
dispatchers: AppCoroutineDispatchers
) : FlowInteractor<GetPinnedObjectTypes.Params, List<TypeId>>(dispatchers.io) {
override fun build(): Flow<List<TypeId>> {
throw UnsupportedOperationException()
}
override fun build(params: Params) = repo.getPinnedObjectTypes(
space = params.space
).catch { emit(emptyList()) }
class Params(val space: SpaceId)
}

View File

@ -0,0 +1,26 @@
package com.anytypeio.anytype.domain.types
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import javax.inject.Inject
class SetPinnedObjectTypes @Inject constructor(
private val repo: UserSettingsRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<SetPinnedObjectTypes.Params, Unit>(dispatchers.io) {
override suspend fun doWork(params: Params) {
repo.setPinnedObjectTypes(
space = params.space,
types = params.types
)
}
class Params(
val space: SpaceId,
val types: List<TypeId>
)
}

View File

@ -28,4 +28,6 @@ class RestoreWallpaper(
.catch {
// Do nothing.
}
override fun build(params: Unit): Flow<Unit> = build()
}

View File

@ -64,6 +64,7 @@ timberVersion = '5.0.1'
protobufJavaVersion = '3.9.2'
protocVersion = '3.9.0'
roomVersion = '2.5.2'
dataStoreVersion = '1.0.0'
amplitudeVersion = '2.36.1'
coilComposeVersion = '2.2.2'
sentryVersion = '6.0.0'
@ -153,6 +154,7 @@ protobufJava = { module = "com.google.protobuf:protobuf-java", version.ref = "pr
protobufJavaUtil = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobufJavaVersion" }
protoc = { module = "com.google.protobuf:protoc", version.ref = "protocVersion" }
room = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
dataStore = { module = "androidx.datastore:datastore", version.ref = "dataStoreVersion" }
roomKtx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
annotations = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
roomTesting = { module = "androidx.room:room-testing", version.ref = "roomVersion" }

View File

@ -3,6 +3,7 @@ plugins {
id "kotlin-android"
id "kotlin-kapt"
id "kotlinx-serialization"
id "com.squareup.wire"
}
dependencies {
@ -18,6 +19,7 @@ dependencies {
implementation libs.room
implementation libs.roomKtx
implementation libs.dataStore
kapt libs.annotations
@ -30,6 +32,7 @@ dependencies {
testImplementation libs.mockitoKotlin
testImplementation libs.robolectric
testImplementation libs.archCoreTesting
testImplementation libs.androidXTestCore
testImplementation libs.coroutineTesting
}
@ -42,4 +45,9 @@ android {
jvmToolchain(17)
}
namespace 'com.anytypeio.anytype.persistence'
}
wire {
protoPath { srcDir 'src/main/proto' }
kotlin {}
}

View File

@ -0,0 +1,23 @@
package com.anytypeio.anytype.persistence.preferences
import androidx.datastore.core.Serializer
import com.anytypeio.anytype.persistence.SpacePreferences
import java.io.InputStream
import java.io.OutputStream
object SpacePrefSerializer : Serializer<SpacePreferences> {
override val defaultValue: SpacePreferences = SpacePreferences()
override suspend fun readFrom(input: InputStream): SpacePreferences {
return SpacePreferences.ADAPTER.decode(input)
}
override suspend fun writeTo(t: SpacePreferences, output: OutputStream) {
SpacePreferences.ADAPTER.encode(
stream = output,
value = t
)
}
}
const val SPACE_PREFERENCE_FILENAME = "space-preferences.pb"

View File

@ -1,6 +1,9 @@
package com.anytypeio.anytype.persistence.repo
import android.content.Context
import android.content.SharedPreferences
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NO_VALUE
import com.anytypeio.anytype.core_models.ThemeMode
@ -9,6 +12,8 @@ import com.anytypeio.anytype.core_models.WidgetSession
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.data.auth.repo.UserSettingsCache
import com.anytypeio.anytype.persistence.SpacePreference
import com.anytypeio.anytype.persistence.SpacePreferences
import com.anytypeio.anytype.persistence.common.JsonString
import com.anytypeio.anytype.persistence.common.deserializeWallpaperSettings
import com.anytypeio.anytype.persistence.common.serializeWallpaperSettings
@ -16,8 +21,20 @@ import com.anytypeio.anytype.persistence.common.toJsonString
import com.anytypeio.anytype.persistence.common.toStringMap
import com.anytypeio.anytype.persistence.model.asSettings
import com.anytypeio.anytype.persistence.model.asWallpaper
import com.anytypeio.anytype.persistence.preferences.SPACE_PREFERENCE_FILENAME
import com.anytypeio.anytype.persistence.preferences.SpacePrefSerializer
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSettingsCache {
class DefaultUserSettingsCache(
private val prefs: SharedPreferences,
private val context: Context
) : UserSettingsCache {
private val Context.spacePrefsStore: DataStore<SpacePreferences> by dataStore(
fileName = SPACE_PREFERENCE_FILENAME,
serializer = SpacePrefSerializer
)
override suspend fun setCurrentSpace(space: SpaceId) {
prefs.edit()
@ -46,6 +63,22 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
.edit()
.putString(DEFAULT_OBJECT_TYPES_KEY, updated.toJsonString())
.apply()
context
.spacePrefsStore
.updateData { prefs ->
val givenSpacePreferences = prefs.preferences.getOrDefault(
space.id,
SpacePreference()
)
val updatedSpacePreferences = givenSpacePreferences.copy(
defaultObjectTypeKey = type.id
)
val result = prefs.preferences + mapOf(space.id to updatedSpacePreferences)
prefs.copy(preferences = result)
}
}
override suspend fun getDefaultObjectType(space: SpaceId): TypeId? {
@ -168,7 +201,41 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
.apply()
}
override suspend fun setPinnedObjectTypes(space: SpaceId, types: List<TypeId>) {
context.spacePrefsStore.updateData { existingPreferences ->
val givenSpacePreference = existingPreferences
.preferences
.getOrDefault(key = space.id, defaultValue = SpacePreference())
val updated = givenSpacePreference.copy(
pinnedObjectTypeKeys = types.map { type -> type.id }
)
val result = buildMap {
putAll(existingPreferences.preferences)
put(key = space.id, updated)
}
SpacePreferences(
preferences = result
)
}
}
override fun getPinnedObjectTypes(space: SpaceId): Flow<List<TypeId>> {
return context.spacePrefsStore
.data
.map { preferences ->
preferences
.preferences[space.id]
?.pinnedObjectTypeKeys?.map { id -> TypeId(id) } ?: emptyList()
}
}
override suspend fun clear() {
// Clearing shared preferences
prefs.edit()
.remove(DEFAULT_OBJECT_TYPE_ID_KEY)
.remove(DEFAULT_OBJECT_TYPE_NAME_KEY)
@ -176,6 +243,12 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
.remove(ACTIVE_WIDGETS_VIEWS_KEY)
.remove(CURRENT_SPACE_KEY)
.apply()
// Clearing data stores
context.spacePrefsStore.updateData {
SpacePreferences(emptyMap())
}
}
companion object {

View File

@ -0,0 +1,14 @@
syntax = "proto3";
option java_package = "com.anytypeio.anytype.persistence";
option java_multiple_files = true;
message SpacePreferences {
// maps space id to space preference
map <string, SpacePreference> preferences = 1;
}
message SpacePreference {
optional string defaultObjectTypeKey = 1;
repeated string pinnedObjectTypeKeys = 2;
}

View File

@ -2,6 +2,7 @@ package com.anytypeio.anytype.persistence
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import com.anytypeio.anytype.core_models.Wallpaper
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeId
@ -32,7 +33,8 @@ class UserSettingsCacheTest {
fun `should save and return default wallpaper`() = runTest {
val cache = DefaultUserSettingsCache(
prefs = defaultPrefs
prefs = defaultPrefs,
context = ApplicationProvider.getApplicationContext()
)
val space = MockDataFactory.randomUuid()
@ -55,7 +57,8 @@ class UserSettingsCacheTest {
fun `should save and return gradient wallpaper`() = runTest {
val cache = DefaultUserSettingsCache(
prefs = defaultPrefs
prefs = defaultPrefs,
context = ApplicationProvider.getApplicationContext()
)
val space = MockDataFactory.randomUuid()
@ -81,7 +84,8 @@ class UserSettingsCacheTest {
fun `should not save wallpaper if space id is empty, should return default`() = runTest {
val cache = DefaultUserSettingsCache(
prefs = defaultPrefs
prefs = defaultPrefs,
context = ApplicationProvider.getApplicationContext()
)
val space = ""
@ -108,7 +112,8 @@ class UserSettingsCacheTest {
fun `should save and return solid-color wallpaper`() = runTest {
val cache = DefaultUserSettingsCache(
prefs = defaultPrefs
prefs = defaultPrefs,
context = ApplicationProvider.getApplicationContext()
)
val space = MockDataFactory.randomUuid()
@ -134,7 +139,8 @@ class UserSettingsCacheTest {
fun `should save and return image wallpaper`() = runTest {
val cache = DefaultUserSettingsCache(
prefs = defaultPrefs
prefs = defaultPrefs,
context = ApplicationProvider.getApplicationContext()
)
val space = MockDataFactory.randomUuid()
@ -160,7 +166,8 @@ class UserSettingsCacheTest {
fun `should return default wallpaper`() = runTest {
val cache = DefaultUserSettingsCache(
prefs = defaultPrefs
prefs = defaultPrefs,
context = ApplicationProvider.getApplicationContext()
)
val space = MockDataFactory.randomUuid()
@ -178,7 +185,8 @@ class UserSettingsCacheTest {
fun `should save new default object type for given space`() = runTest {
val cache = DefaultUserSettingsCache(
prefs = defaultPrefs
prefs = defaultPrefs,
context = ApplicationProvider.getApplicationContext()
)
val space = SpaceId(MockDataFactory.randomUuid())
@ -210,7 +218,8 @@ class UserSettingsCacheTest {
fun `should save default object type for two given spaces`() = runTest {
val cache = DefaultUserSettingsCache(
prefs = defaultPrefs
prefs = defaultPrefs,
context = ApplicationProvider.getApplicationContext()
)
val space1 = SpaceId(MockDataFactory.randomUuid())

View File

@ -11,17 +11,23 @@ import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.ext.mapToObjectWrapperType
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.core_models.primitives.TypeKey
import com.anytypeio.anytype.domain.base.Resultat
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.spaces.AddObjectToSpace
import com.anytypeio.anytype.domain.types.GetPinnedObjectTypes
import com.anytypeio.anytype.domain.types.SetPinnedObjectTypes
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@ -34,6 +40,8 @@ class SelectObjectTypeViewModel(
private val getObjectTypes: GetObjectTypes,
private val spaceManager: SpaceManager,
private val addObjectToSpace: AddObjectToSpace,
private val setPinnedObjectTypes: SetPinnedObjectTypes,
private val getPinnedObjectTypes: GetPinnedObjectTypes
) : BaseViewModel() {
val viewState = MutableStateFlow<SelectTypeViewState>(SelectTypeViewState.Loading)
@ -49,7 +57,7 @@ class SelectObjectTypeViewModel(
viewModelScope.launch {
space = spaceManager.get()
query.onStart { emit(EMPTY_QUERY) }.flatMapLatest { query ->
getObjectTypes.stream(
val types = getObjectTypes.stream(
GetObjectTypes.Params(
sorts = ObjectSearchConstants.defaultObjectTypeSearchSorts(),
filters = ObjectSearchConstants.filterTypes(
@ -65,10 +73,23 @@ class SelectObjectTypeViewModel(
keys = ObjectSearchConstants.defaultKeysObjectType,
query = query
)
).filterIsInstance<Resultat.Success<List<ObjectWrapper.Type>>>().map { result ->
).filterIsInstance<Resultat.Success<List<ObjectWrapper.Type>>>()
combine(
types,
getPinnedObjectTypes.flow(GetPinnedObjectTypes.Params(SpaceId(space)))
) { result, pinned ->
_objectTypes.clear()
_objectTypes.addAll(result.getOrNull() ?: emptyList())
val pinnedObjectTypesIds = pinned.map { it.id }
val allTypes = (result.getOrNull() ?: emptyList())
val pinnedTypes = allTypes
.filter { pinnedObjectTypesIds.contains(it.id) }
.sortedBy { obj -> pinnedObjectTypesIds.indexOf(obj.id) }
val (allUserTypes, allLibraryTypes) = allTypes.partition { type ->
type.getValue<Id>(Relations.SPACE_ID) == space
}
@ -78,33 +99,57 @@ class SelectObjectTypeViewModel(
val (groups, objects) = allUserTypes.partition { type ->
type.uniqueKey == ObjectTypeUniqueKeys.SET || type.uniqueKey == ObjectTypeUniqueKeys.COLLECTION
}
val notPinnedObjects = objects.filter { !pinnedObjectTypesIds.contains(it.id) }
buildList {
if (pinnedTypes.isNotEmpty()) {
add(
SelectTypeView.Section.Pinned
)
addAll(
pinnedTypes.mapIndexed { index, type ->
SelectTypeView.Type(
id = type.id,
typeKey = type.uniqueKey,
name = type.name.orEmpty(),
icon = type.iconEmoji.orEmpty(),
isPinned = true,
isFirstInSection = index == 0
)
}
)
}
if (groups.isNotEmpty()) {
add(
SelectTypeView.Section.Groups
)
addAll(
groups.map { type ->
groups.mapIndexed { index, type ->
SelectTypeView.Type(
id = type.id,
typeKey = type.uniqueKey,
name = type.name.orEmpty(),
icon = type.iconEmoji.orEmpty()
icon = type.iconEmoji.orEmpty(),
isFirstInSection = index == 0,
isPinnable = false,
isPinned = false,
)
}
)
}
if (objects.isNotEmpty()) {
if (notPinnedObjects.isNotEmpty()) {
add(
SelectTypeView.Section.Objects
)
addAll(
objects.map { type ->
notPinnedObjects.mapIndexed { index, type ->
SelectTypeView.Type(
id = type.id,
typeKey = type.uniqueKey,
name = type.name.orEmpty(),
icon = type.iconEmoji.orEmpty()
icon = type.iconEmoji.orEmpty(),
isPinnable = true,
isFirstInSection = index == 0,
isPinned = false
)
}
)
@ -112,13 +157,16 @@ class SelectObjectTypeViewModel(
if (filteredLibraryTypes.isNotEmpty()) {
add(SelectTypeView.Section.Library)
addAll(
filteredLibraryTypes.map { type ->
filteredLibraryTypes.mapIndexed { index, type ->
SelectTypeView.Type(
id = type.id,
typeKey = type.uniqueKey,
name = type.name.orEmpty(),
icon = type.iconEmoji.orEmpty(),
isFromLibrary = true
isFromLibrary = true,
isPinned = false,
isPinnable = false,
isFirstInSection = index == 0
)
}
)
@ -142,6 +190,49 @@ class SelectObjectTypeViewModel(
}
}
fun onPinTypeClicked(typeView: SelectTypeView.Type) {
Timber.d("onPinTypeClicked: ${typeView.id}")
val state = viewState.value
if (state is SelectTypeViewState.Content) {
val pinned = buildSet {
add(TypeId(typeView.id))
state.views.forEach { view ->
if (view is SelectTypeView.Type && view.isPinned)
add(TypeId(view.id))
}
}
viewModelScope.launch {
setPinnedObjectTypes.async(
SetPinnedObjectTypes.Params(
space = SpaceId(id = space),
types = pinned.toList()
)
)
}
}
}
fun onUnpinTypeClicked(typeView: SelectTypeView.Type) {
Timber.d("onUnpinTypeClicked: ${typeView.id}")
val state = viewState.value
if (state is SelectTypeViewState.Content) {
val pinned = buildSet {
state.views.forEach { view ->
if (view is SelectTypeView.Type && view.isPinned && view.id != typeView.id)
add(TypeId(view.id))
}
}
viewModelScope.launch {
setPinnedObjectTypes.async(
SetPinnedObjectTypes.Params(
space = SpaceId(id = space),
types = pinned.toList()
)
)
}
}
}
fun onTypeClicked(typeView: SelectTypeView.Type) {
viewModelScope.launch {
if (typeView.isFromLibrary) {
@ -179,7 +270,9 @@ class SelectObjectTypeViewModel(
private val params: Params,
private val getObjectTypes: GetObjectTypes,
private val spaceManager: SpaceManager,
private val addObjectToSpace: AddObjectToSpace
private val addObjectToSpace: AddObjectToSpace,
private val setPinnedObjectTypes: SetPinnedObjectTypes,
private val getPinnedObjectTypes: GetPinnedObjectTypes
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
@ -188,7 +281,9 @@ class SelectObjectTypeViewModel(
params = params,
getObjectTypes = getObjectTypes,
spaceManager = spaceManager,
addObjectToSpace = addObjectToSpace
addObjectToSpace = addObjectToSpace,
setPinnedObjectTypes = setPinnedObjectTypes,
getPinnedObjectTypes = getPinnedObjectTypes
) as T
}
@ -205,6 +300,7 @@ sealed class SelectTypeViewState{
sealed class SelectTypeView {
sealed class Section : SelectTypeView() {
object Pinned : Section()
object Objects : Section()
object Groups : Section()
object Library : Section()
@ -215,7 +311,10 @@ sealed class SelectTypeView {
val typeKey: Key,
val name: String,
val icon: String,
val isFromLibrary: Boolean = false
val isFromLibrary: Boolean = false,
val isPinned: Boolean = false,
val isFirstInSection: Boolean = false,
val isPinnable: Boolean = true
) : SelectTypeView()
}