From c214b48409a788514a5cb4abf10ee90161aff50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 5 Apr 2020 20:06:35 +0200 Subject: [PATCH] [Messages] Add a search bar. Fix Grades not loading. --- .../pl/szczodrzynski/edziennik/Extensions.kt | 33 ++- .../edziennik/data/db/full/MessageFull.kt | 6 + .../ui/modules/grades/GradesAdapter.kt | 2 +- .../ui/modules/grades/GradesListFragment.kt | 2 +- .../grades/viewholder/BindableViewHolder.kt | 5 +- .../grades/viewholder/EmptyViewHolder.kt | 2 +- .../grades/viewholder/GradeViewHolder.kt | 2 +- .../grades/viewholder/SemesterViewHolder.kt | 2 +- .../grades/viewholder/StatsViewHolder.kt | 2 +- .../grades/viewholder/SubjectViewHolder.kt | 2 +- .../ui/modules/messages/MessageFragment.kt | 2 +- .../ui/modules/messages/MessagesAdapter.kt | 230 ++++++++++++++---- .../modules/messages/MessagesListFragment.kt | 7 +- .../MessagesComposeSuggestionAdapter.kt | 57 +++-- .../modules/messages/models/MessagesSearch.kt | 11 + .../messages/viewholder/MessageViewHolder.kt | 79 ++++++ .../messages/viewholder/SearchViewHolder.kt | 46 ++++ .../edziennik/utils/TextInputKeyboardEdit.kt | 6 +- .../res/layout/messages_compose_fragment.xml | 4 +- .../res/layout/messages_list_item_search.xml | 26 ++ app/src/main/res/values/strings.xml | 2 + 21 files changed, 435 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt create mode 100644 app/src/main/res/layout/messages_list_item_search.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index d911e132..0383be1e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -572,7 +572,7 @@ fun CharSequence?.asBoldSpannable(): Spannable { spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) return spannable } -fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false): Spannable { +fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false, ignoreDiacritics: Boolean = false): Spannable { val spannable = SpannableString(this) if (substring == null) { spans.forEach { @@ -580,17 +580,44 @@ fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignor } } else if (substring.isNotEmpty()) { - var index = indexOf(substring, ignoreCase = ignoreCase) + val string = + if (ignoreDiacritics) + this.cleanDiacritics() + else this + + var index = string.indexOf(substring, ignoreCase = ignoreCase) + .takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase) while (index >= 0) { spans.forEach { spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } - index = indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) + index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) + .takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) } } return spannable } +fun CharSequence.cleanDiacritics(): String { + val nameClean = StringBuilder() + forEach { + val ch = when (it) { + 'ż' -> 'z' + 'ó' -> 'o' + 'ł' -> 'l' + 'ć' -> 'c' + 'ę' -> 'e' + 'ś' -> 's' + 'ą' -> 'a' + 'ź' -> 'z' + 'ń' -> 'n' + else -> it + } + nameClean.append(ch) + } + return nameClean.toString() +} + /** * Returns a new read-only list only of those given elements, that are not empty. * Applies for CharSequence and descendants. diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt index 04f83595..5b873b7a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt @@ -3,6 +3,7 @@ */ package pl.szczodrzynski.edziennik.data.db.full +import androidx.room.Ignore import androidx.room.Relation import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient @@ -24,6 +25,11 @@ class MessageFull( return this } + @Ignore + var filterWeight = 0 + @Ignore + var searchHighlightText: String? = null + // metadata var seen = false var notified = false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt index 4b1c4f1e..137dc265 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt @@ -166,7 +166,7 @@ class GradesAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = items[position] - if (holder !is BindableViewHolder<*>) + if (holder !is BindableViewHolder<*, *>) return val viewType = when (holder) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt index 93773dd3..b80e5539 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt @@ -71,7 +71,7 @@ class GradesListFragment : Fragment(), CoroutineScope { val adapter = GradesAdapter(activity) var firstRun = true - app.db.gradeDao().getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()).observe(this@GradesListFragment, Observer { items -> launch { + app.db.gradeDao().getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()).observe(this@GradesListFragment, Observer { items -> this@GradesListFragment.launch { if (!isAdded) return@launch // load & configure the adapter diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt index 876d9055..6dd83384 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt @@ -6,8 +6,7 @@ package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder import androidx.appcompat.app.AppCompatActivity import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter -interface BindableViewHolder { - fun onBind(activity: AppCompatActivity, app: App, item: T, position: Int, adapter: GradesAdapter) +interface BindableViewHolder { + fun onBind(activity: AppCompatActivity, app: App, item: T, position: Int, adapter: A) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt index ea28847e..ec0eb8ba 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt @@ -17,7 +17,7 @@ class EmptyViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemEmptyBinding = GradesItemEmptyBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "EmptyViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt index 9927eabe..e71b7819 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt @@ -21,7 +21,7 @@ class GradeViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemGradeBinding = GradesItemGradeBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "GradeViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt index d10d671e..5686a37f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt @@ -21,7 +21,7 @@ class SemesterViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemSemesterBinding = GradesItemSemesterBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "SemesterViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt index 3e6a6be2..de1b071f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt @@ -24,7 +24,7 @@ class StatsViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemStatsBinding = GradesItemStatsBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "StatsViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt index e20a5177..a8ccabd2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt @@ -28,7 +28,7 @@ class SubjectViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemSubjectBinding = GradesItemSubjectBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "SubjectViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt index 35e19c67..b38fe5b6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt @@ -208,7 +208,7 @@ class MessageFragment : Fragment(), CoroutineScope { } private fun showMessage() { - b.body.text = MessagesUtils.htmlToSpannable(activity, message.body ?: "") + b.body.text = MessagesUtils.htmlToSpannable(activity, message.body.toString()) b.date.text = getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).formattedStringShort, Time.fromMillis(message.addedDate).stringHM) val messageInfo = MessagesUtils.getMessageInfo(app, message, 40, 20, 14, 10) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt index c003bb50..564a19d6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt @@ -1,31 +1,38 @@ package pl.szczodrzynski.edziennik.ui.modules.messages import android.graphics.Typeface +import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.Filter +import android.widget.Filterable import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.cleanDiacritics import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.MessageFull -import pl.szczodrzynski.edziennik.databinding.MessagesListItemBinding -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder +import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.SearchViewHolder +import java.util.* import kotlin.coroutines.CoroutineContext +import kotlin.math.min class MessagesAdapter( val activity: AppCompatActivity, val teachers: List, val onItemClick: ((item: MessageFull) -> Unit)? = null -) : RecyclerView.Adapter(), CoroutineScope { +) : RecyclerView.Adapter(), CoroutineScope, Filterable { companion object { - private const val TAG = "TemplateAdapter" + private const val TAG = "MessagesAdapter" + private const val ITEM_TYPE_MESSAGE = 0 + private const val ITEM_TYPE_SEARCH = 1 } private val app = activity.applicationContext as App @@ -35,55 +42,184 @@ class MessagesAdapter( override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main - var items = listOf() - private val typefaceNormal by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } - private val typefaceBold by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } + var items = mutableListOf() + var allItems = mutableListOf() + val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } + val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } + private val comparator by lazy { Comparator { o1: Any, o2: Any -> + if (o1 !is MessageFull || o2 !is MessageFull) + return@Comparator 0 + when { + // standard sorting + o1.filterWeight > o2.filterWeight -> return@Comparator 1 + o1.filterWeight < o2.filterWeight -> return@Comparator -1 + else -> when { + // reversed sorting + o1.addedDate > o2.addedDate -> return@Comparator -1 + o1.addedDate < o2.addedDate -> return@Comparator 1 + else -> return@Comparator 0 + } + } + }} - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val textWatcher by lazy { + object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + getFilter().filter(s.toString()) + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + /*items.getOrNull(0)?.let { + if (it is MessagesSearch) { + it.searchText = s?.toString() ?: "" + } + }*/ + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) - val view = MessagesListItemBinding.inflate(inflater, parent, false) - return ViewHolder(view) + return when (viewType) { + ITEM_TYPE_MESSAGE -> MessageViewHolder(inflater, parent) + ITEM_TYPE_SEARCH -> SearchViewHolder(inflater, parent) + else -> throw IllegalArgumentException("Incorrect viewType") + } + } + + override fun getItemViewType(position: Int): Int { + return when (items[position]) { + is MessageFull -> ITEM_TYPE_MESSAGE + is MessagesSearch -> ITEM_TYPE_SEARCH + else -> throw IllegalArgumentException("Incorrect viewType") + } } @Suppress("DEPRECATION") - override fun onBindViewHolder(holder: ViewHolder, position: Int) { + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = items[position] - val b = holder.b + if (holder !is BindableViewHolder<*, *>) + return - item.recipients?.forEach { recipient -> - if (recipient.fullName == null) { - recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: "" - } - } - - b.messageSubject.text = item.subject - b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort - b.messageAttachmentImage.isVisible = item.hasAttachments - - val text = item.body?.take(200) ?: "" - b.messageBody.text = MessagesUtils.htmlToSpannable(activity, text) - - val isRead = item.type == Message.TYPE_SENT || item.type == Message.TYPE_DRAFT || item.seen - val typeface = if (isRead) typefaceNormal else typefaceBold - val style = if (isRead) R.style.NavView_TextView_Small else R.style.NavView_TextView_Normal - // set text styles - b.messageSender.setTextAppearance(activity, style) - b.messageSender.typeface = typeface - b.messageSubject.setTextAppearance(activity, style) - b.messageSubject.typeface = typeface - b.messageDate.setTextAppearance(activity, style) - b.messageDate.typeface = typeface - - val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12) - b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) - b.messageSender.text = messageInfo.profileName - - onItemClick?.let { listener -> - b.root.onClick { listener(item) } + when { + holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this) + holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) } } override fun getItemCount() = items.size + override fun getFilter() = filter + private var prevCount = -1 + private val filter by lazy { object : Filter() { + override fun performFiltering(prefix: CharSequence?): FilterResults { + val results = FilterResults() - class ViewHolder(val b: MessagesListItemBinding) : RecyclerView.ViewHolder(b.root) + if (prevCount == -1) + prevCount = allItems.size + + if (prefix.isNullOrEmpty()) { + allItems.forEach { + if (it is MessageFull) + it.searchHighlightText = null + } + results.values = allItems.toList() + results.count = allItems.size + return results + } + + val items = mutableListOf() + val prefixString = prefix.toString() + + allItems.forEach { + if (it !is MessageFull) { + items.add(it) + return@forEach + } + it.filterWeight = 100 + it.searchHighlightText = null + + var weight = getMatchWeight(it.senderName, prefixString) + if (weight != 100) { + if (weight == 3) + weight = 31 + it.filterWeight = min(it.filterWeight, 10 + weight) + } + + weight = getMatchWeight(it.subject, prefixString) + if (weight != 100) { + if (weight == 3) + weight = 22 + it.filterWeight = min(it.filterWeight, 20 + weight) + } + + if (it.filterWeight != 100) { + it.searchHighlightText = prefixString + items.add(it) + } + } + + Collections.sort(items, comparator) + results.values = items + results.count = items.size + return results + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + results.values?.let { items = it as MutableList } + // do not re-bind the search box + val count = results.count - 1 + + // this tries to update every item except the search field + when { + count > prevCount -> { + notifyItemRangeInserted(prevCount + 1, count - prevCount) + notifyItemRangeChanged(1, prevCount) + } + count < prevCount -> { + notifyItemRangeRemoved(prevCount + 1, prevCount - count) + notifyItemRangeChanged(1, count) + } + else -> { + notifyItemRangeChanged(1, count) + } + } + + /*if (prevCount != count) { + items.getOrNull(0)?.let { + if (it is MessagesSearch) { + it.count = count + notifyItemChanged(0) + } + } + }*/ + + prevCount = count + } + }} + + private fun getMatchWeight(name: CharSequence?, prefix: String): Int { + if (name == null) + return 100 + + val nameClean = name.cleanDiacritics() + + // First match against the whole, non-split value + if (nameClean.startsWith(prefix, ignoreCase = true) || name.startsWith(prefix, ignoreCase = true)) { + return 1 + } else { + // check if prefix matches any of the words + val words = nameClean.split(" ").toTypedArray() + name.split(" ").toTypedArray() + for (word in words) { + if (word.startsWith(prefix, ignoreCase = true)) { + return 2 + } + } + } + // finally check if the prefix matches any part of the name + if (nameClean.contains(prefix, ignoreCase = true) || name.contains(prefix, ignoreCase = true)) { + return 3 + } + + return 100 + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt index de9a0c60..02831fda 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt @@ -20,6 +20,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.databinding.MessagesListFragmentBinding import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -68,7 +69,11 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { } // load & configure the adapter - adapter.items = items + adapter.items = items.toMutableList() + adapter.items.add(0, MessagesSearch().also { + it.count = items.size + }) + adapter.allItems = adapter.items.toMutableList() if (items.isNotNullNorEmpty() && b.list.adapter == null) { b.list.adapter = adapter b.list.apply { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt index 88600864..83a5ba39 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt @@ -13,6 +13,7 @@ import android.widget.ImageView import android.widget.TextView import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.asSpannable +import pl.szczodrzynski.edziennik.cleanDiacritics import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.resolveAttr import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage @@ -85,36 +86,15 @@ class MessagesComposeSuggestionAdapter( val list = mutableListOf() originalList.forEach { teacher -> - val teacherFullName = teacher.fullName - teacher.recipientWeight = 0 + teacher.recipientWeight = getMatchWeight(teacher.fullName, prefixString) - // First match against the whole, non-split value - var found = false - if (teacherFullName.startsWith(prefixString, ignoreCase = true)) { - teacher.recipientWeight = 1 - found = true - } else { - // check if prefix matches any of the words - val words = teacherFullName.split(" ").toTypedArray() - for (word in words) { - if (word.startsWith(prefixString, ignoreCase = true)) { - teacher.recipientWeight = 2 - found = true - break - } - } - } - // finally check if the prefix matches any part of the name - if (!found && teacherFullName.contains(prefixString, ignoreCase = true)) { - teacher.recipientWeight = 3 - } - - if (teacher.recipientWeight != 0) { - teacher.recipientDisplayName = teacherFullName.asSpannable( + if (teacher.recipientWeight != 100) { + teacher.recipientDisplayName = teacher.fullName.asSpannable( StyleSpan(BOLD), BackgroundColorSpan(R.attr.colorControlHighlight.resolveAttr(context)), substring = prefixString, - ignoreCase = true + ignoreCase = true, + ignoreDiacritics = true ) list += teacher } @@ -137,4 +117,29 @@ class MessagesComposeSuggestionAdapter( } } + private fun getMatchWeight(name: CharSequence?, prefix: String): Int { + if (name == null) + return 100 + + val nameClean = name.cleanDiacritics() + + // First match against the whole, non-split value + if (nameClean.startsWith(prefix, ignoreCase = true) || name.startsWith(prefix, ignoreCase = true)) { + return 1 + } else { + // check if prefix matches any of the words + val words = nameClean.split(" ").toTypedArray() + name.split(" ").toTypedArray() + for (word in words) { + if (word.startsWith(prefix, ignoreCase = true)) { + return 2 + } + } + } + // finally check if the prefix matches any part of the name + if (nameClean.contains(prefix, ignoreCase = true) || name.contains(prefix, ignoreCase = true)) { + return 3 + } + + return 100 + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt new file mode 100644 index 00000000..ad206c62 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt @@ -0,0 +1,11 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.models + +class MessagesSearch { + var isFocused = false + var searchText = "" + var count = 0 +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt new file mode 100644 index 00000000..484be3ab --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder + +import android.graphics.Typeface +import android.text.style.BackgroundColorSpan +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.databinding.MessagesListItemBinding +import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils +import pl.szczodrzynski.edziennik.utils.models.Date + +class MessageViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "MessageViewHolder" + } + + override fun onBind(activity: AppCompatActivity, app: App, item: MessageFull, position: Int, adapter: MessagesAdapter) { + val manager = app.gradesManager + + item.recipients?.forEach { recipient -> + if (recipient.fullName == null) { + recipient.fullName = adapter.teachers.firstOrNull { it.id == recipient.id }?.fullName ?: "" + } + } + + b.messageSubject.text = item.subject + b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort + b.messageAttachmentImage.isVisible = item.hasAttachments + + val text = item.body?.take(200) ?: "" + b.messageBody.text = MessagesUtils.htmlToSpannable(activity, text) + + val isRead = item.type == Message.TYPE_SENT || item.type == Message.TYPE_DRAFT || item.seen + val typeface = if (isRead) adapter.typefaceNormal else adapter.typefaceBold + val style = if (isRead) R.style.NavView_TextView_Small else R.style.NavView_TextView_Normal + // set text styles + b.messageSender.setTextAppearance(activity, style) + b.messageSender.typeface = typeface + b.messageSubject.setTextAppearance(activity, style) + b.messageSubject.typeface = typeface + b.messageDate.setTextAppearance(activity, style) + b.messageDate.typeface = typeface + + val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12) + b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) + b.messageSender.text = messageInfo.profileName + + item.searchHighlightText?.let { highlight -> + val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) + + b.messageSubject.text = b.messageSubject.text.asSpannable( + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true) + b.messageSender.text = b.messageSender.text.asSpannable( + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true) + } + + adapter.onItemClick?.let { listener -> + b.root.onClick { listener(item) } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt new file mode 100644 index 00000000..5adb1984 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding +import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter +import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch + +class SearchViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(inflater, parent, false) +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "SearchViewHolder" + } + + override fun onBind(activity: AppCompatActivity, app: App, item: MessagesSearch, position: Int, adapter: MessagesAdapter) { + b.searchEdit.removeTextChangedListener(adapter.textWatcher) + b.searchEdit.addTextChangedListener(adapter.textWatcher) + + /*b.searchEdit.setOnKeyboardListener(object : TextInputKeyboardEdit.KeyboardListener { + override fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) { + item.isFocused = showing + } + })*/ + + /*if (b.searchEdit.text.toString() != item.searchText) { + b.searchEdit.setText(item.searchText) + b.searchEdit.setSelection(item.searchText.length) + }*/ + + //b.searchLayout.helperText = app.getString(R.string.messages_search_results, item.count) + + /*if (item.isFocused && !b.searchEdit.isFocused) + b.searchEdit.requestFocus()*/ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt index 477e0d27..cbb4f5a0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt @@ -10,9 +10,9 @@ import android.util.AttributeSet import android.view.KeyEvent import android.view.KeyEvent.KEYCODE_BACK import androidx.annotation.NonNull -import com.google.android.material.textfield.TextInputEditText +import androidx.appcompat.widget.AppCompatEditText -class TextInputKeyboardEdit : TextInputEditText { +class TextInputKeyboardEdit : AppCompatEditText { /** * Keyboard Listener @@ -53,4 +53,4 @@ class TextInputKeyboardEdit : TextInputEditText { interface KeyboardListener { fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/messages_compose_fragment.xml b/app/src/main/res/layout/messages_compose_fragment.xml index 19055549..fa056966 100644 --- a/app/src/main/res/layout/messages_compose_fragment.xml +++ b/app/src/main/res/layout/messages_compose_fragment.xml @@ -47,7 +47,7 @@ app:counterEnabled="true" tools:counterMaxLength="180"> - - + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae5cd7ec..d7dc1a79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1289,4 +1289,6 @@ Laboratorium Nie masz żadnych wiadomości. Usunięte + Szukaj + Znaleziono %d wiadomości