mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
Refact | Profile on dashboard with new subscription mechanism (#2051)
This commit is contained in:
parent
00afad8d9e
commit
4b40bf9f6e
28 changed files with 792 additions and 308 deletions
|
@ -8,7 +8,8 @@ import com.anytypeio.anytype.domain.auth.repo.AuthRepository
|
|||
import com.anytypeio.anytype.domain.block.interactor.Move
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.config.*
|
||||
import com.anytypeio.anytype.domain.dashboard.interactor.*
|
||||
import com.anytypeio.anytype.domain.dashboard.interactor.CloseDashboard
|
||||
import com.anytypeio.anytype.domain.dashboard.interactor.OpenDashboard
|
||||
import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects
|
||||
import com.anytypeio.anytype.domain.event.interactor.EventChannel
|
||||
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
|
||||
|
@ -17,6 +18,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
|
|||
import com.anytypeio.anytype.domain.objects.DeleteObjects
|
||||
import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived
|
||||
import com.anytypeio.anytype.domain.page.CreatePage
|
||||
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
|
||||
import com.anytypeio.anytype.presentation.dashboard.HomeDashboardEventConverter
|
||||
import com.anytypeio.anytype.presentation.dashboard.HomeDashboardViewModelFactory
|
||||
import com.anytypeio.anytype.ui.dashboard.DashboardFragment
|
||||
|
@ -87,9 +89,11 @@ object HomeDashboardModule {
|
|||
@Provides
|
||||
@PerScreen
|
||||
fun provideGetProfileUseCase(
|
||||
repository: BlockRepository
|
||||
repository: BlockRepository,
|
||||
subscriptionEventChannel: SubscriptionEventChannel
|
||||
): GetProfile = GetProfile(
|
||||
repo = repository
|
||||
repo = repository,
|
||||
channel = subscriptionEventChannel
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
|
@ -167,7 +171,7 @@ object HomeDashboardModule {
|
|||
@PerScreen
|
||||
fun provideGetDebugSettings(
|
||||
repo: InfrastructureRepository
|
||||
) : GetDebugSettings = GetDebugSettings(
|
||||
): GetDebugSettings = GetDebugSettings(
|
||||
repo = repo
|
||||
)
|
||||
|
||||
|
@ -176,7 +180,7 @@ object HomeDashboardModule {
|
|||
@PerScreen
|
||||
fun provideSearchObjects(
|
||||
repo: BlockRepository
|
||||
) : SearchObjects = SearchObjects(
|
||||
): SearchObjects = SearchObjects(
|
||||
repo = repo
|
||||
)
|
||||
|
||||
|
@ -191,7 +195,7 @@ object HomeDashboardModule {
|
|||
@PerScreen
|
||||
fun deleteObjects(
|
||||
repo: BlockRepository
|
||||
) : DeleteObjects = DeleteObjects(
|
||||
): DeleteObjects = DeleteObjects(
|
||||
repo = repo
|
||||
)
|
||||
|
||||
|
@ -200,7 +204,7 @@ object HomeDashboardModule {
|
|||
@PerScreen
|
||||
fun setObjectListIsArchived(
|
||||
repo: BlockRepository
|
||||
) : SetObjectListIsArchived = SetObjectListIsArchived(
|
||||
): SetObjectListIsArchived = SetObjectListIsArchived(
|
||||
repo = repo
|
||||
)
|
||||
}
|
|
@ -2,13 +2,17 @@ package com.anytypeio.anytype.di.main
|
|||
|
||||
import com.anytypeio.anytype.data.auth.event.EventDataChannel
|
||||
import com.anytypeio.anytype.data.auth.event.EventRemoteChannel
|
||||
import com.anytypeio.anytype.data.auth.event.SubscriptionDataChannel
|
||||
import com.anytypeio.anytype.data.auth.event.SubscriptionEventRemoteChannel
|
||||
import com.anytypeio.anytype.data.auth.status.ThreadStatusDataChannel
|
||||
import com.anytypeio.anytype.data.auth.status.ThreadStatusRemoteChannel
|
||||
import com.anytypeio.anytype.domain.event.interactor.EventChannel
|
||||
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
|
||||
import com.anytypeio.anytype.domain.status.ThreadStatusChannel
|
||||
import com.anytypeio.anytype.middleware.EventProxy
|
||||
import com.anytypeio.anytype.middleware.interactor.EventHandler
|
||||
import com.anytypeio.anytype.middleware.interactor.MiddlewareEventChannel
|
||||
import com.anytypeio.anytype.middleware.interactor.MiddlewareSubscriptionEventChannel
|
||||
import com.anytypeio.anytype.middleware.interactor.ThreadStatusMiddlewareChannel
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
@ -65,4 +69,25 @@ object EventModule {
|
|||
fun provideEventProxy(): EventProxy {
|
||||
return EventHandler()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSubscriptionEventChannel(
|
||||
channel: SubscriptionDataChannel
|
||||
): SubscriptionEventChannel = channel
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSubscriptionEventDataChannel(
|
||||
remote: SubscriptionEventRemoteChannel
|
||||
): SubscriptionDataChannel = SubscriptionDataChannel(remote)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSubscriptionEventRemoteChannel(
|
||||
proxy: EventProxy
|
||||
): SubscriptionEventRemoteChannel = MiddlewareSubscriptionEventChannel(events = proxy)
|
||||
}
|
|
@ -1,20 +1,8 @@
|
|||
package com.anytypeio.anytype.ui.dashboard
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.core_ui.tools.DefaultDragAndDropBehavior
|
||||
|
||||
class DashboardDragAndDropBehavior(
|
||||
onItemMoved: (Int, Int) -> Boolean,
|
||||
onItemDropped: (Int) -> Unit
|
||||
) : DefaultDragAndDropBehavior(onItemMoved, onItemDropped) {
|
||||
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
return if (viewHolder is ProfileContainerAdapter.ProfileContainerHolder)
|
||||
makeMovementFlags(0, 0)
|
||||
else
|
||||
super.getMovementFlags(recyclerView, viewHolder)
|
||||
}
|
||||
}
|
||||
) : DefaultDragAndDropBehavior(onItemMoved, onItemDropped)
|
|
@ -10,8 +10,10 @@ import androidx.transition.ChangeBounds
|
|||
import androidx.transition.TransitionManager
|
||||
import androidx.transition.TransitionSet
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_ui.reactive.clicks
|
||||
import com.anytypeio.anytype.core_utils.ext.*
|
||||
import com.anytypeio.anytype.core_utils.ui.ViewState
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.presentation.dashboard.DashboardView
|
||||
|
@ -223,6 +225,32 @@ class DashboardFragment : ViewStateFragment<State>(R.layout.fragment_dashboard)
|
|||
objectRemovalProgressBar.gone()
|
||||
}
|
||||
}
|
||||
jobs += lifecycleScope.subscribe(vm.profile) { profile ->
|
||||
setProfile(profile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setProfile(profile: ViewState<ObjectWrapper.Basic>) {
|
||||
when (profile) {
|
||||
is ViewState.Success -> {
|
||||
val obj = profile.data
|
||||
avatarContainer.bind(
|
||||
name = obj.name.orEmpty(),
|
||||
color = context?.getColor(R.color.dashboard_default_avatar_circle_color)
|
||||
)
|
||||
obj.iconImage?.let { avatar ->
|
||||
avatarContainer.icon(avatar)
|
||||
}
|
||||
if (obj.name.isNullOrEmpty()) {
|
||||
tvGreeting.text = getText(R.string.greet_user)
|
||||
} else {
|
||||
tvGreeting.text = getString(R.string.greet, obj.name)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// TODO reset profile view to zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
@ -264,31 +292,10 @@ class DashboardFragment : ViewStateFragment<State>(R.layout.fragment_dashboard)
|
|||
|
||||
override fun render(state: State) {
|
||||
when {
|
||||
state.error != null -> {
|
||||
requireActivity().toast("Error: ${state.error}")
|
||||
}
|
||||
state.error != null -> toast("Error: ${state.error}")
|
||||
state.isInitialzed -> {
|
||||
state.blocks.let { views ->
|
||||
val profile = views.filterIsInstance<DashboardView.Profile>()
|
||||
val links = views.filter { it !is DashboardView.Profile && it !is DashboardView.Archive }.groupBy { it.isArchived }
|
||||
if (profile.isNotEmpty()) {
|
||||
val view = profile.first()
|
||||
avatarContainer.bind(
|
||||
name = view.name,
|
||||
color = context?.getColor(R.color.dashboard_default_avatar_circle_color)
|
||||
)
|
||||
view.avatar?.let { avatar ->
|
||||
avatarContainer.icon(avatar)
|
||||
}
|
||||
if (view.name.isNotEmpty()) {
|
||||
tvGreeting.text = getString(R.string.greet, view.name)
|
||||
} else {
|
||||
tvGreeting.text = getText(R.string.greet_user)
|
||||
}
|
||||
}
|
||||
// TODO refact (no need to filter anything in fragment)
|
||||
dashboardDefaultAdapter.update(links[false] ?: emptyList())
|
||||
}
|
||||
val links = state.blocks.filter { it !is DashboardView.Archive }.groupBy { it.isArchived }
|
||||
dashboardDefaultAdapter.update(links[false] ?: emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
package com.anytypeio.anytype.ui.dashboard
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_ui.extensions.avatarColor
|
||||
import com.anytypeio.anytype.core_utils.ext.firstDigitByHash
|
||||
import com.anytypeio.anytype.core_utils.ext.typeOf
|
||||
import com.anytypeio.anytype.presentation.dashboard.DashboardView
|
||||
import kotlinx.android.synthetic.main.item_dashboard_profile_header.view.*
|
||||
|
||||
class DashboardProfileAdapter(
|
||||
private var data: MutableList<DashboardView>,
|
||||
private val onProfileClicked: () -> Unit
|
||||
) : RecyclerView.Adapter<DashboardProfileAdapter.ProfileHolder>() {
|
||||
|
||||
fun update(views: List<DashboardView>) {
|
||||
data.clear()
|
||||
data.addAll(views)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return ProfileHolder(inflater.inflate(R.layout.item_dashboard_profile_header, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ProfileHolder, position: Int) {
|
||||
val item = data[position] as DashboardView.Profile
|
||||
with(holder) {
|
||||
bindClick(onProfileClicked)
|
||||
bindName(item.name)
|
||||
bindAvatar(name = item.name, avatar = item.avatar)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ProfileHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
} else {
|
||||
payloads.typeOf<DesktopDiffUtil.Payload>().forEach { payload ->
|
||||
val item = data[position] as DashboardView.Profile
|
||||
with(holder) {
|
||||
if (payload.titleChanged()) {
|
||||
bindName(item.name)
|
||||
}
|
||||
if (payload.imageChanged()) {
|
||||
bindAvatar(item.name, item.avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
class ProfileHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
fun bindName(name: String) {
|
||||
itemView.greeting.text = itemView.context.getString(R.string.greet, name)
|
||||
}
|
||||
|
||||
fun bindAvatar(name: String, avatar: String?) {
|
||||
val pos = name.firstDigitByHash()
|
||||
itemView.avatar.bind(
|
||||
name = name,
|
||||
color = itemView.context.avatarColor(pos)
|
||||
)
|
||||
avatar?.let { itemView.avatar.icon(it) }
|
||||
}
|
||||
|
||||
fun bindClick(onClick: () -> Unit) {
|
||||
itemView.avatar.setOnClickListener { onClick() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -58,15 +58,6 @@ class DesktopDiffUtil(
|
|||
}
|
||||
}
|
||||
|
||||
if (oldDoc is DashboardView.Profile && newDoc is DashboardView.Profile) {
|
||||
if (oldDoc.avatar != newDoc.avatar) {
|
||||
changes.add(IMAGE_CHANGED)
|
||||
}
|
||||
if (oldDoc.name != newDoc.name) {
|
||||
changes.add(TITLE_CHANGED)
|
||||
}
|
||||
}
|
||||
|
||||
if (oldDoc.isSelected != newDoc.isSelected) {
|
||||
changes.add(SELECTION_CHANGED)
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package com.anytypeio.anytype.ui.dashboard
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.R
|
||||
import kotlinx.android.synthetic.main.item_profile_container.view.*
|
||||
|
||||
class ProfileContainerAdapter(
|
||||
val adapter: DashboardProfileAdapter
|
||||
) : RecyclerView.Adapter<ProfileContainerAdapter.ProfileContainerHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileContainerHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view = inflater.inflate(R.layout.item_profile_container, parent, false)
|
||||
view.recyclerView.apply {
|
||||
layoutManager = LinearLayoutManager(parent.context)
|
||||
val lp = (layoutParams as FrameLayout.LayoutParams)
|
||||
lp.height = (parent.height / 2) - lp.topMargin - lp.bottomMargin
|
||||
}
|
||||
return ProfileContainerHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ProfileContainerHolder, position: Int) {
|
||||
holder.bind(adapter)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
class ProfileContainerHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
fun bind(adapter: DashboardProfileAdapter) {
|
||||
itemView.recyclerView.adapter = adapter
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package com.anytypeio.anytype.core_models
|
||||
|
||||
data class SearchResult(
|
||||
val results: List<ObjectWrapper>,
|
||||
val dependencies: List<ObjectWrapper>
|
||||
val results: List<ObjectWrapper.Basic>,
|
||||
val dependencies: List<ObjectWrapper.Basic>
|
||||
)
|
|
@ -0,0 +1,32 @@
|
|||
package com.anytypeio.anytype.core_models
|
||||
|
||||
/**
|
||||
* Events related to changes or transformations of objects.
|
||||
* @see ObjectWrapper.Basic
|
||||
*/
|
||||
sealed class SubscriptionEvent {
|
||||
/**
|
||||
* @property [target] id of the object
|
||||
* @property [diff] slice of changes to apply to the object
|
||||
*/
|
||||
data class Amend(
|
||||
val target: Id,
|
||||
val diff: Map<Id, Any?>
|
||||
) : SubscriptionEvent()
|
||||
/**
|
||||
* @property [target] id of the object
|
||||
* @property [keys] keys, whose values should be removed
|
||||
*/
|
||||
data class Unset(
|
||||
val target: Id,
|
||||
val keys: List<Id>
|
||||
) : SubscriptionEvent()
|
||||
/**
|
||||
* @property [target] id of the object
|
||||
* @property [data] new set of data for the object
|
||||
*/
|
||||
data class Set(
|
||||
val target: Id,
|
||||
val data: Map<String, Any?>
|
||||
) : SubscriptionEvent()
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.anytypeio.anytype.data.auth.event
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
|
||||
|
||||
class SubscriptionDataChannel(
|
||||
private val remote: SubscriptionEventRemoteChannel
|
||||
) : SubscriptionEventChannel {
|
||||
override fun subscribe(subscriptions: List<Id>) = remote.subscribe(subscriptions)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.anytypeio.anytype.data.auth.event
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.SubscriptionEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SubscriptionEventRemoteChannel {
|
||||
fun subscribe(subscriptions: List<Id>): Flow<List<SubscriptionEvent>>
|
||||
}
|
|
@ -13,6 +13,7 @@ dependencies {
|
|||
def unitTestDependencies = rootProject.ext.unitTesting
|
||||
|
||||
testImplementation unitTestDependencies.kotlinTest
|
||||
testImplementation unitTestDependencies.turbine
|
||||
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
testImplementation unitTestDependencies.coroutineTesting
|
||||
|
|
|
@ -1,25 +1,87 @@
|
|||
package com.anytypeio.anytype.domain.auth.interactor
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_models.SubscriptionEvent
|
||||
import com.anytypeio.anytype.domain.`object`.amend
|
||||
import com.anytypeio.anytype.domain.`object`.unset
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.base.Either
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/** Use case for getting currently selected user account.
|
||||
*/
|
||||
class GetProfile(
|
||||
private val repo: BlockRepository
|
||||
) : BaseUseCase<Payload, BaseUseCase.None>() {
|
||||
private val repo: BlockRepository,
|
||||
private val channel: SubscriptionEventChannel
|
||||
) : BaseUseCase<ObjectWrapper.Basic, GetProfile.Params>() {
|
||||
|
||||
override suspend fun run(params: None) = try {
|
||||
fun subscribe(subscription: Id) = channel.subscribe(subscriptions = listOf(subscription))
|
||||
|
||||
val config = repo.getConfig()
|
||||
fun observe(
|
||||
subscription: Id,
|
||||
keys: List<String>,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
): Flow<ObjectWrapper.Basic> {
|
||||
|
||||
val payload = repo.openProfile(config.profile)
|
||||
|
||||
Either.Right(payload)
|
||||
|
||||
} catch (t: Throwable) {
|
||||
Either.Left(t)
|
||||
return flow {
|
||||
val profile = getProfile(subscription, keys)
|
||||
emitAll(
|
||||
channel.subscribe(subscriptions = listOf(subscription)).scan(profile) { prev, payload ->
|
||||
var result = prev
|
||||
payload.forEach { event ->
|
||||
result = when (event) {
|
||||
is SubscriptionEvent.Amend -> {
|
||||
result.amend(event.diff)
|
||||
}
|
||||
is SubscriptionEvent.Set -> {
|
||||
ObjectWrapper.Basic(event.data)
|
||||
}
|
||||
is SubscriptionEvent.Unset -> {
|
||||
result.unset(event.keys)
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
)
|
||||
}.flowOn(dispatcher)
|
||||
}
|
||||
|
||||
private suspend fun getProfile(
|
||||
subscription: Id,
|
||||
keys: List<String>
|
||||
): ObjectWrapper.Basic {
|
||||
val config = repo.getConfig()
|
||||
val result = repo.searchObjectsByIdWithSubscription(
|
||||
subscription = subscription,
|
||||
ids = listOf(config.profile),
|
||||
keys = keys
|
||||
)
|
||||
val profile = result.results.first { obj ->
|
||||
obj.id == config.profile
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
@Deprecated("Should not be used. Will be changed.")
|
||||
override suspend fun run(params: Params) = safe {
|
||||
val config = repo.getConfig()
|
||||
val result = repo.searchObjectsByIdWithSubscription(
|
||||
subscription = params.subscription,
|
||||
ids = listOf(config.profile),
|
||||
keys = params.keys
|
||||
)
|
||||
result.results.find { obj ->
|
||||
obj.id == config.profile
|
||||
} ?: throw Exception("Profile not found")
|
||||
}
|
||||
|
||||
class Params(
|
||||
val subscription: Id,
|
||||
val keys: List<String>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.anytypeio.anytype.domain.`object`
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
|
||||
/**
|
||||
* Function for applying granular changes in object, replacing existing values with the new ones.
|
||||
* @param [diff] difference
|
||||
*/
|
||||
fun ObjectWrapper.Basic.amend(diff: Map<Id, Any?>) = ObjectWrapper.Basic(map + diff)
|
||||
/**
|
||||
* Function for applying granular changes in object.
|
||||
*/
|
||||
fun ObjectWrapper.Basic.unset(keys: List<Id>) = ObjectWrapper.Basic(
|
||||
map.toMutableMap().apply {
|
||||
keys.forEach { k -> remove(k) }
|
||||
}
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package com.anytypeio.anytype.domain.search
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.SubscriptionEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Channel for events related to changes and transformations of objects.
|
||||
* @see SubscriptionEvent
|
||||
*/
|
||||
interface SubscriptionEventChannel {
|
||||
fun subscribe(subscriptions: List<Id>): Flow<List<SubscriptionEvent>>
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
package com.anytypeio.anytype.domain.dashboard
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.anytypeio.anytype.core_models.*
|
||||
import com.anytypeio.anytype.domain.auth.interactor.GetProfile
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.common.MockDataFactory
|
||||
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.stub
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class GetProfileTest {
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@get:Rule
|
||||
var rule = CoroutineTestRule()
|
||||
|
||||
@Mock
|
||||
lateinit var repo: BlockRepository
|
||||
|
||||
@Mock
|
||||
lateinit var channel: SubscriptionEventChannel
|
||||
|
||||
private lateinit var usecase: GetProfile
|
||||
|
||||
val config = Config(
|
||||
home = MockDataFactory.randomUuid(),
|
||||
profile = MockDataFactory.randomUuid(),
|
||||
gateway = MockDataFactory.randomUuid()
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
usecase = GetProfile(
|
||||
repo = repo,
|
||||
channel = channel
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit initial data with profile and complete`() = runBlockingTest {
|
||||
|
||||
val subscription = MockDataFactory.randomUuid()
|
||||
|
||||
val profile = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.ID to config.profile,
|
||||
Relations.NAME to "Friedrich Kittler"
|
||||
)
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking { getConfig() } doReturn config
|
||||
}
|
||||
|
||||
channel.stub {
|
||||
on { subscribe(listOf(subscription)) } doReturn flowOf()
|
||||
}
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
searchObjectsByIdWithSubscription(
|
||||
subscription = subscription,
|
||||
keys = emptyList(),
|
||||
ids = listOf(config.profile)
|
||||
)
|
||||
} doReturn SearchResult(
|
||||
results = listOf(profile),
|
||||
dependencies = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
usecase.observe(
|
||||
subscription = subscription,
|
||||
keys = emptyList(),
|
||||
dispatcher = rule.testDispatcher
|
||||
).test {
|
||||
assertEquals(
|
||||
expected = profile.map,
|
||||
actual = awaitItem().map
|
||||
)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit transformated profile object, then complete`() = runBlockingTest {
|
||||
|
||||
val subscription = MockDataFactory.randomUuid()
|
||||
|
||||
val nameBeforeUpdate = "Friedrich"
|
||||
val nameAfterUpdate = "Friedrich Kittler"
|
||||
|
||||
val profileObjectBeforeUpdate = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.ID to config.profile,
|
||||
Relations.NAME to nameBeforeUpdate
|
||||
)
|
||||
)
|
||||
|
||||
val profileObjectAfterUpdate = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.ID to config.profile,
|
||||
Relations.NAME to nameAfterUpdate
|
||||
)
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking { getConfig() } doReturn config
|
||||
}
|
||||
|
||||
channel.stub {
|
||||
on { subscribe(listOf(subscription)) } doReturn flow {
|
||||
emit(
|
||||
listOf(
|
||||
SubscriptionEvent.Amend(
|
||||
diff = mapOf(
|
||||
Relations.NAME to nameAfterUpdate
|
||||
),
|
||||
target = config.profile
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
searchObjectsByIdWithSubscription(
|
||||
subscription = subscription,
|
||||
keys = emptyList(),
|
||||
ids = listOf(config.profile)
|
||||
)
|
||||
} doReturn SearchResult(
|
||||
results = listOf(profileObjectBeforeUpdate),
|
||||
dependencies = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
usecase.observe(
|
||||
subscription = subscription,
|
||||
keys = emptyList(),
|
||||
dispatcher = rule.testDispatcher
|
||||
).test {
|
||||
assertEquals(
|
||||
expected = profileObjectBeforeUpdate.map,
|
||||
actual = awaitItem().map
|
||||
)
|
||||
assertEquals(
|
||||
expected = profileObjectAfterUpdate.map,
|
||||
actual = awaitItem().map
|
||||
)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should apply several transformations, then complete`() = runBlockingTest {
|
||||
|
||||
val subscription = MockDataFactory.randomUuid()
|
||||
|
||||
val nameBeforeUpdate = "Friedrich"
|
||||
val nameAfterUpdate = "Friedrich Kittler"
|
||||
|
||||
val iconImageBeforeUpdate = null
|
||||
val iconImageAfterUpdate = MockDataFactory.randomUuid()
|
||||
|
||||
val profileObjectBeforeUpdate = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.ID to config.profile,
|
||||
Relations.NAME to nameBeforeUpdate,
|
||||
Relations.ICON_IMAGE to iconImageBeforeUpdate
|
||||
)
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking { getConfig() } doReturn config
|
||||
}
|
||||
|
||||
channel.stub {
|
||||
on { subscribe(listOf(subscription)) } doReturn flow {
|
||||
emit(
|
||||
listOf(
|
||||
SubscriptionEvent.Amend(
|
||||
diff = mapOf(
|
||||
Relations.NAME to nameAfterUpdate
|
||||
),
|
||||
target = config.profile
|
||||
),
|
||||
SubscriptionEvent.Amend(
|
||||
diff = mapOf(
|
||||
Relations.ICON_IMAGE to iconImageAfterUpdate
|
||||
),
|
||||
target = config.profile
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
searchObjectsByIdWithSubscription(
|
||||
subscription = subscription,
|
||||
keys = emptyList(),
|
||||
ids = listOf(config.profile)
|
||||
)
|
||||
} doReturn SearchResult(
|
||||
results = listOf(profileObjectBeforeUpdate),
|
||||
dependencies = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
usecase.observe(
|
||||
subscription = subscription,
|
||||
keys = emptyList(),
|
||||
dispatcher = rule.testDispatcher
|
||||
).test {
|
||||
assertEquals(
|
||||
expected = profileObjectBeforeUpdate.map,
|
||||
actual = awaitItem().map
|
||||
)
|
||||
assertEquals(
|
||||
expected = mapOf(
|
||||
Relations.ID to config.profile,
|
||||
Relations.NAME to nameAfterUpdate,
|
||||
Relations.ICON_IMAGE to iconImageAfterUpdate
|
||||
),
|
||||
actual = awaitItem().map
|
||||
)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should apply all transformations, then complete`() = runBlockingTest {
|
||||
|
||||
val subscription = MockDataFactory.randomUuid()
|
||||
|
||||
val nameBeforeUpdate = "Friedrich"
|
||||
val nameAfterUpdate = "Friedrich Kittler"
|
||||
|
||||
val iconImageBeforeUpdate = null
|
||||
val iconImageAfterUpdate = MockDataFactory.randomUuid()
|
||||
|
||||
val profileObjectBeforeUpdate = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.ID to config.profile,
|
||||
Relations.NAME to nameBeforeUpdate,
|
||||
Relations.ICON_IMAGE to iconImageBeforeUpdate
|
||||
)
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking { getConfig() } doReturn config
|
||||
}
|
||||
|
||||
channel.stub {
|
||||
on { subscribe(listOf(subscription)) } doReturn flow {
|
||||
emit(
|
||||
listOf(
|
||||
SubscriptionEvent.Amend(
|
||||
diff = mapOf(
|
||||
Relations.NAME to nameAfterUpdate
|
||||
),
|
||||
target = config.profile
|
||||
)
|
||||
)
|
||||
)
|
||||
emit(
|
||||
listOf(
|
||||
SubscriptionEvent.Amend(
|
||||
diff = mapOf(
|
||||
Relations.ICON_IMAGE to iconImageAfterUpdate
|
||||
),
|
||||
target = config.profile
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
searchObjectsByIdWithSubscription(
|
||||
subscription = subscription,
|
||||
keys = emptyList(),
|
||||
ids = listOf(config.profile)
|
||||
)
|
||||
} doReturn SearchResult(
|
||||
results = listOf(profileObjectBeforeUpdate),
|
||||
dependencies = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
usecase.observe(
|
||||
subscription = subscription,
|
||||
keys = emptyList(),
|
||||
dispatcher = rule.testDispatcher
|
||||
).test {
|
||||
assertEquals(
|
||||
expected = profileObjectBeforeUpdate.map,
|
||||
actual = awaitItem().map
|
||||
)
|
||||
assertEquals(
|
||||
expected = mapOf(
|
||||
Relations.ID to config.profile,
|
||||
Relations.NAME to nameAfterUpdate,
|
||||
Relations.ICON_IMAGE to iconImageBeforeUpdate
|
||||
),
|
||||
actual = awaitItem().map
|
||||
)
|
||||
assertEquals(
|
||||
expected = mapOf(
|
||||
Relations.ID to config.profile,
|
||||
Relations.NAME to nameAfterUpdate,
|
||||
Relations.ICON_IMAGE to iconImageAfterUpdate
|
||||
),
|
||||
actual = awaitItem().map
|
||||
)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package com.anytypeio.anytype.domain.ext
|
||||
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.domain.`object`.amend
|
||||
import com.anytypeio.anytype.domain.`object`.unset
|
||||
import com.anytypeio.anytype.domain.common.MockDataFactory
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ObjectWrapperExtTest {
|
||||
|
||||
@Test
|
||||
fun `should update several fields with amend operation and preserve old fields - amend operation`() {
|
||||
|
||||
val initial = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.NAME to "Friedrich Kittler",
|
||||
Relations.DONE to false,
|
||||
Relations.DESCRIPTION to null,
|
||||
Relations.IS_FAVORITE to false
|
||||
)
|
||||
)
|
||||
|
||||
val expected = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.NAME to "Friedrich Kittler",
|
||||
Relations.DONE to true,
|
||||
Relations.DESCRIPTION to "German media philosopher",
|
||||
Relations.IS_FAVORITE to true
|
||||
)
|
||||
)
|
||||
|
||||
val result = initial.amend(
|
||||
mapOf(
|
||||
Relations.DONE to true,
|
||||
Relations.IS_FAVORITE to true,
|
||||
Relations.DESCRIPTION to "German media philosopher",
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = expected.map,
|
||||
actual = result.map
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should remove several fields - unset operation`() {
|
||||
|
||||
val firstRelationId = MockDataFactory.randomUuid()
|
||||
val firstRelationValue = MockDataFactory.randomString()
|
||||
val secondRelationId = MockDataFactory.randomUuid()
|
||||
val secondRelationValue = MockDataFactory.randomInt()
|
||||
val thirdRelationId = MockDataFactory.randomUuid()
|
||||
val thirdRelationValue = MockDataFactory.randomBoolean()
|
||||
|
||||
val initial = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.NAME to "Friedrich Kittler",
|
||||
Relations.DONE to false,
|
||||
Relations.DESCRIPTION to null,
|
||||
Relations.IS_FAVORITE to false,
|
||||
firstRelationId to firstRelationValue,
|
||||
secondRelationId to secondRelationValue,
|
||||
thirdRelationId to thirdRelationValue
|
||||
)
|
||||
)
|
||||
|
||||
val expected = ObjectWrapper.Basic(
|
||||
mapOf(
|
||||
Relations.NAME to "Friedrich Kittler",
|
||||
Relations.DONE to false,
|
||||
Relations.DESCRIPTION to null,
|
||||
Relations.IS_FAVORITE to false,
|
||||
)
|
||||
)
|
||||
|
||||
val result = initial.unset(
|
||||
keys = listOf(
|
||||
firstRelationId,
|
||||
secondRelationId,
|
||||
thirdRelationId
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = expected.map,
|
||||
actual = result.map
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package com.anytypeio.anytype.middleware.interactor
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.SubscriptionEvent
|
||||
import com.anytypeio.anytype.data.auth.event.SubscriptionEventRemoteChannel
|
||||
import com.anytypeio.anytype.middleware.EventProxy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
class MiddlewareSubscriptionEventChannel(
|
||||
private val events: EventProxy
|
||||
) : SubscriptionEventRemoteChannel {
|
||||
|
||||
override fun subscribe(subscriptions: List<Id>) = events
|
||||
.flow()
|
||||
.mapNotNull { payload ->
|
||||
payload.messages.mapNotNull { e ->
|
||||
when {
|
||||
e.objectDetailsAmend != null -> {
|
||||
val event = e.objectDetailsAmend
|
||||
checkNotNull(event)
|
||||
if (subscriptions.any { it in event.subIds }) {
|
||||
SubscriptionEvent.Amend(
|
||||
target = event.id,
|
||||
diff = event.details.associate { it.key to it.value }
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
e.objectDetailsUnset != null -> {
|
||||
val event = e.objectDetailsUnset
|
||||
checkNotNull(event)
|
||||
if (subscriptions.any { it in event.subIds }) {
|
||||
SubscriptionEvent.Unset(
|
||||
target = event.id,
|
||||
keys = event.keys
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
e.objectDetailsSet != null -> {
|
||||
val event = e.objectDetailsSet
|
||||
checkNotNull(event)
|
||||
val data = event.details
|
||||
if (subscriptions.any { it in event.subIds } && data != null) {
|
||||
SubscriptionEvent.Set(
|
||||
target = event.id,
|
||||
data = data
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.anytypeio.anytype.presentation.dashboard
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Url
|
||||
|
||||
data class DashboardProfileView(
|
||||
val id: Id,
|
||||
val name: String,
|
||||
val image: Url?
|
||||
)
|
|
@ -2,7 +2,6 @@ package com.anytypeio.anytype.presentation.dashboard
|
|||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_models.Url
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
|
||||
sealed class DashboardView {
|
||||
|
@ -12,15 +11,6 @@ sealed class DashboardView {
|
|||
abstract val isSelected: Boolean
|
||||
abstract val isLoading: Boolean
|
||||
|
||||
data class Profile(
|
||||
override val id: Id,
|
||||
val name: String,
|
||||
val avatar: Url? = null,
|
||||
override val isArchived: Boolean = false,
|
||||
override val isSelected: Boolean = false,
|
||||
override val isLoading: Boolean = false
|
||||
) : DashboardView()
|
||||
|
||||
data class Document(
|
||||
override val id: Id,
|
||||
val target: Id,
|
||||
|
|
|
@ -34,14 +34,6 @@ interface HomeDashboardEventConverter {
|
|||
objectTypes = objectTypesProvider.get()
|
||||
)
|
||||
}
|
||||
SmartBlockType.PROFILE_PAGE -> {
|
||||
HomeDashboardStateMachine.Event.OnShowProfile(
|
||||
blocks = event.blocks,
|
||||
context = event.context,
|
||||
details = event.details,
|
||||
builder = builder
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
|
|
|
@ -75,13 +75,6 @@ sealed class HomeDashboardStateMachine {
|
|||
val objectTypes: List<ObjectType>
|
||||
) : Event()
|
||||
|
||||
data class OnShowProfile(
|
||||
val context: String,
|
||||
val blocks: List<Block>,
|
||||
val details: Block.Details,
|
||||
val builder: UrlBuilder
|
||||
) : Event()
|
||||
|
||||
data class OnDetailsUpdated(
|
||||
val context: String,
|
||||
val target: String,
|
||||
|
@ -157,45 +150,24 @@ sealed class HomeDashboardStateMachine {
|
|||
)
|
||||
is Event.OnShowDashboard -> {
|
||||
|
||||
val current = state.blocks.filterIsInstance<DashboardView.Profile>()
|
||||
|
||||
val new = event.blocks
|
||||
.toDashboardViews(
|
||||
val new = event.blocks.toDashboardViews(
|
||||
details = event.details,
|
||||
builder = event.builder,
|
||||
objectTypes = event.objectTypes
|
||||
)
|
||||
|
||||
val childrenIdsList = event.blocks.getChildrenIdsList(
|
||||
parent = event.context
|
||||
)
|
||||
|
||||
val childrenIdsList = event.blocks.getChildrenIdsList(parent = event.context)
|
||||
|
||||
state.copy(
|
||||
isInitialzed = true,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
blocks = current.addAndSortByIds(childrenIdsList, new),
|
||||
blocks = new,
|
||||
childrenIdsList = childrenIdsList,
|
||||
objectTypes = event.objectTypes,
|
||||
details = event.details
|
||||
)
|
||||
}
|
||||
is Event.OnShowProfile -> {
|
||||
|
||||
val current = state.blocks.filter { it !is DashboardView.Profile }
|
||||
|
||||
val new = event.blocks.toDashboardViews(
|
||||
details = event.details,
|
||||
builder = event.builder
|
||||
).filterIsInstance<DashboardView.Profile>()
|
||||
|
||||
state.copy(
|
||||
isInitialzed = true,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
blocks = current.addAndSortByIds(state.childrenIdsList, new)
|
||||
)
|
||||
}
|
||||
is Event.OnStartedCreatingPage -> state.copy(
|
||||
isLoading = true
|
||||
)
|
||||
|
|
|
@ -16,15 +16,12 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.TAB_RECENT
|
|||
import com.anytypeio.anytype.analytics.base.EventsDictionary.TAB_SETS
|
||||
import com.anytypeio.anytype.analytics.base.sendEvent
|
||||
import com.anytypeio.anytype.analytics.props.Props
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_models.Position
|
||||
import com.anytypeio.anytype.core_models.*
|
||||
import com.anytypeio.anytype.core_utils.common.EventWrapper
|
||||
import com.anytypeio.anytype.core_utils.ext.withLatestFrom
|
||||
import com.anytypeio.anytype.core_utils.ui.ViewState
|
||||
import com.anytypeio.anytype.core_utils.ui.ViewStateViewModel
|
||||
import com.anytypeio.anytype.domain.auth.interactor.GetProfile
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.block.interactor.Move
|
||||
import com.anytypeio.anytype.domain.config.FlavourConfigProvider
|
||||
import com.anytypeio.anytype.domain.config.GetConfig
|
||||
|
@ -48,6 +45,7 @@ import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
|||
import com.anytypeio.anytype.presentation.objects.SupportedLayouts
|
||||
import com.anytypeio.anytype.presentation.objects.getProperName
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
|
||||
import com.anytypeio.anytype.presentation.search.Subscriptions
|
||||
import com.anytypeio.anytype.presentation.settings.EditorSettings
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
@ -76,7 +74,6 @@ class HomeDashboardViewModel(
|
|||
HomeDashboardEventConverter by eventConverter,
|
||||
SupportNavigation<EventWrapper<AppNavigation.Command>> {
|
||||
|
||||
private val isProfileNavigationEnabled = MutableStateFlow(false)
|
||||
val toasts = MutableSharedFlow<String>()
|
||||
|
||||
private val machine = Interactor(scope = viewModelScope)
|
||||
|
@ -89,7 +86,6 @@ class HomeDashboardViewModel(
|
|||
override val navigation = MutableLiveData<EventWrapper<AppNavigation.Command>>()
|
||||
|
||||
private var ctx: Id = ""
|
||||
private var profile: Id = ""
|
||||
|
||||
val tabs = MutableStateFlow(listOf(TAB.FAVOURITE, TAB.RECENT, TAB.SETS, TAB.BIN))
|
||||
|
||||
|
@ -109,9 +105,23 @@ class HomeDashboardViewModel(
|
|||
private val views: List<DashboardView>
|
||||
get() = stateData.value?.blocks ?: emptyList()
|
||||
|
||||
val profile = MutableStateFlow<ViewState<ObjectWrapper.Basic>>(ViewState.Init)
|
||||
|
||||
init {
|
||||
startProcessingState()
|
||||
proceedWithGettingConfig()
|
||||
viewModelScope.launch {
|
||||
getProfile.observe(
|
||||
subscription = Subscriptions.SUBSCRIPTION_PROFILE,
|
||||
keys = listOf(
|
||||
Relations.ID,
|
||||
Relations.NAME,
|
||||
Relations.ICON_IMAGE
|
||||
)
|
||||
).collect {
|
||||
profile.value = ViewState.Success(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startProcessingState() {
|
||||
|
@ -134,8 +144,6 @@ class HomeDashboardViewModel(
|
|||
result.either(
|
||||
fnR = { config ->
|
||||
ctx = config.home
|
||||
profile = config.profile
|
||||
isProfileNavigationEnabled.value = true
|
||||
startInterceptingEvents(context = config.home)
|
||||
processDragAndDrop(context = config.home)
|
||||
},
|
||||
|
@ -144,15 +152,6 @@ class HomeDashboardViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedWithGettingAccount() {
|
||||
getProfile(viewModelScope, BaseUseCase.None) { result ->
|
||||
result.either(
|
||||
fnL = { Timber.e(it, "Error while getting account") },
|
||||
fnR = { payload -> processEvents(payload.events) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processDragAndDrop(context: String) {
|
||||
viewModelScope.launch {
|
||||
dropChanges
|
||||
|
@ -193,7 +192,6 @@ class HomeDashboardViewModel(
|
|||
|
||||
fun onViewCreated() {
|
||||
Timber.d("onViewCreated, ")
|
||||
proceedWithGettingAccount()
|
||||
proceedWithOpeningHomeDashboard()
|
||||
}
|
||||
|
||||
|
@ -346,14 +344,19 @@ class HomeDashboardViewModel(
|
|||
|
||||
fun onAvatarClicked() {
|
||||
Timber.d("onAvatarClicked, ")
|
||||
if (isProfileNavigationEnabled.value) {
|
||||
viewModelScope.sendEvent(
|
||||
analytics = analytics,
|
||||
eventName = SCREEN_PROFILE
|
||||
)
|
||||
proceedWithOpeningDocument(profile)
|
||||
} else {
|
||||
toast("Profile is not ready yet. Please, try again later.")
|
||||
profile.value.let { state ->
|
||||
when(state) {
|
||||
is ViewState.Success -> {
|
||||
viewModelScope.sendEvent(
|
||||
analytics = analytics,
|
||||
eventName = SCREEN_PROFILE
|
||||
)
|
||||
proceedWithOpeningDocument(state.data.id)
|
||||
}
|
||||
else -> {
|
||||
toast("Profile is not ready yet. Please, try again later.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ fun List<DashboardView>.sortByIds(
|
|||
}
|
||||
|
||||
fun List<DashboardView>.filterByNotArchivedPages(): List<DashboardView> =
|
||||
this.filterNot { it is DashboardView.Profile || it.isArchived }
|
||||
this.filterNot { it.isArchived }
|
||||
|
||||
fun List<DashboardView>.updateDetails(
|
||||
target: String,
|
||||
|
@ -34,19 +34,6 @@ fun List<DashboardView>.updateDetails(
|
|||
): List<DashboardView> {
|
||||
return mapNotNull { view ->
|
||||
when (view) {
|
||||
is DashboardView.Profile -> {
|
||||
if (view.id == target) {
|
||||
view.copy(
|
||||
name = details.name.orEmpty(),
|
||||
avatar = details.iconImage.let {
|
||||
if (it.isNullOrEmpty()) null
|
||||
else builder.image(it)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
view
|
||||
}
|
||||
}
|
||||
is DashboardView.Document -> {
|
||||
if (view.target == target) {
|
||||
val obj = ObjectWrapper.Basic(details.map)
|
||||
|
|
|
@ -258,21 +258,6 @@ fun List<Block>.toDashboardViews(
|
|||
objectTypes: List<ObjectType> = emptyList()
|
||||
): List<DashboardView> = this.mapNotNull { block ->
|
||||
when (val content = block.content) {
|
||||
is Block.Content.Smart -> {
|
||||
when (content.type) {
|
||||
SmartBlockType.PROFILE_PAGE -> {
|
||||
DashboardView.Profile(
|
||||
id = block.id,
|
||||
name = details.details[block.id]?.name.orEmpty(),
|
||||
avatar = details.details[block.id]?.iconImage.let {
|
||||
if (it.isNullOrEmpty()) null
|
||||
else builder.image(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
is Block.Content.Link -> {
|
||||
val targetDetails = details.details[content.target]
|
||||
val typeUrl = targetDetails?.map?.type
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package com.anytypeio.anytype.presentation.search
|
||||
|
||||
object Subscriptions {
|
||||
const val SUBSCRIPTION_PROFILE = "subscription.profile"
|
||||
}
|
|
@ -10,7 +10,8 @@ import com.anytypeio.anytype.domain.auth.interactor.GetProfile
|
|||
import com.anytypeio.anytype.domain.base.Either
|
||||
import com.anytypeio.anytype.domain.block.interactor.Move
|
||||
import com.anytypeio.anytype.domain.config.*
|
||||
import com.anytypeio.anytype.domain.dashboard.interactor.*
|
||||
import com.anytypeio.anytype.domain.dashboard.interactor.CloseDashboard
|
||||
import com.anytypeio.anytype.domain.dashboard.interactor.OpenDashboard
|
||||
import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects
|
||||
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
|
||||
import com.anytypeio.anytype.domain.launch.GetDefaultEditorType
|
||||
|
@ -22,6 +23,7 @@ import com.anytypeio.anytype.presentation.navigation.AppNavigation
|
|||
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
|
||||
import com.jraska.livedata.test
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Before
|
||||
|
@ -135,6 +137,7 @@ class HomeDashboardViewModelTest {
|
|||
|
||||
stubGetConfig(response)
|
||||
stubObserveEvents(params = InterceptEvents.Params(context = null))
|
||||
stubObserveProfile()
|
||||
|
||||
// TESTING
|
||||
|
||||
|
@ -142,7 +145,6 @@ class HomeDashboardViewModelTest {
|
|||
|
||||
verify(getConfig, times(1)).invoke(any(), any(), any())
|
||||
verifyZeroInteractions(openDashboard)
|
||||
verifyZeroInteractions(getProfile)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -174,6 +176,7 @@ class HomeDashboardViewModelTest {
|
|||
|
||||
stubGetConfig(response)
|
||||
stubObserveEvents(params = InterceptEvents.Params(context = config.home))
|
||||
stubObserveProfile()
|
||||
|
||||
// TESTING
|
||||
|
||||
|
@ -181,7 +184,6 @@ class HomeDashboardViewModelTest {
|
|||
|
||||
verify(getConfig, times(1)).invoke(any(), any(), any())
|
||||
verifyZeroInteractions(openDashboard)
|
||||
verifyZeroInteractions(getProfile)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -286,27 +288,25 @@ class HomeDashboardViewModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `should proceed with getting profile and opening dashboard when view is created`() {
|
||||
fun `should proceed opening dashboard when view is created`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
stubGetConfig(Either.Right(config))
|
||||
stubObserveEvents(params = InterceptEvents.Params(context = config.home))
|
||||
stubOpenDashboard()
|
||||
stubObserveProfile()
|
||||
|
||||
// TESTING
|
||||
|
||||
vm = buildViewModel()
|
||||
vm.onViewCreated()
|
||||
|
||||
verify(getProfile, times(1)).invoke(any(), any(), any())
|
||||
|
||||
runBlockingTest {
|
||||
verify(openDashboard, times(1)).invoke(eq(null))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `should start creating page when requested from UI`() {
|
||||
|
||||
|
@ -395,7 +395,24 @@ class HomeDashboardViewModelTest {
|
|||
|
||||
private fun stubGetDefaultObjectType(type: String? = null, name: String? = null) {
|
||||
getDefaultEditorType.stub {
|
||||
onBlocking { invoke(Unit) } doReturn Either.Right(GetDefaultEditorType.Response(type, name))
|
||||
onBlocking { invoke(Unit) } doReturn Either.Right(
|
||||
GetDefaultEditorType.Response(
|
||||
type,
|
||||
name
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stubObserveProfile() {
|
||||
getProfile.stub {
|
||||
on {
|
||||
observe(
|
||||
subscription = any(),
|
||||
keys = any(),
|
||||
dispatcher = any()
|
||||
)
|
||||
} doReturn emptyFlow()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -670,7 +670,6 @@ class DashboardViewExtensionKtTest {
|
|||
val target1 = MockDataFactory.randomUuid()
|
||||
val id2 = MockDataFactory.randomUuid()
|
||||
val target2 = MockDataFactory.randomUuid()
|
||||
val id3 = MockDataFactory.randomUuid()
|
||||
|
||||
val views = listOf(
|
||||
DashboardView.Document(
|
||||
|
@ -686,11 +685,6 @@ class DashboardViewExtensionKtTest {
|
|||
target = target2,
|
||||
title = "Title2"
|
||||
),
|
||||
DashboardView.Profile(
|
||||
isArchived = false,
|
||||
id = id3,
|
||||
name = "Profile"
|
||||
),
|
||||
DashboardView.Document(
|
||||
isArchived = false,
|
||||
id = "profileId",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue