forked from github/wulkanowy-mirror
Message fuzzy search (#869)
This commit is contained in:
parent
924bcb0d64
commit
6e1ddb482e
@ -142,7 +142,7 @@ dependencies {
|
||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
|
||||
implementation "com.google.android.material:material:1.1.0"
|
||||
implementation "com.github.wulkanowy:material-chips-input:2.0.1"
|
||||
implementation "com.github.wulkanowy:material-chips-input:2.1.1"
|
||||
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
|
||||
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
|
||||
|
||||
@ -180,6 +180,7 @@ dependencies {
|
||||
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
|
||||
implementation "io.coil-kt:coil:0.11.0"
|
||||
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
|
||||
|
||||
playImplementation 'com.google.firebase:firebase-analytics:17.4.3'
|
||||
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.7'
|
||||
|
@ -4,10 +4,9 @@ import android.graphics.Typeface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.repositories.message.MessageFolder
|
||||
@ -20,39 +19,23 @@ class MessageTabAdapter @Inject constructor() :
|
||||
|
||||
var onClickListener: (Message, position: Int) -> Unit = { _, _ -> }
|
||||
|
||||
private val items = SortedList(Message::class.java, object :
|
||||
SortedListAdapterCallback<Message>(this) {
|
||||
private var items = mutableListOf<Message>()
|
||||
|
||||
override fun compare(item1: Message, item2: Message): Int {
|
||||
return item2.date.compareTo(item1.date)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Message?, newItem: Message?): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: Message, item2: Message): Boolean {
|
||||
return item1 == item2
|
||||
}
|
||||
})
|
||||
|
||||
fun replaceAll(models: List<Message>) {
|
||||
items.beginBatchedUpdates()
|
||||
for (i in items.size() - 1 downTo 0) {
|
||||
val model = items.get(i)
|
||||
if (model !in models) {
|
||||
items.remove(model)
|
||||
}
|
||||
}
|
||||
items.addAll(models)
|
||||
items.endBatchedUpdates()
|
||||
fun setDataItems(data: List<Message>) {
|
||||
val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data))
|
||||
items = data.toMutableList()
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
fun updateItem(position: Int, item: Message) {
|
||||
items.updateItemAt(position, item)
|
||||
val currentItem = items[position]
|
||||
items[position] = item
|
||||
if (item != currentItem) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size()
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
|
||||
ItemMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
@ -85,4 +68,19 @@ class MessageTabAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
private class MessageTabDiffUtil(private val old: List<Message>, private val new: List<Message>) :
|
||||
DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int = old.size
|
||||
|
||||
override fun getNewListSize(): Int = new.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return old[oldItemPosition].id == new[newItemPosition].id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return old[oldItemPosition] == new[newItemPosition]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
|
||||
}
|
||||
|
||||
override fun updateData(data: List<Message>) {
|
||||
tabAdapter.replaceAll(data)
|
||||
tabAdapter.setDataItems(data)
|
||||
}
|
||||
|
||||
override fun updateItem(item: Message, position: Int) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package io.github.wulkanowy.ui.modules.message.tab
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.repositories.message.MessageFolder
|
||||
import io.github.wulkanowy.data.repositories.message.MessageRepository
|
||||
@ -11,8 +10,13 @@ import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
|
||||
import io.github.wulkanowy.utils.SchedulersProvider
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.pow
|
||||
|
||||
class MessageTabPresenter @Inject constructor(
|
||||
schedulers: SchedulersProvider,
|
||||
@ -31,9 +35,12 @@ class MessageTabPresenter @Inject constructor(
|
||||
|
||||
private var messages = emptyList<Message>()
|
||||
|
||||
private val searchQuery = PublishSubject.create<String>()
|
||||
|
||||
fun onAttachView(view: MessageTabView, folder: MessageFolder) {
|
||||
super.onAttachView(view)
|
||||
view.initView()
|
||||
initializeSearchStream()
|
||||
errorHandler.showErrorMessage = ::showErrorViewOnError
|
||||
this.folder = folder
|
||||
}
|
||||
@ -76,38 +83,35 @@ class MessageTabPresenter @Inject constructor(
|
||||
|
||||
private fun loadData(forceRefresh: Boolean) {
|
||||
Timber.i("Loading $folder message data started")
|
||||
disposable.apply {
|
||||
clear()
|
||||
add(studentRepository.getCurrentStudent()
|
||||
.flatMap { student ->
|
||||
semesterRepository.getCurrentSemester(student)
|
||||
.flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) }
|
||||
disposable.add(studentRepository.getCurrentStudent()
|
||||
.flatMap { student ->
|
||||
semesterRepository.getCurrentSemester(student)
|
||||
.flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) }
|
||||
}
|
||||
.subscribeOn(schedulers.backgroundThread)
|
||||
.observeOn(schedulers.mainThread)
|
||||
.doFinally {
|
||||
view?.run {
|
||||
showRefresh(false)
|
||||
showProgress(false)
|
||||
enableSwipe(true)
|
||||
notifyParentDataLoaded()
|
||||
}
|
||||
.subscribeOn(schedulers.backgroundThread)
|
||||
.observeOn(schedulers.mainThread)
|
||||
.doFinally {
|
||||
view?.run {
|
||||
showRefresh(false)
|
||||
showProgress(false)
|
||||
enableSwipe(true)
|
||||
notifyParentDataLoaded()
|
||||
}
|
||||
}
|
||||
.subscribe({
|
||||
Timber.i("Loading $folder message result: Success")
|
||||
messages = it
|
||||
onSearchQueryTextChange(lastSearchQuery)
|
||||
analytics.logEvent(
|
||||
"load_data",
|
||||
"type" to "messages",
|
||||
"items" to it.size,
|
||||
"folder" to folder.name
|
||||
)
|
||||
}) {
|
||||
Timber.i("Loading $folder message result: An exception occurred")
|
||||
errorHandler.dispatch(it)
|
||||
})
|
||||
}
|
||||
}
|
||||
.subscribe({
|
||||
Timber.i("Loading $folder message result: Success")
|
||||
messages = it
|
||||
view?.updateData(getFilteredData(lastSearchQuery))
|
||||
analytics.logEvent(
|
||||
"load_data",
|
||||
"type" to "messages",
|
||||
"items" to it.size,
|
||||
"folder" to folder.name
|
||||
)
|
||||
}) {
|
||||
Timber.i("Loading $folder message result: An exception occurred")
|
||||
errorHandler.dispatch(it)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showErrorViewOnError(message: String, error: Throwable) {
|
||||
@ -121,25 +125,36 @@ class MessageTabPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun onSearchQueryTextChange(query: String) {
|
||||
lastSearchQuery = query
|
||||
if (query != searchQuery.toString())
|
||||
searchQuery.onNext(query)
|
||||
}
|
||||
|
||||
val lowerCaseQuery = query.toLowerCase()
|
||||
val filteredList = mutableListOf<Message>()
|
||||
messages.forEach {
|
||||
if (lowerCaseQuery in it.subject.toLowerCase() ||
|
||||
lowerCaseQuery in it.sender.toLowerCase() ||
|
||||
lowerCaseQuery in it.recipient.toLowerCase() ||
|
||||
lowerCaseQuery in it.date.toFormattedString()
|
||||
) {
|
||||
filteredList.add(it)
|
||||
private fun initializeSearchStream() {
|
||||
disposable.add(searchQuery
|
||||
.debounce(250, TimeUnit.MILLISECONDS)
|
||||
.map { query ->
|
||||
lastSearchQuery = query
|
||||
getFilteredData(query)
|
||||
}
|
||||
.subscribeOn(schedulers.backgroundThread)
|
||||
.observeOn(schedulers.mainThread)
|
||||
.subscribe({
|
||||
Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${it.size}")
|
||||
updateData(it)
|
||||
}) { Timber.e(it) })
|
||||
}
|
||||
|
||||
private fun getFilteredData(query: String): List<Message> {
|
||||
return if (query.trim().isEmpty()) {
|
||||
messages.sortedByDescending { it.date }
|
||||
} else {
|
||||
messages
|
||||
.map { it to calculateMatchRatio(it, query) }
|
||||
.sortedByDescending { it.second }
|
||||
.filter { it.second > 5000 }
|
||||
.map { it.first }
|
||||
}
|
||||
|
||||
Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${filteredList.size}")
|
||||
|
||||
updateData(filteredList)
|
||||
}
|
||||
|
||||
private fun updateData(data: List<Message>) {
|
||||
@ -151,4 +166,42 @@ class MessageTabPresenter @Inject constructor(
|
||||
resetListPosition()
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateMatchRatio(message: Message, query: String): Int {
|
||||
val subjectRatio = FuzzySearch.tokenSortPartialRatio(
|
||||
query.toLowerCase(Locale.getDefault()),
|
||||
message.subject
|
||||
)
|
||||
|
||||
val senderOrRecipientRatio = FuzzySearch.tokenSortPartialRatio(
|
||||
query.toLowerCase(Locale.getDefault()),
|
||||
if (message.sender.isNotEmpty()) message.sender.toLowerCase(Locale.getDefault())
|
||||
else message.recipient.toLowerCase(Locale.getDefault())
|
||||
)
|
||||
|
||||
val dateRatio = listOf(
|
||||
FuzzySearch.ratio(
|
||||
query.toLowerCase(Locale.getDefault()),
|
||||
message.date.toFormattedString("dd.MM").toLowerCase(Locale.getDefault())
|
||||
),
|
||||
FuzzySearch.ratio(
|
||||
query.toLowerCase(Locale.getDefault()),
|
||||
message.date.toFormattedString("dd.MM.yyyy").toLowerCase(Locale.getDefault())
|
||||
),
|
||||
FuzzySearch.ratio(
|
||||
query.toLowerCase(Locale.getDefault()),
|
||||
message.date.toFormattedString("d MMMM").toLowerCase(Locale.getDefault())
|
||||
),
|
||||
FuzzySearch.ratio(
|
||||
query.toLowerCase(Locale.getDefault()),
|
||||
message.date.toFormattedString("d MMMM yyyy").toLowerCase(Locale.getDefault())
|
||||
)
|
||||
).max() ?: 0
|
||||
|
||||
|
||||
return (subjectRatio.toDouble().pow(2)
|
||||
+ senderOrRecipientRatio.toDouble().pow(2)
|
||||
+ dateRatio.toDouble().pow(2) * 2
|
||||
).toInt()
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user