mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
Feature/emoji picker search (#549)
This commit is contained in:
parent
bce7fbbfd5
commit
458668d0d7
36 changed files with 32969 additions and 16181 deletions
|
@ -151,7 +151,6 @@ open class EditorTestSetup {
|
|||
documentEventReducer = DocumentExternalEventReducer(),
|
||||
archiveDocument = archiveDocument,
|
||||
createDocument = createDocument,
|
||||
uploadUrl = uploadBlock,
|
||||
urlBuilder = urlBuilder,
|
||||
renderer = DefaultBlockViewRenderer(
|
||||
urlBuilder = urlBuilder,
|
||||
|
@ -187,7 +186,8 @@ open class EditorTestSetup {
|
|||
proxies = proxies,
|
||||
stores = stores,
|
||||
matcher = DefaultPatternMatcher()
|
||||
)
|
||||
),
|
||||
uploadBlock = uploadBlock
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,13 +4,11 @@ import com.agileburo.anytype.presentation.page.PageViewModelFactory
|
|||
import com.agileburo.anytype.ui.page.PageFragment
|
||||
|
||||
class TestPageFragment : PageFragment() {
|
||||
|
||||
init {
|
||||
this.factory =
|
||||
testViewModelFactory
|
||||
factory = testViewModelFactory
|
||||
}
|
||||
|
||||
override fun injectDependencies() {}
|
||||
override fun releaseDependencies() {}
|
||||
|
||||
companion object {
|
||||
lateinit var testViewModelFactory: PageViewModelFactory
|
||||
|
|
|
@ -0,0 +1,333 @@
|
|||
package com.agileburo.anytype.features.emoji
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.testing.FragmentScenario
|
||||
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.*
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.domain.block.repo.BlockRepository
|
||||
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
|
||||
import com.agileburo.anytype.emojifier.data.Emoji
|
||||
import com.agileburo.anytype.emojifier.data.EmojiProvider
|
||||
import com.agileburo.anytype.emojifier.suggest.EmojiSuggester
|
||||
import com.agileburo.anytype.emojifier.suggest.model.EmojiModel
|
||||
import com.agileburo.anytype.mocking.MockDataFactory
|
||||
import com.agileburo.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModelFactory
|
||||
import com.agileburo.anytype.utils.TestUtils.withRecyclerView
|
||||
import com.nhaarman.mockitokotlin2.*
|
||||
import kotlinx.android.synthetic.main.fragment_page_icon_picker.*
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class DocumentEmojiPickerFragmentTest {
|
||||
|
||||
@Mock
|
||||
lateinit var suggester: EmojiSuggester
|
||||
|
||||
@Mock
|
||||
lateinit var provider: EmojiProvider
|
||||
|
||||
@Mock
|
||||
lateinit var repo: BlockRepository
|
||||
|
||||
private lateinit var setEmojiIcon: SetDocumentEmojiIcon
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
setEmojiIcon = SetDocumentEmojiIcon(repo = repo)
|
||||
TestDocumentEmojiPickerFragment.testViewModelFactory =
|
||||
DocumentEmojiIconPickerViewModelFactory(
|
||||
emojiProvider = provider,
|
||||
emojiSuggester = suggester,
|
||||
setEmojiIcon = setEmojiIcon
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldHaveNothingVisible() {
|
||||
|
||||
// SETUP
|
||||
|
||||
provider.stub {
|
||||
on { emojis } doReturn emptyArray()
|
||||
}
|
||||
|
||||
launchFragment(bundleOf())
|
||||
|
||||
// TESTING
|
||||
|
||||
onView(withId(R.id.clearSearchText)).apply {
|
||||
check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
onView(withId(R.id.progressBar)).apply {
|
||||
check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
onView(withId(R.id.pickerRecycler)).apply {
|
||||
check(matches(isDisplayed()))
|
||||
check(matches(hasChildCount(0)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldHaveHeaderAndSixVisibleEmojis() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val emojis = arrayOf(
|
||||
arrayOf(
|
||||
"😀",
|
||||
"😃",
|
||||
"😄",
|
||||
"😁",
|
||||
"😆",
|
||||
"😅"
|
||||
)
|
||||
)
|
||||
|
||||
provider.stub {
|
||||
on { provider.emojis } doReturn emojis
|
||||
}
|
||||
|
||||
launchFragment(bundleOf())
|
||||
|
||||
// TESTING
|
||||
|
||||
onView(withId(R.id.clearSearchText)).apply {
|
||||
check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
onView(withId(R.id.progressBar)).apply {
|
||||
check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
onView(withId(R.id.pickerRecycler)).apply {
|
||||
check(matches(isDisplayed()))
|
||||
check(matches(hasChildCount(emojis.first().size.inc())))
|
||||
}
|
||||
|
||||
onView(withRecyclerView(R.id.pickerRecycler).atPositionOnView(0, R.id.category)).apply {
|
||||
check(matches(withText(R.string.category_smileys_and_people)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldFindOnlyOneDogEmoji() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val query = "dog"
|
||||
|
||||
val dog = "🐶"
|
||||
|
||||
val emojis = arrayOf(
|
||||
Emoji.DATA[0].take(6).toTypedArray(),
|
||||
Emoji.DATA[1].take(6).toTypedArray(),
|
||||
Emoji.DATA[2].take(6).toTypedArray(),
|
||||
Emoji.DATA[3].take(6).toTypedArray(),
|
||||
Emoji.DATA[4].take(6).toTypedArray(),
|
||||
Emoji.DATA[5].take(6).toTypedArray(),
|
||||
Emoji.DATA[6].take(6).toTypedArray(),
|
||||
Emoji.DATA[7].take(6).toTypedArray()
|
||||
)
|
||||
|
||||
provider.stub {
|
||||
on { provider.emojis } doReturn emojis
|
||||
}
|
||||
|
||||
suggester.stub {
|
||||
onBlocking { search(query) } doReturn listOf(
|
||||
EmojiModel(
|
||||
category = MockDataFactory.randomString(),
|
||||
name = MockDataFactory.randomString(),
|
||||
emoji = dog
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
launchFragment(bundleOf())
|
||||
|
||||
// TESTING
|
||||
|
||||
onView(withId(R.id.filterInputField)).apply {
|
||||
perform(click())
|
||||
perform(typeText(query))
|
||||
}
|
||||
|
||||
// Verifying results
|
||||
|
||||
Thread.sleep(500)
|
||||
|
||||
onView(withId(R.id.pickerRecycler)).apply {
|
||||
check(matches(hasChildCount(1)))
|
||||
}
|
||||
|
||||
verifyBlocking(suggester, times(1)) { search(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldNotSearchIfTextIsCleared() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val query = "dog"
|
||||
|
||||
val dog = "🐶"
|
||||
|
||||
val emojis = arrayOf(
|
||||
Emoji.DATA[0].take(6).toTypedArray(),
|
||||
Emoji.DATA[1].take(6).toTypedArray(),
|
||||
Emoji.DATA[2].take(6).toTypedArray(),
|
||||
Emoji.DATA[3].take(6).toTypedArray(),
|
||||
Emoji.DATA[4].take(6).toTypedArray(),
|
||||
Emoji.DATA[5].take(6).toTypedArray(),
|
||||
Emoji.DATA[6].take(6).toTypedArray(),
|
||||
Emoji.DATA[7].take(6).toTypedArray()
|
||||
)
|
||||
|
||||
provider.stub {
|
||||
on { provider.emojis } doReturn emojis
|
||||
}
|
||||
|
||||
suggester.stub {
|
||||
onBlocking { search(query) } doReturn listOf(
|
||||
EmojiModel(
|
||||
category = MockDataFactory.randomString(),
|
||||
name = MockDataFactory.randomString(),
|
||||
emoji = dog
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val scenario = launchFragment(bundleOf())
|
||||
|
||||
// TESTING
|
||||
|
||||
onView(withId(R.id.filterInputField)).apply {
|
||||
perform(click())
|
||||
perform(typeText(query))
|
||||
}
|
||||
|
||||
Thread.sleep(500)
|
||||
|
||||
onView(withId(R.id.pickerRecycler)).apply {
|
||||
check(matches(hasChildCount(1)))
|
||||
}
|
||||
|
||||
// Clearing text after first query
|
||||
|
||||
onView(withId(R.id.clearSearchText)).apply {
|
||||
perform(click())
|
||||
perform(closeSoftKeyboard())
|
||||
}
|
||||
|
||||
// Verifying results
|
||||
|
||||
Thread.sleep(500)
|
||||
|
||||
scenario.onFragment { fragment ->
|
||||
val count = fragment.pickerRecycler.adapter?.itemCount
|
||||
assertEquals(
|
||||
expected = (6 * 8) + 8,
|
||||
actual = count
|
||||
)
|
||||
}
|
||||
|
||||
verifyBlocking(suggester, times(1)) { search(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldNotSearchIfTextIsDeletedByBackspace() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val query = "dog"
|
||||
|
||||
val dog = "🐶"
|
||||
|
||||
val emojis = arrayOf(
|
||||
Emoji.DATA[0].take(6).toTypedArray(),
|
||||
Emoji.DATA[1].take(6).toTypedArray(),
|
||||
Emoji.DATA[2].take(6).toTypedArray(),
|
||||
Emoji.DATA[3].take(6).toTypedArray(),
|
||||
Emoji.DATA[4].take(6).toTypedArray(),
|
||||
Emoji.DATA[5].take(6).toTypedArray(),
|
||||
Emoji.DATA[6].take(6).toTypedArray(),
|
||||
Emoji.DATA[7].take(6).toTypedArray()
|
||||
)
|
||||
|
||||
provider.stub {
|
||||
on { provider.emojis } doReturn emojis
|
||||
}
|
||||
|
||||
suggester.stub {
|
||||
onBlocking { search(query) } doReturn listOf(
|
||||
EmojiModel(
|
||||
category = MockDataFactory.randomString(),
|
||||
name = MockDataFactory.randomString(),
|
||||
emoji = dog
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val scenario = launchFragment(bundleOf())
|
||||
|
||||
// TESTING
|
||||
|
||||
onView(withId(R.id.filterInputField)).apply {
|
||||
perform(click())
|
||||
perform(typeText(query))
|
||||
}
|
||||
|
||||
Thread.sleep(500)
|
||||
|
||||
onView(withId(R.id.pickerRecycler)).apply {
|
||||
check(matches(hasChildCount(1)))
|
||||
}
|
||||
|
||||
// Removing text after first query
|
||||
|
||||
onView(withId(R.id.filterInputField)).apply {
|
||||
repeat(query.length) { perform(pressKey(KeyEvent.KEYCODE_DEL)) }
|
||||
perform(closeSoftKeyboard())
|
||||
}
|
||||
|
||||
// Verifying results
|
||||
|
||||
Thread.sleep(500)
|
||||
|
||||
scenario.onFragment { fragment ->
|
||||
val count = fragment.pickerRecycler.adapter?.itemCount
|
||||
assertEquals(
|
||||
expected = (6 * 8) + 8,
|
||||
actual = count
|
||||
)
|
||||
}
|
||||
|
||||
verifyBlocking(suggester, times(1)) { search(query) }
|
||||
verifyBlocking(suggester, times(1)) { search(any()) }
|
||||
}
|
||||
|
||||
private fun launchFragment(args: Bundle): FragmentScenario<TestDocumentEmojiPickerFragment> {
|
||||
return launchFragmentInContainer<TestDocumentEmojiPickerFragment>(
|
||||
fragmentArgs = args,
|
||||
themeResId = R.style.AppTheme
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.agileburo.anytype.features.emoji
|
||||
|
||||
import com.agileburo.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModelFactory
|
||||
import com.agileburo.anytype.ui.page.modals.DocumentEmojiIconPickerFragment
|
||||
|
||||
class TestDocumentEmojiPickerFragment : DocumentEmojiIconPickerFragment() {
|
||||
init {
|
||||
factory = testViewModelFactory
|
||||
}
|
||||
|
||||
override fun injectDependencies() {}
|
||||
override fun releaseDependencies() {}
|
||||
|
||||
companion object {
|
||||
lateinit var testViewModelFactory: DocumentEmojiIconPickerViewModelFactory
|
||||
}
|
||||
}
|
32178
app/src/main/assets/emoji.json
Normal file
32178
app/src/main/assets/emoji.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -3,6 +3,8 @@ 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.SetDocumentEmojiIcon
|
||||
import com.agileburo.anytype.emojifier.data.Emoji
|
||||
import com.agileburo.anytype.emojifier.suggest.EmojiSuggester
|
||||
import com.agileburo.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModelFactory
|
||||
import com.agileburo.anytype.ui.page.modals.DocumentEmojiIconPickerFragment
|
||||
import dagger.Module
|
||||
|
@ -28,9 +30,12 @@ class DocumentEmojiIconPickerModule {
|
|||
@Provides
|
||||
@PerScreen
|
||||
fun provideDocumentEmojiIconPickerViewModel(
|
||||
setEmojiIcon: SetDocumentEmojiIcon
|
||||
setEmojiIcon: SetDocumentEmojiIcon,
|
||||
emojiSuggester: EmojiSuggester
|
||||
): DocumentEmojiIconPickerViewModelFactory = DocumentEmojiIconPickerViewModelFactory(
|
||||
setEmojiIcon = setEmojiIcon
|
||||
setEmojiIcon = setEmojiIcon,
|
||||
emojiSuggester = emojiSuggester,
|
||||
emojiProvider = Emoji
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
package com.agileburo.anytype.di.main
|
||||
|
||||
import com.agileburo.anytype.domain.emoji.Emojifier
|
||||
import com.agileburo.anytype.emojifier.DefaultEmojifier
|
||||
|
||||
import android.content.Context
|
||||
import com.agileburo.anytype.emojifier.suggest.EmojiSuggester
|
||||
import com.agileburo.anytype.emojifier.suggest.data.DefaultEmojiSuggestStorage
|
||||
import com.agileburo.anytype.emojifier.suggest.data.DefaultEmojiSuggester
|
||||
import com.agileburo.anytype.emojifier.suggest.data.EmojiSuggestStorage
|
||||
import com.agileburo.anytype.emojifier.suggest.data.EmojiSuggesterCache
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
@ -11,5 +17,25 @@ class EmojiModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEmojifier(): Emojifier = DefaultEmojifier()
|
||||
fun provideEmojiSuggester(
|
||||
cache: EmojiSuggesterCache,
|
||||
storage: EmojiSuggestStorage
|
||||
): EmojiSuggester {
|
||||
return DefaultEmojiSuggester(
|
||||
cache = cache,
|
||||
storage = storage
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEmojiSuggesterCache(): EmojiSuggesterCache {
|
||||
return EmojiSuggesterCache.DefaultCache()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEmojiSuggestStorage(context: Context): EmojiSuggestStorage {
|
||||
return DefaultEmojiSuggestStorage(context, Gson())
|
||||
}
|
||||
}
|
|
@ -7,12 +7,14 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_utils.ext.toast
|
||||
import com.agileburo.anytype.core_utils.ext.invisible
|
||||
import com.agileburo.anytype.core_utils.ext.visible
|
||||
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.DocumentEmojiIconPickerAdapter
|
||||
|
@ -27,7 +29,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
|
||||
open class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
|
||||
|
||||
private val target: String
|
||||
get() = requireArguments()
|
||||
|
@ -51,7 +53,6 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
|
|||
private val emojiPickerAdapter by lazy {
|
||||
DocumentEmojiIconPickerAdapter(
|
||||
views = emptyList(),
|
||||
onFilterQueryChanged = { toast("not implemented yet") },
|
||||
onEmojiClicked = { unicode ->
|
||||
vm.onEmojiClicked(
|
||||
unicode = unicode,
|
||||
|
@ -82,10 +83,15 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupRecycler()
|
||||
clearSearchText.setOnClickListener {
|
||||
filterInputField.setText(EMPTY_FILTER_TEXT)
|
||||
clearSearchText.invisible()
|
||||
}
|
||||
filterInputField.doAfterTextChanged { vm.onQueryChanged(it.toString()) }
|
||||
}
|
||||
|
||||
private fun setupRecycler() {
|
||||
recyler.apply {
|
||||
pickerRecycler.apply {
|
||||
setItemViewCacheSize(EMOJI_RECYCLER_ITEM_VIEW_CACHE_SIZE)
|
||||
setHasFixedSize(true)
|
||||
layoutManager = GridLayoutManager(context, PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT).apply {
|
||||
|
@ -94,7 +100,6 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
|
|||
when (val type = emojiPickerAdapter.getItemViewType(position)) {
|
||||
DocumentEmojiIconPickerViewHolder.HOLDER_EMOJI_ITEM -> 1
|
||||
DocumentEmojiIconPickerViewHolder.HOLDER_EMOJI_CATEGORY_HEADER -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT
|
||||
DocumentEmojiIconPickerViewHolder.HOLDER_EMOJI_FILTER -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT
|
||||
else -> throw IllegalStateException("$UNEXPECTED_VIEW_TYPE_MESSAGE: $type")
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +112,18 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
|
|||
super.onActivityCreated(savedInstanceState)
|
||||
vm.state().onEach { state ->
|
||||
when (state) {
|
||||
is ViewState.Success -> emojiPickerAdapter.update(state.views)
|
||||
is ViewState.Success -> {
|
||||
if (filterInputField.text.isNotEmpty())
|
||||
clearSearchText.visible()
|
||||
else
|
||||
clearSearchText.invisible()
|
||||
emojiPickerAdapter.update(state.views)
|
||||
progressBar.invisible()
|
||||
}
|
||||
is ViewState.Loading -> {
|
||||
clearSearchText.invisible()
|
||||
progressBar.visible()
|
||||
}
|
||||
is ViewState.Exit -> dismiss()
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
|
@ -150,6 +166,7 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
|
|||
)
|
||||
}
|
||||
|
||||
private const val EMPTY_FILTER_TEXT = ""
|
||||
private const val PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT = 6
|
||||
private const val EMOJI_RECYCLER_ITEM_VIEW_CACHE_SIZE = 2000
|
||||
private const val ARG_CONTEXT_ID_KEY = "arg.picker.context.id"
|
||||
|
|
|
@ -23,8 +23,75 @@
|
|||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="@drawable/page_icon_picker_filter_background">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/searchIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/ic_page_icon_picker_search"
|
||||
android:contentDescription="@string/content_description_loop_icon"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/filterInputField"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@null"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:hint="@string/page_icon_picker_emoji_filter"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/clearSearchText"
|
||||
app:layout_constraintStart_toEndOf="@+id/searchIcon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:visibility="invisible"
|
||||
android:id="@+id/clearSearchText"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/ic_clear_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:theme="@style/GreyProgressBar"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyler"
|
||||
android:id="@+id/pickerRecycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="20dp"
|
||||
|
|
|
@ -25,6 +25,10 @@
|
|||
<item name="colorAccent">@color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="GreyProgressBar" parent="ThemeOverlay.AppCompat.Light">
|
||||
<item name="colorAccent">#ACA996</item>
|
||||
</style>
|
||||
|
||||
<style name="AppBottomSheetDialogTheme" parent="Theme.Design.Light.BottomSheetDialog">
|
||||
<item name="bottomSheetStyle">@style/AppModalStyle</item>
|
||||
</style>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
android:paddingEnd="20dp">
|
||||
|
||||
<FrameLayout
|
||||
android:background="@drawable/rectangle_default_page_logo_background"
|
||||
android:transitionName="@string/logo_transition"
|
||||
android:id="@+id/documentIconContainer"
|
||||
android:layout_marginTop="45dp"
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package com.agileburo.anytype.core_utils.ext
|
||||
|
||||
import android.content.Context
|
||||
import java.io.IOException
|
||||
|
||||
fun Context.getJsonDataFromAsset(fileName: String): String? = try {
|
||||
assets
|
||||
.open(fileName)
|
||||
.bufferedReader()
|
||||
.use { stream -> stream.readText() }
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package com.agileburo.anytype.domain.emoji
|
||||
|
||||
data class Emoji(
|
||||
val unicode: String,
|
||||
val alias: String
|
||||
) {
|
||||
val name: String
|
||||
get() = ":$alias:"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package com.agileburo.anytype.domain.emoji
|
||||
|
||||
interface Emojifier {
|
||||
suspend fun fromAlias(alias: String): Emoji
|
||||
suspend fun fromShortName(name: String): Emoji
|
||||
}
|
|
@ -40,9 +40,18 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':domain')
|
||||
|
||||
def appDependencies = rootProject.ext.mainApplication
|
||||
def unitTestDependencies = rootProject.ext.unitTesting
|
||||
def lib = rootProject.ext.libraryPageIconPicker
|
||||
|
||||
implementation project(':domain')
|
||||
implementation project(':core-utils')
|
||||
implementation(lib.emojiJava) {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
implementation appDependencies.gson
|
||||
implementation appDependencies.coroutines
|
||||
|
||||
testImplementation unitTestDependencies.junit
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
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(
|
||||
/**
|
||||
* Fix pirate flag emoji render, after fixing
|
||||
* in table https://github.com/vdurmont/emoji-java/blob/master/EMOJIS.md
|
||||
* can be removed
|
||||
*/
|
||||
unicode = result.unicode.filterTextByChar(
|
||||
value = '☠',
|
||||
filterBy = '♾'
|
||||
),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.filterTextByChar(value: Char, filterBy: Char): String =
|
||||
if (contains(value)) {
|
||||
filterNot { it == filterBy }
|
||||
} else {
|
||||
this
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package com.agileburo.anytype.emojifier
|
||||
|
||||
import com.agileburo.anytype.emojifier.data.Emoji
|
||||
|
||||
object Emojifier {
|
||||
|
||||
/**
|
||||
|
@ -36,10 +38,10 @@ object Emojifier {
|
|||
|
||||
var result: Pair<Int, Int>? = null
|
||||
|
||||
Emoji.data.forEachIndexed { idx, category ->
|
||||
val index = category.indexOfFirst { emoji -> emoji == unicode }
|
||||
if (index != -1) {
|
||||
val pair = Pair(idx, index)
|
||||
Emoji.DATA.forEachIndexed { categoryIndex, emojis ->
|
||||
val emojiIndex = emojis.indexOfFirst { emoji -> emoji == unicode }
|
||||
if (emojiIndex != -1) {
|
||||
val pair = Pair(categoryIndex, emojiIndex)
|
||||
result = pair
|
||||
cache[unicode] = pair
|
||||
return@forEachIndexed
|
||||
|
@ -47,4 +49,8 @@ object Emojifier {
|
|||
}
|
||||
return result ?: throw IllegalStateException("Result not found for: $unicode")
|
||||
}
|
||||
|
||||
object Config {
|
||||
const val EMOJI_FILE = "emoji.json"
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package com.agileburo.anytype.emojifier
|
||||
package com.agileburo.anytype.emojifier.data
|
||||
|
||||
object Emoji {
|
||||
object Emoji : EmojiProvider {
|
||||
|
||||
val colors = listOf(
|
||||
val COLORS = listOf(
|
||||
"🏻",
|
||||
"🏼",
|
||||
"🏽",
|
||||
|
@ -10,7 +10,7 @@ object Emoji {
|
|||
"🏿"
|
||||
)
|
||||
|
||||
val data: Array<Array<String>> = arrayOf(
|
||||
val DATA: Array<Array<String>> = arrayOf(
|
||||
arrayOf(
|
||||
"😀",
|
||||
"😃",
|
||||
|
@ -3153,6 +3153,8 @@ object Emoji {
|
|||
)
|
||||
)
|
||||
|
||||
override val emojis: Array<Array<String>> get() = DATA
|
||||
|
||||
const val CATEGORY_SMILEYS_AND_PEOPLE = 0
|
||||
const val CATEGORY_ANIMALS_AND_NATURE = 1
|
||||
const val CATEGORY_FOOD_AND_DRINK = 2
|
|
@ -0,0 +1,5 @@
|
|||
package com.agileburo.anytype.emojifier.data
|
||||
|
||||
interface EmojiProvider {
|
||||
val emojis: Array<Array<String>>
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.agileburo.anytype.emojifier.suggest
|
||||
|
||||
import com.agileburo.anytype.emojifier.suggest.model.EmojiSuggest
|
||||
|
||||
interface EmojiSuggester {
|
||||
/**
|
||||
* @return a complete list of emoji suggests.
|
||||
*/
|
||||
suspend fun fetch(): List<EmojiSuggest>
|
||||
|
||||
/**
|
||||
* @param query user-generated query (for searching emojis)
|
||||
* @return a list of emoji suggests corresponding to this [query]
|
||||
*/
|
||||
suspend fun search(query: String): List<EmojiSuggest>
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.agileburo.anytype.emojifier.suggest.data
|
||||
|
||||
import android.content.Context
|
||||
import com.agileburo.anytype.core_utils.ext.getJsonDataFromAsset
|
||||
import com.agileburo.anytype.emojifier.Emojifier.Config.EMOJI_FILE
|
||||
import com.agileburo.anytype.emojifier.suggest.model.EmojiModel
|
||||
import com.agileburo.anytype.emojifier.suggest.model.EmojiSuggest
|
||||
import com.google.gson.Gson
|
||||
|
||||
class DefaultEmojiSuggestStorage(
|
||||
private val context: Context,
|
||||
private val gson: Gson
|
||||
) : EmojiSuggestStorage {
|
||||
|
||||
override suspend fun fetch(): List<EmojiSuggest> {
|
||||
val json = context.getJsonDataFromAsset(EMOJI_FILE)
|
||||
return if (json != null) {
|
||||
gson.fromJson(json, Array<EmojiModel>::class.java).toList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.agileburo.anytype.emojifier.suggest.data
|
||||
|
||||
import com.agileburo.anytype.emojifier.suggest.EmojiSuggester
|
||||
import com.agileburo.anytype.emojifier.suggest.model.EmojiSuggest
|
||||
|
||||
class DefaultEmojiSuggester(
|
||||
private val cache: EmojiSuggesterCache,
|
||||
private val storage: EmojiSuggestStorage
|
||||
) : EmojiSuggester {
|
||||
|
||||
override suspend fun fetch(): List<EmojiSuggest> {
|
||||
return cache.getIfPresent() ?: storage.fetch().also { cache.put(it) }
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<EmojiSuggest> {
|
||||
return fetch().filter { value ->
|
||||
value.name.contains(query, true) || value.category.contains(query, true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.agileburo.anytype.emojifier.suggest.data
|
||||
|
||||
import com.agileburo.anytype.emojifier.suggest.model.EmojiSuggest
|
||||
|
||||
interface EmojiSuggestStorage {
|
||||
suspend fun fetch(): List<EmojiSuggest>
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.agileburo.anytype.emojifier.suggest.data
|
||||
|
||||
import com.agileburo.anytype.emojifier.suggest.model.EmojiSuggest
|
||||
|
||||
interface EmojiSuggesterCache {
|
||||
suspend fun getIfPresent(): List<EmojiSuggest>?
|
||||
suspend fun put(suggests: List<EmojiSuggest>)
|
||||
|
||||
class DefaultCache : EmojiSuggesterCache {
|
||||
|
||||
var list: List<EmojiSuggest>? = null
|
||||
|
||||
override suspend fun getIfPresent(): List<EmojiSuggest>? {
|
||||
return list
|
||||
}
|
||||
|
||||
override suspend fun put(suggests: List<EmojiSuggest>) {
|
||||
list = suggests
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.agileburo.anytype.emojifier.suggest.model
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class EmojiModel(
|
||||
@SerializedName("category")
|
||||
override val category: String,
|
||||
@SerializedName("char")
|
||||
override val emoji: String,
|
||||
@SerializedName("name")
|
||||
override val name: String
|
||||
) : EmojiSuggest
|
|
@ -0,0 +1,12 @@
|
|||
package com.agileburo.anytype.emojifier.suggest.model
|
||||
|
||||
/**
|
||||
* @property emoji emoji char
|
||||
* @property name short name for this [emoji]
|
||||
* @property category category for this [emoji]
|
||||
*/
|
||||
interface EmojiSuggest {
|
||||
val emoji: String
|
||||
val name: String
|
||||
val category: String
|
||||
}
|
|
@ -2,12 +2,14 @@ package com.agileburo.anytype.library_page_icon_picker_widget.model
|
|||
|
||||
import com.agileburo.anytype.core_ui.common.ViewType
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_CATEGORY_HEADER
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_FILTER
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_ITEM
|
||||
|
||||
sealed class EmojiPickerView : ViewType {
|
||||
|
||||
/**
|
||||
* @property alias short name or convenient name for an emoji.
|
||||
* @property page emoji's page (emoji category)
|
||||
* @property index emoji's index on the [page]
|
||||
* @property unicode emoji's char
|
||||
*/
|
||||
data class Emoji(
|
||||
val unicode: String,
|
||||
|
@ -25,11 +27,4 @@ sealed class EmojiPickerView : ViewType {
|
|||
) : EmojiPickerView() {
|
||||
override fun getViewType() = HOLDER_EMOJI_CATEGORY_HEADER
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji filter.
|
||||
*/
|
||||
object EmojiFilter : EmojiPickerView() {
|
||||
override fun getViewType() = HOLDER_EMOJI_FILTER
|
||||
}
|
||||
}
|
|
@ -2,20 +2,14 @@ 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.library_page_icon_picker_widget.R
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.model.EmojiPickerView
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.model.PageIconPickerViewDiffUtil
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_CATEGORY_HEADER
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_FILTER
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_ITEM
|
||||
import kotlinx.android.synthetic.main.item_page_icon_picker_emoji_filter.view.*
|
||||
|
||||
class DocumentEmojiIconPickerAdapter(
|
||||
private var views: List<EmojiPickerView>,
|
||||
private val onFilterQueryChanged: (String) -> Unit,
|
||||
private val onEmojiClicked: (String) -> Unit
|
||||
) : RecyclerView.Adapter<DocumentEmojiIconPickerViewHolder>() {
|
||||
|
||||
|
@ -40,17 +34,6 @@ class DocumentEmojiIconPickerAdapter(
|
|||
false
|
||||
)
|
||||
)
|
||||
HOLDER_EMOJI_FILTER -> DocumentEmojiIconPickerViewHolder.EmojiFilter(
|
||||
view = LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.item_page_icon_picker_emoji_filter,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
itemView.filterInputField.doOnTextChanged { text, _, _, _ ->
|
||||
onFilterQueryChanged(text.toString())
|
||||
}
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected view type: $viewType")
|
||||
}
|
||||
}
|
||||
|
@ -73,13 +56,7 @@ class DocumentEmojiIconPickerAdapter(
|
|||
}
|
||||
|
||||
fun update(update: List<EmojiPickerView>) {
|
||||
val result = DiffUtil.calculateDiff(
|
||||
PageIconPickerViewDiffUtil(
|
||||
old = views,
|
||||
new = update
|
||||
)
|
||||
)
|
||||
views = update
|
||||
result.dispatchUpdatesTo(this)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -2,14 +2,15 @@ package com.agileburo.anytype.library_page_icon_picker_widget.ui
|
|||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.agileburo.anytype.emojifier.Emoji
|
||||
import com.agileburo.anytype.emojifier.Emojifier
|
||||
import com.agileburo.anytype.emojifier.data.Emoji
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.R
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.model.EmojiPickerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import kotlinx.android.synthetic.main.item_page_icon_picker_emoji_category_header.view.*
|
||||
import kotlinx.android.synthetic.main.item_page_icon_picker_emoji_item.view.*
|
||||
import timber.log.Timber
|
||||
|
||||
sealed class DocumentEmojiIconPickerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
|
@ -27,9 +28,9 @@ sealed class DocumentEmojiIconPickerViewHolder(view: View) : RecyclerView.ViewHo
|
|||
Emoji.CATEGORY_OBJECTS -> category.setText(R.string.category_objects)
|
||||
Emoji.CATEGORY_SYMBOLS -> category.setText(R.string.category_symbols)
|
||||
Emoji.CATEGORY_FLAGS -> category.setText(R.string.category_flags)
|
||||
else -> Timber.d("Unexpected category: ${item.category}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EmojiItem(view: View) : DocumentEmojiIconPickerViewHolder(view) {
|
||||
|
@ -50,11 +51,8 @@ sealed class DocumentEmojiIconPickerViewHolder(view: View) : RecyclerView.ViewHo
|
|||
}
|
||||
}
|
||||
|
||||
class EmojiFilter(view: View) : DocumentEmojiIconPickerViewHolder(view)
|
||||
|
||||
companion object {
|
||||
const val HOLDER_EMOJI_CATEGORY_HEADER = 1
|
||||
const val HOLDER_EMOJI_ITEM = 2
|
||||
const val HOLDER_EMOJI_FILTER = 3
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M22,12C22,17.5228 17.5228,22 12,22C6.4771,22 2,17.5228 2,12C2,6.4771 6.4771,2 12,2C17.5228,2 22,6.4771 22,12ZM16.5656,7.4343C16.8781,7.7467 16.8781,8.2532 16.5656,8.5656L13.1314,11.9999L16.5657,15.4343C16.8782,15.7467 16.8782,16.2532 16.5657,16.5656C16.2533,16.8781 15.7468,16.8781 15.4344,16.5656L12,13.1313L8.5656,16.5656C8.2532,16.8781 7.7467,16.8781 7.4343,16.5656C7.1219,16.2532 7.1219,15.7467 7.4343,15.4343L10.8686,11.9999L7.4344,8.5656C7.1219,8.2532 7.1219,7.7467 7.4344,7.4343C7.7468,7.1219 8.2533,7.1219 8.5657,7.4343L12,10.8685L15.4343,7.4343C15.7467,7.1219 16.2532,7.1219 16.5656,7.4343Z" />
|
||||
</vector>
|
|
@ -1,34 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/page_icon_picker_filter_background"
|
||||
android:orientation="horizontal">
|
||||
android:background="@drawable/page_icon_picker_filter_background">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/searchIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/ic_page_icon_picker_search"
|
||||
android:contentDescription="@string/content_description_loop_icon" />
|
||||
android:contentDescription="@string/content_description_loop_icon"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:id="@+id/filterInputField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="9dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="9dp"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@null"
|
||||
android:fontFamily="@font/graphik_regular"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:hint="@string/page_icon_picker_emoji_filter"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:textSize="15sp" />
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/clearSearchText"
|
||||
app:layout_constraintStart_toEndOf="@+id/searchIcon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</LinearLayout>
|
||||
<ImageView
|
||||
android:id="@+id/clearSearchText"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/ic_clear_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -7,29 +7,6 @@ import kotlin.test.assertEquals
|
|||
|
||||
class DocumentEmojiIconPickerViewDiffUtilTest {
|
||||
|
||||
@Test
|
||||
fun `two emoji-filter items should be considered the same`() {
|
||||
val old = listOf(
|
||||
EmojiPickerView.EmojiFilter
|
||||
)
|
||||
|
||||
val new = listOf(
|
||||
EmojiPickerView.EmojiFilter
|
||||
)
|
||||
|
||||
val util = PageIconPickerViewDiffUtil(
|
||||
old = old,
|
||||
new = new
|
||||
)
|
||||
|
||||
val result = util.areItemsTheSame(0, 0)
|
||||
|
||||
assertEquals(
|
||||
expected = true,
|
||||
actual = result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two emoji items should be considered the same`() {
|
||||
|
||||
|
|
|
@ -4,61 +4,118 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
|
||||
import com.agileburo.anytype.emojifier.Emoji
|
||||
import com.agileburo.anytype.emojifier.data.Emoji
|
||||
import com.agileburo.anytype.emojifier.data.EmojiProvider
|
||||
import com.agileburo.anytype.emojifier.suggest.EmojiSuggester
|
||||
import com.agileburo.anytype.emojifier.suggest.model.EmojiSuggest
|
||||
import com.agileburo.anytype.library_page_icon_picker_widget.model.EmojiPickerView
|
||||
import com.agileburo.anytype.presentation.page.editor.Proxy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class DocumentEmojiIconPickerViewModel(
|
||||
private val setEmojiIcon: SetDocumentEmojiIcon
|
||||
private val setEmojiIcon: SetDocumentEmojiIcon,
|
||||
private val provider: EmojiProvider,
|
||||
private val suggester: EmojiSuggester
|
||||
) : ViewModel() {
|
||||
|
||||
/**
|
||||
* Default emoji list, including categories.
|
||||
*/
|
||||
private val default = MutableStateFlow<List<EmojiPickerView>>(emptyList())
|
||||
|
||||
/**
|
||||
* UI state stream.
|
||||
*/
|
||||
private val state: MutableStateFlow<ViewState> = MutableStateFlow(ViewState.Init)
|
||||
|
||||
/**
|
||||
* Stream of user-generated queries.
|
||||
*/
|
||||
private val queries = Proxy.Subject<String>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
state.value = ViewState.Loading
|
||||
state.value = ViewState.Success(views = load())
|
||||
val loaded = loadEmojiWithCategories()
|
||||
default.value = loaded
|
||||
state.value = ViewState.Success(views = default.value)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
queries
|
||||
.stream()
|
||||
.debounce(DEBOUNCE_DURATION)
|
||||
.distinctUntilChanged()
|
||||
.onEach { state.value = ViewState.Loading }
|
||||
.mapLatest { query ->
|
||||
if (query.isEmpty())
|
||||
default.value
|
||||
else
|
||||
select(suggester.search(query))
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.collect { state.value = ViewState.Success(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun state(): StateFlow<ViewState> = state
|
||||
/**
|
||||
* Maps found search suggests to emoji data, then adapts the latter to UI.
|
||||
*/
|
||||
private fun select(suggests: List<EmojiSuggest>): MutableList<EmojiPickerView.Emoji> {
|
||||
val result = mutableListOf<EmojiPickerView.Emoji>()
|
||||
suggests.forEach { suggest ->
|
||||
provider.emojis.forEachIndexed loop@{ categoryIndex, emojis ->
|
||||
emojis.forEachIndexed { emojiIndex, emoji ->
|
||||
if (emoji == suggest.emoji) {
|
||||
val skin = Emoji.COLORS.any { color -> emoji.contains(color) }
|
||||
if (!skin) {
|
||||
result.add(
|
||||
EmojiPickerView.Emoji(
|
||||
unicode = emoji,
|
||||
index = emojiIndex,
|
||||
page = categoryIndex
|
||||
)
|
||||
)
|
||||
return@loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun load(): List<EmojiPickerView> = withContext(Dispatchers.IO) {
|
||||
private suspend fun loadEmojiWithCategories() = withContext(Dispatchers.IO) {
|
||||
|
||||
val views = mutableListOf<EmojiPickerView>()
|
||||
|
||||
views.add(EmojiPickerView.EmojiFilter)
|
||||
|
||||
Emoji.data.forEachIndexed { category, emojis ->
|
||||
|
||||
provider.emojis.forEachIndexed { categoryIndex, emojis ->
|
||||
views.add(
|
||||
EmojiPickerView.GroupHeader(
|
||||
category = category
|
||||
category = categoryIndex
|
||||
)
|
||||
)
|
||||
|
||||
emojis.forEachIndexed { index, unicode ->
|
||||
val skin = Emoji.colors.any { color -> unicode.contains(color) }
|
||||
if (!skin) {
|
||||
emojis.forEachIndexed { emojiIndex, emoji ->
|
||||
val skin = Emoji.COLORS.any { color -> emoji.contains(color) }
|
||||
if (!skin)
|
||||
views.add(
|
||||
EmojiPickerView.Emoji(
|
||||
unicode = unicode,
|
||||
page = category,
|
||||
index = index
|
||||
unicode = emoji,
|
||||
page = categoryIndex,
|
||||
index = emojiIndex
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
views
|
||||
}
|
||||
|
||||
fun state(): StateFlow<ViewState> = state
|
||||
|
||||
fun onEmojiClicked(unicode: String, target: Id, context: Id) {
|
||||
viewModelScope.launch {
|
||||
setEmojiIcon(
|
||||
|
@ -74,10 +131,18 @@ class DocumentEmojiIconPickerViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onQueryChanged(query: String) {
|
||||
viewModelScope.launch { queries.send(query) }
|
||||
}
|
||||
|
||||
sealed class ViewState {
|
||||
object Init : ViewState()
|
||||
object Loading : ViewState()
|
||||
data class Success(val views: List<EmojiPickerView>) : ViewState()
|
||||
object Exit : ViewState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEBOUNCE_DURATION = 300L
|
||||
}
|
||||
}
|
|
@ -3,15 +3,21 @@ package com.agileburo.anytype.presentation.page.picker
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
|
||||
import com.agileburo.anytype.emojifier.data.EmojiProvider
|
||||
import com.agileburo.anytype.emojifier.suggest.EmojiSuggester
|
||||
|
||||
class DocumentEmojiIconPickerViewModelFactory(
|
||||
private val setEmojiIcon: SetDocumentEmojiIcon
|
||||
private val setEmojiIcon: SetDocumentEmojiIcon,
|
||||
private val emojiSuggester: EmojiSuggester,
|
||||
private val emojiProvider: EmojiProvider
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return DocumentEmojiIconPickerViewModel(
|
||||
setEmojiIcon = setEmojiIcon
|
||||
setEmojiIcon = setEmojiIcon,
|
||||
suggester = emojiSuggester,
|
||||
provider = emojiProvider
|
||||
) as T
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
|
||||
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
|
||||
import com.agileburo.anytype.domain.icon.SetDocumentImageIcon
|
||||
import com.agileburo.anytype.emojifier.Emoji
|
||||
import com.agileburo.anytype.emojifier.data.Emoji
|
||||
import com.agileburo.anytype.presentation.common.StateReducer
|
||||
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModel.Contract.*
|
||||
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModel.ViewState
|
||||
|
@ -71,7 +71,7 @@ class DocumentIconActionMenuViewModel(
|
|||
success = { events.send(Event.OnCompleted) }
|
||||
)
|
||||
is Action.PickRandomEmoji -> {
|
||||
val random = Emoji.data.random().random()
|
||||
val random = Emoji.DATA.random().random()
|
||||
events.send(
|
||||
Event.OnRandomEmojiSelected(
|
||||
target = action.target,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue