diff --git a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreen.kt index c525937498..a332ee5fb8 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreen.kt @@ -52,6 +52,7 @@ import com.anytypeio.anytype.presentation.widgets.FromIndex import com.anytypeio.anytype.presentation.widgets.ToIndex import com.anytypeio.anytype.presentation.widgets.TreePath import com.anytypeio.anytype.presentation.widgets.ViewId +import com.anytypeio.anytype.presentation.widgets.Widget import com.anytypeio.anytype.presentation.widgets.WidgetId import com.anytypeio.anytype.presentation.widgets.WidgetView import com.anytypeio.anytype.ui.widgets.menu.WidgetActionButton @@ -71,6 +72,7 @@ fun HomeScreen( widgets: List, onExpand: (TreePath) -> Unit, onWidgetObjectClicked: (ObjectWrapper.Basic) -> Unit, + onWidgetSourceClicked: (Widget.Source) -> Unit, onBundledWidgetClicked: (WidgetId) -> Unit, onCreateWidget: () -> Unit, onEditWidgets: () -> Unit, @@ -91,6 +93,7 @@ fun HomeScreen( onExpand = onExpand, onWidgetMenuAction = onWidgetMenuAction, onWidgetObjectClicked = onWidgetObjectClicked, + onWidgetSourceClicked = onWidgetSourceClicked, onBundledWidgetHeaderClicked = onBundledWidgetClicked, onToggleExpandedWidgetState = onToggleExpandedWidgetState, mode = mode, @@ -152,6 +155,7 @@ private fun WidgetList( onExpand: (TreePath) -> Unit, onWidgetMenuAction: (WidgetId, DropDownMenuAction) -> Unit, onWidgetObjectClicked: (ObjectWrapper.Basic) -> Unit, + onWidgetSourceClicked: (Widget.Source) -> Unit, onBundledWidgetHeaderClicked: (WidgetId) -> Unit, onToggleExpandedWidgetState: (WidgetId) -> Unit, mode: InteractionMode, @@ -215,6 +219,7 @@ private fun WidgetList( onWidgetMenuAction(item.id, action) }, onWidgetObjectClicked = onWidgetObjectClicked, + onWidgetSourceClicked = onWidgetSourceClicked, onToggleExpandedWidgetState = onToggleExpandedWidgetState, mode = mode ) @@ -266,7 +271,7 @@ private fun WidgetList( onDropDownMenuAction = { action -> onWidgetMenuAction(item.id, action) }, - onWidgetObjectClicked = onWidgetObjectClicked, + onWidgetSourceClicked = onWidgetSourceClicked, isInEditMode = mode is InteractionMode.Edit ) AnimatedVisibility( @@ -317,6 +322,7 @@ private fun WidgetList( DataViewListWidgetCard( item = item, onWidgetObjectClicked = onWidgetObjectClicked, + onWidgetSourceClicked = onWidgetSourceClicked, onDropDownMenuAction = { action -> onWidgetMenuAction(item.id, action) }, diff --git a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt index da93f63068..3e35979307 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt @@ -68,6 +68,7 @@ class HomeScreenFragment : BaseComposeFragment() { vm.onDropDownMenuAction(widget, action) }, onWidgetObjectClicked = vm::onWidgetObjectClicked, + onWidgetSourceClicked = vm::onWidgetSourceClicked, onChangeWidgetView = vm::onChangeCurrentWidgetView, onToggleExpandedWidgetState = vm::onToggleCollapsedWidgetState, onSearchClicked = { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/linking/LinkToObjectFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/linking/LinkToObjectFragment.kt index addfb725bb..62b6d05029 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/linking/LinkToObjectFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/linking/LinkToObjectFragment.kt @@ -40,9 +40,9 @@ import com.anytypeio.anytype.ui.moving.hideProgress import com.anytypeio.anytype.ui.moving.showProgress import com.anytypeio.anytype.ui.search.ObjectSearchFragment import com.google.android.material.bottomsheet.BottomSheetBehavior +import javax.inject.Inject import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject class LinkToObjectFragment : BaseBottomSheetTextInputFragment() { @@ -62,7 +62,7 @@ class LinkToObjectFragment : BaseBottomSheetTextInputFragment Unit, + onWidgetSourceClicked: (Widget.Source) -> Unit, onDropDownMenuAction: (DropDownMenuAction) -> Unit, onChangeWidgetView: (WidgetId, ViewId) -> Unit, onToggleExpandedWidgetState: (WidgetId) -> Unit @@ -72,10 +75,15 @@ fun DataViewListWidgetCard( .padding(horizontal = 0.dp, vertical = 6.dp) ) { WidgetHeader( - title = item.obj.getProperName(), + title = when (val source = item.source) { + is Widget.Source.Default -> { + source.obj.getWidgetObjectName() ?: stringResource(id = R.string.untitled) + } + is Widget.Source.Bundled -> { stringResource(id = source.res()) } + }, isCardMenuExpanded = isCardMenuExpanded, isHeaderMenuExpanded = isHeaderMenuExpanded, - onWidgetHeaderClicked = { onWidgetObjectClicked(item.obj) }, + onWidgetHeaderClicked = { onWidgetSourceClicked(item.source) }, onExpandElement = { onToggleExpandedWidgetState(item.id) }, isExpanded = item.isExpanded, onDropDownMenuAction = onDropDownMenuAction @@ -217,4 +225,11 @@ fun ListWidgetElement( ) } } +} + +@StringRes +fun Widget.Source.Bundled.res() : Int = when(this) { + Widget.Source.Bundled.Favorites -> R.string.favorites + Widget.Source.Bundled.Recent -> R.string.recent + Widget.Source.Bundled.Sets -> R.string.sets } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/LinkWidget.kt b/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/LinkWidget.kt index 03a3e18172..0913172342 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/LinkWidget.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/LinkWidget.kt @@ -29,9 +29,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.anytypeio.anytype.R -import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction +import com.anytypeio.anytype.presentation.widgets.Widget import com.anytypeio.anytype.presentation.widgets.WidgetView import com.anytypeio.anytype.presentation.widgets.getWidgetObjectName import com.anytypeio.anytype.ui.widgets.menu.WidgetMenu @@ -40,7 +40,7 @@ import com.anytypeio.anytype.ui.widgets.menu.WidgetMenu @Composable fun LinkWidgetCard( item: WidgetView.Link, - onWidgetObjectClicked: (ObjectWrapper.Basic) -> Unit, + onWidgetSourceClicked: (Widget.Source) -> Unit, onDropDownMenuAction: (DropDownMenuAction) -> Unit, isInEditMode: Boolean ) { @@ -66,7 +66,7 @@ fun LinkWidgetCard( } else Modifier.combinedClickable( - onClick = { onWidgetObjectClicked(item.obj) }, + onClick = { onWidgetSourceClicked(item.source) }, onLongClick = { isCardMenuExpanded.value = !isCardMenuExpanded.value }, @@ -82,7 +82,12 @@ fun LinkWidgetCard( .height(40.dp) ) { Text( - text = item.obj.getWidgetObjectName() ?: stringResource(id = R.string.untitled), + text = when (val source = item.source) { + is Widget.Source.Default -> { + source.obj.getWidgetObjectName() ?: stringResource(id = R.string.untitled) + } + is Widget.Source.Bundled -> { stringResource(id = source.res()) } + }, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier diff --git a/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/TreeWidget.kt b/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/TreeWidget.kt index 218d94be9c..701de92af0 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/TreeWidget.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/TreeWidget.kt @@ -48,6 +48,7 @@ import com.anytypeio.anytype.presentation.home.InteractionMode import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction import com.anytypeio.anytype.presentation.widgets.TreePath +import com.anytypeio.anytype.presentation.widgets.Widget import com.anytypeio.anytype.presentation.widgets.WidgetId import com.anytypeio.anytype.presentation.widgets.WidgetView import com.anytypeio.anytype.presentation.widgets.getWidgetObjectName @@ -59,6 +60,7 @@ fun TreeWidgetCard( item: WidgetView.Tree, onExpandElement: (TreePath) -> Unit, onWidgetObjectClicked: (ObjectWrapper.Basic) -> Unit, + onWidgetSourceClicked: (Widget.Source) -> Unit, onDropDownMenuAction: (DropDownMenuAction) -> Unit, onToggleExpandedWidgetState: (WidgetId) -> Unit ) { @@ -94,10 +96,15 @@ fun TreeWidgetCard( ) ) { WidgetHeader( - title = item.obj.getWidgetObjectName().orEmpty(), + title = when (val source = item.source) { + is Widget.Source.Default -> { + source.obj.getWidgetObjectName() ?: stringResource(id = R.string.untitled) + } + is Widget.Source.Bundled -> { stringResource(id = source.res()) } + }, isCardMenuExpanded = isCardMenuExpanded, isHeaderMenuExpanded = isHeaderMenuExpanded, - onWidgetHeaderClicked = { onWidgetObjectClicked(item.obj) }, + onWidgetHeaderClicked = { onWidgetSourceClicked(item.source) }, onExpandElement = { onToggleExpandedWidgetState(item.id) }, isExpanded = item.isExpanded, onDropDownMenuAction = onDropDownMenuAction, diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/navigation/PageLinksAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/navigation/PageLinksAdapter.kt index c8ba4d5311..cae42d0074 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/navigation/PageLinksAdapter.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/navigation/PageLinksAdapter.kt @@ -17,7 +17,9 @@ import com.anytypeio.anytype.core_utils.ext.visible import com.anytypeio.anytype.presentation.navigation.DefaultObjectView import com.anytypeio.anytype.presentation.navigation.DefaultSearchItem import com.anytypeio.anytype.presentation.navigation.ObjectView +import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.search.ObjectSearchSection +import com.anytypeio.anytype.presentation.widgets.source.BundledWidgetSourceView @Deprecated("LEGACY SUSPECT") class PageLinksAdapter( @@ -76,28 +78,42 @@ class PageLinksAdapter( } class DefaultObjectViewAdapter( - private val onClick: (DefaultObjectView) -> Unit, + private val onDefaultObjectClicked: (DefaultObjectView) -> Unit, + private val onBundledWidgetSourceClicked: (BundledWidgetSourceView) -> Unit = {} ) : ListAdapter(Differ) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int - ): ObjectViewHolder { - return when (viewType) { - TYPE_ITEM -> ObjectItemViewHolder(inflate(parent, R.layout.item_list_object)).apply { - itemView.setOnClickListener { - val pos = bindingAdapterPosition - if (pos != RecyclerView.NO_POSITION) { - val item = getItem(pos) - if (item is DefaultObjectView) { - onClick(item) - } + ): ObjectViewHolder = when (viewType) { + TYPE_ITEM -> ObjectItemViewHolder(inflate(parent, R.layout.item_list_object)).apply { + itemView.setOnClickListener { + val pos = bindingAdapterPosition + if (pos != RecyclerView.NO_POSITION) { + val item = getItem(pos) + if (item is DefaultObjectView) { + onDefaultObjectClicked(item) } } } - TYPE_SECTION_RECENTLY_OPENED -> RecentlyOpenedHolder(inflate(parent, R.layout.item_search_section_recently_opened)) - else -> throw IllegalStateException("Unexpected view type: $viewType") } + TYPE_BUNDLED_WIDGET_SOURCE -> BundledWidgetSourceHolder( + ItemListObjectBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ).apply { + itemView.setOnClickListener { + val pos = bindingAdapterPosition + if (pos != RecyclerView.NO_POSITION) { + val item = getItem(pos) + if (item is BundledWidgetSourceView) { + onBundledWidgetSourceClicked(item) + } + } + } + } + TYPE_SECTION -> SectionViewHolder(inflate(parent, R.layout.item_object_search_section)) + else -> throw IllegalStateException("Unexpected view type: $viewType") } override fun onBindViewHolder(holder: ObjectViewHolder, position: Int) { @@ -107,20 +123,39 @@ class DefaultObjectViewAdapter( check(item is DefaultObjectView) holder.bind(item) } - else -> {} + is SectionViewHolder -> { + check(item is ObjectSearchSection) + when(item) { + ObjectSearchSection.RecentlyOpened -> { + holder.title.setText(R.string.object_search_recently_opened_section_title) + } + ObjectSearchSection.SelectWidgetSource.FromLibrary -> { + holder.title.setText(R.string.widget_source_anytype_library) + } + ObjectSearchSection.SelectWidgetSource.FromMyObjects -> { + holder.title.setText(R.string.objects) + } + } + } + is BundledWidgetSourceHolder -> { + check(item is BundledWidgetSourceView) + holder.bind(item) + } } } - override fun getItemViewType(position: Int): Int { - return when (val item = getItem(position)) { - is DefaultObjectView -> TYPE_ITEM - is ObjectSearchSection.RecentlyOpened -> TYPE_SECTION_RECENTLY_OPENED - else -> throw IllegalStateException("Unexpected item type: ${item.javaClass.name}") - } + override fun getItemViewType(position: Int): Int = when (val item = getItem(position)) { + is DefaultObjectView -> TYPE_ITEM + is ObjectSearchSection -> TYPE_SECTION + is BundledWidgetSourceView -> TYPE_BUNDLED_WIDGET_SOURCE + else -> throw IllegalStateException("Unexpected item type: ${item.javaClass.name}") } open class ObjectViewHolder(val view: View) : RecyclerView.ViewHolder(view) - inner class RecentlyOpenedHolder(view: View) : ObjectViewHolder(view) + + inner class SectionViewHolder(view: View) : ObjectViewHolder(view) { + val title : TextView get() = view.findViewById(R.id.tvTitle) + } class ObjectItemViewHolder(view: View) : ObjectViewHolder(view) { @@ -158,8 +193,39 @@ class ObjectItemViewHolder(view: View) : ObjectViewHolder(view) { ): Boolean = (oldItem as? DefaultObjectView) == (newItem as? DefaultObjectView) } +} +class BundledWidgetSourceHolder( + private val binding: ItemListObjectBinding +) : DefaultObjectViewAdapter.ObjectViewHolder(binding.root) { + + fun bind(item: BundledWidgetSourceView) { + when (item) { + BundledWidgetSourceView.Favorites -> { + with(binding) { + tvTitle.setText(R.string.favorites) + tvSubtitle.setText(R.string.your_favorite_objects) + ivIcon.setIcon(ObjectIcon.Basic.Emoji("⭐️")) + } + } + BundledWidgetSourceView.Recent -> { + with(binding) { + tvTitle.setText(R.string.recent) + tvSubtitle.setText(R.string.recently_opened_objects) + ivIcon.setIcon(ObjectIcon.Basic.Emoji("🗓")) + } + } + BundledWidgetSourceView.Sets -> { + with(binding) { + tvTitle.setText(R.string.sets) + tvSubtitle.setText(R.string.sets_of_objects) + ivIcon.setIcon(ObjectIcon.Basic.Emoji("📚")) + } + } + } + } } private const val TYPE_ITEM = 0 -private const val TYPE_SECTION_RECENTLY_OPENED = 1 \ No newline at end of file +private const val TYPE_SECTION = 1 +private const val TYPE_BUNDLED_WIDGET_SOURCE = 2 \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/search/ObjectSearchDividerItemDecoration.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/search/ObjectSearchDividerItemDecoration.kt index 3fdeb8e133..f2c11e756e 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/search/ObjectSearchDividerItemDecoration.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/search/ObjectSearchDividerItemDecoration.kt @@ -30,7 +30,7 @@ class ObjectSearchDividerItemDecoration( val right: Int = parent.width val childCount = parent.childCount val startPosition = - if (parent.getChildViewHolder(parent.getChildAt(0)) is DefaultObjectViewAdapter.RecentlyOpenedHolder) { + if (parent.getChildViewHolder(parent.getChildAt(0)) is DefaultObjectViewAdapter.SectionViewHolder) { POSITION_HEADER } else { POSITION_DATA diff --git a/core-ui/src/main/res/layout/item_search_section_recently_opened.xml b/core-ui/src/main/res/layout/item_object_search_section.xml similarity index 100% rename from core-ui/src/main/res/layout/item_search_section_recently_opened.xml rename to core-ui/src/main/res/layout/item_object_search_section.xml diff --git a/core-ui/src/main/res/values/dimens.xml b/core-ui/src/main/res/values/dimens.xml index f8a0973aec..f0e2e9c644 100644 --- a/core-ui/src/main/res/values/dimens.xml +++ b/core-ui/src/main/res/values/dimens.xml @@ -183,7 +183,7 @@ 34dp 80dp 20dp - 28dp + 24dp 24dp 28sp 2dp diff --git a/core-ui/src/main/res/values/strings.xml b/core-ui/src/main/res/values/strings.xml index 74130f0c64..d9fed40132 100644 --- a/core-ui/src/main/res/values/strings.xml +++ b/core-ui/src/main/res/values/strings.xml @@ -604,6 +604,10 @@ The source of the inline set has no type Recently opened + Anytype Library + Your favorite objects + Recently opened objects + Sets of objects from your workspace Customize-view button No objects diff --git a/library-emojifier/src/main/java/com/anytypeio/anytype/emojifier/Emojifier.kt b/library-emojifier/src/main/java/com/anytypeio/anytype/emojifier/Emojifier.kt index f5be074a66..2971da0ee2 100644 --- a/library-emojifier/src/main/java/com/anytypeio/anytype/emojifier/Emojifier.kt +++ b/library-emojifier/src/main/java/com/anytypeio/anytype/emojifier/Emojifier.kt @@ -48,7 +48,7 @@ object Emojifier { /** * @param unicode emoji unicode - * @return a pair constisting of emoji's page and emoji's index for this [unicode] + * @return a pair consisting of emoji's page and emoji's index for this [unicode] */ private fun search(unicode: String): Pair? { val cached = cache[unicode] diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt index 7a3c0e7561..3f546bdd73 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt @@ -38,7 +38,6 @@ import com.anytypeio.anytype.presentation.widgets.CollapsedWidgetStateHolder import com.anytypeio.anytype.presentation.widgets.DataViewListWidgetContainer import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction import com.anytypeio.anytype.presentation.widgets.LinkWidgetContainer -import com.anytypeio.anytype.presentation.widgets.ListWidgetContainer import com.anytypeio.anytype.presentation.widgets.TreePath import com.anytypeio.anytype.presentation.widgets.TreeWidgetBranchStateHolder import com.anytypeio.anytype.presentation.widgets.TreeWidgetContainer @@ -99,52 +98,9 @@ class HomeScreenViewModel( private val containers = MutableStateFlow>(emptyList()) private val treeWidgetBranchStateHolder = TreeWidgetBranchStateHolder() - // Bundled widget containing recently opened objects - private val recent by lazy { - ListWidgetContainer( - workspace = configStorage.get().workspace, - isWidgetCollapsed = isCollapsed(Subscriptions.SUBSCRIPTION_RECENT), - storage = storelessSubscriptionContainer, - urlBuilder = urlBuilder, - subscription = Subscriptions.SUBSCRIPTION_RECENT - ) - } - - // Bundled widget containing sets - private val sets by lazy { - ListWidgetContainer( - workspace = configStorage.get().workspace, - isWidgetCollapsed = isCollapsed(Subscriptions.SUBSCRIPTION_SETS), - storage = storelessSubscriptionContainer, - urlBuilder = urlBuilder, - subscription = Subscriptions.SUBSCRIPTION_SETS - ) - } - - // Bundled widget containing objects from favorites - private val favorites by lazy { - ListWidgetContainer( - workspace = configStorage.get().workspace, - isWidgetCollapsed = isCollapsed(Subscriptions.SUBSCRIPTION_FAVORITES), - storage = storelessSubscriptionContainer, - urlBuilder = urlBuilder, - subscription = Subscriptions.SUBSCRIPTION_FAVORITES - ) - } - // Bundled widget containing archived objects private val bin = WidgetView.Bin(Subscriptions.SUBSCRIPTION_ARCHIVED) - private val bundledWidgets by lazy { - combine( - listOf( - favorites.view, - recent.view, - sets.view - ) - ) { array -> array } - } - init { viewModelScope.launch { unsubscriber.start() } @@ -211,7 +167,8 @@ class HomeScreenViewModel( expandedBranches = treeWidgetBranchStateHolder.stream(widget.id), isWidgetCollapsed = isCollapsed(widget.id), urlBuilder = urlBuilder, - dispatchers = appCoroutineDispatchers + dispatchers = appCoroutineDispatchers, + workspace = config.workspace ) is Widget.List -> DataViewListWidgetContainer( widget = widget, @@ -241,8 +198,6 @@ class HomeScreenViewModel( } else { flowOf(emptyList()) } - }.combine(bundledWidgets) { fromWidgetObject, bundled -> - bundled.toList() + fromWidgetObject }.flowOn(appCoroutineDispatchers.io).collect { views.value = it + bin + actions } @@ -276,7 +231,12 @@ class HomeScreenViewModel( private fun proceedWithClosingWidgetObject(widgetObject: Id) { viewModelScope.launch { - val subscriptions = widgets.value.map { widget -> widget.id } + val subscriptions = widgets.value.map { widget -> + if (widget.source is Widget.Source.Bundled) + widget.source.id + else + widget.id + } if (subscriptions.isNotEmpty()) unsubscribe(subscriptions) closeObject.stream(widgetObject).collect { status -> status.fold( @@ -296,7 +256,7 @@ class HomeScreenViewModel( widgetEventDispatcher.flow().collect { dispatch -> Timber.d("New dispatch: $dispatch") when (dispatch) { - is WidgetDispatchEvent.SourcePicked -> { + is WidgetDispatchEvent.SourcePicked.Default -> { commands.emit( Command.SelectWidgetType( ctx = configStorage.get().widgets, @@ -305,6 +265,15 @@ class HomeScreenViewModel( ) ) } + is WidgetDispatchEvent.SourcePicked.Bundled -> { + commands.emit( + Command.SelectWidgetType( + ctx = configStorage.get().widgets, + source = dispatch.source, + layout = ObjectType.Layout.SET.code + ) + ) + } is WidgetDispatchEvent.SourceChanged -> { proceedWithUpdatingWidget( widget = dispatch.widget, @@ -471,6 +440,30 @@ class HomeScreenViewModel( } } + fun onWidgetSourceClicked(source: Widget.Source) { + when (source) { + is Widget.Source.Bundled.Favorites -> { + // TODO switch to bundled widgets id + navigate(Navigation.ExpandWidget(Subscription.Favorites)) + } + is Widget.Source.Bundled.Sets -> { + // TODO switch to bundled widgets id + navigate(Navigation.ExpandWidget(Subscription.Sets)) + } + is Widget.Source.Bundled.Recent -> { + // TODO switch to bundled widgets id + navigate(Navigation.ExpandWidget(Subscription.Recent)) + } + is Widget.Source.Default -> { + if (source.obj.isArchived != true) { + proceedWithOpeningObject(source.obj) + } else { + sendToast("Open bin to restore your archived object") + } + } + } + } + fun onDropDownMenuAction(widget: Id, action: DropDownMenuAction) { when (action) { DropDownMenuAction.ChangeWidgetSource -> { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchSection.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchSection.kt index e558a3f1e2..2817aae44f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchSection.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchSection.kt @@ -4,4 +4,8 @@ import com.anytypeio.anytype.presentation.navigation.DefaultSearchItem sealed class ObjectSearchSection : DefaultSearchItem { object RecentlyOpened: ObjectSearchSection() + sealed class SelectWidgetSource : ObjectSearchSection() { + object FromMyObjects: SelectWidgetSource() + object FromLibrary : SelectWidgetSource() + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchViewModel.kt index e25111a6af..3da4c5c4d4 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchViewModel.kt @@ -49,7 +49,7 @@ open class ObjectSearchViewModel( private val jobs = mutableListOf() - private val userInput = MutableStateFlow(EMPTY_QUERY) + protected val userInput = MutableStateFlow(EMPTY_QUERY) private val searchQuery = userInput .take(1) .onCompletion { @@ -78,26 +78,30 @@ open class ObjectSearchViewModel( ) ) } - }.collectLatest { views -> - if (views.isSuccess) { - with(views.getOrThrow()) { - if (this.isEmpty()) { - stateData.postValue(ObjectSearchView.NoResults(userInput.value)) - } else { - if (userInput.value.isEmpty()) { - val items = - mutableListOf(ObjectSearchSection.RecentlyOpened) - items.addAll(this) - stateData.postValue(ObjectSearchView.Success(items)) - } else { - stateData.postValue(ObjectSearchView.Success(this)) - } - } - } + }.collectLatest { result -> + resolveViews(result) + } + } + } + + protected open fun resolveViews(result: Resultat>) { + if (result.isSuccess) { + with(result.getOrThrow()) { + if (this.isEmpty()) { + stateData.postValue(ObjectSearchView.NoResults(userInput.value)) } else { - stateData.postValue(ObjectSearchView.Loading) + if (userInput.value.isEmpty()) { + val items = + mutableListOf(ObjectSearchSection.RecentlyOpened) + items.addAll(this) + stateData.postValue(ObjectSearchView.Success(items)) + } else { + stateData.postValue(ObjectSearchView.Success(this)) + } } } + } else { + stateData.postValue(ObjectSearchView.Loading) } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/DataViewListWidgetContainer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/DataViewListWidgetContainer.kt index b195f62b25..f37ef4b3ea 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/DataViewListWidgetContainer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/DataViewListWidgetContainer.kt @@ -31,48 +31,53 @@ class DataViewListWidgetContainer( activeView.distinctUntilChanged(), isWidgetCollapsed ) { view, isCollapsed -> Pair(view, isCollapsed) }.flatMapLatest { (view, isCollapsed) -> - if (isCollapsed) { - flowOf( - WidgetView.SetOfObjects( - id = widget.id, - obj = widget.source, - tabs = emptyList(), - elements = emptyList(), - isExpanded = false - ) - ) - } else { - val obj = getObject.run(widget.source.id) - val params = obj.parse(viewer = view, source = widget.source) - if (params != null) { - storage.subscribe(params).map { objects -> - WidgetView.SetOfObjects( - id = widget.id, - obj = widget.source, - tabs = obj.tabs(viewer = view), - elements = objects.map { obj -> - WidgetView.SetOfObjects.Element( - obj = obj, - icon = ObjectIcon.from( - obj = obj, - layout = obj.layout, - builder = urlBuilder - ) + when(widget.source) { + is Widget.Source.Bundled -> throw IllegalStateException("Bundled widgets do not support data view layout") + is Widget.Source.Default -> { + if (isCollapsed) { + flowOf( + WidgetView.SetOfObjects( + id = widget.id, + source = widget.source, + tabs = emptyList(), + elements = emptyList(), + isExpanded = false + ) + ) + } else { + val obj = getObject.run(widget.source.id) + val params = obj.parse(viewer = view, source = widget.source.obj) + if (params != null) { + storage.subscribe(params).map { objects -> + WidgetView.SetOfObjects( + id = widget.id, + source = widget.source, + tabs = obj.tabs(viewer = view), + elements = objects.map { obj -> + WidgetView.SetOfObjects.Element( + obj = obj, + icon = ObjectIcon.from( + obj = obj, + layout = obj.layout, + builder = urlBuilder + ) + ) + }, + isExpanded = true ) - }, - isExpanded = true - ) + } + } else { + flowOf( + WidgetView.SetOfObjects( + id = widget.id, + source = widget.source, + tabs = emptyList(), + elements = emptyList(), + isExpanded = true + ) + ) + } } - } else { - flowOf( - WidgetView.SetOfObjects( - id = widget.id, - obj = widget.source, - tabs = emptyList(), - elements = emptyList(), - isExpanded = true - ) - ) } } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/LinkWidgetContainer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/LinkWidgetContainer.kt index c903400ab9..bba6263d0d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/LinkWidgetContainer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/LinkWidgetContainer.kt @@ -9,7 +9,7 @@ class LinkWidgetContainer( override val view: Flow = flowOf( WidgetView.Link( id = widget.id, - obj = widget.source + source = widget.source ) ) } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/ListWidgetContainer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/ListWidgetContainer.kt index a232ffcdd0..ce79711a07 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/ListWidgetContainer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/ListWidgetContainer.kt @@ -54,31 +54,45 @@ class ListWidgetContainer( private fun buildParams() = params(subscription = subscription, workspace = workspace) private fun resolveType() = when (subscription) { - Subscriptions.SUBSCRIPTION_RECENT -> WidgetView.ListOfObjects.Type.Recent - Subscriptions.SUBSCRIPTION_SETS -> WidgetView.ListOfObjects.Type.Sets - Subscriptions.SUBSCRIPTION_FAVORITES -> WidgetView.ListOfObjects.Type.Favorites + BundledWidgetSourceIds.RECENT -> WidgetView.ListOfObjects.Type.Recent + BundledWidgetSourceIds.SETS -> WidgetView.ListOfObjects.Type.Sets + BundledWidgetSourceIds.FAVORITE -> WidgetView.ListOfObjects.Type.Favorites else -> throw IllegalStateException("Unexpected subscription: $subscription") } companion object { private const val MAX_COUNT = 3 - fun params(subscription: Id, workspace: Id) = when (subscription) { - Subscriptions.SUBSCRIPTION_RECENT -> { + fun params( + subscription: Id, + workspace: Id, + keys: List = ObjectSearchConstants.defaultKeys, + limit: Int = MAX_COUNT + ) = when (subscription) { + BundledWidgetSourceIds.RECENT -> { StoreSearchParams( subscription = subscription, sorts = ObjectSearchConstants.sortTabRecent, filters = ObjectSearchConstants.filterTabRecent(workspace), - keys = ObjectSearchConstants.defaultKeys, - limit = MAX_COUNT + keys = keys, + limit = limit ) } - Subscriptions.SUBSCRIPTION_SETS -> { + BundledWidgetSourceIds.SETS -> { StoreSearchParams( subscription = subscription, sorts = ObjectSearchConstants.sortTabSets, filters = ObjectSearchConstants.filterTabSets(workspace), - keys = ObjectSearchConstants.defaultKeys, - limit = MAX_COUNT + keys = keys, + limit = limit + ) + } + BundledWidgetSourceIds.FAVORITE -> { + StoreSearchParams( + subscription = subscription, + sorts = emptyList(), + filters = ObjectSearchConstants.filterTabFavorites(workspace), + keys = keys, + limit = limit ) } Subscriptions.SUBSCRIPTION_ARCHIVED -> { @@ -86,17 +100,8 @@ class ListWidgetContainer( subscription = subscription, sorts = ObjectSearchConstants.sortTabArchive, filters = ObjectSearchConstants.filterTabArchive(workspace), - keys = ObjectSearchConstants.defaultKeys, - limit = MAX_COUNT - ) - } - Subscriptions.SUBSCRIPTION_FAVORITES -> { - StoreSearchParams( - subscription = subscription, - sorts = emptyList(), - filters = ObjectSearchConstants.filterTabFavorites(workspace), - keys = ObjectSearchConstants.defaultKeys, - limit = MAX_COUNT + keys = keys, + limit = limit ) } else -> throw IllegalStateException("Unexpected subscription: $subscription") diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetSourceViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetSourceViewModel.kt index a5b7e004cb..1a2ecb3571 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetSourceViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetSourceViewModel.kt @@ -5,13 +5,18 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.Id +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.misc.UrlBuilder import com.anytypeio.anytype.domain.search.SearchObjects import com.anytypeio.anytype.domain.workspace.WorkspaceManager import com.anytypeio.anytype.presentation.navigation.DefaultObjectView +import com.anytypeio.anytype.presentation.search.ObjectSearchSection +import com.anytypeio.anytype.presentation.search.ObjectSearchView import com.anytypeio.anytype.presentation.search.ObjectSearchViewModel import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.presentation.widgets.source.BundledWidgetSourceView import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.take @@ -47,6 +52,43 @@ class SelectWidgetSourceViewModel( } } + override fun resolveViews(result: Resultat>) { + result.fold( + onSuccess = { views -> + if (views.isEmpty()) { + stateData.postValue(ObjectSearchView.NoResults(userInput.value)) + } else { + if (userInput.value.isEmpty()) { + stateData.postValue( + ObjectSearchView.Success( + buildList { + add(ObjectSearchSection.SelectWidgetSource.FromLibrary) + addAll( + listOf( + BundledWidgetSourceView.Favorites, + BundledWidgetSourceView.Recent, + BundledWidgetSourceView.Sets + ) + ) + add(ObjectSearchSection.SelectWidgetSource.FromMyObjects) + addAll(views) + } + ) + ) + } else { + stateData.postValue(ObjectSearchView.Success(views)) + } + } + }, + onLoading = { + stateData.postValue(ObjectSearchView.Loading) + }, + onFailure = { + Timber.e(it, "Error while selecting source for widget") + } + ) + } + fun onStartWithNewWidget() { Timber.d("onStart with picking source for new widget") config = Config.NewWidget @@ -74,13 +116,42 @@ class SelectWidgetSourceViewModel( startProcessingSearchQuery(null) } + fun onBundledWidgetSourceClicked(view: BundledWidgetSourceView) { + Timber.d("onBundledWidgetSourceClicked, view:[$view]") + when (val curr = config) { + is Config.NewWidget -> { + viewModelScope.launch { + dispatcher.send( + WidgetDispatchEvent.SourcePicked.Bundled(source = view.id) + ) + } + } + is Config.ExistingWidget -> { + viewModelScope.launch { + dispatcher.send( + WidgetDispatchEvent.SourceChanged( + ctx = curr.ctx, + widget = curr.widget, + source = view.id, + type = curr.type + ) + ) + isDismissed.value = true + } + } + Config.None -> { + // Do nothing. + } + } + } + override fun onObjectClicked(view: DefaultObjectView) { Timber.d("onObjectClicked, view:[$view]") when(val curr = config) { is Config.NewWidget -> { viewModelScope.launch { dispatcher.send( - WidgetDispatchEvent.SourcePicked( + WidgetDispatchEvent.SourcePicked.Default( source = view.id, sourceLayout = view.layout?.code ?: -1 ) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetTypeViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetTypeViewModel.kt index 278882eca1..2f7f12c6eb 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetTypeViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/SelectWidgetTypeViewModel.kt @@ -34,18 +34,33 @@ class SelectWidgetTypeViewModel( val isDismissed = MutableStateFlow(false) - fun onStartForExistingWidget(currentType: Int) { - views.value = views.value.map { view -> view.setIsSelected(currentType) } + fun onStartForExistingWidget(currentType: Int, source: Id) { + Timber.d("onStart for existing widget") + if (BundledWidgetSourceIds.ids.contains(source)) { + views.value = listOf( + WidgetTypeView.Tree().setIsSelected(currentType), + WidgetTypeView.List().setIsSelected(currentType) + ) + } else { + views.value = views.value.map { view -> view.setIsSelected(currentType) } + } } - fun onStartForNewWidget(layout: Int) { - Timber.d("onStart for new widget: $layout") - val objectLayout = ObjectType.Layout.values().find { it.code == layout } - if (objectLayout == ObjectType.Layout.SET) { + fun onStartForNewWidget(layout: Int, source: Id) { + Timber.d("onStart for new widget") + if (BundledWidgetSourceIds.ids.contains(source)) { views.value = listOf( - WidgetTypeView.List(isSelected = false), - WidgetTypeView.Link(isSelected = false) + WidgetTypeView.Tree(isSelected = false), + WidgetTypeView.List(isSelected = false) ) + } else { + val objectLayout = ObjectType.Layout.values().find { it.code == layout } + if (objectLayout == ObjectType.Layout.SET) { + views.value = listOf( + WidgetTypeView.List(isSelected = false), + WidgetTypeView.Link(isSelected = false) + ) + } } } @@ -65,7 +80,7 @@ class SelectWidgetTypeViewModel( type = when (view) { is WidgetTypeView.Link -> WidgetLayout.LINK is WidgetTypeView.Tree -> WidgetLayout.TREE - is WidgetTypeView.List -> TODO() + is WidgetTypeView.List -> WidgetLayout.LINK } ) ).flowOn(appCoroutineDispatchers.io).collect { result -> diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/TreeWidgetContainer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/TreeWidgetContainer.kt index c0d56f2e3e..0106e553b8 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/TreeWidgetContainer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/TreeWidgetContainer.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.map class TreeWidgetContainer( private val widget: Widget.Tree, + private val workspace: Id, private val container: StorelessSubscriptionContainer, private val urlBuilder: UrlBuilder, dispatchers: AppCoroutineDispatchers, @@ -34,49 +35,113 @@ class TreeWidgetContainer( ) { paths, isWidgetCollapsed -> paths to isWidgetCollapsed }.flatMapLatest { (paths, isWidgetCollapsed) -> - container.subscribe( - StoreSearchByIdsParams( - subscription = widget.id, - keys = keys, - targets = if (!isWidgetCollapsed) { - getSubscriptionTargets(paths = paths) - } else { - emptyList() + when (widget.source) { + is Widget.Source.Bundled -> { + container.subscribe( + ListWidgetContainer.params( + subscription = widget.source.id, + workspace = workspace + ) + ).map { rootLevelObjects -> + rootLevelObjects.map { it.id } + }.flatMapLatest { rootLevelObjects -> + container.subscribe( + StoreSearchByIdsParams( + subscription = widget.id, + keys = keys, + targets = if (!isWidgetCollapsed) { + getBundledSubscriptionTargets( + paths = paths, + links = rootLevelObjects + ) + } else { + emptyList() + } + ) + ).map { data -> + rootLevelObjects to data + } + }.map { (rootLevelLinks, objectWrappers) -> + val valid = objectWrappers.filter { obj -> isValidObject(obj) } + val data = valid.associateBy { r -> r.id } + with(nodes) { + clear() + putAll(valid.associate { obj -> obj.id to obj.links }) + } + WidgetView.Tree( + id = widget.id, + source = widget.source, + isExpanded = !isWidgetCollapsed, + elements = buildTree( + links = rootLevelLinks, + level = ROOT_INDENT, + expanded = paths, + path = widget.id + SEPARATOR + widget.source.id + SEPARATOR, + data = data + ) + ) + } + } + is Widget.Source.Default -> { + container.subscribe( + StoreSearchByIdsParams( + subscription = widget.id, + keys = keys, + targets = if (!isWidgetCollapsed) { + getDefaultSubscriptionTargets( + paths = paths, + source = widget.source + ) + } else { + emptyList() + } + ) + ).map { results -> + val valid = results.filter { obj -> isValidObject(obj) } + val data = valid.associateBy { r -> r.id } + with(nodes) { + clear() + putAll(valid.associate { obj -> obj.id to obj.links }) + } + WidgetView.Tree( + id = widget.id, + source = widget.source, + isExpanded = !isWidgetCollapsed, + elements = buildTree( + links = widget.source.obj.links, + level = ROOT_INDENT, + expanded = paths, + path = widget.id + SEPARATOR + widget.source.id + SEPARATOR, + data = data + ) + ) } - ) - ).map { results -> - val valid = results.filter { obj -> isValidObject(obj) } - val data = valid.associateBy { r -> r.id } - with(nodes) { - clear() - putAll(valid.associate { obj -> obj.id to obj.links }) } - WidgetView.Tree( - id = widget.id, - obj = widget.source, - isExpanded = !isWidgetCollapsed, - elements = buildTree( - links = widget.source.links, - level = ROOT_INDENT, - expanded = paths, - path = widget.id + SEPARATOR + widget.source.id + SEPARATOR, - data = data - ) - ) } }.flowOn(dispatchers.io) - private fun getSubscriptionTargets( - paths: List + private fun getDefaultSubscriptionTargets( + paths: List, + source: Widget.Source.Default ) = buildList { - if (widget.source.isArchived != true && widget.source.isDeleted != true) { - addAll(widget.source.links) + if (source.obj.isArchived != true && source.obj.isDeleted != true) { + addAll(source.obj.links) nodes.forEach { (id, links) -> if (paths.any { path -> path.contains(id) }) addAll(links) } } }.distinct() + private fun getBundledSubscriptionTargets( + paths: List, + links: List, + ) = buildList { + addAll(links) + nodes.forEach { (id, links) -> + if (paths.any { path -> path.contains(id) }) addAll(links) + } + }.distinct() + private fun buildTree( links: List, expanded: List, @@ -145,11 +210,6 @@ class TreeWidgetContainer( add(Relations.LINKS) } } - - data class Node( - val id: Id, - val children: List - ) } /** diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt index 7731bc979e..b1d994fc3a 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt @@ -3,6 +3,7 @@ package com.anytypeio.anytype.presentation.widgets import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.Struct import com.anytypeio.anytype.core_models.ext.asMap @@ -10,7 +11,7 @@ sealed class Widget { abstract val id: Id - abstract val source: ObjectWrapper.Basic + abstract val source: Source /** * @property [id] id of the widget @@ -18,7 +19,7 @@ sealed class Widget { */ data class Tree( override val id: Id, - override val source: ObjectWrapper.Basic + override val source: Source ) : Widget() /** @@ -27,7 +28,7 @@ sealed class Widget { */ data class Link( override val id: Id, - override val source: ObjectWrapper.Basic + override val source: Source ) : Widget() /** @@ -36,8 +37,36 @@ sealed class Widget { */ data class List( override val id: Id, - override val source: ObjectWrapper.Basic + override val source: Source ) : Widget() + + sealed class Source { + + abstract val id: Id + abstract val type: Id? + + data class Default(val obj: ObjectWrapper.Basic) : Source() { + override val id: Id = obj.id + override val type: Id? = obj.type.firstOrNull() + } + + sealed class Bundled : Source() { + object Favorites : Bundled() { + override val id: Id = BundledWidgetSourceIds.FAVORITE + override val type: Id? = null + } + + object Sets : Bundled() { + override val id: Id = BundledWidgetSourceIds.SETS + override val type: Id? = null + } + + object Recent : Bundled() { + override val id: Id = BundledWidgetSourceIds.RECENT + override val type: Id? = null + } + } + } } fun List.parseWidgets( @@ -47,22 +76,34 @@ fun List.parseWidgets( val map = asMap() val widgets = map[root] ?: emptyList() widgets.forEach { w -> - val content = w.content - if (content is Block.Content.Widget) { + val widgetContent = w.content + if (widgetContent is Block.Content.Widget) { val child = (map[w.id] ?: emptyList()).firstOrNull() if (child != null) { - val source = child.content - if (source is Block.Content.Link) { - val raw = details[source.target] ?: emptyMap() - val data = ObjectWrapper.Basic(raw) - val type = data.type.firstOrNull() - if (!WidgetConfig.excludedTypes.contains(type)) { - when (content.layout) { + val sourceContent = child.content + if (sourceContent is Block.Content.Link) { + val target = sourceContent.target + val raw = details[target] ?: run { + if (BundledWidgetSourceIds.ids.contains(sourceContent.target)) { + mapOf(Relations.ID to sourceContent.target) + } else { + emptyMap() + } + } + val source = if (BundledWidgetSourceIds.ids.contains(target)) { + target.bundled() + } else { + Widget.Source.Default( + ObjectWrapper.Basic(raw) + ) + } + if (!WidgetConfig.excludedTypes.contains(source.type)) { + when (widgetContent.layout) { Block.Content.Widget.Layout.TREE -> { add( Widget.Tree( id = w.id, - source = data + source = source ) ) } @@ -70,7 +111,7 @@ fun List.parseWidgets( add( Widget.Link( id = w.id, - source = data + source = source ) ) } @@ -82,6 +123,13 @@ fun List.parseWidgets( } } +fun Id.bundled() : Widget.Source.Bundled = when (this) { + BundledWidgetSourceIds.RECENT -> Widget.Source.Bundled.Recent + BundledWidgetSourceIds.SETS -> Widget.Source.Bundled.Sets + BundledWidgetSourceIds.FAVORITE -> Widget.Source.Bundled.Favorites + else -> throw throw IllegalStateException() +} + typealias WidgetId = Id typealias ViewId = Id typealias FromIndex = Int diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetConfig.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetConfig.kt index 4c63540107..aa9802eb9c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetConfig.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetConfig.kt @@ -24,4 +24,11 @@ object WidgetConfig { && obj.isDeleted != true && SupportedLayouts.isSupported(obj.layout) } +} + +object BundledWidgetSourceIds { + const val FAVORITE = "favorite" + const val RECENT = "recent" + const val SETS = "sets" + val ids = listOf(FAVORITE, RECENT, SETS) } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetDispatchEvent.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetDispatchEvent.kt index 8d2ada4f67..9b0b2e10c5 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetDispatchEvent.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetDispatchEvent.kt @@ -3,7 +3,14 @@ package com.anytypeio.anytype.presentation.widgets import com.anytypeio.anytype.core_models.Id sealed class WidgetDispatchEvent { - data class SourcePicked(val source: Id, val sourceLayout: Int) : WidgetDispatchEvent() + sealed class SourcePicked : WidgetDispatchEvent() { + data class Default(val source: Id, val sourceLayout: Int) : SourcePicked() + + /** + * [source] bundled source - one of [BundledWidgetSourceIds] + */ + data class Bundled(val source: Id) : SourcePicked() + } data class TypePicked(val source: Id, val widgetType: Int) : WidgetDispatchEvent() data class SourceChanged( val ctx: Id, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetTypeView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetTypeView.kt index 378bbbae05..c534b7a08a 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetTypeView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetTypeView.kt @@ -7,9 +7,9 @@ import com.anytypeio.anytype.presentation.home.Command.ChangeWidgetType.Companio sealed class WidgetTypeView { abstract val isSelected: Boolean - data class List(override val isSelected: Boolean) : WidgetTypeView() - data class Tree(override val isSelected: Boolean) : WidgetTypeView() - data class Link(override val isSelected: Boolean) : WidgetTypeView() + data class List(override val isSelected: Boolean = false) : WidgetTypeView() + data class Tree(override val isSelected: Boolean = false) : WidgetTypeView() + data class Link(override val isSelected: Boolean = false) : WidgetTypeView() fun setIsSelected(type: Int): WidgetTypeView = when (this) { is Link -> copy(isSelected = type == TYPE_LINK) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetView.kt index f530ef558d..9e2531970c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetView.kt @@ -12,7 +12,7 @@ sealed class WidgetView { data class Tree( override val id: Id, - val obj: ObjectWrapper.Basic, + val source: Widget.Source, val elements: List = emptyList(), val isExpanded: Boolean = false, val isEditable: Boolean = true @@ -34,12 +34,12 @@ sealed class WidgetView { data class Link( override val id: Id, - val obj: ObjectWrapper.Basic, + val source: Widget.Source, ) : WidgetView(), Draggable data class SetOfObjects( override val id: Id, - val obj: ObjectWrapper.Basic, + val source: Widget.Source, val tabs: List, val elements: List, val isExpanded: Boolean diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/source/BundledWidgetSourceView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/source/BundledWidgetSourceView.kt new file mode 100644 index 0000000000..df3370b53a --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/source/BundledWidgetSourceView.kt @@ -0,0 +1,21 @@ +package com.anytypeio.anytype.presentation.widgets.source + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.presentation.navigation.DefaultSearchItem +import com.anytypeio.anytype.presentation.widgets.BundledWidgetSourceIds + +/** + * Used for picking bundled widget source from list of objects. + */ +sealed class BundledWidgetSourceView : DefaultSearchItem { + abstract val id: Id + object Favorites : BundledWidgetSourceView() { + override val id: Id get() = BundledWidgetSourceIds.FAVORITE + } + object Sets: BundledWidgetSourceView() { + override val id: Id get() = BundledWidgetSourceIds.SETS + } + object Recent: BundledWidgetSourceView() { + override val id: Id get() = BundledWidgetSourceIds.RECENT + } +} \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt index e9839a5f94..74d74b588c 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt @@ -5,7 +5,9 @@ import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Event import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectView +import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_models.SmartBlockType import com.anytypeio.anytype.core_models.StubConfig @@ -22,6 +24,7 @@ import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.config.Gateway import com.anytypeio.anytype.domain.event.interactor.InterceptEvents import com.anytypeio.anytype.domain.library.StoreSearchByIdsParams +import com.anytypeio.anytype.domain.library.StoreSearchParams import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.`object`.GetObject @@ -31,13 +34,16 @@ import com.anytypeio.anytype.domain.page.CreateObject import com.anytypeio.anytype.domain.widgets.CreateWidget import com.anytypeio.anytype.domain.widgets.DeleteWidget import com.anytypeio.anytype.domain.widgets.UpdateWidget +import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.search.Subscriptions import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.presentation.widgets.BundledWidgetSourceIds import com.anytypeio.anytype.presentation.widgets.CollapsedWidgetStateHolder import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction import com.anytypeio.anytype.presentation.widgets.ListWidgetContainer import com.anytypeio.anytype.presentation.widgets.TreeWidgetContainer +import com.anytypeio.anytype.presentation.widgets.Widget import com.anytypeio.anytype.presentation.widgets.WidgetActiveViewStateHolder import com.anytypeio.anytype.presentation.widgets.WidgetConfig import com.anytypeio.anytype.presentation.widgets.WidgetDispatchEvent @@ -183,7 +189,7 @@ class HomeScreenViewModelTest { } @Test - fun `should emit only default widgets with bin and actions when home screen has no associated widgets except the default ones`() = + fun `should emit only bin and actions when home screen has no associated widgets except the default ones`() = runTest { // SETUP @@ -201,34 +207,12 @@ class HomeScreenViewModelTest { details = emptyMap() ) - val defaultWidgets = listOf( - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_FAVORITES, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Favorites - ), - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_RECENT, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Recent - ), - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_SETS, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Sets - ) - ) - val binWidget = WidgetView.Bin(id = Subscriptions.SUBSCRIPTION_ARCHIVED) stubConfig() stubInterceptEvents(events = emptyFlow()) stubOpenObject(givenObjectView) stubCollapsedWidgetState(any()) - stubDefaultContainerSubscriptions() val vm = buildViewModel() @@ -245,7 +229,6 @@ class HomeScreenViewModelTest { val secondTimeItem = awaitItem() assertEquals( expected = buildList { - addAll(defaultWidgets) add(binWidget) addAll(HomeScreenViewModel.actions) }, @@ -256,115 +239,433 @@ class HomeScreenViewModelTest { } @Test - fun `should emit default widgets, tree-widget with empty elements and bin when source has no links`() = - runTest { + fun `should emit tree-widget with empty elements and bin when source has no links`() = runTest { - // SETUP + // SETUP - val sourceObject = StubObject( - id = "SOURCE OBJECT", - links = emptyList() + val sourceObject = StubObject( + id = "SOURCE OBJECT", + links = emptyList() + ) + + val sourceLink = StubLinkToObjectBlock( + id = "SOURCE LINK", + target = sourceObject.id + ) + + val widgetBlock = StubWidgetBlock( + id = "WIDGET BLOCK", + layout = Block.Content.Widget.Layout.TREE, + children = listOf(sourceLink.id) + ) + + val smartBlock = StubSmartBlock( + id = WIDGET_OBJECT_ID, + children = listOf(widgetBlock.id), + type = SmartBlockType.WIDGET + ) + + val givenObjectView = StubObjectView( + root = WIDGET_OBJECT_ID, + type = SmartBlockType.WIDGET, + blocks = listOf( + smartBlock, + widgetBlock, + sourceLink + ), + details = mapOf( + sourceObject.id to sourceObject.map ) + ) - val sourceLink = StubLinkToObjectBlock( - id = "SOURCE LINK", - target = sourceObject.id + val binWidget = WidgetView.Bin(id = Subscriptions.SUBSCRIPTION_ARCHIVED) + + stubConfig() + stubInterceptEvents(events = emptyFlow()) + stubOpenObject(givenObjectView) + stubSearchByIds( + subscription = widgetBlock.id, + targets = emptyList() + ) + stubCollapsedWidgetState(any()) + stubWidgetActiveView(widgetBlock) + + val vm = buildViewModel() + + // TESTING + + vm.onStart() + + vm.views.test { + val firstTimeState = awaitItem() + assertEquals( + actual = firstTimeState, + expected = HomeScreenViewModel.actions ) - - val widgetBlock = StubWidgetBlock( - id = "WIDGET BLOCK", - layout = Block.Content.Widget.Layout.TREE, - children = listOf(sourceLink.id) + val secondTimeState = awaitItem() + assertEquals( + actual = secondTimeState, + expected = buildList { + add(binWidget) + addAll(HomeScreenViewModel.actions) + } ) - - val smartBlock = StubSmartBlock( - id = WIDGET_OBJECT_ID, - children = listOf(widgetBlock.id), - type = SmartBlockType.WIDGET - ) - - val givenObjectView = StubObjectView( - root = WIDGET_OBJECT_ID, - type = SmartBlockType.WIDGET, - blocks = listOf( - smartBlock, - widgetBlock, - sourceLink - ), - details = mapOf( - sourceObject.id to sourceObject.map - ) - ) - - val defaultWidgets = listOf( - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_FAVORITES, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Favorites - ), - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_RECENT, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Recent - ), - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_SETS, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Sets - ) - ) - - val binWidget = WidgetView.Bin(id = Subscriptions.SUBSCRIPTION_ARCHIVED) - - stubConfig() - stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) - stubStorelessSubscriptionContainer( - subscription = widgetBlock.id, - targets = emptyList() - ) - stubCollapsedWidgetState(any()) - stubWidgetActiveView(widgetBlock) - stubDefaultContainerSubscriptions() - - val vm = buildViewModel() - - // TESTING - - vm.onStart() - - vm.views.test { - val firstTimeState = awaitItem() - assertEquals( - actual = firstTimeState, - expected = HomeScreenViewModel.actions - ) - val secondTimeItem = awaitItem() - assertEquals( - expected = buildList { - addAll(defaultWidgets) - add( - WidgetView.Tree( - id = widgetBlock.id, - obj = sourceObject, - elements = emptyList(), - isExpanded = true - ) + val thirdTimeState = awaitItem() + assertEquals( + expected = buildList { + add( + WidgetView.Tree( + id = widgetBlock.id, + source = Widget.Source.Default(sourceObject), + elements = emptyList(), + isExpanded = true ) - add(binWidget) - addAll(HomeScreenViewModel.actions) - }, - actual = secondTimeItem - ) - verify(openObject, times(1)).stream(OpenObject.Params(WIDGET_OBJECT_ID, false)) - } + ) + add(binWidget) + addAll(HomeScreenViewModel.actions) + }, + actual = thirdTimeState + ) + verify(openObject, times(1)).stream(OpenObject.Params(WIDGET_OBJECT_ID, false)) } + } @Test - fun `should emit default widgets, link-widget, bin and actions`() = runTest { + fun `should emit tree-widget with 2 elements and bin`() = runTest { + + // SETUP + + val firstLink = StubObject( + id = "First link", + layout = ObjectType.Layout.BASIC.code.toDouble() + ) + val secondLink = StubObject( + id = "Second link", + layout = ObjectType.Layout.BASIC.code.toDouble() + ) + + val sourceObject = StubObject( + id = "SOURCE OBJECT", + links = listOf(firstLink.id, secondLink.id) + ) + + val sourceLink = StubLinkToObjectBlock( + id = "SOURCE LINK", + target = sourceObject.id + ) + + val widgetBlock = StubWidgetBlock( + id = "WIDGET BLOCK", + layout = Block.Content.Widget.Layout.TREE, + children = listOf(sourceLink.id) + ) + + val smartBlock = StubSmartBlock( + id = WIDGET_OBJECT_ID, + children = listOf(widgetBlock.id), + type = SmartBlockType.WIDGET + ) + + val givenObjectView = StubObjectView( + root = WIDGET_OBJECT_ID, + type = SmartBlockType.WIDGET, + blocks = listOf( + smartBlock, + widgetBlock, + sourceLink + ), + details = mapOf( + sourceObject.id to sourceObject.map + ) + ) + + val binWidget = WidgetView.Bin(id = Subscriptions.SUBSCRIPTION_ARCHIVED) + + stubConfig() + stubInterceptEvents(events = emptyFlow()) + stubOpenObject(givenObjectView) + + stubSearchByIds( + subscription = widgetBlock.id, + targets = listOf(firstLink.id, secondLink.id), + results = listOf(firstLink, secondLink) + ) + + stubCollapsedWidgetState(any()) + stubWidgetActiveView(widgetBlock) + + val vm = buildViewModel() + + // TESTING + + vm.onStart() + + vm.views.test { + val firstTimeState = awaitItem() + assertEquals( + actual = firstTimeState, + expected = HomeScreenViewModel.actions + ) + val secondTimeState = awaitItem() + assertEquals( + actual = secondTimeState, + expected = buildList { + add(binWidget) + addAll(HomeScreenViewModel.actions) + } + ) + val thirdTimeState = awaitItem() + assertEquals( + expected = buildList { + add( + WidgetView.Tree( + id = widgetBlock.id, + source = Widget.Source.Default(sourceObject), + elements = listOf( + WidgetView.Tree.Element( + elementIcon = WidgetView.Tree.ElementIcon.Leaf, + obj = firstLink, + objectIcon = ObjectIcon.Basic.Avatar(firstLink.name.orEmpty()), + indent = 0, + path = widgetBlock.id + "/" + sourceObject.id + "/" + firstLink.id + ), + WidgetView.Tree.Element( + elementIcon = WidgetView.Tree.ElementIcon.Leaf, + obj = secondLink, + objectIcon = ObjectIcon.Basic.Avatar(secondLink.name.orEmpty()), + indent = 0, + path = widgetBlock.id + "/" + sourceObject.id + "/" + secondLink.id + ) + ), + isExpanded = true + ) + ) + add(binWidget) + addAll(HomeScreenViewModel.actions) + }, + actual = thirdTimeState + ) + } + } + + @Test + fun `should emit three bundled widgets, each having 2 elements, and bin`() = runTest { + + // SETUP + + val firstLink = StubObject( + id = "First link", + layout = ObjectType.Layout.BASIC.code.toDouble() + ) + val secondLink = StubObject( + id = "Second link", + layout = ObjectType.Layout.BASIC.code.toDouble() + ) + + val favoriteSource = StubObject(id = BundledWidgetSourceIds.FAVORITE) + val recentSource = StubObject(id = BundledWidgetSourceIds.RECENT) + val setsSource = StubObject(id = BundledWidgetSourceIds.SETS) + + val favoriteLink = StubLinkToObjectBlock( + target = favoriteSource.id + ) + + val recentLink = StubLinkToObjectBlock( + target = recentSource.id + ) + + val setsLink = StubLinkToObjectBlock( + target = setsSource.id + ) + + val favoriteWidgetBlock = StubWidgetBlock( + layout = Block.Content.Widget.Layout.TREE, + children = listOf(favoriteLink.id) + ) + + val recentWidgetBlock = StubWidgetBlock( + layout = Block.Content.Widget.Layout.TREE, + children = listOf(recentLink.id) + ) + + val setsWidgetBlock = StubWidgetBlock( + layout = Block.Content.Widget.Layout.TREE, + children = listOf(setsLink.id) + ) + + val smartBlock = StubSmartBlock( + id = WIDGET_OBJECT_ID, + children = listOf(favoriteWidgetBlock.id, recentWidgetBlock.id, setsWidgetBlock.id), + type = SmartBlockType.WIDGET + ) + + val givenObjectView = StubObjectView( + root = WIDGET_OBJECT_ID, + type = SmartBlockType.WIDGET, + blocks = listOf( + smartBlock, + favoriteWidgetBlock, + favoriteLink, + recentWidgetBlock, + recentLink, + setsWidgetBlock, + setsLink + ) + ) + + val binWidget = WidgetView.Bin(id = Subscriptions.SUBSCRIPTION_ARCHIVED) + + stubConfig() + stubInterceptEvents(events = emptyFlow()) + stubOpenObject(givenObjectView) + + stubSearchByIds( + subscription = favoriteWidgetBlock.id, + targets = listOf(firstLink.id, secondLink.id), + results = listOf(firstLink, secondLink) + ) + + stubSearchByIds( + subscription = recentWidgetBlock.id, + targets = listOf(firstLink.id, secondLink.id), + results = listOf(firstLink, secondLink) + ) + + stubSearchByIds( + subscription = setsWidgetBlock.id, + targets = listOf(firstLink.id, secondLink.id), + results = listOf(firstLink, secondLink) + ) + + stubDefaultSearch( + params = ListWidgetContainer.params( + subscription = BundledWidgetSourceIds.FAVORITE, + workspace = config.workspace + ), + results = listOf(firstLink, secondLink) + ) + + stubDefaultSearch( + params = ListWidgetContainer.params( + subscription = BundledWidgetSourceIds.RECENT, + workspace = config.workspace + ), + results = listOf(firstLink, secondLink) + ) + + stubDefaultSearch( + params = ListWidgetContainer.params( + subscription = BundledWidgetSourceIds.SETS, + workspace = config.workspace + ), + results = listOf(firstLink, secondLink) + ) + + stubCollapsedWidgetState(any()) + stubWidgetActiveView(favoriteWidgetBlock) + + val vm = buildViewModel() + + // TESTING + + vm.onStart() + + vm.views.test { + val firstTimeState = awaitItem() + assertEquals( + actual = firstTimeState, + expected = HomeScreenViewModel.actions + ) + val secondTimeState = awaitItem() + assertEquals( + actual = secondTimeState, + expected = buildList { + add(binWidget) + addAll(HomeScreenViewModel.actions) + } + ) + val thirdTimeState = awaitItem() + assertEquals( + expected = buildList { + add( + WidgetView.Tree( + id = favoriteWidgetBlock.id, + source = Widget.Source.Bundled.Favorites, + elements = listOf( + WidgetView.Tree.Element( + elementIcon = WidgetView.Tree.ElementIcon.Leaf, + obj = firstLink, + objectIcon = ObjectIcon.Basic.Avatar(firstLink.name.orEmpty()), + indent = 0, + path = favoriteWidgetBlock.id + "/" + favoriteSource.id + "/" + firstLink.id + ), + WidgetView.Tree.Element( + elementIcon = WidgetView.Tree.ElementIcon.Leaf, + obj = secondLink, + objectIcon = ObjectIcon.Basic.Avatar(secondLink.name.orEmpty()), + indent = 0, + path = favoriteWidgetBlock.id + "/" + favoriteSource.id + "/" + secondLink.id + ) + ), + isExpanded = true + ) + ) + add( + WidgetView.Tree( + id = recentWidgetBlock.id, + source = Widget.Source.Bundled.Recent, + elements = listOf( + WidgetView.Tree.Element( + elementIcon = WidgetView.Tree.ElementIcon.Leaf, + obj = firstLink, + objectIcon = ObjectIcon.Basic.Avatar(firstLink.name.orEmpty()), + indent = 0, + path = recentWidgetBlock.id + "/" + recentSource.id + "/" + firstLink.id + ), + WidgetView.Tree.Element( + elementIcon = WidgetView.Tree.ElementIcon.Leaf, + obj = secondLink, + objectIcon = ObjectIcon.Basic.Avatar(secondLink.name.orEmpty()), + indent = 0, + path = recentWidgetBlock.id + "/" + recentSource.id + "/" + secondLink.id + ) + ), + isExpanded = true + ) + ) + add( + WidgetView.Tree( + id = setsWidgetBlock.id, + source = Widget.Source.Bundled.Sets, + elements = listOf( + WidgetView.Tree.Element( + elementIcon = WidgetView.Tree.ElementIcon.Leaf, + obj = firstLink, + objectIcon = ObjectIcon.Basic.Avatar(firstLink.name.orEmpty()), + indent = 0, + path = setsWidgetBlock.id + "/" + setsSource.id + "/" + firstLink.id + ), + WidgetView.Tree.Element( + elementIcon = WidgetView.Tree.ElementIcon.Leaf, + obj = secondLink, + objectIcon = ObjectIcon.Basic.Avatar(secondLink.name.orEmpty()), + indent = 0, + path = setsWidgetBlock.id + "/" + setsSource.id + "/" + secondLink.id + ) + ), + isExpanded = true + ) + ) + add(binWidget) + addAll(HomeScreenViewModel.actions) + }, + actual = thirdTimeState + ) + } + } + + @Test + fun `should emit link-widget, bin and actions`() = runTest { // SETUP @@ -403,37 +704,15 @@ class HomeScreenViewModelTest { ) ) - val defaultWidgets = listOf( - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_FAVORITES, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Favorites - ), - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_RECENT, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Recent - ), - WidgetView.ListOfObjects( - id = Subscriptions.SUBSCRIPTION_SETS, - elements = emptyList(), - isExpanded = true, - type = WidgetView.ListOfObjects.Type.Sets - ) - ) - val binWidget = WidgetView.Bin(id = Subscriptions.SUBSCRIPTION_ARCHIVED) stubConfig() stubInterceptEvents(events = emptyFlow()) stubOpenObject(givenObjectView) - stubStorelessSubscriptionContainer( + stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() ) - stubDefaultContainerSubscriptions() stubCollapsedWidgetState(any()) @@ -450,20 +729,28 @@ class HomeScreenViewModelTest { expected = HomeScreenViewModel.actions ) delay(1) - val secondTimeItem = awaitItem() + val secondTimeState = awaitItem() + assertEquals( + actual = secondTimeState, + expected = buildList { + add(binWidget) + addAll(HomeScreenViewModel.actions) + } + ) + delay(1) + val thirdTimeState = awaitItem() assertEquals( expected = buildList { - addAll(defaultWidgets) add( WidgetView.Link( id = widgetBlock.id, - obj = sourceObject + source = Widget.Source.Default(sourceObject), ) ) add(binWidget) addAll(HomeScreenViewModel.actions) }, - actual = secondTimeItem + actual = thirdTimeState ) verify(openObject, times(1)).stream(OpenObject.Params(WIDGET_OBJECT_ID, false)) } @@ -512,12 +799,11 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) stubOpenObject(givenObjectView) - stubStorelessSubscriptionContainer( + stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() ) stubCollapsedWidgetState(any()) - stubDefaultContainerSubscriptions() val givenPayload = Payload( context = WIDGET_OBJECT_ID, @@ -628,12 +914,11 @@ class HomeScreenViewModelTest { ) stubConfig() stubOpenObject(givenObjectView) - stubStorelessSubscriptionContainer( + stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() ) stubCollapsedWidgetState(any()) - stubDefaultContainerSubscriptions() val vm = buildViewModel() @@ -654,28 +939,116 @@ class HomeScreenViewModelTest { } @Test - fun `should close widget object and unsubscribe when widget on onStop lifecycle event callback`() = runTest { + fun `should close widget-object and unsubscribe on onStop lifecycle event callback`() { + runTest { + + // SETUP + + val sourceObject = StubObject( + id = "SOURCE OBJECT", + links = emptyList() + ) + + val sourceLinkBlock = StubLinkToObjectBlock( + id = "SOURCE LINK", + target = sourceObject.id + ) + + val widgetBlock = StubWidgetBlock( + id = "WIDGET BLOCK", + layout = Block.Content.Widget.Layout.LINK, + children = listOf(sourceLinkBlock.id) + ) + + val smartBlock = StubSmartBlock( + id = WIDGET_OBJECT_ID, + children = listOf(widgetBlock.id), + type = SmartBlockType.WIDGET + ) + + val givenObjectView = StubObjectView( + root = WIDGET_OBJECT_ID, + type = SmartBlockType.WIDGET, + blocks = listOf( + smartBlock, + widgetBlock, + sourceLinkBlock + ), + details = mapOf( + sourceObject.id to sourceObject.map + ) + ) + + stubInterceptEvents(events = emptyFlow()) + stubConfig() + stubOpenObject(givenObjectView) + stubSearchByIds( + subscription = widgetBlock.id, + targets = emptyList() + ) + stubCollapsedWidgetState(any()) + stubCloseObject() + + val vm = buildViewModel() + + // TESTING + + vm.onStart() + + delay(1) + + vm.onStop() + + delay(1) + + verifyBlocking(unsubscriber, times(1)) { + unsubscribe(subscriptions = listOf(widgetBlock.id)) + } + + verify(closeObject, times(1)).stream(params = WIDGET_OBJECT_ID) + } + } + + @Test + fun `should close object and unsubscribe three bundled widgets on onStop callback`() = runTest { + // SETUP - val sourceObject = StubObject( - id = "SOURCE OBJECT", - links = emptyList() + val firstLink = StubObject( + id = "First link", + layout = ObjectType.Layout.BASIC.code.toDouble() + ) + val secondLink = StubObject( + id = "Second link", + layout = ObjectType.Layout.BASIC.code.toDouble() ) - val sourceLinkBlock = StubLinkToObjectBlock( - id = "SOURCE LINK", - target = sourceObject.id + val favoriteSource = StubObject(id = BundledWidgetSourceIds.FAVORITE) + val recentSource = StubObject(id = BundledWidgetSourceIds.RECENT) + val setsSource = StubObject(id = BundledWidgetSourceIds.SETS) + + val favoriteLink = StubLinkToObjectBlock(target = favoriteSource.id) + val recentLink = StubLinkToObjectBlock(target = recentSource.id) + val setsLink = StubLinkToObjectBlock(target = setsSource.id) + + val favoriteWidgetBlock = StubWidgetBlock( + layout = Block.Content.Widget.Layout.TREE, + children = listOf(favoriteLink.id) ) - val widgetBlock = StubWidgetBlock( - id = "WIDGET BLOCK", + val recentWidgetBlock = StubWidgetBlock( layout = Block.Content.Widget.Layout.LINK, - children = listOf(sourceLinkBlock.id) + children = listOf(recentLink.id) + ) + + val setsWidgetBlock = StubWidgetBlock( + layout = Block.Content.Widget.Layout.TREE, + children = listOf(setsLink.id) ) val smartBlock = StubSmartBlock( id = WIDGET_OBJECT_ID, - children = listOf(widgetBlock.id), + children = listOf(favoriteWidgetBlock.id, recentWidgetBlock.id, setsWidgetBlock.id), type = SmartBlockType.WIDGET ) @@ -684,24 +1057,65 @@ class HomeScreenViewModelTest { type = SmartBlockType.WIDGET, blocks = listOf( smartBlock, - widgetBlock, - sourceLinkBlock - ), - details = mapOf( - sourceObject.id to sourceObject.map + favoriteWidgetBlock, + favoriteLink, + recentWidgetBlock, + recentLink, + setsWidgetBlock, + setsLink ) ) - stubInterceptEvents(events = emptyFlow()) + val binWidget = WidgetView.Bin(id = Subscriptions.SUBSCRIPTION_ARCHIVED) + stubConfig() + stubInterceptEvents(events = emptyFlow()) stubOpenObject(givenObjectView) - stubStorelessSubscriptionContainer( - subscription = widgetBlock.id, - targets = emptyList() + + stubSearchByIds( + subscription = favoriteWidgetBlock.id, + targets = listOf(firstLink.id, secondLink.id), + results = listOf(firstLink, secondLink) ) + + stubSearchByIds( + subscription = recentWidgetBlock.id, + targets = listOf(firstLink.id, secondLink.id), + results = listOf(firstLink, secondLink) + ) + + stubSearchByIds( + subscription = setsWidgetBlock.id, + targets = listOf(firstLink.id, secondLink.id), + results = listOf(firstLink, secondLink) + ) + + stubDefaultSearch( + params = ListWidgetContainer.params( + subscription = BundledWidgetSourceIds.FAVORITE, + workspace = config.workspace + ), + results = listOf(firstLink, secondLink) + ) + + stubDefaultSearch( + params = ListWidgetContainer.params( + subscription = BundledWidgetSourceIds.RECENT, + workspace = config.workspace + ), + results = listOf(firstLink, secondLink) + ) + + stubDefaultSearch( + params = ListWidgetContainer.params( + subscription = BundledWidgetSourceIds.SETS, + workspace = config.workspace + ), + results = listOf(firstLink, secondLink) + ) + stubCollapsedWidgetState(any()) - stubDefaultContainerSubscriptions() - stubCloseObject() + stubWidgetActiveView(favoriteWidgetBlock) val vm = buildViewModel() @@ -716,7 +1130,11 @@ class HomeScreenViewModelTest { delay(1) verifyBlocking(unsubscriber, times(1)) { - unsubscribe(subscriptions = listOf(widgetBlock.id)) + unsubscribe( + subscriptions = listOf( + favoriteSource.id, recentSource.id, setsSource.id + ) + ) } verify(closeObject, times(1)).stream(params = WIDGET_OBJECT_ID) @@ -766,11 +1184,10 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) stubOpenObject(givenObjectView) - stubStorelessSubscriptionContainer( + stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() ) - stubDefaultContainerSubscriptions() stubCollapsedWidgetState(any()) @@ -788,7 +1205,8 @@ class HomeScreenViewModelTest { ) delay(1) val secondTimeItem = awaitItem() - assertTrue { secondTimeItem.none { it.id == widgetBlock.id } } } + assertTrue { secondTimeItem.none { it.id == widgetBlock.id } } + } } @Test @@ -835,11 +1253,10 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) stubOpenObject(givenObjectView) - stubStorelessSubscriptionContainer( + stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() ) - stubDefaultContainerSubscriptions() stubCollapsedWidgetState(any()) @@ -900,10 +1317,11 @@ class HomeScreenViewModelTest { } } - private fun stubStorelessSubscriptionContainer( + private fun stubSearchByIds( subscription: Id, targets: List, - keys: List = TreeWidgetContainer.keys + keys: List = TreeWidgetContainer.keys, + results: List = emptyList() ) { storelessSubscriptionContainer.stub { onBlocking { @@ -914,7 +1332,18 @@ class HomeScreenViewModelTest { targets = targets ) ) - } doReturn flowOf(emptyList()) + } doReturn flowOf(results) + } + } + + private fun stubDefaultSearch( + params: StoreSearchParams, + results: List = emptyList(), + ) { + storelessSubscriptionContainer.stub { + onBlocking { + subscribe(params) + } doReturn flowOf(results) } } @@ -930,41 +1359,6 @@ class HomeScreenViewModelTest { } } - private fun stubDefaultContainerSubscriptions() { - storelessSubscriptionContainer.stub { - on { - subscribe( - ListWidgetContainer.params( - subscription = Subscriptions.SUBSCRIPTION_RECENT, - workspace = config.workspace - ) - ) - } doReturn flowOf(emptyList()) - } - - storelessSubscriptionContainer.stub { - on { - subscribe( - ListWidgetContainer.params( - subscription = Subscriptions.SUBSCRIPTION_FAVORITES, - workspace = config.workspace - ) - ) - } doReturn flowOf(emptyList()) - } - - storelessSubscriptionContainer.stub { - on { - subscribe( - ListWidgetContainer.params( - subscription = Subscriptions.SUBSCRIPTION_SETS, - workspace = config.workspace - ) - ) - } doReturn flowOf(emptyList()) - } - } - private fun buildViewModel() = HomeScreenViewModel( configStorage = configStorage, interceptEvents = interceptEvents, diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/TreeWidgetContainerTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/TreeWidgetContainerTest.kt index 1aa44e1fbc..9aea1acd69 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/TreeWidgetContainerTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/TreeWidgetContainerTest.kt @@ -51,6 +51,8 @@ class TreeWidgetContainerTest { private lateinit var urlBuilder: UrlBuilder + private val workspace = MockDataFactory.randomUuid() + @Before fun setup() { MockitoAnnotations.openMocks(this) @@ -58,7 +60,7 @@ class TreeWidgetContainerTest { } @Test - fun `should search for data for source object's links when no data is available about expanded branches`() = + fun `should search for data for source object links when no data is available about expanded branches`() = runTest { // SETUP @@ -74,7 +76,7 @@ class TreeWidgetContainerTest { val widget = Widget.Tree( id = MockDataFactory.randomUuid(), - source = source + source = Widget.Source.Default(source) ) val expanded = flowOf(emptyList()) @@ -85,7 +87,8 @@ class TreeWidgetContainerTest { expandedBranches = expanded, isWidgetCollapsed = flowOf(false), urlBuilder = urlBuilder, - dispatchers = dispatchers + dispatchers = dispatchers, + workspace = workspace ) stubObjectSearch( @@ -136,7 +139,7 @@ class TreeWidgetContainerTest { val widget = Widget.Tree( id = "widget", - source = source + source = Widget.Source.Default(source) ) val expanded = flowOf( @@ -152,7 +155,8 @@ class TreeWidgetContainerTest { expandedBranches = expanded, isWidgetCollapsed = flowOf(false), urlBuilder = urlBuilder, - dispatchers = dispatchers + dispatchers = dispatchers, + workspace = workspace ) stubObjectSearch( @@ -217,7 +221,7 @@ class TreeWidgetContainerTest { val widget = Widget.Tree( id = "widget", - source = source + source = Widget.Source.Default(source) ) val delayBeforeExpanded = 100L @@ -234,7 +238,8 @@ class TreeWidgetContainerTest { expandedBranches = expanded, isWidgetCollapsed = flowOf(false), urlBuilder = urlBuilder, - dispatchers = dispatchers + dispatchers = dispatchers, + workspace = workspace ) stubObjectSearch( @@ -256,7 +261,7 @@ class TreeWidgetContainerTest { assertEquals( expected = WidgetView.Tree( id = widget.id, - obj = widget.source, + source = widget.source, elements = listOf( WidgetView.Tree.Element( indent = 0, @@ -290,7 +295,7 @@ class TreeWidgetContainerTest { assertEquals( expected = WidgetView.Tree( id = widget.id, - obj = widget.source, + source = widget.source, elements = listOf( WidgetView.Tree.Element( indent = 0, @@ -360,7 +365,7 @@ class TreeWidgetContainerTest { val widget = Widget.Tree( id = MockDataFactory.randomUuid(), - source = source + source = Widget.Source.Default(source) ) val expanded = flowOf(emptyList()) @@ -371,7 +376,8 @@ class TreeWidgetContainerTest { expandedBranches = expanded, isWidgetCollapsed = flowOf(false), urlBuilder = urlBuilder, - dispatchers = dispatchers + dispatchers = dispatchers, + workspace = workspace ) stubObjectSearch( @@ -414,7 +420,7 @@ class TreeWidgetContainerTest { val widget = Widget.Tree( id = MockDataFactory.randomUuid(), - source = source + source = Widget.Source.Default(source) ) val expanded = flowOf(emptyList()) @@ -425,7 +431,8 @@ class TreeWidgetContainerTest { expandedBranches = expanded, isWidgetCollapsed = flowOf(false), urlBuilder = urlBuilder, - dispatchers = dispatchers + dispatchers = dispatchers, + workspace = workspace ) stubObjectSearch(