RoomDetail: lazy load EmojiDataSource data (+ async)

This commit is contained in:
ganfra 2021-09-23 13:13:16 +02:00
parent ebd5095662
commit fc5c6b9b00
11 changed files with 194 additions and 105 deletions

View file

@ -17,6 +17,10 @@
package im.vector.app.features.reactions.data package im.vector.app.features.reactions.data
import im.vector.app.InstrumentedTest import im.vector.app.InstrumentedTest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
@ -30,64 +34,80 @@ import kotlin.system.measureTimeMillis
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
class EmojiDataSourceTest : InstrumentedTest { class EmojiDataSourceTest : InstrumentedTest {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
@Test @Test
fun checkParsingTime() { fun checkParsingTime() {
val time = measureTimeMillis { val time = measureTimeMillis {
EmojiDataSource(context().resources) createEmojiDataSource()
} }
assertTrue("Too long to parse", time < 100) assertTrue("Too long to parse", time < 100)
} }
@Test @Test
fun checkNumberOfResult() { fun checkNumberOfResult() {
val emojiDataSource = EmojiDataSource(context().resources) val emojiDataSource = createEmojiDataSource()
assertTrue("Wrong number of emojis", emojiDataSource.rawData.emojis.size >= 500) val rawData = runBlocking {
assertTrue("Wrong number of categories", emojiDataSource.rawData.categories.size >= 8) emojiDataSource.rawData.await()
}
assertTrue("Wrong number of emojis", rawData.emojis.size >= 500)
assertTrue("Wrong number of categories", rawData.categories.size >= 8)
} }
@Test @Test
fun searchTestEmptySearch() { fun searchTestEmptySearch() {
val emojiDataSource = EmojiDataSource(context().resources) val emojiDataSource = createEmojiDataSource()
val result = runBlocking {
assertTrue("Empty search should return at least 500 results", emojiDataSource.filterWith("").size >= 500) emojiDataSource.filterWith("")
}
assertTrue("Empty search should return at least 500 results", result.size >= 500)
} }
@Test @Test
fun searchTestNoResult() { fun searchTestNoResult() {
val emojiDataSource = EmojiDataSource(context().resources) val emojiDataSource = createEmojiDataSource()
val result = runBlocking {
assertTrue("Should not have result", emojiDataSource.filterWith("noresult").isEmpty()) emojiDataSource.filterWith("noresult")
}
assertTrue("Should not have result", result.isEmpty())
} }
@Test @Test
fun searchTestOneResult() { fun searchTestOneResult() {
val emojiDataSource = EmojiDataSource(context().resources) val emojiDataSource = createEmojiDataSource()
val result = runBlocking {
assertEquals("Should have 1 result", 1, emojiDataSource.filterWith("france").size) emojiDataSource.filterWith("france")
}
assertEquals("Should have 1 result", 1, result.size)
} }
@Test @Test
fun searchTestManyResult() { fun searchTestManyResult() {
val emojiDataSource = EmojiDataSource(context().resources) val emojiDataSource = createEmojiDataSource()
val result = runBlocking {
assertTrue("Should have many result", emojiDataSource.filterWith("fra").size > 1) emojiDataSource.filterWith("fra")
}
assertTrue("Should have many result", result.size > 1)
} }
@Test @Test
fun testTada() { fun testTada() {
val emojiDataSource = EmojiDataSource(context().resources) val emojiDataSource = createEmojiDataSource()
val result = runBlocking {
val result = emojiDataSource.filterWith("tada") emojiDataSource.filterWith("tada")
}
assertEquals("Should find tada emoji", 1, result.size) assertEquals("Should find tada emoji", 1, result.size)
assertEquals("Should find tada emoji", "🎉", result[0].emoji) assertEquals("Should find tada emoji", "🎉", result[0].emoji)
} }
@Test @Test
fun testQuickReactions() { fun testQuickReactions() {
val emojiDataSource = EmojiDataSource(context().resources) val emojiDataSource = createEmojiDataSource()
val result = runBlocking {
emojiDataSource.getQuickReactions()
}
assertEquals("Should have 8 quick reactions", 8, result.size)
}
assertEquals("Should have 8 quick reactions", 8, emojiDataSource.getQuickReactions().size) private fun createEmojiDataSource() = EmojiDataSource(coroutineScope, context().resources)
}
} }

View file

@ -98,6 +98,7 @@ import im.vector.app.features.usercode.UserCodeActivity
import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetActivity
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment
import kotlinx.coroutines.CoroutineScope
@Component( @Component(
dependencies = [ dependencies = [
@ -129,6 +130,7 @@ interface ScreenComponent {
fun uiStateRepository(): UiStateRepository fun uiStateRepository(): UiStateRepository
fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog
fun autoAcceptInvites(): AutoAcceptInvites fun autoAcceptInvites(): AutoAcceptInvites
fun appCoroutineScope(): CoroutineScope
/* ========================================================================================== /* ==========================================================================================
* Activities * Activities

View file

@ -60,6 +60,7 @@ import im.vector.app.features.reactions.data.EmojiDataSource
import im.vector.app.features.session.SessionListener import im.vector.app.features.session.SessionListener
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.HomeServerHistoryService
@ -165,6 +166,8 @@ interface VectorComponent {
fun webRtcCallManager(): WebRtcCallManager fun webRtcCallManager(): WebRtcCallManager
fun appCoroutineScope(): CoroutineScope
fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder
@Component.Factory @Component.Factory

View file

@ -33,12 +33,18 @@ import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.SharedPrefPinCodeStore import im.vector.app.features.pin.SharedPrefPinCodeStore
import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.SharedPreferencesUiStateRepository
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import javax.inject.Singleton
@Module @Module
abstract class VectorModule { abstract class VectorModule {
@ -94,6 +100,14 @@ abstract class VectorModule {
fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService {
return matrix.homeServerHistoryService() return matrix.homeServerHistoryService()
} }
@Provides
@JvmStatic
@Singleton
fun providesApplicationCoroutineScope(): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.Main)
}
} }
@Binds @Binds

View file

@ -21,6 +21,11 @@ import androidx.recyclerview.widget.RecyclerView
import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.RecyclerViewPresenter import im.vector.app.features.autocomplete.RecyclerViewPresenter
import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiDataSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class AutocompleteEmojiPresenter @Inject constructor(context: Context, class AutocompleteEmojiPresenter @Inject constructor(context: Context,
@ -28,11 +33,14 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context,
private val controller: AutocompleteEmojiController) : private val controller: AutocompleteEmojiController) :
RecyclerViewPresenter<String>(context), AutocompleteClickListener<String> { RecyclerViewPresenter<String>(context), AutocompleteClickListener<String> {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
init { init {
controller.listener = this controller.listener = this
} }
fun clear() { fun clear() {
coroutineScope.coroutineContext.cancelChildren()
controller.listener = null controller.listener = null
} }
@ -45,6 +53,7 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context,
} }
override fun onQuery(query: CharSequence?) { override fun onQuery(query: CharSequence?) {
coroutineScope.launch {
val data = if (query.isNullOrBlank()) { val data = if (query.isNullOrBlank()) {
// Return common emojis // Return common emojis
emojiDataSource.getQuickReactions() emojiDataSource.getQuickReactions()
@ -54,3 +63,4 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context,
controller.setData(data) controller.setData(data)
} }
} }
}

View file

@ -41,15 +41,15 @@ class EmojiChooserFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
emojiRecyclerAdapter.reactionClickListener = this emojiRecyclerAdapter.reactionClickListener = this
emojiRecyclerAdapter.interactionListener = this emojiRecyclerAdapter.interactionListener = this
views.emojiRecyclerView.adapter = emojiRecyclerAdapter views.emojiRecyclerView.adapter = emojiRecyclerAdapter
viewModel.moveToSection.observe(viewLifecycleOwner) { section -> viewModel.moveToSection.observe(viewLifecycleOwner) { section ->
emojiRecyclerAdapter.scrollToSection(section) emojiRecyclerAdapter.scrollToSection(section)
} }
viewModel.emojiData.observe(viewLifecycleOwner) {
emojiRecyclerAdapter.update(it)
}
} }
override fun getCoroutineScope() = lifecycleScope override fun getCoroutineScope() = lifecycleScope

View file

@ -17,11 +17,16 @@ package im.vector.app.features.reactions
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import im.vector.app.core.utils.LiveEvent import im.vector.app.core.utils.LiveEvent
import im.vector.app.features.reactions.data.EmojiData
import im.vector.app.features.reactions.data.EmojiDataSource
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class EmojiChooserViewModel @Inject constructor() : ViewModel() { class EmojiChooserViewModel @Inject constructor(private val emojiDataSource: EmojiDataSource) : ViewModel() {
val emojiData: MutableLiveData<EmojiData> = MutableLiveData()
val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData() val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var selectedReaction: String? = null var selectedReaction: String? = null
var eventId: String? = null var eventId: String? = null
@ -29,6 +34,17 @@ class EmojiChooserViewModel @Inject constructor() : ViewModel() {
val currentSection: MutableLiveData<Int> = MutableLiveData() val currentSection: MutableLiveData<Int> = MutableLiveData()
val moveToSection: MutableLiveData<Int> = MutableLiveData() val moveToSection: MutableLiveData<Int> = MutableLiveData()
init {
loadEmojiData()
}
private fun loadEmojiData() {
viewModelScope.launch {
val rawData = emojiDataSource.rawData.await()
emojiData.postValue(rawData)
}
}
fun onReactionSelected(reaction: String) { fun onReactionSelected(reaction: String) {
selectedReaction = reaction selectedReaction = reaction
navigateEvent.value = LiveEvent(NAVIGATE_FINISH) navigateEvent.value = LiveEvent(NAVIGATE_FINISH)

View file

@ -25,6 +25,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.SearchView import android.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxbinding3.widget.queryTextChanges import com.jakewharton.rxbinding3.widget.queryTextChanges
@ -36,6 +37,7 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityEmojiReactionPickerBinding import im.vector.app.databinding.ActivityEmojiReactionPickerBinding
import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiDataSource
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -91,18 +93,20 @@ class EmojiReactionPickerActivity : VectorBaseActivity<ActivityEmojiReactionPick
viewModel = viewModelProvider.get(EmojiChooserViewModel::class.java) viewModel = viewModelProvider.get(EmojiChooserViewModel::class.java)
viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID) viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID)
lifecycleScope.launch {
emojiDataSource.rawData.categories.forEach { category -> val rawData = emojiDataSource.rawData.await()
rawData.categories.forEach { category ->
val s = category.emojis[0] val s = category.emojis[0]
views.tabs.newTab() views.tabs.newTab()
.also { tab -> .also { tab ->
tab.text = emojiDataSource.rawData.emojis[s]!!.emoji tab.text = rawData.emojis[s]!!.emoji
tab.contentDescription = category.name tab.contentDescription = category.name
} }
.also { tab -> .also { tab ->
views.tabs.addTab(tab) views.tabs.addTab(tab)
} }
} }
}
views.tabs.addOnTabSelectedListener(tabLayoutSelectionListener) views.tabs.addOnTabSelectedListener(tabLayoutSelectionListener)
viewModel.currentSection.observe(this) { section -> viewModel.currentSection.observe(this) { section ->

View file

@ -15,6 +15,7 @@
*/ */
package im.vector.app.features.reactions package im.vector.app.features.reactions
import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.os.Trace import android.os.Trace
import android.text.Layout import android.text.Layout
@ -30,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.reactions.data.EmojiData
import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiDataSource
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -43,13 +45,13 @@ import kotlin.math.abs
* TODO: Performances * TODO: Performances
* TODO: Scroll to section - Find a way to snap section to the top * TODO: Scroll to section - Find a way to snap section to the top
*/ */
class EmojiRecyclerAdapter @Inject constructor( class EmojiRecyclerAdapter @Inject constructor() :
private val dataSource: EmojiDataSource
) :
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {
var reactionClickListener: ReactionClickListener? = null var reactionClickListener: ReactionClickListener? = null
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
private var rawData: EmojiData = EmojiData(emptyList(), emptyMap(), emptyMap())
private var mRecyclerView: RecyclerView? = null private var mRecyclerView: RecyclerView? = null
private var currentFirstVisibleSection = 0 private var currentFirstVisibleSection = 0
@ -61,6 +63,12 @@ class EmojiRecyclerAdapter @Inject constructor(
UNKNOWN UNKNOWN
} }
@SuppressLint("NotifyDataSetChanged")
fun update(emojiData: EmojiData){
rawData = emojiData
notifyDataSetChanged()
}
private var scrollState = ScrollState.UNKNOWN private var scrollState = ScrollState.UNKNOWN
private var isFastScroll = false private var isFastScroll = false
@ -71,10 +79,10 @@ class EmojiRecyclerAdapter @Inject constructor(
if (itemPosition != RecyclerView.NO_POSITION) { if (itemPosition != RecyclerView.NO_POSITION) {
val sectionNumber = getSectionForAbsoluteIndex(itemPosition) val sectionNumber = getSectionForAbsoluteIndex(itemPosition)
if (!isSection(itemPosition)) { if (!isSection(itemPosition)) {
val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis val sectionMojis = rawData.categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber) val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[itemPosition - sectionOffset] val emoji = sectionMojis[itemPosition - sectionOffset]
val item = dataSource.rawData.emojis.getValue(emoji).emoji val item = rawData.emojis.getValue(emoji).emoji
reactionClickListener?.onReactionSelected(item) reactionClickListener?.onReactionSelected(item)
} }
} }
@ -115,7 +123,7 @@ class EmojiRecyclerAdapter @Inject constructor(
} }
fun scrollToSection(section: Int) { fun scrollToSection(section: Int) {
if (section < 0 || section >= dataSource.rawData.categories.size) { if (section < 0 || section >= rawData.categories.size) {
// ignore // ignore
return return
} }
@ -149,7 +157,7 @@ class EmojiRecyclerAdapter @Inject constructor(
private fun isSection(position: Int): Boolean { private fun isSection(position: Int): Boolean {
var sectionOffset = 1 var sectionOffset = 1
var lastItemInSection: Int var lastItemInSection: Int
dataSource.rawData.categories.forEach { category -> rawData.categories.forEach { category ->
lastItemInSection = sectionOffset + category.emojis.size - 1 lastItemInSection = sectionOffset + category.emojis.size - 1
if (position == sectionOffset - 1) return true if (position == sectionOffset - 1) return true
sectionOffset = lastItemInSection + 2 sectionOffset = lastItemInSection + 2
@ -161,7 +169,7 @@ class EmojiRecyclerAdapter @Inject constructor(
var sectionOffset = 1 var sectionOffset = 1
var lastItemInSection: Int var lastItemInSection: Int
var index = 0 var index = 0
dataSource.rawData.categories.forEach { category -> rawData.categories.forEach { category ->
lastItemInSection = sectionOffset + category.emojis.size - 1 lastItemInSection = sectionOffset + category.emojis.size - 1
if (position <= lastItemInSection) return index if (position <= lastItemInSection) return index
sectionOffset = lastItemInSection + 2 sectionOffset = lastItemInSection + 2
@ -174,7 +182,7 @@ class EmojiRecyclerAdapter @Inject constructor(
// Todo cache this for fast access // Todo cache this for fast access
var sectionOffset = 1 var sectionOffset = 1
var lastItemInSection: Int var lastItemInSection: Int
dataSource.rawData.categories.forEachIndexed { index, category -> rawData.categories.forEachIndexed { index, category ->
lastItemInSection = sectionOffset + category.emojis.size - 1 lastItemInSection = sectionOffset + category.emojis.size - 1
if (section == index) return sectionOffset if (section == index) return sectionOffset
sectionOffset = lastItemInSection + 2 sectionOffset = lastItemInSection + 2
@ -186,12 +194,12 @@ class EmojiRecyclerAdapter @Inject constructor(
Trace.beginSection("MyAdapter.onBindViewHolder") Trace.beginSection("MyAdapter.onBindViewHolder")
val sectionNumber = getSectionForAbsoluteIndex(position) val sectionNumber = getSectionForAbsoluteIndex(position)
if (isSection(position)) { if (isSection(position)) {
holder.bind(dataSource.rawData.categories[sectionNumber].name) holder.bind(rawData.categories[sectionNumber].name)
} else { } else {
val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis val sectionMojis = rawData.categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber) val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[position - sectionOffset] val emoji = sectionMojis[position - sectionOffset]
val item = dataSource.rawData.emojis[emoji]!!.emoji val item = rawData.emojis[emoji]!!.emoji
(holder as EmojiViewHolder).data = item (holder as EmojiViewHolder).data = item
if (scrollState != ScrollState.SETTLING || !isFastScroll) { if (scrollState != ScrollState.SETTLING || !isFastScroll) {
// Log.i("PERF","Bind with draw at position:$position") // Log.i("PERF","Bind with draw at position:$position")
@ -220,7 +228,7 @@ class EmojiRecyclerAdapter @Inject constructor(
super.onViewRecycled(holder) super.onViewRecycled(holder)
} }
override fun getItemCount() = dataSource.rawData.categories override fun getItemCount() = rawData.categories
.sumOf { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size } .sumOf { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size }
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

View file

@ -15,17 +15,19 @@
*/ */
package im.vector.app.features.reactions package im.vector.app.features.reactions
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiDataSource
import im.vector.app.features.reactions.data.EmojiItem import im.vector.app.features.reactions.data.EmojiItem
import kotlinx.coroutines.launch
data class EmojiSearchResultViewState( data class EmojiSearchResultViewState(
val query: String = "", val query: String = "",
@ -58,11 +60,14 @@ class EmojiSearchResultViewModel @AssistedInject constructor(
} }
private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
viewModelScope.launch {
val results = dataSource.filterWith(action.queryString)
setState { setState {
copy( copy(
query = action.queryString, query = action.queryString,
results = dataSource.filterWith(action.queryString) results = results
) )
} }
} }
} }
}

View file

@ -20,15 +20,21 @@ import android.graphics.Paint
import androidx.core.graphics.PaintCompat import androidx.core.graphics.PaintCompat
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import im.vector.app.R import im.vector.app.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class EmojiDataSource @Inject constructor( class EmojiDataSource @Inject constructor(
appScope: CoroutineScope,
resources: Resources resources: Resources
) { ) {
private val paint = Paint() private val paint = Paint()
val rawData = resources.openRawResource(R.raw.emoji_picker_datasource) val rawData = appScope.async(Dispatchers.IO, CoroutineStart.LAZY) {
resources.openRawResource(R.raw.emoji_picker_datasource)
.use { input -> .use { input ->
Moshi.Builder() Moshi.Builder()
.build() .build()
@ -67,6 +73,7 @@ class EmojiDataSource @Inject constructor(
) )
} }
?: EmojiData(emptyList(), emptyMap(), emptyMap()) ?: EmojiData(emptyList(), emptyMap(), emptyMap())
}
private val quickReactions = mutableListOf<EmojiItem>() private val quickReactions = mutableListOf<EmojiItem>()
@ -74,9 +81,9 @@ class EmojiDataSource @Inject constructor(
return PaintCompat.hasGlyph(paint, emoji) return PaintCompat.hasGlyph(paint, emoji)
} }
fun filterWith(query: String): List<EmojiItem> { suspend fun filterWith(query: String): List<EmojiItem> {
val words = query.split("\\s".toRegex()) val words = query.split("\\s".toRegex())
val rawData = this.rawData.await()
// First add emojis with name matching query, sorted by name // First add emojis with name matching query, sorted by name
return (rawData.emojis.values return (rawData.emojis.values
.asSequence() .asSequence()
@ -87,9 +94,9 @@ class EmojiDataSource @Inject constructor(
// Then emojis with keyword matching any of the word in the query, sorted by name // Then emojis with keyword matching any of the word in the query, sorted by name
rawData.emojis.values rawData.emojis.values
.filter { emojiItem -> .filter { emojiItem ->
words.fold(true, { prev, word -> words.fold(true) { prev, word ->
prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
}) }
} }
.sortedBy { it.name }) .sortedBy { it.name })
// and ensure they will not be present twice // and ensure they will not be present twice
@ -97,7 +104,7 @@ class EmojiDataSource @Inject constructor(
.toList() .toList()
} }
fun getQuickReactions(): List<EmojiItem> { suspend fun getQuickReactions(): List<EmojiItem> {
if (quickReactions.isEmpty()) { if (quickReactions.isEmpty()) {
listOf( listOf(
"thumbs-up", // 👍 "thumbs-up", // 👍
@ -109,7 +116,7 @@ class EmojiDataSource @Inject constructor(
"rocket", // 🚀 "rocket", // 🚀
"eyes" // 👀 "eyes" // 👀
) )
.mapNotNullTo(quickReactions) { rawData.emojis[it] } .mapNotNullTo(quickReactions) { rawData.await().emojis[it] }
} }
return quickReactions return quickReactions