diff --git a/app/build.gradle b/app/build.gradle index b1f45de354..0d4a6d1ae2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,6 +82,8 @@ dependencies { implementation project(':core-utils') implementation project(':core-ui') implementation project(':library-kanban-widget') + implementation project(':library-page-icon-picker-widget') + implementation project(':library-emojifier') def applicationDependencies = rootProject.ext.mainApplication def unitTestDependencies = rootProject.ext.unitTesting diff --git a/app/src/main/java/com/agileburo/anytype/di/common/ComponentManager.kt b/app/src/main/java/com/agileburo/anytype/di/common/ComponentManager.kt index 765cbad5f4..ba37692f5f 100644 --- a/app/src/main/java/com/agileburo/anytype/di/common/ComponentManager.kt +++ b/app/src/main/java/com/agileburo/anytype/di/common/ComponentManager.kt @@ -115,6 +115,13 @@ class ComponentManager(private val main: MainComponent) { .build() } + val pageIconPickerSubComponent = Component { + main + .pageIconPickerBuilder() + .pageIconPickerModule(PageIconPickerModule()) + .build() + } + class Component(private val builder: () -> T) { private var instance: T? = null diff --git a/app/src/main/java/com/agileburo/anytype/di/feature/LinkAddDI.kt b/app/src/main/java/com/agileburo/anytype/di/feature/LinkAddDI.kt index 828371b450..9e143c6365 100644 --- a/app/src/main/java/com/agileburo/anytype/di/feature/LinkAddDI.kt +++ b/app/src/main/java/com/agileburo/anytype/di/feature/LinkAddDI.kt @@ -3,7 +3,7 @@ package com.agileburo.anytype.di.feature import com.agileburo.anytype.core_utils.di.scope.PerScreen import com.agileburo.anytype.domain.page.CheckForUnlink import com.agileburo.anytype.presentation.page.LinkAddViewModelFactory -import com.agileburo.anytype.ui.page.modals.LinkFragment +import com.agileburo.anytype.ui.page.modals.SetLinkFragment import dagger.Module import dagger.Provides import dagger.Subcomponent @@ -18,7 +18,7 @@ interface LinkSubComponent { fun build(): LinkSubComponent } - fun inject(fragment: LinkFragment) + fun inject(fragment: SetLinkFragment) } @Module diff --git a/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt b/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt index 1a2a680692..aa486c3140 100644 --- a/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt +++ b/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt @@ -5,6 +5,7 @@ import com.agileburo.anytype.domain.block.interactor.* import com.agileburo.anytype.domain.block.repo.BlockRepository import com.agileburo.anytype.domain.download.DownloadFile import com.agileburo.anytype.domain.download.Downloader +import com.agileburo.anytype.domain.emoji.Emojifier import com.agileburo.anytype.domain.event.interactor.EventChannel import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.misc.UrlBuilder @@ -56,7 +57,8 @@ class PageModule { createPage: CreatePage, documentExternalEventReducer: DocumentExternalEventReducer, urlBuilder: UrlBuilder, - downloadFile: DownloadFile + downloadFile: DownloadFile, + emojifier: Emojifier ): PageViewModelFactory = PageViewModelFactory( openPage = openPage, closePage = closePage, @@ -76,7 +78,8 @@ class PageModule { splitBlock = splitBlock, documentEventReducer = documentExternalEventReducer, urlBuilder = urlBuilder, - downloadFile = downloadFile + downloadFile = downloadFile, + emojifier = emojifier ) @Provides diff --git a/app/src/main/java/com/agileburo/anytype/di/feature/PageIconPickerDI.kt b/app/src/main/java/com/agileburo/anytype/di/feature/PageIconPickerDI.kt new file mode 100644 index 0000000000..616809c66a --- /dev/null +++ b/app/src/main/java/com/agileburo/anytype/di/feature/PageIconPickerDI.kt @@ -0,0 +1,43 @@ +package com.agileburo.anytype.di.feature + +import com.agileburo.anytype.core_utils.di.scope.PerScreen +import com.agileburo.anytype.domain.block.repo.BlockRepository +import com.agileburo.anytype.domain.icon.SetIconName +import com.agileburo.anytype.presentation.page.picker.PageIconPickerViewModelFactory +import com.agileburo.anytype.ui.page.modals.PageIconPickerFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [PageIconPickerModule::class]) +@PerScreen +interface PageIconPickerSubComponent { + + @Subcomponent.Builder + interface Builder { + fun pageIconPickerModule(module: PageIconPickerModule): Builder + fun build(): PageIconPickerSubComponent + } + + fun inject(fragment: PageIconPickerFragment) +} + +@Module +class PageIconPickerModule { + + @Provides + @PerScreen + fun providePageIconPickerViewModelFactory( + setIconName: SetIconName + ): PageIconPickerViewModelFactory = PageIconPickerViewModelFactory( + setIconName = setIconName + ) + + @Provides + @PerScreen + fun provideSetIconNameUseCase( + repo: BlockRepository + ): SetIconName = SetIconName( + repo = repo + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/di/main/EmojiModule.kt b/app/src/main/java/com/agileburo/anytype/di/main/EmojiModule.kt new file mode 100644 index 0000000000..fd540f89af --- /dev/null +++ b/app/src/main/java/com/agileburo/anytype/di/main/EmojiModule.kt @@ -0,0 +1,15 @@ +package com.agileburo.anytype.di.main + +import com.agileburo.anytype.domain.emoji.Emojifier +import com.agileburo.anytype.emojifier.DefaultEmojifier +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class EmojiModule { + + @Provides + @Singleton + fun provideEmojifier(): Emojifier = DefaultEmojifier() +} \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt b/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt index f1fa3d6372..4ab1c1c6ed 100644 --- a/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt +++ b/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt @@ -13,7 +13,8 @@ import javax.inject.Singleton ImageModule::class, ConfigModule::class, DeviceModule::class, - UtilModule::class + UtilModule::class, + EmojiModule::class ] ) interface MainComponent { @@ -32,4 +33,5 @@ interface MainComponent { fun detailsReorderBuilder(): DetailsReorderSubComponent.Builder fun pageComponentBuilder(): PageSubComponent.Builder fun linkAddComponentBuilder(): LinkSubComponent.Builder + fun pageIconPickerBuilder(): PageIconPickerSubComponent.Builder } \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/ui/desktop/DashboardAdapter.kt b/app/src/main/java/com/agileburo/anytype/ui/desktop/DashboardAdapter.kt index fdf37c0b23..e920ded46f 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/desktop/DashboardAdapter.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/desktop/DashboardAdapter.kt @@ -19,6 +19,8 @@ class DashboardAdapter( companion object { const val VIEW_TYPE_DOCUMENT = 0 + const val UNEXPECTED_TYPE_ERROR_MESSAGE = "Unexpected type" + const val EMPTY_EMOJI = "" } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -36,7 +38,7 @@ class DashboardAdapter( override fun getItemViewType(position: Int): Int { return when (data[position]) { is DashboardView.Document -> VIEW_TYPE_DOCUMENT - else -> throw IllegalStateException("Unexpected type") + else -> throw IllegalStateException(UNEXPECTED_TYPE_ERROR_MESSAGE) } } @@ -60,7 +62,7 @@ class DashboardAdapter( fun bind(doc: DashboardView.Document, onClick: (DashboardView.Document) -> Unit) { itemView.setOnClickListener { onClick(doc) } itemView.title.text = doc.title - itemView.emoji.text = if (doc.emoji.isNotEmpty()) doc.emoji else "🎬" + itemView.emoji.text = doc.emoji ?: EMPTY_EMOJI } } } diff --git a/app/src/main/java/com/agileburo/anytype/ui/desktop/HomeDashboardFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/desktop/HomeDashboardFragment.kt index 4f8cdbe261..b5c72c6e63 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/desktop/HomeDashboardFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/desktop/HomeDashboardFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import com.agileburo.anytype.R @@ -15,6 +16,7 @@ import com.agileburo.anytype.core_utils.ext.visible import com.agileburo.anytype.core_utils.ui.EqualSpacingItemDecoration import com.agileburo.anytype.core_utils.ui.EqualSpacingItemDecoration.Companion.GRID import com.agileburo.anytype.di.common.componentManager +import com.agileburo.anytype.domain.emoji.Emojifier import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.State import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModelFactory @@ -22,6 +24,9 @@ import com.agileburo.anytype.presentation.mapper.toView import com.agileburo.anytype.presentation.profile.ProfileView import com.agileburo.anytype.ui.base.ViewStateFragment import kotlinx.android.synthetic.main.fragment_desktop.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -62,6 +67,9 @@ class HomeDashboardFragment : ViewStateFragment(R.layout.fragment_desktop @Inject lateinit var factory: HomeDashboardViewModelFactory + @Inject + lateinit var emojifier: Emojifier + private val dashboardAdapter by lazy { DashboardAdapter( data = mutableListOf(), @@ -97,7 +105,13 @@ class HomeDashboardFragment : ViewStateFragment(R.layout.fragment_desktop state.dashboard != null -> { progress.invisible() fab.visible() - state.dashboard?.let { dashboardAdapter.update(it.toView()) } + state.dashboard?.let { dashboard -> + lifecycleScope.launch { + val result = + withContext(Dispatchers.IO) { dashboard.toView(emojifier = emojifier) } + dashboardAdapter.update(result) + } + } } } } diff --git a/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt index e741654fe0..29c95dc7a3 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt @@ -31,6 +31,7 @@ import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionC import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_TEXT_HIGHLIGHTED import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_TEXT_TEXT import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_TOOL_PAGE +import com.agileburo.anytype.core_utils.common.EventWrapper import com.agileburo.anytype.core_utils.ext.gone import com.agileburo.anytype.core_utils.ext.hexColorCode import com.agileburo.anytype.core_utils.ext.hideSoftInput @@ -44,7 +45,8 @@ import com.agileburo.anytype.ext.extractMarks import com.agileburo.anytype.presentation.page.PageViewModel import com.agileburo.anytype.presentation.page.PageViewModelFactory import com.agileburo.anytype.ui.base.NavigationFragment -import com.agileburo.anytype.ui.page.modals.LinkFragment +import com.agileburo.anytype.ui.page.modals.PageIconPickerFragment +import com.agileburo.anytype.ui.page.modals.SetLinkFragment import com.google.android.material.bottomsheet.BottomSheetBehavior import kotlinx.android.synthetic.main.fragment_page.* import kotlinx.coroutines.delay @@ -90,7 +92,8 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page), onFooterClicked = vm::onOutsideClicked, onPageClicked = vm::onPageClicked, onTextInputClicked = vm::onTextInputClicked, - onDownloadFileClicked = vm::onDownloadFileClicked + onDownloadFileClicked = vm::onDownloadFileClicked, + onPageIconClicked = vm::onPageIconClicked ) } @@ -329,6 +332,7 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page), vm.navigation.observe(viewLifecycleOwner, navObserver) vm.controlPanelViewState.observe(viewLifecycleOwner, Observer { render(it) }) vm.focus.observe(viewLifecycleOwner, Observer { handleFocus(it) }) + vm.commands.observe(viewLifecycleOwner, Observer { execute(it) }) } private fun handleFocus(focus: Id) { @@ -342,13 +346,26 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page), } } + private fun execute(event: EventWrapper) { + event.getContentIfNotHandled()?.let { command -> + when (command) { + is PageViewModel.Command.OpenPagePicker -> { + PageIconPickerFragment.newInstance( + context = requireArguments().getString(ID_KEY, ID_EMPTY_VALUE), + target = command.target + ).show(childFragmentManager, null) + } + } + } + } + private fun render(state: PageViewModel.ViewState) { when (state) { is PageViewModel.ViewState.Success -> { pageAdapter.updateWithDiffUtil(state.blocks) } is PageViewModel.ViewState.OpenLinkScreen -> { - LinkFragment.newInstance( + SetLinkFragment.newInstance( blockId = state.block.id, initUrl = state.block.getFirstLinkMarkupParam(state.range), text = state.block.getSubstring(state.range), diff --git a/app/src/main/java/com/agileburo/anytype/ui/page/modals/PageIconPickerFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/page/modals/PageIconPickerFragment.kt new file mode 100644 index 0000000000..ec62299ff9 --- /dev/null +++ b/app/src/main/java/com/agileburo/anytype/ui/page/modals/PageIconPickerFragment.kt @@ -0,0 +1,157 @@ +package com.agileburo.anytype.ui.page.modals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.GridLayoutManager +import com.agileburo.anytype.R +import com.agileburo.anytype.core_utils.ext.toast +import com.agileburo.anytype.core_utils.ui.BaseBottomSheetFragment +import com.agileburo.anytype.di.common.componentManager +import com.agileburo.anytype.library_page_icon_picker_widget.ui.PageIconPickerAdapter +import com.agileburo.anytype.library_page_icon_picker_widget.ui.PageIconPickerViewHolder +import com.agileburo.anytype.presentation.page.picker.PageIconPickerViewModel +import com.agileburo.anytype.presentation.page.picker.PageIconPickerViewModel.Contract +import com.agileburo.anytype.presentation.page.picker.PageIconPickerViewModel.ViewState +import com.agileburo.anytype.presentation.page.picker.PageIconPickerViewModelFactory +import kotlinx.android.synthetic.main.fragment_page_icon_picker.* +import javax.inject.Inject + +class PageIconPickerFragment : BaseBottomSheetFragment() { + + private val target: String + get() = requireArguments() + .getString(ARG_TARGET_ID_KEY) + ?: throw IllegalStateException(MISSING_TARGET_ERROR) + + private val context: String + get() = requireArguments() + .getString(ARG_CONTEXT_ID_KEY) + ?: throw IllegalStateException(MISSING_CONTEXT_ERROR) + + @Inject + lateinit var factory: PageIconPickerViewModelFactory + + private val vm by lazy { + ViewModelProviders + .of(this, factory) + .get(PageIconPickerViewModel::class.java) + } + + private val pageIconPickerAdapter by lazy { + PageIconPickerAdapter( + views = emptyList(), + onUploadPhotoClicked = { toast(NOT_IMPLEMENTED_MESSAGE) }, + onFilterQueryChanged = { vm.onEvent(Contract.Event.OnFilterQueryChanged(it)) }, + onSetRandomEmojiClicked = { + vm.onEvent( + Contract.Event.OnSetRandomEmojiClicked( + target = target, + context = context + ) + ) + }, + onEmojiClicked = { unicode, alias -> + vm.onEvent( + Contract.Event.OnEmojiClicked( + unicode = unicode, + alias = alias, + target = target, + context = context + ) + ) + } + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogStyle) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_page_icon_picker, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + recyler.apply { + setItemViewCacheSize(EMOJI_RECYCLER_ITEM_VIEW_CACHE_SIZE) + setHasFixedSize(true) + layoutManager = GridLayoutManager(context, PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int) = + when (val type = pageIconPickerAdapter.getItemViewType(position)) { + PageIconPickerViewHolder.HOLDER_UPLOAD_PHOTO -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT + PageIconPickerViewHolder.HOLDER_CHOOSE_EMOJI -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT + PageIconPickerViewHolder.HOLDER_PICK_RANDOM_EMOJI -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT + PageIconPickerViewHolder.HOLDER_EMOJI_CATEGORY_HEADER -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT + PageIconPickerViewHolder.HOLDER_EMOJI_FILTER -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT + PageIconPickerViewHolder.HOLDER_EMOJI_ITEM -> 1 + else -> throw IllegalStateException("$UNEXPECTED_VIEW_TYPE_MESSAGE: $type") + } + } + } + adapter = pageIconPickerAdapter.apply { + setHasStableIds(true) + } + } + + remove.setOnClickListener { + vm.onEvent( + Contract.Event.OnRemoveEmojiSelected( + context = context, + target = target + ) + ) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + vm.state.observe(viewLifecycleOwner, Observer { render(it) }) + } + + fun render(state: ViewState) { + when (state) { + is ViewState.Success -> pageIconPickerAdapter.update(state.views) + is ViewState.Exit -> dismiss() + is ViewState.Error -> toast(state.message) + } + } + + override fun injectDependencies() { + componentManager().pageIconPickerSubComponent.get().inject(this) + } + + override fun releaseDependencies() { + componentManager().pageIconPickerSubComponent.release() + } + + companion object { + + fun newInstance( + context: String, + target: String + ) = PageIconPickerFragment().apply { + arguments = bundleOf(ARG_CONTEXT_ID_KEY to context, ARG_TARGET_ID_KEY to target) + } + + private const val PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT = 8 + private const val EMOJI_RECYCLER_ITEM_VIEW_CACHE_SIZE = 100 + private const val ARG_CONTEXT_ID_KEY = "arg.picker.context.id" + private const val ARG_TARGET_ID_KEY = "arg.picker.target.id" + private const val MISSING_TARGET_ERROR = "Missing target id" + private const val MISSING_CONTEXT_ERROR = "Missing context id" + private const val NOT_IMPLEMENTED_MESSAGE = "Not implemented" + private const val UNEXPECTED_VIEW_TYPE_MESSAGE = "Unexpected view type" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/ui/page/modals/LinkFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/page/modals/SetLinkFragment.kt similarity index 97% rename from app/src/main/java/com/agileburo/anytype/ui/page/modals/LinkFragment.kt rename to app/src/main/java/com/agileburo/anytype/ui/page/modals/SetLinkFragment.kt index 9bbc98bd88..7b59314143 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/page/modals/LinkFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/page/modals/SetLinkFragment.kt @@ -18,7 +18,7 @@ import com.agileburo.anytype.ui.page.OnFragmentInteractionListener import kotlinx.android.synthetic.main.fragment_link.* import javax.inject.Inject -class LinkFragment : BaseBottomSheetFragment() { +class SetLinkFragment : BaseBottomSheetFragment() { companion object { const val ARG_URL = "arg.link.url" @@ -34,7 +34,7 @@ class LinkFragment : BaseBottomSheetFragment() { rangeEnd: Int, blockId: String ) = - LinkFragment().apply { + SetLinkFragment().apply { arguments = bundleOf( ARG_TEXT to text, ARG_URL to initUrl, diff --git a/app/src/main/res/layout/fragment_page_icon_picker.xml b/app/src/main/res/layout/fragment_page_icon_picker.xml new file mode 100644 index 0000000000..339caa53c2 --- /dev/null +++ b/app/src/main/res/layout/fragment_page_icon_picker.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91b1799c21..a30c4a21c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,4 +91,7 @@ Do the computation of an expensive paragraph of text on a background thread: Unlink Link + Page icon + Remove + diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt index 81275c510a..698d4d071a 100644 --- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt @@ -48,6 +48,7 @@ class BlockAdapter( private val onFooterClicked: () -> Unit, private val onPageClicked: (String) -> Unit, private val onTextInputClicked: () -> Unit, + private val onPageIconClicked: () -> Unit, private val onDownloadFileClicked: (String) -> Unit ) : RecyclerView.Adapter() { @@ -310,7 +311,8 @@ class BlockAdapter( holder.bind( item = blocks[position] as BlockView.Title, onTextChanged = onTextChanged, - onFocusChanged = onFocusChanged + onFocusChanged = onFocusChanged, + onPageIconClicked = onPageIconClicked ) } is BlockViewHolder.HeaderOne -> { diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockViewHolder.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockViewHolder.kt index 8ae71d5c22..a58e042efe 100644 --- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockViewHolder.kt +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockViewHolder.kt @@ -239,6 +239,8 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) { class Title(view: View) : BlockViewHolder(view), TextHolder { + private val icon = itemView.logo + override val root: View = itemView override val content: TextInputWidget = itemView.title @@ -249,7 +251,8 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) { fun bind( item: BlockView.Title, onTextChanged: (String, Editable) -> Unit, - onFocusChanged: (String, Boolean) -> Unit + onFocusChanged: (String, Boolean) -> Unit, + onPageIconClicked: () -> Unit ) { content.clearTextWatchers() content.setOnFocusChangeListener { _, hasFocus -> @@ -257,6 +260,8 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) { } content.setText(item.text, BufferType.EDITABLE) setupTextWatcher(onTextChanged, item) + icon.text = item.emoji ?: EMPTY_EMOJI + icon.setOnClickListener { onPageIconClicked() } } fun processPayloads( @@ -279,6 +284,10 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) { onEmptyBlockBackspaceClicked: () -> Unit, onNonEmptyBlockBackspaceClicked: () -> Unit ) = Unit + + companion object { + private const val EMPTY_EMOJI = "" + } } class HeaderOne(view: View) : BlockViewHolder(view), TextHolder { diff --git a/sample/src/main/res/drawable/page_icon_picker_dragger_background.xml b/core-ui/src/main/res/drawable/page_icon_picker_dragger_background.xml similarity index 100% rename from sample/src/main/res/drawable/page_icon_picker_dragger_background.xml rename to core-ui/src/main/res/drawable/page_icon_picker_dragger_background.xml diff --git a/core-ui/src/main/res/layout/item_block_title.xml b/core-ui/src/main/res/layout/item_block_title.xml index dfb108f63c..766cc66b35 100644 --- a/core-ui/src/main/res/layout/item_block_title.xml +++ b/core-ui/src/main/res/layout/item_block_title.xml @@ -14,7 +14,6 @@ android:background="@drawable/rectangle_default_page_logo_background" android:gravity="center" android:layout_marginTop="45dp" - android:text="✍️" android:textColor="@color/emoji_color" android:textSize="28sp" /> diff --git a/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt b/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt index 16853c4834..d6a17614b4 100644 --- a/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt +++ b/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt @@ -847,7 +847,8 @@ class BlockAdapterTest { onFooterClicked = {}, onPageClicked = {}, onTextInputClicked = {}, - onDownloadFileClicked = {} + onDownloadFileClicked = {}, + onPageIconClicked = {} ) } } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt index 47280a83df..ef0f2539f4 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt @@ -82,6 +82,7 @@ fun BlockEntity.Content.toDomain(): Block.Content = when (this) { is BlockEntity.Content.Link -> toDomain() is BlockEntity.Content.Divider -> toDomain() is BlockEntity.Content.File -> toDomain() + is BlockEntity.Content.Icon -> toDomain() } fun BlockEntity.Content.File.toDomain(): Block.Content.File { @@ -96,6 +97,10 @@ fun BlockEntity.Content.File.toDomain(): Block.Content.File { ) } +fun BlockEntity.Content.Icon.toDomain(): Block.Content.Icon = Block.Content.Icon( + name = name +) + fun BlockEntity.Content.File.Type.toDomain(): Block.Content.File.Type { return when (this) { BlockEntity.Content.File.Type.NONE -> Block.Content.File.Type.NONE @@ -197,6 +202,7 @@ fun Block.Content.toEntity(): BlockEntity.Content = when (this) { is Block.Content.Link -> toEntity() is Block.Content.Divider -> toEntity() is Block.Content.File -> toEntity() + is Block.Content.Icon -> toEntity() } fun Block.Content.File.toEntity(): BlockEntity.Content.File { @@ -211,6 +217,10 @@ fun Block.Content.File.toEntity(): BlockEntity.Content.File { ) } +fun Block.Content.Icon.toEntity(): BlockEntity.Content.Icon = BlockEntity.Content.Icon( + name = name +) + fun Block.Content.File.Type.toEntity(): BlockEntity.Content.File.Type { return when (this) { Block.Content.File.Type.NONE -> BlockEntity.Content.File.Type.NONE @@ -349,6 +359,12 @@ fun Command.Split.toEntity(): CommandEntity.Split = CommandEntity.Split( index = index ) +fun Command.SetIconName.toEntity() = CommandEntity.SetIconName( + target = target, + context = context, + name = name +) + fun Position.toEntity(): PositionEntity { return PositionEntity.valueOf(name) } @@ -410,6 +426,13 @@ fun EventEntity.toDomain(): Event { fields = fields?.let { Block.Fields(it.map) } ) } + is EventEntity.Command.UpdateFields -> { + Event.Command.UpdateFields( + context = context, + target = target, + fields = Block.Fields(fields.map) + ) + } } } diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt index 0a71f23a93..03a5a8436f 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt @@ -9,7 +9,7 @@ data class BlockEntity( val content: Content, val fields: Fields ) { - data class Fields(val map: MutableMap = mutableMapOf()) + data class Fields(val map: MutableMap = mutableMapOf()) sealed class Content { @@ -52,6 +52,10 @@ data class BlockEntity( val path: String ) : Content() + data class Icon( + val name: String + ) : Content() + data class Dashboard(val type: Type) : Content() { enum class Type { MAIN_SCREEN, ARCHIVE } } diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt index d48d86a083..0cbfa3adcd 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt @@ -71,4 +71,10 @@ class CommandEntity { val target: String, val index: Int ) + + data class SetIconName( + val context: String, + val target: String, + val name: String + ) } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/EventEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/EventEntity.kt index 54045d2c50..6764e2ea40 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/model/EventEntity.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/EventEntity.kt @@ -50,5 +50,11 @@ sealed class EventEntity { override val context: String, val targets: List ) : Command() + + data class UpdateFields( + override val context: String, + val target: String, + val fields: BlockEntity.Fields + ) : Command() } } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt index 76bc9b2e4b..b4f6e51978 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -67,4 +67,7 @@ class BlockDataRepository( } override suspend fun split(command: Command.Split) = factory.remote.split(command.toEntity()) + + override suspend fun setIconName(command: Command.SetIconName) = + factory.remote.setIconName(command.toEntity()) } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt index 5c8b8390ea..eb88aaadd3 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt @@ -22,4 +22,5 @@ interface BlockDataStore { suspend fun closePage(id: String) suspend fun openDashboard(contextId: String, id: String) suspend fun closeDashboard(id: String) + suspend fun setIconName(command: CommandEntity.SetIconName) } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt index 5bd14a4c64..34f8b47951 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt @@ -22,4 +22,5 @@ interface BlockRemote { suspend fun closePage(id: String) suspend fun openDashboard(contextId: String, id: String) suspend fun closeDashboard(id: String) + suspend fun setIconName(command: CommandEntity.SetIconName) } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt index fd74f436ed..c4dfd1d415 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt @@ -60,4 +60,7 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore { } override suspend fun split(command: CommandEntity.Split): String = remote.split(command) + + override suspend fun setIconName(command: CommandEntity.SetIconName) = + remote.setIconName(command) } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt index 7542c91a55..79fe89bba2 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt @@ -22,16 +22,15 @@ data class Block( * Block fields containing useful block properties. * @property map map containing fields */ - data class Fields(val map: Map) { + data class Fields(val map: Map) { val name: String by map - val icon: String by map + val icon: String? by map fun hasName() = map.containsKey(NAME_KEY) companion object { fun empty(): Fields = Fields(emptyMap()) - const val NAME_KEY = "name" } } @@ -140,6 +139,14 @@ data class Block( enum class Type { PAGE, DATA_VIEW, DASHBOARD, ARCHIVE } } + /** + * Page icon. + * @property name conventional emoji short name. + */ + data class Icon( + val name: String + ) : Content() + /** * File block. * @property hash file hash diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt index 878e422376..1ae428d9af 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt @@ -127,4 +127,10 @@ sealed class Command { val target: Id, val index: Int ) + + data class SetIconName( + val context: Id, + val target: Id, + val name: String + ) } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt index 5fb31aef65..552b323b96 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt @@ -34,4 +34,6 @@ interface BlockRepository { suspend fun closePage(id: String) suspend fun openDashboard(contextId: String, id: String) suspend fun closeDashboard(id: String) + + suspend fun setIconName(command: Command.SetIconName) } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/emoji/Emoji.kt b/domain/src/main/java/com/agileburo/anytype/domain/emoji/Emoji.kt new file mode 100644 index 0000000000..229b4306dc --- /dev/null +++ b/domain/src/main/java/com/agileburo/anytype/domain/emoji/Emoji.kt @@ -0,0 +1,9 @@ +package com.agileburo.anytype.domain.emoji + +data class Emoji( + val unicode: String, + val alias: String +) { + val name: String + get() = ":$alias:" +} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/emoji/Emojifier.kt b/domain/src/main/java/com/agileburo/anytype/domain/emoji/Emojifier.kt new file mode 100644 index 0000000000..461a465061 --- /dev/null +++ b/domain/src/main/java/com/agileburo/anytype/domain/emoji/Emojifier.kt @@ -0,0 +1,6 @@ +package com.agileburo.anytype.domain.emoji + +interface Emojifier { + suspend fun fromAlias(alias: String): Emoji + suspend fun fromShortName(name: String): Emoji +} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/event/model/Event.kt b/domain/src/main/java/com/agileburo/anytype/domain/event/model/Event.kt index 9cc13d9bc1..0c6e342371 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/event/model/Event.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/event/model/Event.kt @@ -80,5 +80,11 @@ sealed class Event { val id: Id, val children: List ) : Command() + + data class UpdateFields( + override val context: Id, + val target: Id, + val fields: Block.Fields + ) : Command() } } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/icon/SetIconName.kt b/domain/src/main/java/com/agileburo/anytype/domain/icon/SetIconName.kt new file mode 100644 index 0000000000..ccf22a34ad --- /dev/null +++ b/domain/src/main/java/com/agileburo/anytype/domain/icon/SetIconName.kt @@ -0,0 +1,39 @@ +package com.agileburo.anytype.domain.icon + +import com.agileburo.anytype.domain.base.BaseUseCase +import com.agileburo.anytype.domain.base.Either +import com.agileburo.anytype.domain.block.model.Command +import com.agileburo.anytype.domain.block.repo.BlockRepository +import com.agileburo.anytype.domain.common.Id + +/** + * Use-case for setting emoji icon's short name + */ +class SetIconName(private val repo: BlockRepository) : BaseUseCase() { + + override suspend fun run(params: Params) = try { + repo.setIconName( + command = Command.SetIconName( + context = params.context, + target = params.target, + name = params.name + ) + ).let { + Either.Right(it) + } + } catch (t: Throwable) { + Either.Left(t) + } + + /** + * Params for setting icon name + * @property name emoji's short-name code + * @property target id of the target block (icon) + * @property context id of the context for this operation + */ + data class Params( + val name: String, + val target: Id, + val context: Id + ) +} \ No newline at end of file diff --git a/library-emojifier/.gitignore b/library-emojifier/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/library-emojifier/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library-emojifier/build.gradle b/library-emojifier/build.gradle new file mode 100644 index 0000000000..b121980f81 --- /dev/null +++ b/library-emojifier/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + def config = rootProject.extensions.getByName("ext") + + compileSdkVersion config["compile_sdk"] + + defaultConfig { + minSdkVersion config["min_sdk"] + targetSdkVersion config["target_sdk"] + versionCode config["version_code"] + versionName config["version_name"] + + testInstrumentationRunner config["test_runner"] + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation project(':domain') + def lib = rootProject.ext.libraryPageIconPicker + implementation lib.emojiJava +} \ No newline at end of file diff --git a/library-emojifier/consumer-rules.pro b/library-emojifier/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/library-emojifier/proguard-rules.pro b/library-emojifier/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/library-emojifier/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/library-emojifier/src/main/AndroidManifest.xml b/library-emojifier/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a9c5ca62ec --- /dev/null +++ b/library-emojifier/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/library-emojifier/src/main/java/com/agileburo/anytype/emojifier/DefaultEmojifier.kt b/library-emojifier/src/main/java/com/agileburo/anytype/emojifier/DefaultEmojifier.kt new file mode 100644 index 0000000000..07d01050be --- /dev/null +++ b/library-emojifier/src/main/java/com/agileburo/anytype/emojifier/DefaultEmojifier.kt @@ -0,0 +1,24 @@ +package com.agileburo.anytype.emojifier + +import com.agileburo.anytype.domain.emoji.Emoji +import com.agileburo.anytype.domain.emoji.Emojifier +import com.vdurmont.emoji.EmojiManager + +class DefaultEmojifier : Emojifier { + + override suspend fun fromAlias(alias: String): Emoji { + check(alias.isNotEmpty()) { "Alias cannot be empty" } + return EmojiManager.getForAlias(alias).let { result -> + Emoji( + unicode = result.unicode, + alias = result.aliases.first() + ) + } + } + + override suspend fun fromShortName(name: String): Emoji { + check(name.isNotEmpty()) { "Short name cannot be empty" } + val alias = name.substring(1, name.length - 1) + return fromAlias(alias) + } +} \ No newline at end of file diff --git a/library-emojifier/src/main/res/values/strings.xml b/library-emojifier/src/main/res/values/strings.xml new file mode 100644 index 0000000000..c23cb24f37 --- /dev/null +++ b/library-emojifier/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Library Emojifier + diff --git a/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/model/PageIconPickerView.kt b/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/model/PageIconPickerView.kt index c19f967b59..a4de7ceb9b 100644 --- a/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/model/PageIconPickerView.kt +++ b/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/model/PageIconPickerView.kt @@ -14,7 +14,7 @@ sealed class PageIconPickerView : ViewType { * @property alias short name or convenient name for an emoji. */ data class Emoji( - val alias: String = "", + val alias: String, val unicode: String ) : PageIconPickerView() { override fun getViewType() = HOLDER_EMOJI_ITEM diff --git a/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/ui/PageIconPickerAdapter.kt b/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/ui/PageIconPickerAdapter.kt index 9316fec90a..4fe684a5d0 100644 --- a/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/ui/PageIconPickerAdapter.kt +++ b/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/ui/PageIconPickerAdapter.kt @@ -3,9 +3,11 @@ package com.agileburo.anytype.library_page_icon_picker_widget.ui import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.widget.doOnTextChanged +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.agileburo.anytype.R import com.agileburo.anytype.library_page_icon_picker_widget.model.PageIconPickerView +import com.agileburo.anytype.library_page_icon_picker_widget.model.PageIconPickerViewDiffUtil import com.agileburo.anytype.library_page_icon_picker_widget.ui.PageIconPickerViewHolder.Companion.HOLDER_CHOOSE_EMOJI import com.agileburo.anytype.library_page_icon_picker_widget.ui.PageIconPickerViewHolder.Companion.HOLDER_EMOJI_CATEGORY_HEADER import com.agileburo.anytype.library_page_icon_picker_widget.ui.PageIconPickerViewHolder.Companion.HOLDER_EMOJI_FILTER @@ -18,7 +20,8 @@ class PageIconPickerAdapter( private var views: List, private val onUploadPhotoClicked: () -> Unit, private val onSetRandomEmojiClicked: () -> Unit, - private val onFilterQueryChanged: (String) -> Unit + private val onFilterQueryChanged: (String) -> Unit, + private val onEmojiClicked: (String, String) -> Unit ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageIconPickerViewHolder { @@ -88,8 +91,22 @@ class PageIconPickerAdapter( holder.bind(views[position] as PageIconPickerView.GroupHeader) } is PageIconPickerViewHolder.EmojiItem -> { - holder.bind(views[position] as PageIconPickerView.Emoji) + holder.bind( + item = views[position] as PageIconPickerView.Emoji, + onEmojiClicked = onEmojiClicked + ) } } } + + fun update(update: List) { + val result = DiffUtil.calculateDiff( + PageIconPickerViewDiffUtil( + old = views, + new = update + ) + ) + views = update + result.dispatchUpdatesTo(this) + } } \ No newline at end of file diff --git a/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/ui/PageIconPickerViewHolder.kt b/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/ui/PageIconPickerViewHolder.kt index bf39895f31..07052c454c 100644 --- a/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/ui/PageIconPickerViewHolder.kt +++ b/library-page-icon-picker-widget/src/main/java/com/agileburo/anytype/library_page_icon_picker_widget/ui/PageIconPickerViewHolder.kt @@ -29,9 +29,11 @@ sealed class PageIconPickerViewHolder(view: View) : RecyclerView.ViewHolder(view private val emoji = itemView.emoji fun bind( - item: PageIconPickerView.Emoji + item: PageIconPickerView.Emoji, + onEmojiClicked: (String, String) -> Unit ) { emoji.text = item.unicode + itemView.setOnClickListener { onEmojiClicked(item.unicode, item.alias) } } } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt index 562fce6330..26c86f0b0a 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt @@ -216,6 +216,10 @@ fun Block.link(): BlockEntity.Content.Link = BlockEntity.Content.Link( fun Block.divider(): BlockEntity.Content.Divider = BlockEntity.Content.Divider +fun Block.icon(): BlockEntity.Content.Icon = BlockEntity.Content.Icon( + name = icon.name +) + fun Block.file(): BlockEntity.Content.File = BlockEntity.Content.File( hash = file.hash, name = file.name, @@ -296,6 +300,14 @@ fun List.blocks(): List = mapNotNull { block -> content = block.file() ) } + Block.ContentCase.ICON -> { + BlockEntity( + id = block.id, + children = block.childrenIdsList, + fields = block.fields(), + content = block.icon() + ) + } else -> { null } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt index 4f90cc3324..7590c31979 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt @@ -82,4 +82,7 @@ class BlockMiddleware( } override suspend fun split(command: CommandEntity.Split): String = middleware.split(command) + + override suspend fun setIconName(command: CommandEntity.SetIconName) = + middleware.setIconName(command) } \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java index 1fad56a3b6..c594ad793b 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java @@ -544,4 +544,17 @@ public class Middleware { return response.getBlockId(); } + + public void setIconName(CommandEntity.SetIconName command) throws Exception { + Block.Set.Icon.Name.Request request = Block.Set.Icon.Name.Request + .newBuilder() + .setBlockId(command.getTarget()) + .setContextId(command.getContext()) + .setName(command.getName()) + .build(); + + Timber.d("Setting icon name with the following request:\n%s", request.toString()); + + service.blockSetIconName(request); + } } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventChannel.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventChannel.kt index a77454fdf0..2a8f35d13c 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventChannel.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventChannel.kt @@ -22,7 +22,8 @@ class MiddlewareEventChannel( Events.Event.Message.ValueCase.BLOCKSETTEXT, Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS, Events.Event.Message.ValueCase.BLOCKDELETE, - Events.Event.Message.ValueCase.BLOCKSETLINK + Events.Event.Message.ValueCase.BLOCKSETLINK, + Events.Event.Message.ValueCase.BLOCKSETFIELDS ) override fun observeEvents( @@ -103,6 +104,13 @@ class MiddlewareEventChannel( null ) } + Events.Event.Message.ValueCase.BLOCKSETFIELDS -> { + EventEntity.Command.UpdateFields( + context = context, + target = event.blockSetFields.id, + fields = event.blockSetFields.fields.fields() + ) + } else -> null } } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java b/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java index 6fe3c631ea..204ed13f3c 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java @@ -251,4 +251,15 @@ public class DefaultMiddlewareService implements MiddlewareService { return response; } } + + @Override + public Block.Set.Icon.Name.Response blockSetIconName(Block.Set.Icon.Name.Request request) throws Exception { + byte[] encoded = Lib.blockSetIconName(request.toByteArray()); + Block.Set.Icon.Name.Response response = Block.Set.Icon.Name.Response.parseFrom(encoded); + if (response.getError() != null && response.getError().getCode() != Block.Set.Icon.Name.Response.Error.Code.NULL) { + throw new Exception(response.getError().getDescription()); + } else { + return response; + } + } } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java b/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java index 65cbec3f28..0fe7b57a20 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java @@ -54,4 +54,6 @@ public interface MiddlewareService { Block.Split.Response blockSplit(Block.Split.Request request) throws Exception; BlockList.Duplicate.Response blockListDuplicate(BlockList.Duplicate.Request request) throws Exception; + + Block.Set.Icon.Name.Response blockSetIconName(Block.Set.Icon.Name.Request request) throws Exception; } diff --git a/presentation/build.gradle b/presentation/build.gradle index 0c289c43ed..1a1f85da63 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -26,9 +26,13 @@ dependencies { implementation project(':domain') implementation project(':core-utils') implementation project(':core-ui') + implementation project(':library-page-icon-picker-widget') def applicationDependencies = rootProject.ext.mainApplication def unitTestDependencies = rootProject.ext.unitTesting + def lib = rootProject.ext.libraryPageIconPicker + + implementation lib.emojiJava implementation applicationDependencies.kotlin implementation applicationDependencies.coroutines @@ -49,5 +53,4 @@ dependencies { testImplementation unitTestDependencies.archCoreTesting androidTestImplementation 'androidx.test:core:1.2.0' - } diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/common/SupportCommand.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/common/SupportCommand.kt new file mode 100644 index 0000000000..f91dad9251 --- /dev/null +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/common/SupportCommand.kt @@ -0,0 +1,13 @@ +package com.agileburo.anytype.presentation.common + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.agileburo.anytype.core_utils.common.EventWrapper + +interface SupportCommand { + val commands: MutableLiveData> + fun receive(): LiveData> = commands + fun dispatch(command: Command) { + commands.postValue(EventWrapper(command)) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/DashboardView.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/DashboardView.kt index 1643672b79..b6b48ee21d 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/DashboardView.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/DashboardView.kt @@ -1,5 +1,6 @@ package com.agileburo.anytype.presentation.desktop sealed class DashboardView { - data class Document(val id: String, val title: String, val emoji: String = "") : DashboardView() + data class Document(val id: String, val title: String, val emoji: String? = null) : + DashboardView() } \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/mapper/MapperExtension.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/mapper/MapperExtension.kt index 58baa7bc9d..0d7c81e9f8 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/mapper/MapperExtension.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/mapper/MapperExtension.kt @@ -5,8 +5,10 @@ import com.agileburo.anytype.core_ui.features.page.BlockView import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.block.model.Block.Content.Text.Style import com.agileburo.anytype.domain.dashboard.model.HomeDashboard +import com.agileburo.anytype.domain.emoji.Emojifier import com.agileburo.anytype.domain.misc.UrlBuilder import com.agileburo.anytype.presentation.desktop.DashboardView +import com.vdurmont.emoji.EmojiManager fun Block.toView( focused: Boolean = false, @@ -43,7 +45,15 @@ fun Block.toView( ) Style.TITLE -> BlockView.Title( id = id, - text = content.text + text = content.text, + emoji = fields.icon?.let { name -> + EmojiManager.getForAlias( + name.substring( + 1, + name.lastIndex + ) + ).unicode + } ) Style.QUOTE -> BlockView.Highlight( id = id, @@ -179,8 +189,9 @@ private fun mapMarks(content: Block.Content.Text): List = } } -fun HomeDashboard.toView( - defaultTitle: String = "Untitled" +suspend fun HomeDashboard.toView( + defaultTitle: String = "Untitled", + emojifier: Emojifier ): List = children.mapNotNull { id -> blocks.find { block -> block.id == id }?.let { model -> when (val content = model.content) { @@ -188,7 +199,13 @@ fun HomeDashboard.toView( if (content.type == Block.Content.Link.Type.PAGE) { DashboardView.Document( id = content.target, - title = if (content.fields.hasName()) content.fields.name else defaultTitle + title = if (content.fields.hasName()) content.fields.name else defaultTitle, + emoji = content.fields.icon?.let { name -> + if (name.isNotEmpty()) + emojifier.fromShortName(name).unicode + else + null + } ) } else { null diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/DocumentExternalEventReducer.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/DocumentExternalEventReducer.kt index d037d756f8..90dabbfe58 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/DocumentExternalEventReducer.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/DocumentExternalEventReducer.kt @@ -5,6 +5,7 @@ import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.event.model.Event import com.agileburo.anytype.domain.ext.content import com.agileburo.anytype.presentation.common.StateReducer +import timber.log.Timber /** * Reduces external events (coming not from user, but from outside) to state. @@ -44,6 +45,10 @@ class DocumentExternalEventReducer : StateReducer, Event> { }, target = { block -> block.id == event.id } ) - else -> state + is Event.Command.UpdateFields -> state.replace( + replacement = { block -> block.copy(fields = event.fields) }, + target = { block -> block.id == event.target } + ) + else -> state.also { Timber.d("Ignoring event: $event") } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt index 4d8d341ea7..99b9d82bef 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt @@ -17,6 +17,7 @@ import com.agileburo.anytype.domain.block.model.Block.Prototype import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.download.DownloadFile +import com.agileburo.anytype.domain.emoji.Emojifier import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.event.model.Event import com.agileburo.anytype.domain.ext.* @@ -25,6 +26,7 @@ import com.agileburo.anytype.domain.page.ClosePage import com.agileburo.anytype.domain.page.CreatePage import com.agileburo.anytype.domain.page.OpenPage import com.agileburo.anytype.presentation.common.StateReducer +import com.agileburo.anytype.presentation.common.SupportCommand import com.agileburo.anytype.presentation.mapper.toView import com.agileburo.anytype.presentation.navigation.AppNavigation import com.agileburo.anytype.presentation.navigation.SupportNavigation @@ -54,9 +56,11 @@ class PageViewModel( private val splitBlock: SplitBlock, private val downloadFile: DownloadFile, private val documentExternalEventReducer: StateReducer, Event>, - private val urlBuilder: UrlBuilder + private val urlBuilder: UrlBuilder, + private val emojifier: Emojifier ) : ViewStateViewModel(), SupportNavigation>, + SupportCommand, StateReducer, Event> by documentExternalEventReducer { private val controlPanelInteractor = Interactor(viewModelScope) @@ -91,6 +95,7 @@ class PageViewModel( val focus: LiveData = _focus override val navigation = MutableLiveData>() + override val commands = MutableLiveData>() init { startHandlingTextChanges() @@ -255,16 +260,30 @@ class PageViewModel( val render = models.asMap().asRender(context) + val page = models.first { it.id == context } + val numbers = render.numbers() render.mapNotNull { block -> - when (block.content) { + when (val content = block.content) { is Content.Text -> { - block.toView( - focused = block.id == focus, - numbers = numbers, - urlBuilder = urlBuilder - ) + if (content.style == Content.Text.Style.TITLE) + BlockView.Title( + id = block.id, + text = content.text, + emoji = page.fields.icon?.let { name -> + if (name.isNotEmpty()) + emojifier.fromShortName(name).unicode + else + null + } + ) + else + block.toView( + focused = block.id == focus, + numbers = numbers, + urlBuilder = urlBuilder + ) } is Content.Image -> { block.toView( @@ -841,6 +860,11 @@ class PageViewModel( } } + fun onPageIconClicked() { + val target = blocks.first { it.content is Content.Icon }.id + dispatch(Command.OpenPagePicker(target)) + } + fun onDownloadFileClicked(id: String) { val block = blocks.first { it.id == id } val file = block.content() @@ -874,8 +898,17 @@ class PageViewModel( object Loading : ViewState() data class Success(val blocks: List) : ViewState() data class Error(val message: String) : ViewState() - data class OpenLinkScreen(val pageId: String, val block: Block, val range: IntRange) : - ViewState() + data class OpenLinkScreen( + val pageId: String, + val block: Block, + val range: IntRange + ) : ViewState() + } + + sealed class Command { + data class OpenPagePicker( + val target: String + ) : Command() } companion object { diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModelFactory.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModelFactory.kt index 3d01e1446b..489cb3c1e9 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModelFactory.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModelFactory.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider import com.agileburo.anytype.domain.block.interactor.* import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.download.DownloadFile +import com.agileburo.anytype.domain.emoji.Emojifier import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.event.model.Event import com.agileburo.anytype.domain.misc.UrlBuilder @@ -32,7 +33,8 @@ open class PageViewModelFactory( private val splitBlock: SplitBlock, private val documentEventReducer: StateReducer, Event>, private val urlBuilder: UrlBuilder, - private val downloadFile: DownloadFile + private val downloadFile: DownloadFile, + private val emojifier: Emojifier ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -56,7 +58,8 @@ open class PageViewModelFactory( createPage = createPage, documentExternalEventReducer = documentEventReducer, urlBuilder = urlBuilder, - downloadFile = downloadFile + downloadFile = downloadFile, + emojifier = emojifier ) as T } } \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/picker/PageIconPickerViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/picker/PageIconPickerViewModel.kt new file mode 100644 index 0000000000..137c970961 --- /dev/null +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/picker/PageIconPickerViewModel.kt @@ -0,0 +1,342 @@ +package com.agileburo.anytype.presentation.page.picker + +import androidx.lifecycle.viewModelScope +import com.agileburo.anytype.core_utils.ui.ViewStateViewModel +import com.agileburo.anytype.domain.base.Either +import com.agileburo.anytype.domain.icon.SetIconName +import com.agileburo.anytype.library_page_icon_picker_widget.model.PageIconPickerView +import com.agileburo.anytype.presentation.common.StateReducer +import com.agileburo.anytype.presentation.page.picker.PageIconPickerViewModel.Contract.Event +import com.agileburo.anytype.presentation.page.picker.PageIconPickerViewModel.Contract.State +import com.agileburo.anytype.presentation.page.picker.PageIconPickerViewModel.ViewState +import com.vdurmont.emoji.Emoji +import com.vdurmont.emoji.EmojiManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.withContext + +class PageIconPickerViewModel( + private val setIconName: SetIconName +) : ViewStateViewModel(), StateReducer { + + private val channel = ConflatedBroadcastChannel() + private val actions = Channel() + private val flow: Flow = channel.asFlow().scan(State.init(), function) + + override val function: suspend (State, Event) -> State + get() = { state, event -> reduce(state, event) } + + + private val headers = listOf( + PageIconPickerView.Action.UploadPhoto, + PageIconPickerView.Action.PickRandomly, + PageIconPickerView.Action.ChooseEmoji, + PageIconPickerView.EmojiFilter + ) + + init { + processViewState() + processActions() + initialize() + } + + private fun initialize() { + channel.offer(Event.Init) + } + + private fun processViewState() { + flow + .map { state -> + when { + state.error != null -> ViewState.Error(state.error) + state.isLoading -> ViewState.Loading + state.isCompleted -> ViewState.Exit + else -> ViewState.Success( + views = headers + map(state.selection) + ) + } + } + .onEach { stateData.postValue(it) } + .launchIn(viewModelScope) + } + + private fun processActions() { + actions + .consumeAsFlow() + .map { action -> + when (action) { + is Contract.Action.FetchEmojis -> { + val emojis = loadEmoji() + Event.OnEmojiLoaded(emojis) + } + is Contract.Action.SearchEmojis -> { + val emojis = findEmojis(action.query) + Event.OnSearchResult(emojis) + } + is Contract.Action.SetEmoji -> { + setIconEmojiName(action).let { result -> + when (result) { + is Either.Left -> Event.Failure(result.a) + is Either.Right -> Event.OnCompleted + } + } + } + is Contract.Action.ClearEmoji -> { + clearIconEmojiName(action).let { result -> + when (result) { + is Either.Left -> Event.Failure(result.a) + is Either.Right -> Event.OnCompleted + } + } + } + is Contract.Action.PickRandomEmoji -> { + val emoji = pickRandomEmoji(action.emojis) + Event.OnRandomEmojiSelected( + target = action.target, + context = action.context, + emoji = emoji + ) + } + } + } + .onEach(channel::send) + .launchIn(viewModelScope) + } + + private suspend fun setIconEmojiName( + action: Contract.Action.SetEmoji + ): Either = withContext(Dispatchers.IO) { + setIconName.run( + params = SetIconName.Params( + target = action.target, + name = ":${action.alias}:", + context = action.context + ) + ) + } + + private suspend fun clearIconEmojiName( + action: Contract.Action.ClearEmoji + ): Either = withContext(Dispatchers.IO) { + setIconName.run( + params = SetIconName.Params( + target = action.target, + name = "", + context = action.context + ) + ) + } + + private suspend fun loadEmoji(): List = withContext(Dispatchers.IO) { + EmojiManager.getAll().toList() + } + + private suspend fun findEmojis(query: String): List = withContext(Dispatchers.IO) { + EmojiManager.getAll().filter { emoji -> + emoji.aliases.any { alias -> + alias.contains(query, ignoreCase = true) || query == alias + } + } + } + + private suspend fun pickRandomEmoji(emojis: List): Emoji = withContext(Dispatchers.IO) { + emojis.random() + } + + private suspend fun map(emojis: List) = withContext(Dispatchers.IO) { + emojis.map { emoji -> + PageIconPickerView.Emoji( + alias = emoji.aliases.first(), + unicode = emoji.unicode + ) + } + } + + fun onEvent(event: Event) { + channel.offer(event) + } + + sealed class ViewState { + object Loading : ViewState() + object Exit : ViewState() + data class Success(val views: List) : ViewState() + data class Error(val message: String) : ViewState() + } + + sealed class Contract { + + sealed class Action { + + object FetchEmojis : Action() + + data class SearchEmojis( + val query: String + ) : Action() + + data class PickRandomEmoji( + val emojis: List, + val context: String, + val target: String + ) : Action() + + data class ClearEmoji( + val target: String, + val context: String + ) : Action() + + data class SetEmoji( + val unicode: String, + val alias: String, + val target: String, + val context: String + ) : Action() + } + + data class State( + val isLoading: Boolean, + val isCompleted: Boolean = false, + val error: String? = null, + val emojis: List, + val selection: List + ) : Contract() { + companion object { + fun init() = State( + isLoading = false, + emojis = emptyList(), + selection = emptyList() + ) + } + } + + sealed class Event : Contract() { + + object Init : Event() + + data class OnEmojiClicked( + val unicode: String, + val alias: String, + val target: String, + val context: String + ) : Event() + + data class OnFilterQueryChanged( + val query: String + ) : Event() + + data class OnSetRandomEmojiClicked( + val target: String, + val context: String + ) : Event() + + data class OnEmojiLoaded( + val emojis: List + ) : Event() + + data class OnRandomEmojiSelected( + val emoji: Emoji, + val context: String, + val target: String + ) : Event() + + data class OnRemoveEmojiSelected( + val context: String, + val target: String + ) : Event() + + data class OnSearchResult( + val emojis: List + ) : Event() + + object OnCompleted : Event() + + data class Failure(val error: Throwable) : Event() + } + } + + override suspend fun reduce(state: State, event: Event): State { + return when (event) { + is Event.Init -> state.copy(isLoading = true).also { + actions.send(Contract.Action.FetchEmojis) + } + is Event.OnEmojiLoaded -> state.copy( + isLoading = false, + emojis = event.emojis, + selection = event.emojis + ) + is Event.OnSearchResult -> state.copy( + isLoading = false, + selection = event.emojis + ) + is Event.OnRandomEmojiSelected -> state.copy( + isLoading = true + ).also { + actions.send( + Contract.Action.SetEmoji( + target = event.target, + context = event.context, + unicode = event.emoji.unicode, + alias = event.emoji.aliases.first() + ) + ) + } + is Event.OnFilterQueryChanged -> state.copy( + isLoading = true + ).also { + actions.send( + Contract.Action.SearchEmojis( + query = event.query + ) + ) + } + is Event.OnEmojiClicked -> { + state.copy(isLoading = true).also { + actions.send( + Contract.Action.SetEmoji( + unicode = event.unicode, + target = event.target, + alias = event.alias, + context = event.context + ) + ) + } + } + is Event.OnSetRandomEmojiClicked -> { + state.copy( + isLoading = true + ).also { + actions.send( + Contract.Action.PickRandomEmoji( + emojis = state.emojis, + target = event.target, + context = event.context + ) + ) + } + } + is Event.OnRemoveEmojiSelected -> { + state.copy( + isLoading = true + ).also { + actions.send( + Contract.Action.ClearEmoji( + context = event.context, + target = event.target + ) + ) + } + } + is Event.OnCompleted -> state.copy( + isLoading = false, + isCompleted = true, + error = null + ) + is Event.Failure -> state.copy( + isLoading = false, + isCompleted = false, + error = event.error.toString() + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/picker/PageIconPickerViewModelFactory.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/picker/PageIconPickerViewModelFactory.kt new file mode 100644 index 0000000000..47d7d9bf34 --- /dev/null +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/picker/PageIconPickerViewModelFactory.kt @@ -0,0 +1,15 @@ +package com.agileburo.anytype.presentation.page.picker + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.agileburo.anytype.domain.icon.SetIconName + +class PageIconPickerViewModelFactory( + private val setIconName: SetIconName +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = PageIconPickerViewModel( + setIconName = setIconName + ) as T +} \ No newline at end of file diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewMapperTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewMapperTest.kt index ca494fca03..bb38faaea7 100644 --- a/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewMapperTest.kt +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewMapperTest.kt @@ -3,13 +3,30 @@ package com.agileburo.anytype.presentation.home import MockDataFactory import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.dashboard.model.HomeDashboard +import com.agileburo.anytype.domain.emoji.Emoji +import com.agileburo.anytype.domain.emoji.Emojifier import com.agileburo.anytype.presentation.desktop.DashboardView import com.agileburo.anytype.presentation.mapper.toView +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.stub +import kotlinx.coroutines.runBlocking +import org.junit.Before import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations import kotlin.test.assertEquals class HomeDashboardViewMapperTest { + @Mock + lateinit var emojifier: Emojifier + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + @Test fun `should return empty list if home dashboard contains only data view`() { @@ -32,7 +49,11 @@ class HomeDashboardViewMapperTest { type = Block.Content.Dashboard.Type.MAIN_SCREEN ) - val view = dashboard.toView() + val view = runBlocking { + dashboard.toView( + emojifier = emojifier + ) + } assertEquals( expected = emptyList(), @@ -43,11 +64,16 @@ class HomeDashboardViewMapperTest { @Test fun `should return one page link`() { + val emoji = Emoji( + unicode = MockDataFactory.randomString(), + alias = MockDataFactory.randomString() + ) + val child = Block( id = MockDataFactory.randomUuid(), content = Block.Content.Link( target = MockDataFactory.randomUuid(), - fields = Block.Fields.empty(), + fields = Block.Fields(mapOf("icon" to emoji.name)), type = Block.Content.Link.Type.PAGE ), children = emptyList(), @@ -62,13 +88,18 @@ class HomeDashboardViewMapperTest { type = Block.Content.Dashboard.Type.MAIN_SCREEN ) - val view = dashboard.toView() + emojifier.stub { + onBlocking { fromShortName(any()) } doReturn emoji + } + + val view: List = runBlocking { dashboard.toView(emojifier = emojifier) } assertEquals( expected = listOf( DashboardView.Document( id = child.content.asLink().target, - title = "Untitled" + title = "Untitled", + emoji = emoji.unicode ) ), actual = view diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewModelTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewModelTest.kt index c89ec2dd95..05baff7682 100644 --- a/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewModelTest.kt +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewModelTest.kt @@ -16,6 +16,8 @@ import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard import com.agileburo.anytype.domain.dashboard.interactor.toHomeDashboard import com.agileburo.anytype.domain.dashboard.model.HomeDashboard +import com.agileburo.anytype.domain.emoji.Emoji +import com.agileburo.anytype.domain.emoji.Emojifier import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.event.model.Event import com.agileburo.anytype.domain.image.LoadImage @@ -32,6 +34,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Rule import org.junit.Test @@ -71,6 +74,9 @@ class HomeDashboardViewModelTest { @Mock lateinit var dnd: DragAndDrop + @Mock + lateinit var emojifier: Emojifier + private lateinit var vm: HomeDashboardViewModel @Before @@ -242,8 +248,15 @@ class HomeDashboardViewModelTest { @Test fun `block dragging events do not alter overall state`() { - val config = - Config(home = MockDataFactory.randomUuid(), gateway = MockDataFactory.randomUuid()) + val config = Config( + home = MockDataFactory.randomUuid(), + gateway = MockDataFactory.randomUuid() + ) + + val emoji = Emoji( + unicode = MockDataFactory.randomString(), + alias = MockDataFactory.randomString() + ) val pages = listOf( Block( @@ -253,7 +266,12 @@ class HomeDashboardViewModelTest { content = Block.Content.Link( target = MockDataFactory.randomUuid(), type = Block.Content.Link.Type.PAGE, - fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())) + fields = Block.Fields( + map = mapOf( + "name" to MockDataFactory.randomString(), + "icon" to MockDataFactory.randomString() + ) + ) ) ), Block( @@ -263,7 +281,12 @@ class HomeDashboardViewModelTest { content = Block.Content.Link( target = MockDataFactory.randomUuid(), type = Block.Content.Link.Type.PAGE, - fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())) + fields = Block.Fields( + map = mapOf( + "name" to MockDataFactory.randomString(), + "icon" to emoji.name + ) + ) ) ) ) @@ -274,7 +297,8 @@ class HomeDashboardViewModelTest { type = Block.Content.Dashboard.Type.MAIN_SCREEN ), children = pages.map { page -> page.id }, - fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())) + + fields = Block.Fields.empty() ) val delayInMillis = 100L @@ -292,6 +316,10 @@ class HomeDashboardViewModelTest { ) } + emojifier.stub { + onBlocking { fromShortName(any()) } doReturn emoji + } + stubGetConfig(Either.Right(config)) stubObserveEvents( params = InterceptEvents.Params(context = null), @@ -316,8 +344,15 @@ class HomeDashboardViewModelTest { error = null ) - val views = - listOf(dashboard, pages.first(), pages.last()).toHomeDashboard(dashboard.id).toView() + val views = runBlocking { + listOf( + dashboard, + pages.first(), + pages.last() + ).toHomeDashboard(dashboard.id).toView( + emojifier = emojifier + ) + } val from = 0 val to = 1 @@ -337,8 +372,15 @@ class HomeDashboardViewModelTest { @Test fun `should start dispatching drag-and-drop actions when the dragged item is dropped`() { - val config = - Config(home = MockDataFactory.randomUuid(), gateway = MockDataFactory.randomUuid()) + val config = Config( + home = MockDataFactory.randomUuid(), + gateway = MockDataFactory.randomUuid() + ) + + val emoji = Emoji( + unicode = MockDataFactory.randomString(), + alias = MockDataFactory.randomString() + ) val pages = listOf( Block( @@ -348,7 +390,12 @@ class HomeDashboardViewModelTest { content = Block.Content.Link( type = Block.Content.Link.Type.PAGE, target = MockDataFactory.randomUuid(), - fields = Block.Fields.empty() + fields = Block.Fields( + map = mapOf( + "name" to MockDataFactory.randomString(), + "icon" to emoji.name + ) + ) ) ), Block( @@ -358,7 +405,12 @@ class HomeDashboardViewModelTest { content = Block.Content.Link( type = Block.Content.Link.Type.PAGE, target = MockDataFactory.randomUuid(), - fields = Block.Fields.empty() + fields = Block.Fields( + map = mapOf( + "name" to MockDataFactory.randomString(), + "icon" to emoji.name + ) + ) ) ) ) @@ -378,6 +430,10 @@ class HomeDashboardViewModelTest { emit(dashboard) } + emojifier.stub { + onBlocking { fromShortName(any()) } doReturn emoji + } + stubGetConfig(Either.Right(config)) stubObserveEvents(params = InterceptEvents.Params(context = null)) stubOpenDashboard() @@ -388,7 +444,11 @@ class HomeDashboardViewModelTest { coroutineTestRule.advanceTime(delayInMillis) - val views = dashboard.toView() + val views = runBlocking { + dashboard.toView( + emojifier = emojifier + ) + } val from = 0 val to = 1 diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt index f914d05852..e3bc1e0d96 100644 --- a/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt @@ -10,6 +10,7 @@ import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.domain.config.Config import com.agileburo.anytype.domain.download.DownloadFile +import com.agileburo.anytype.domain.emoji.Emojifier import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.event.model.Event import com.agileburo.anytype.domain.ext.content @@ -96,6 +97,9 @@ class PageViewModelTest { @Mock lateinit var downloadFile: DownloadFile + @Mock + lateinit var emojifier: Emojifier + private lateinit var vm: PageViewModel @Before @@ -2840,7 +2844,8 @@ class PageViewModelTest { splitBlock = splitBlock, documentExternalEventReducer = DocumentExternalEventReducer(), urlBuilder = urlBuilder, - downloadFile = downloadFile + downloadFile = downloadFile, + emojifier = emojifier ) } } \ No newline at end of file diff --git a/sample/src/androidTest/java/com/agileburo/anytype/sample/ExampleInstrumentedTest.kt b/sample/src/androidTest/java/com/agileburo/anytype/sample/ExampleInstrumentedTest.kt deleted file mode 100644 index b54bfc5b5a..0000000000 --- a/sample/src/androidTest/java/com/agileburo/anytype/sample/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.agileburo.anytype.sample - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.agileburo.anytype.sample", appContext.packageName) - } -} diff --git a/sample/src/main/java/com/agileburo/anytype/sample/PageIconPickerSampleActivity.kt b/sample/src/main/java/com/agileburo/anytype/sample/PageIconPickerSampleActivity.kt deleted file mode 100644 index 5f44e2402c..0000000000 --- a/sample/src/main/java/com/agileburo/anytype/sample/PageIconPickerSampleActivity.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.agileburo.anytype.sample - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.GridLayoutManager -import com.agileburo.anytype.library_page_icon_picker_widget.model.PageIconPickerView -import com.agileburo.anytype.library_page_icon_picker_widget.ui.PageIconPickerAdapter -import com.agileburo.anytype.library_page_icon_picker_widget.ui.PageIconPickerViewHolder -import com.vdurmont.emoji.EmojiManager -import kotlinx.android.synthetic.main.sample_page_icon_picker_activity.* - -class PageIconPickerSampleActivity : AppCompatActivity(R.layout.sample_page_icon_picker_activity) { - - private val emojis by lazy { - EmojiManager - .getAll() - .map { emoji -> - PageIconPickerView.Emoji( - unicode = emoji.unicode - ) - } - - } - - private val pageIconPickerAdapter = PageIconPickerAdapter( - views = listOf( - PageIconPickerView.Action.UploadPhoto, - PageIconPickerView.Action.PickRandomly, - PageIconPickerView.Action.ChooseEmoji, - PageIconPickerView.EmojiFilter - ) + emojis, - onUploadPhotoClicked = {}, - onSetRandomEmojiClicked = {}, - onFilterQueryChanged = {} - ) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - recyler.apply { - setItemViewCacheSize(100) - setHasFixedSize(true) - layoutManager = GridLayoutManager(context, PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT).apply { - spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int) = - when (val type = pageIconPickerAdapter.getItemViewType(position)) { - PageIconPickerViewHolder.HOLDER_UPLOAD_PHOTO -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT - PageIconPickerViewHolder.HOLDER_CHOOSE_EMOJI -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT - PageIconPickerViewHolder.HOLDER_PICK_RANDOM_EMOJI -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT - PageIconPickerViewHolder.HOLDER_EMOJI_CATEGORY_HEADER -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT - PageIconPickerViewHolder.HOLDER_EMOJI_FILTER -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT - PageIconPickerViewHolder.HOLDER_EMOJI_ITEM -> 1 - else -> throw IllegalStateException("Unexpected view type: $type") - } - } - } - adapter = pageIconPickerAdapter.apply { - setHasStableIds(true) - } - } - } - - companion object { - const val PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT = 8 - } -} \ No newline at end of file diff --git a/sample/src/main/res/layout/sample_page_icon_picker_activity.xml b/sample/src/main/res/layout/sample_page_icon_picker_activity.xml deleted file mode 100644 index 05898b8cef..0000000000 --- a/sample/src/main/res/layout/sample_page_icon_picker_activity.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sample/src/test/java/com/agileburo/anytype/sample/ExampleUnitTest.kt b/sample/src/test/java/com/agileburo/anytype/sample/ExampleUnitTest.kt deleted file mode 100644 index df82d91f98..0000000000 --- a/sample/src/test/java/com/agileburo/anytype/sample/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.agileburo.anytype.sample - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/settings.gradle b/settings.gradle index e470ebf2c9..5b6f97ac18 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -include ':app', +include ':app', ':library-emojifier', ':sample', ':core-utils', ':middleware',