[Messages] Add a search bar. Fix Grades not loading.

This commit is contained in:
Kuba Szczodrzyński 2020-04-05 20:06:35 +02:00
parent 91a6366548
commit c214b48409
21 changed files with 435 additions and 93 deletions

View File

@ -572,7 +572,7 @@ fun CharSequence?.asBoldSpannable(): Spannable {
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannable 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) val spannable = SpannableString(this)
if (substring == null) { if (substring == null) {
spans.forEach { spans.forEach {
@ -580,17 +580,44 @@ fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignor
} }
} }
else if (substring.isNotEmpty()) { 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) { while (index >= 0) {
spans.forEach { spans.forEach {
spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 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 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. * Returns a new read-only list only of those given elements, that are not empty.
* Applies for CharSequence and descendants. * Applies for CharSequence and descendants.

View File

@ -3,6 +3,7 @@
*/ */
package pl.szczodrzynski.edziennik.data.db.full package pl.szczodrzynski.edziennik.data.db.full
import androidx.room.Ignore
import androidx.room.Relation import androidx.room.Relation
import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
@ -24,6 +25,11 @@ class MessageFull(
return this return this
} }
@Ignore
var filterWeight = 0
@Ignore
var searchHighlightText: String? = null
// metadata // metadata
var seen = false var seen = false
var notified = false var notified = false

View File

@ -166,7 +166,7 @@ class GradesAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position] val item = items[position]
if (holder !is BindableViewHolder<*>) if (holder !is BindableViewHolder<*, *>)
return return
val viewType = when (holder) { val viewType = when (holder) {

View File

@ -71,7 +71,7 @@ class GradesListFragment : Fragment(), CoroutineScope {
val adapter = GradesAdapter(activity) val adapter = GradesAdapter(activity)
var firstRun = true 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 if (!isAdded) return@launch
// load & configure the adapter // load & configure the adapter

View File

@ -6,8 +6,7 @@ package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter
interface BindableViewHolder<T> { interface BindableViewHolder<T, A> {
fun onBind(activity: AppCompatActivity, app: App, item: T, position: Int, adapter: GradesAdapter) fun onBind(activity: AppCompatActivity, app: App, item: T, position: Int, adapter: A)
} }

View File

@ -17,7 +17,7 @@ class EmptyViewHolder(
inflater: LayoutInflater, inflater: LayoutInflater,
parent: ViewGroup, parent: ViewGroup,
val b: GradesItemEmptyBinding = GradesItemEmptyBinding.inflate(inflater, parent, false) val b: GradesItemEmptyBinding = GradesItemEmptyBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesEmpty> { ) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesEmpty, GradesAdapter> {
companion object { companion object {
private const val TAG = "EmptyViewHolder" private const val TAG = "EmptyViewHolder"
} }

View File

@ -21,7 +21,7 @@ class GradeViewHolder(
inflater: LayoutInflater, inflater: LayoutInflater,
parent: ViewGroup, parent: ViewGroup,
val b: GradesItemGradeBinding = GradesItemGradeBinding.inflate(inflater, parent, false) val b: GradesItemGradeBinding = GradesItemGradeBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradeFull> { ) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradeFull, GradesAdapter> {
companion object { companion object {
private const val TAG = "GradeViewHolder" private const val TAG = "GradeViewHolder"
} }

View File

@ -21,7 +21,7 @@ class SemesterViewHolder(
inflater: LayoutInflater, inflater: LayoutInflater,
parent: ViewGroup, parent: ViewGroup,
val b: GradesItemSemesterBinding = GradesItemSemesterBinding.inflate(inflater, parent, false) val b: GradesItemSemesterBinding = GradesItemSemesterBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesSemester> { ) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesSemester, GradesAdapter> {
companion object { companion object {
private const val TAG = "SemesterViewHolder" private const val TAG = "SemesterViewHolder"
} }

View File

@ -24,7 +24,7 @@ class StatsViewHolder(
inflater: LayoutInflater, inflater: LayoutInflater,
parent: ViewGroup, parent: ViewGroup,
val b: GradesItemStatsBinding = GradesItemStatsBinding.inflate(inflater, parent, false) val b: GradesItemStatsBinding = GradesItemStatsBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesStats> { ) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesStats, GradesAdapter> {
companion object { companion object {
private const val TAG = "StatsViewHolder" private const val TAG = "StatsViewHolder"
} }

View File

@ -28,7 +28,7 @@ class SubjectViewHolder(
inflater: LayoutInflater, inflater: LayoutInflater,
parent: ViewGroup, parent: ViewGroup,
val b: GradesItemSubjectBinding = GradesItemSubjectBinding.inflate(inflater, parent, false) val b: GradesItemSubjectBinding = GradesItemSubjectBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesSubject> { ) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesSubject, GradesAdapter> {
companion object { companion object {
private const val TAG = "SubjectViewHolder" private const val TAG = "SubjectViewHolder"
} }

View File

@ -208,7 +208,7 @@ class MessageFragment : Fragment(), CoroutineScope {
} }
private fun showMessage() { 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) 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) val messageInfo = MessagesUtils.getMessageInfo(app, message, 40, 20, 14, 10)

View File

@ -1,31 +1,38 @@
package pl.szczodrzynski.edziennik.ui.modules.messages package pl.szczodrzynski.edziennik.ui.modules.messages
import android.graphics.Typeface import android.graphics.Typeface
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.cleanDiacritics
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.MessageFull 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.onClick import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
import pl.szczodrzynski.edziennik.utils.models.Date 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.coroutines.CoroutineContext
import kotlin.math.min
class MessagesAdapter( class MessagesAdapter(
val activity: AppCompatActivity, val activity: AppCompatActivity,
val teachers: List<Teacher>, val teachers: List<Teacher>,
val onItemClick: ((item: MessageFull) -> Unit)? = null val onItemClick: ((item: MessageFull) -> Unit)? = null
) : RecyclerView.Adapter<MessagesAdapter.ViewHolder>(), CoroutineScope { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope, Filterable {
companion object { 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 private val app = activity.applicationContext as App
@ -35,55 +42,184 @@ class MessagesAdapter(
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main get() = job + Dispatchers.Main
var items = listOf<MessageFull>() var items = mutableListOf<Any>()
private val typefaceNormal by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } var allItems = mutableListOf<Any>()
private val typefaceBold by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } 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 inflater = LayoutInflater.from(parent.context)
val view = MessagesListItemBinding.inflate(inflater, parent, false) return when (viewType) {
return ViewHolder(view) 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") @Suppress("DEPRECATION")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position] val item = items[position]
val b = holder.b if (holder !is BindableViewHolder<*, *>)
return
item.recipients?.forEach { recipient -> when {
if (recipient.fullName == null) { holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this)
recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: "" holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this)
}
}
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) }
} }
} }
override fun getItemCount() = items.size 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<Any>()
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<Any> }
// 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
}
} }

View File

@ -20,6 +20,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.databinding.MessagesListFragmentBinding import pl.szczodrzynski.edziennik.databinding.MessagesListFragmentBinding
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment 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 pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -68,7 +69,11 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
} }
// load & configure the adapter // 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) { if (items.isNotNullNorEmpty() && b.list.adapter == null) {
b.list.adapter = adapter b.list.adapter = adapter
b.list.apply { b.list.apply {

View File

@ -13,6 +13,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.asSpannable import pl.szczodrzynski.edziennik.asSpannable
import pl.szczodrzynski.edziennik.cleanDiacritics
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.resolveAttr import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
@ -85,36 +86,15 @@ class MessagesComposeSuggestionAdapter(
val list = mutableListOf<Teacher>() val list = mutableListOf<Teacher>()
originalList.forEach { teacher -> originalList.forEach { teacher ->
val teacherFullName = teacher.fullName teacher.recipientWeight = getMatchWeight(teacher.fullName, prefixString)
teacher.recipientWeight = 0
// First match against the whole, non-split value if (teacher.recipientWeight != 100) {
var found = false teacher.recipientDisplayName = teacher.fullName.asSpannable(
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(
StyleSpan(BOLD), StyleSpan(BOLD),
BackgroundColorSpan(R.attr.colorControlHighlight.resolveAttr(context)), BackgroundColorSpan(R.attr.colorControlHighlight.resolveAttr(context)),
substring = prefixString, substring = prefixString,
ignoreCase = true ignoreCase = true,
ignoreDiacritics = true
) )
list += teacher 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
}
} }

View File

@ -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
}

View File

@ -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<MessageFull, MessagesAdapter> {
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) }
}
}
}

View File

@ -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<MessagesSearch, MessagesAdapter> {
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()*/
}
}

View File

@ -10,9 +10,9 @@ import android.util.AttributeSet
import android.view.KeyEvent import android.view.KeyEvent
import android.view.KeyEvent.KEYCODE_BACK import android.view.KeyEvent.KEYCODE_BACK
import androidx.annotation.NonNull import androidx.annotation.NonNull
import com.google.android.material.textfield.TextInputEditText import androidx.appcompat.widget.AppCompatEditText
class TextInputKeyboardEdit : TextInputEditText { class TextInputKeyboardEdit : AppCompatEditText {
/** /**
* Keyboard Listener * Keyboard Listener

View File

@ -47,7 +47,7 @@
app:counterEnabled="true" app:counterEnabled="true"
tools:counterMaxLength="180"> tools:counterMaxLength="180">
<com.google.android.material.textfield.TextInputEditText <pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/subject" android:id="@+id/subject"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox" style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -69,7 +69,7 @@
app:boxBackgroundMode="filled" app:boxBackgroundMode="filled"
app:counterEnabled="true" app:counterEnabled="true"
tools:counterMaxLength="1983"> tools:counterMaxLength="1983">
<androidx.appcompat.widget.AppCompatEditText <pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/text" android:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-4-5.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/searchLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/messages_search"
app:hintEnabled="true"
tools:helperText="Znaleziono 400 wiadomości">
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/searchEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</layout>

View File

@ -1289,4 +1289,6 @@
<string name="menu_lab">Laboratorium</string> <string name="menu_lab">Laboratorium</string>
<string name="messages_no_data">Nie masz żadnych wiadomości.</string> <string name="messages_no_data">Nie masz żadnych wiadomości.</string>
<string name="messages_tab_deleted">Usunięte</string> <string name="messages_tab_deleted">Usunięte</string>
<string name="messages_search">Szukaj</string>
<string name="messages_search_results">Znaleziono %d wiadomości</string>
</resources> </resources>