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.constraintlayout:constraintlayout:1.1.3"
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
|
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
|
||||||
implementation "com.google.android.material:material: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 "com.github.PhilJay:MPAndroidChart:v3.1.0"
|
||||||
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
|
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
|
||||||
|
|
||||||
@ -180,6 +180,7 @@ dependencies {
|
|||||||
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
|
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
|
||||||
implementation "io.coil-kt:coil:0.11.0"
|
implementation "io.coil-kt:coil:0.11.0"
|
||||||
implementation "io.github.wulkanowy:AppKillerManager:3.0.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-analytics:17.4.3'
|
||||||
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.7'
|
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.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
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.R
|
||||||
import io.github.wulkanowy.data.db.entities.Message
|
import io.github.wulkanowy.data.db.entities.Message
|
||||||
import io.github.wulkanowy.data.repositories.message.MessageFolder
|
import io.github.wulkanowy.data.repositories.message.MessageFolder
|
||||||
@ -20,39 +19,23 @@ class MessageTabAdapter @Inject constructor() :
|
|||||||
|
|
||||||
var onClickListener: (Message, position: Int) -> Unit = { _, _ -> }
|
var onClickListener: (Message, position: Int) -> Unit = { _, _ -> }
|
||||||
|
|
||||||
private val items = SortedList(Message::class.java, object :
|
private var items = mutableListOf<Message>()
|
||||||
SortedListAdapterCallback<Message>(this) {
|
|
||||||
|
|
||||||
override fun compare(item1: Message, item2: Message): Int {
|
fun setDataItems(data: List<Message>) {
|
||||||
return item2.date.compareTo(item1.date)
|
val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data))
|
||||||
}
|
items = data.toMutableList()
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
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 updateItem(position: Int, item: Message) {
|
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(
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
|
||||||
ItemMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
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)
|
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>) {
|
override fun updateData(data: List<Message>) {
|
||||||
tabAdapter.replaceAll(data)
|
tabAdapter.setDataItems(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateItem(item: Message, position: Int) {
|
override fun updateItem(item: Message, position: Int) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package io.github.wulkanowy.ui.modules.message.tab
|
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.db.entities.Message
|
||||||
import io.github.wulkanowy.data.repositories.message.MessageFolder
|
import io.github.wulkanowy.data.repositories.message.MessageFolder
|
||||||
import io.github.wulkanowy.data.repositories.message.MessageRepository
|
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.FirebaseAnalyticsHelper
|
||||||
import io.github.wulkanowy.utils.SchedulersProvider
|
import io.github.wulkanowy.utils.SchedulersProvider
|
||||||
import io.github.wulkanowy.utils.toFormattedString
|
import io.github.wulkanowy.utils.toFormattedString
|
||||||
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
class MessageTabPresenter @Inject constructor(
|
class MessageTabPresenter @Inject constructor(
|
||||||
schedulers: SchedulersProvider,
|
schedulers: SchedulersProvider,
|
||||||
@ -31,9 +35,12 @@ class MessageTabPresenter @Inject constructor(
|
|||||||
|
|
||||||
private var messages = emptyList<Message>()
|
private var messages = emptyList<Message>()
|
||||||
|
|
||||||
|
private val searchQuery = PublishSubject.create<String>()
|
||||||
|
|
||||||
fun onAttachView(view: MessageTabView, folder: MessageFolder) {
|
fun onAttachView(view: MessageTabView, folder: MessageFolder) {
|
||||||
super.onAttachView(view)
|
super.onAttachView(view)
|
||||||
view.initView()
|
view.initView()
|
||||||
|
initializeSearchStream()
|
||||||
errorHandler.showErrorMessage = ::showErrorViewOnError
|
errorHandler.showErrorMessage = ::showErrorViewOnError
|
||||||
this.folder = folder
|
this.folder = folder
|
||||||
}
|
}
|
||||||
@ -76,38 +83,35 @@ class MessageTabPresenter @Inject constructor(
|
|||||||
|
|
||||||
private fun loadData(forceRefresh: Boolean) {
|
private fun loadData(forceRefresh: Boolean) {
|
||||||
Timber.i("Loading $folder message data started")
|
Timber.i("Loading $folder message data started")
|
||||||
disposable.apply {
|
disposable.add(studentRepository.getCurrentStudent()
|
||||||
clear()
|
.flatMap { student ->
|
||||||
add(studentRepository.getCurrentStudent()
|
semesterRepository.getCurrentSemester(student)
|
||||||
.flatMap { student ->
|
.flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) }
|
||||||
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)
|
.subscribe({
|
||||||
.doFinally {
|
Timber.i("Loading $folder message result: Success")
|
||||||
view?.run {
|
messages = it
|
||||||
showRefresh(false)
|
view?.updateData(getFilteredData(lastSearchQuery))
|
||||||
showProgress(false)
|
analytics.logEvent(
|
||||||
enableSwipe(true)
|
"load_data",
|
||||||
notifyParentDataLoaded()
|
"type" to "messages",
|
||||||
}
|
"items" to it.size,
|
||||||
}
|
"folder" to folder.name
|
||||||
.subscribe({
|
)
|
||||||
Timber.i("Loading $folder message result: Success")
|
}) {
|
||||||
messages = it
|
Timber.i("Loading $folder message result: An exception occurred")
|
||||||
onSearchQueryTextChange(lastSearchQuery)
|
errorHandler.dispatch(it)
|
||||||
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) {
|
private fun showErrorViewOnError(message: String, error: Throwable) {
|
||||||
@ -121,25 +125,36 @@ class MessageTabPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
fun onSearchQueryTextChange(query: String) {
|
fun onSearchQueryTextChange(query: String) {
|
||||||
lastSearchQuery = query
|
if (query != searchQuery.toString())
|
||||||
|
searchQuery.onNext(query)
|
||||||
|
}
|
||||||
|
|
||||||
val lowerCaseQuery = query.toLowerCase()
|
private fun initializeSearchStream() {
|
||||||
val filteredList = mutableListOf<Message>()
|
disposable.add(searchQuery
|
||||||
messages.forEach {
|
.debounce(250, TimeUnit.MILLISECONDS)
|
||||||
if (lowerCaseQuery in it.subject.toLowerCase() ||
|
.map { query ->
|
||||||
lowerCaseQuery in it.sender.toLowerCase() ||
|
lastSearchQuery = query
|
||||||
lowerCaseQuery in it.recipient.toLowerCase() ||
|
getFilteredData(query)
|
||||||
lowerCaseQuery in it.date.toFormattedString()
|
|
||||||
) {
|
|
||||||
filteredList.add(it)
|
|
||||||
}
|
}
|
||||||
|
.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>) {
|
private fun updateData(data: List<Message>) {
|
||||||
@ -151,4 +166,42 @@ class MessageTabPresenter @Inject constructor(
|
|||||||
resetListPosition()
|
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