mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 02:15:46 +03:00
RoomDetail: lazy load EmojiDataSource data (+ async)
This commit is contained in:
parent
ebd5095662
commit
fc5c6b9b00
11 changed files with 194 additions and 105 deletions
|
@ -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 {
|
||||||
assertEquals("Should have 8 quick reactions", 8, emojiDataSource.getQuickReactions().size)
|
emojiDataSource.getQuickReactions()
|
||||||
}
|
}
|
||||||
|
assertEquals("Should have 8 quick reactions", 8, result.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEmojiDataSource() = EmojiDataSource(coroutineScope, context().resources)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -53,4 +62,5 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context,
|
||||||
}
|
}
|
||||||
controller.setData(data)
|
controller.setData(data)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue