forked from github/szkolny
Merge pull request #39 from szkolny-eu/feature/messages-fixes
[Messages] UI updates and fixes
This commit is contained in:
commit
99021f6b3a
@ -15,6 +15,7 @@ import android.graphics.drawable.Drawable
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.*
|
import android.text.*
|
||||||
|
import android.text.style.CharacterStyle
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.StrikethroughSpan
|
import android.text.style.StrikethroughSpan
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
@ -552,28 +553,46 @@ 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, ignoreDiacritics: Boolean = false): Spannable {
|
fun CharSequence.asSpannable(
|
||||||
|
vararg spans: CharacterStyle,
|
||||||
|
substring: CharSequence? = null,
|
||||||
|
ignoreCase: Boolean = false,
|
||||||
|
ignoreDiacritics: Boolean = false
|
||||||
|
): Spannable {
|
||||||
val spannable = SpannableString(this)
|
val spannable = SpannableString(this)
|
||||||
if (substring == null) {
|
substring?.let { substr ->
|
||||||
spans.forEach {
|
val string = if (ignoreDiacritics)
|
||||||
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (substring.isNotEmpty()) {
|
|
||||||
val string =
|
|
||||||
if (ignoreDiacritics)
|
|
||||||
this.cleanDiacritics()
|
this.cleanDiacritics()
|
||||||
else this
|
else
|
||||||
|
this
|
||||||
|
val search = if (ignoreDiacritics)
|
||||||
|
substr.cleanDiacritics()
|
||||||
|
else
|
||||||
|
substr.toString()
|
||||||
|
|
||||||
var index = string.indexOf(substring, ignoreCase = ignoreCase)
|
var index = 0
|
||||||
.takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase)
|
do {
|
||||||
while (index >= 0) {
|
index = string.indexOf(
|
||||||
|
string = search,
|
||||||
|
startIndex = index,
|
||||||
|
ignoreCase = ignoreCase
|
||||||
|
)
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
spans.forEach {
|
spans.forEach {
|
||||||
spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(
|
||||||
|
CharacterStyle.wrap(it),
|
||||||
|
index,
|
||||||
|
index + substring.length,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
|
index += substring.length.coerceAtLeast(1)
|
||||||
.takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
|
|
||||||
}
|
}
|
||||||
|
} while (index >= 0)
|
||||||
|
|
||||||
|
} ?: spans.forEach {
|
||||||
|
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
}
|
}
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
@ -49,4 +49,24 @@ class ProfileConfigUI(private val config: ProfileConfig) {
|
|||||||
var homeCards: List<HomeCardModel>
|
var homeCards: List<HomeCardModel>
|
||||||
get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() }
|
get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() }
|
||||||
set(value) { config.set("homeCards", value); mHomeCards = value }
|
set(value) { config.set("homeCards", value); mHomeCards = value }
|
||||||
|
|
||||||
|
private var mMessagesGreetingOnCompose: Boolean? = null
|
||||||
|
var messagesGreetingOnCompose: Boolean
|
||||||
|
get() { mMessagesGreetingOnCompose = mMessagesGreetingOnCompose ?: config.values.get("messagesGreetingOnCompose", true); return mMessagesGreetingOnCompose ?: true }
|
||||||
|
set(value) { config.set("messagesGreetingOnCompose", value); mMessagesGreetingOnCompose = value }
|
||||||
|
|
||||||
|
private var mMessagesGreetingOnReply: Boolean? = null
|
||||||
|
var messagesGreetingOnReply: Boolean
|
||||||
|
get() { mMessagesGreetingOnReply = mMessagesGreetingOnReply ?: config.values.get("messagesGreetingOnReply", true); return mMessagesGreetingOnReply ?: true }
|
||||||
|
set(value) { config.set("messagesGreetingOnReply", value); mMessagesGreetingOnReply = value }
|
||||||
|
|
||||||
|
private var mMessagesGreetingOnForward: Boolean? = null
|
||||||
|
var messagesGreetingOnForward: Boolean
|
||||||
|
get() { mMessagesGreetingOnForward = mMessagesGreetingOnForward ?: config.values.get("messagesGreetingOnForward", false); return mMessagesGreetingOnForward ?: false }
|
||||||
|
set(value) { config.set("messagesGreetingOnForward", value); mMessagesGreetingOnForward = value }
|
||||||
|
|
||||||
|
private var mMessagesGreetingText: String? = null
|
||||||
|
var messagesGreetingText: String?
|
||||||
|
get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText }
|
||||||
|
set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value }
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,9 @@ open class Profile(
|
|||||||
val isParent
|
val isParent
|
||||||
get() = accountName != null
|
get() = accountName != null
|
||||||
|
|
||||||
|
val accountOwnerName
|
||||||
|
get() = accountName ?: studentNameLong
|
||||||
|
|
||||||
val registerName
|
val registerName
|
||||||
get() = when (loginStoreType) {
|
get() = when (loginStoreType) {
|
||||||
LOGIN_TYPE_LIBRUS -> "librus"
|
LOGIN_TYPE_LIBRUS -> "librus"
|
||||||
|
@ -30,7 +30,7 @@ class MessageFull(
|
|||||||
@Ignore
|
@Ignore
|
||||||
var filterWeight = 0
|
var filterWeight = 0
|
||||||
@Ignore
|
@Ignore
|
||||||
var searchHighlightText: String? = null
|
var searchHighlightText: CharSequence? = null
|
||||||
|
|
||||||
// metadata
|
// metadata
|
||||||
var seen = false
|
var seen = false
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.ui.dialogs
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import pl.szczodrzynski.edziennik.App
|
||||||
|
import pl.szczodrzynski.edziennik.MainActivity
|
||||||
|
import pl.szczodrzynski.edziennik.R
|
||||||
|
import pl.szczodrzynski.edziennik.databinding.MessagesConfigDialogBinding
|
||||||
|
|
||||||
|
class MessagesConfigDialog(
|
||||||
|
private val activity: AppCompatActivity,
|
||||||
|
private val reloadOnDismiss: Boolean = true,
|
||||||
|
private val onShowListener: ((tag: String) -> Unit)? = null,
|
||||||
|
private val onDismissListener: ((tag: String) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "MessagesConfigDialog"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val app by lazy { activity.application as App }
|
||||||
|
private val config by lazy { app.config.ui }
|
||||||
|
private val profileConfig by lazy { app.config.forProfile().ui }
|
||||||
|
|
||||||
|
private lateinit var b: MessagesConfigDialogBinding
|
||||||
|
private lateinit var dialog: AlertDialog
|
||||||
|
|
||||||
|
init { run {
|
||||||
|
if (activity.isFinishing)
|
||||||
|
return@run
|
||||||
|
b = MessagesConfigDialogBinding.inflate(activity.layoutInflater)
|
||||||
|
onShowListener?.invoke(TAG)
|
||||||
|
dialog = MaterialAlertDialogBuilder(activity)
|
||||||
|
.setTitle(R.string.menu_messages_config)
|
||||||
|
.setView(b.root)
|
||||||
|
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||||
|
.setOnDismissListener {
|
||||||
|
saveConfig()
|
||||||
|
onDismissListener?.invoke(TAG)
|
||||||
|
if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget()
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
loadConfig()
|
||||||
|
dialog.show()
|
||||||
|
}}
|
||||||
|
|
||||||
|
private fun loadConfig() {
|
||||||
|
b.config = profileConfig
|
||||||
|
|
||||||
|
b.greetingText.setText(
|
||||||
|
profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveConfig() {
|
||||||
|
val greetingText = b.greetingText.text?.toString()?.trim()
|
||||||
|
if (greetingText.isNullOrEmpty())
|
||||||
|
profileConfig.messagesGreetingText = null
|
||||||
|
else
|
||||||
|
profileConfig.messagesGreetingText = "\n\n$greetingText"
|
||||||
|
}
|
||||||
|
}
|
@ -30,10 +30,12 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
|
|||||||
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
|
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||||
import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding
|
import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding
|
||||||
|
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||||
import pl.szczodrzynski.edziennik.utils.Anim
|
import pl.szczodrzynski.edziennik.utils.Anim
|
||||||
import pl.szczodrzynski.edziennik.utils.BetterLink
|
import pl.szczodrzynski.edziennik.utils.BetterLink
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||||
|
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||||
import pl.szczodrzynski.navlib.colorAttr
|
import pl.szczodrzynski.navlib.colorAttr
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@ -64,10 +66,20 @@ class MessageFragment : Fragment(), CoroutineScope {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
if (!isAdded) return
|
if (!isAdded) return
|
||||||
|
|
||||||
|
activity.bottomSheet.prependItem(
|
||||||
|
BottomSheetPrimaryItem(true)
|
||||||
|
.withTitle(R.string.menu_messages_config)
|
||||||
|
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||||
|
.withOnClickListener {
|
||||||
|
activity.bottomSheet.close()
|
||||||
|
MessagesConfigDialog(activity, false, null, null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
b.closeButton.setImageDrawable(
|
b.closeButton.setImageDrawable(
|
||||||
IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_window_close).apply {
|
IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_window_close).apply {
|
||||||
colorAttr(activity, android.R.attr.textColorSecondary)
|
colorAttr(activity, android.R.attr.textColorSecondary)
|
||||||
sizeDp = 16
|
sizeDp = 24
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
b.closeButton.setOnClickListener { activity.navigateUp() }
|
b.closeButton.setOnClickListener { activity.navigateUp() }
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
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 android.widget.Filterable
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -13,17 +10,14 @@ 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.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.ui.modules.grades.viewholder.BindableViewHolder
|
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.models.MessagesSearch
|
||||||
|
import pl.szczodrzynski.edziennik.ui.modules.messages.utils.MessagesFilter
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder
|
import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.SearchViewHolder
|
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,
|
||||||
@ -43,41 +37,10 @@ class MessagesAdapter(
|
|||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
get() = job + Dispatchers.Main
|
get() = job + Dispatchers.Main
|
||||||
|
|
||||||
var items = mutableListOf<Any>()
|
var items = listOf<Any>()
|
||||||
var allItems = mutableListOf<Any>()
|
var allItems = listOf<Any>()
|
||||||
val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) }
|
val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) }
|
||||||
val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
|
|
||||||
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 {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
@ -103,138 +66,16 @@ class MessagesAdapter(
|
|||||||
return
|
return
|
||||||
|
|
||||||
when {
|
when {
|
||||||
holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this)
|
holder is MessageViewHolder
|
||||||
holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this)
|
&& item is MessageFull -> holder.onBind(activity, app, item, position, this)
|
||||||
|
holder is SearchViewHolder
|
||||||
|
&& item is MessagesSearch -> holder.onBind(activity, app, item, position, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val messagesFilter by lazy {
|
||||||
|
MessagesFilter(this)
|
||||||
|
}
|
||||||
override fun getItemCount() = items.size
|
override fun getItemCount() = items.size
|
||||||
override fun getFilter() = filter
|
override fun getFilter() = messagesFilter
|
||||||
private var prevCount = -1
|
|
||||||
private val filter by lazy { object : Filter() {
|
|
||||||
override fun performFiltering(prefix: CharSequence?): FilterResults {
|
|
||||||
val results = FilterResults()
|
|
||||||
|
|
||||||
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: Int
|
|
||||||
if (it.type == Message.TYPE_SENT) {
|
|
||||||
it.recipients?.forEach { recipient ->
|
|
||||||
weight = getMatchWeight(recipient.fullName, prefixString)
|
|
||||||
if (weight != 100) {
|
|
||||||
if (weight == 3)
|
|
||||||
weight = 31
|
|
||||||
it.filterWeight = min(it.filterWeight, 10 + weight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,9 @@ import kotlinx.coroutines.Job
|
|||||||
import pl.szczodrzynski.edziennik.*
|
import pl.szczodrzynski.edziennik.*
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
||||||
import pl.szczodrzynski.edziennik.databinding.MessagesFragmentBinding
|
import pl.szczodrzynski.edziennik.databinding.MessagesFragmentBinding
|
||||||
|
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter
|
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter
|
||||||
|
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class MessagesFragment : Fragment(), CoroutineScope {
|
class MessagesFragment : Fragment(), CoroutineScope {
|
||||||
@ -100,9 +102,19 @@ class MessagesFragment : Fragment(), CoroutineScope {
|
|||||||
fabIcon = CommunityMaterial.Icon3.cmd_pencil_outline
|
fabIcon = CommunityMaterial.Icon3.cmd_pencil_outline
|
||||||
}
|
}
|
||||||
|
|
||||||
setFabOnClickListener(View.OnClickListener {
|
bottomSheet.prependItem(
|
||||||
|
BottomSheetPrimaryItem(true)
|
||||||
|
.withTitle(R.string.menu_messages_config)
|
||||||
|
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||||
|
.withOnClickListener {
|
||||||
|
activity.bottomSheet.close()
|
||||||
|
MessagesConfigDialog(activity, false, null, null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
setFabOnClickListener {
|
||||||
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE)
|
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.gainAttentionFAB()
|
activity.gainAttentionFAB()
|
||||||
|
@ -33,6 +33,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
private lateinit var app: App
|
private lateinit var app: App
|
||||||
private lateinit var activity: MainActivity
|
private lateinit var activity: MainActivity
|
||||||
private lateinit var b: MessagesListFragmentBinding
|
private lateinit var b: MessagesListFragmentBinding
|
||||||
|
private var adapter: MessagesAdapter? = null
|
||||||
|
|
||||||
private val job: Job = Job()
|
private val job: Job = Job()
|
||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
@ -53,21 +54,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED)
|
val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED)
|
||||||
var topPosition = arguments.getInt("topPosition", NO_POSITION)
|
var topPosition = arguments.getInt("topPosition", NO_POSITION)
|
||||||
var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION)
|
var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION)
|
||||||
|
val searchText = arguments.getString("searchText", "")
|
||||||
|
|
||||||
teachers = withContext(Dispatchers.Default) {
|
teachers = withContext(Dispatchers.Default) {
|
||||||
app.db.teacherDao().getAllNow(App.profileId)
|
app.db.teacherDao().getAllNow(App.profileId)
|
||||||
}
|
}
|
||||||
|
|
||||||
val adapter = MessagesAdapter(activity, teachers) {
|
adapter = MessagesAdapter(activity, teachers) {
|
||||||
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
|
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
|
||||||
"messageId" to it.id
|
"messageId" to it.id
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { items ->
|
app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { messages ->
|
||||||
if (!isAdded) return@Observer
|
if (!isAdded) return@Observer
|
||||||
|
|
||||||
items.forEach { message ->
|
messages.forEach { message ->
|
||||||
message.recipients?.removeAll { it.profileId != message.profileId }
|
message.recipients?.removeAll { it.profileId != message.profileId }
|
||||||
message.recipients?.forEach { recipient ->
|
message.recipients?.forEach { recipient ->
|
||||||
if (recipient.fullName == null) {
|
if (recipient.fullName == null) {
|
||||||
@ -77,13 +79,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load & configure the adapter
|
// load & configure the adapter
|
||||||
adapter.items = items.toMutableList()
|
val items = messages.toMutableList<Any>()
|
||||||
adapter.items.add(0, MessagesSearch().also {
|
items.add(0, MessagesSearch().also {
|
||||||
it.count = items.size
|
it.searchText = searchText
|
||||||
})
|
})
|
||||||
adapter.allItems = adapter.items.toMutableList()
|
|
||||||
|
adapter?.items = items
|
||||||
|
adapter?.allItems = items
|
||||||
|
|
||||||
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
|
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
|
||||||
|
if (searchText.isNotBlank())
|
||||||
|
adapter?.filter?.filter(searchText) {
|
||||||
b.list.adapter = adapter
|
b.list.adapter = adapter
|
||||||
|
}
|
||||||
|
else
|
||||||
|
b.list.adapter = adapter
|
||||||
|
|
||||||
b.list.apply {
|
b.list.apply {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
@ -92,7 +103,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
addOnScrollListener(onScrollListener)
|
addOnScrollListener(onScrollListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty())
|
setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty())
|
||||||
|
|
||||||
(b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager ->
|
(b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager ->
|
||||||
@ -119,10 +130,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if (!isAdded) return
|
if (!isAdded)
|
||||||
|
return
|
||||||
|
val layoutManager = (b.list.layoutManager as? LinearLayoutManager)
|
||||||
|
val searchItem = adapter?.items?.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
||||||
|
|
||||||
onPageDestroy?.invoke(position, Bundle(
|
onPageDestroy?.invoke(position, Bundle(
|
||||||
"topPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition(),
|
"topPosition" to layoutManager?.findFirstVisibleItemPosition(),
|
||||||
"bottomPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition()
|
"bottomPosition" to layoutManager?.findLastCompletelyVisibleItemPosition(),
|
||||||
|
"searchText" to searchItem?.searchText?.toString()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.ui.modules.messages.compose
|
package pl.szczodrzynski.edziennik.ui.modules.messages.compose
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
@ -43,6 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
|||||||
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.MessagesComposeFragmentBinding
|
import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
|
||||||
|
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
|
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
|
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
|
||||||
import pl.szczodrzynski.edziennik.utils.Colors
|
import pl.szczodrzynski.edziennik.utils.Colors
|
||||||
@ -50,6 +52,7 @@ import pl.szczodrzynski.edziennik.utils.Themes
|
|||||||
import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor
|
import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||||
|
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||||
import pl.szczodrzynski.navlib.elevateSurface
|
import pl.szczodrzynski.navlib.elevateSurface
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.text.replace
|
import kotlin.text.replace
|
||||||
@ -67,6 +70,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
get() = job + Dispatchers.Main
|
get() = job + Dispatchers.Main
|
||||||
|
|
||||||
|
private val profileConfig by lazy { app.config.forProfile().ui }
|
||||||
|
private val greetingText
|
||||||
|
get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
|
||||||
|
|
||||||
private var teachers = mutableListOf<Teacher>()
|
private var teachers = mutableListOf<Teacher>()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
@ -131,6 +138,16 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
activity.bottomSheet.prependItem(
|
||||||
|
BottomSheetPrimaryItem(true)
|
||||||
|
.withTitle(R.string.menu_messages_config)
|
||||||
|
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||||
|
.withOnClickListener {
|
||||||
|
activity.bottomSheet.close()
|
||||||
|
MessagesConfigDialog(activity, false, null, null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
delay(100)
|
delay(100)
|
||||||
getRecipientList()
|
getRecipientList()
|
||||||
@ -290,7 +307,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
b.recipients.setIllegalCharacterIdentifier { c ->
|
b.recipients.setIllegalCharacterIdentifier { c ->
|
||||||
c.toString().matches("[\\n;:_ ]".toRegex())
|
c.toString().matches("[\\n;:_ ]".toRegex())
|
||||||
}
|
}
|
||||||
b.recipients.setOnChipRemoveListener { _ ->
|
b.recipients.setOnChipRemoveListener {
|
||||||
b.recipients.setSelection(b.recipients.text.length)
|
b.recipients.setSelection(b.recipients.text.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,14 +335,15 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
fabExtendedText = getString(R.string.messages_compose_send)
|
fabExtendedText = getString(R.string.messages_compose_send)
|
||||||
fabIcon = CommunityMaterial.Icon3.cmd_send_outline
|
fabIcon = CommunityMaterial.Icon3.cmd_send_outline
|
||||||
|
|
||||||
setFabOnClickListener(View.OnClickListener {
|
setFabOnClickListener {
|
||||||
sendMessage()
|
sendMessage()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.gainAttentionFAB()
|
activity.gainAttentionFAB()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
private fun updateRecipientList(list: List<Teacher>) { launch {
|
private fun updateRecipientList(list: List<Teacher>) { launch {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
teachers = list.sortedBy { it.fullName }.toMutableList()
|
teachers = list.sortedBy { it.fullName }.toMutableList()
|
||||||
@ -344,11 +362,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
val adapter = MessagesComposeSuggestionAdapter(activity, teachers)
|
val adapter = MessagesComposeSuggestionAdapter(activity, teachers)
|
||||||
b.recipients.setAdapter(adapter)
|
b.recipients.setAdapter(adapter)
|
||||||
|
|
||||||
|
if (profileConfig.messagesGreetingOnCompose)
|
||||||
|
b.text.setText(greetingText)
|
||||||
|
|
||||||
handleReplyMessage()
|
handleReplyMessage()
|
||||||
handleMailToIntent()
|
handleMailToIntent()
|
||||||
}}
|
}}
|
||||||
|
|
||||||
private fun handleReplyMessage() { launch {
|
private fun handleReplyMessage() = launch {
|
||||||
val replyMessage = arguments?.getString("message")
|
val replyMessage = arguments?.getString("message")
|
||||||
if (replyMessage != null) {
|
if (replyMessage != null) {
|
||||||
val chipList = mutableListOf<ChipInfo>()
|
val chipList = mutableListOf<ChipInfo>()
|
||||||
@ -370,8 +391,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
|
|
||||||
if (arguments?.getString("type") == "reply") {
|
if (arguments?.getString("type") == "reply") {
|
||||||
// add greeting text
|
// add greeting text
|
||||||
span.replace(0, 0, "\n\nZ poważaniem,\n${app.profile.accountName
|
if (profileConfig.messagesGreetingOnReply)
|
||||||
?: app.profile.studentNameLong ?: ""}\n\n\n")
|
span.replace(0, 0, "$greetingText\n\n\n")
|
||||||
|
else
|
||||||
|
span.replace(0, 0, "\n\n")
|
||||||
|
|
||||||
teachers.firstOrNull { it.id == msg.senderId }?.let { teacher ->
|
teachers.firstOrNull { it.id == msg.senderId }?.let { teacher ->
|
||||||
teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
|
teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
|
||||||
@ -379,7 +402,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
subject = "Re: ${msg.subject}"
|
subject = "Re: ${msg.subject}"
|
||||||
} else {
|
} else {
|
||||||
|
// add greeting text
|
||||||
|
if (profileConfig.messagesGreetingOnForward)
|
||||||
|
span.replace(0, 0, "$greetingText\n\n\n")
|
||||||
|
else
|
||||||
span.replace(0, 0, "\n\n")
|
span.replace(0, 0, "\n\n")
|
||||||
|
|
||||||
subject = "Fwd: ${msg.subject}"
|
subject = "Fwd: ${msg.subject}"
|
||||||
}
|
}
|
||||||
body = MessagesUtils.htmlToSpannable(activity, msg.body
|
body = MessagesUtils.htmlToSpannable(activity, msg.body
|
||||||
@ -401,7 +429,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
else {
|
else {
|
||||||
b.recipients.requestFocus()
|
b.recipients.requestFocus()
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
|
|
||||||
private fun handleMailToIntent() {
|
private fun handleMailToIntent() {
|
||||||
val teacherId = arguments?.getLong("messageRecipientId")
|
val teacherId = arguments?.getLong("messageRecipientId")
|
||||||
|
@ -5,7 +5,5 @@
|
|||||||
package pl.szczodrzynski.edziennik.ui.modules.messages.models
|
package pl.szczodrzynski.edziennik.ui.modules.messages.models
|
||||||
|
|
||||||
class MessagesSearch {
|
class MessagesSearch {
|
||||||
var isFocused = false
|
var searchText: CharSequence = ""
|
||||||
var searchText = ""
|
|
||||||
var count = 0
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.ui.modules.messages.utils
|
||||||
|
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||||
|
|
||||||
|
class MessagesComparator : Comparator<Any> {
|
||||||
|
|
||||||
|
override fun compare(o1: Any?, o2: Any?): Int {
|
||||||
|
if (o1 !is MessageFull || o2 !is MessageFull)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return when {
|
||||||
|
// standard sorting
|
||||||
|
o1.filterWeight > o2.filterWeight -> 1
|
||||||
|
o1.filterWeight < o2.filterWeight -> -1
|
||||||
|
else -> when {
|
||||||
|
// reversed sorting
|
||||||
|
o1.addedDate > o2.addedDate -> -1
|
||||||
|
o1.addedDate < o2.addedDate -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.ui.modules.messages.utils
|
||||||
|
|
||||||
|
import android.widget.Filter
|
||||||
|
import pl.szczodrzynski.edziennik.cleanDiacritics
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||||
|
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class MessagesFilter(
|
||||||
|
private val adapter: MessagesAdapter
|
||||||
|
) : Filter() {
|
||||||
|
companion object {
|
||||||
|
private const val NO_MATCH = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
private val comparator = MessagesComparator()
|
||||||
|
private var prevCount = -1
|
||||||
|
|
||||||
|
private val allItems
|
||||||
|
get() = adapter.allItems
|
||||||
|
|
||||||
|
private fun getMatchWeight(name: CharSequence?, prefix: CharSequence): Int {
|
||||||
|
if (name == null)
|
||||||
|
return NO_MATCH
|
||||||
|
|
||||||
|
val prefixClean = prefix.cleanDiacritics()
|
||||||
|
val nameClean = name.cleanDiacritics()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
// First match against the whole, non-split value
|
||||||
|
nameClean.startsWith(prefixClean, ignoreCase = true) -> 1
|
||||||
|
// check if prefix matches any of the words
|
||||||
|
nameClean.split(" ").any {
|
||||||
|
it.startsWith(prefixClean, ignoreCase = true)
|
||||||
|
} -> 2
|
||||||
|
// finally check if the prefix matches any part of the name
|
||||||
|
nameClean.contains(prefixClean, ignoreCase = true) -> 3
|
||||||
|
|
||||||
|
else -> NO_MATCH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun performFiltering(prefix: CharSequence?): FilterResults {
|
||||||
|
val results = FilterResults()
|
||||||
|
|
||||||
|
if (prevCount == -1)
|
||||||
|
prevCount = allItems.size
|
||||||
|
|
||||||
|
if (prefix.isNullOrBlank()) {
|
||||||
|
allItems.forEach {
|
||||||
|
if (it is MessageFull)
|
||||||
|
it.searchHighlightText = null
|
||||||
|
}
|
||||||
|
results.values = allItems.toList()
|
||||||
|
results.count = allItems.size
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
val items = mutableListOf<Any>()
|
||||||
|
|
||||||
|
allItems.forEach {
|
||||||
|
if (it !is MessageFull) {
|
||||||
|
items.add(it)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
it.filterWeight = NO_MATCH
|
||||||
|
it.searchHighlightText = null
|
||||||
|
|
||||||
|
var weight: Int
|
||||||
|
// weights 11..13 and 110
|
||||||
|
if (it.type == Message.TYPE_SENT) {
|
||||||
|
it.recipients?.forEach { recipient ->
|
||||||
|
weight = getMatchWeight(recipient.fullName, prefix)
|
||||||
|
if (weight != NO_MATCH) {
|
||||||
|
if (weight == 3)
|
||||||
|
weight = 100
|
||||||
|
it.filterWeight = min(it.filterWeight, 10 + weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
weight = getMatchWeight(it.senderName, prefix)
|
||||||
|
if (weight != NO_MATCH) {
|
||||||
|
if (weight == 3)
|
||||||
|
weight = 100
|
||||||
|
it.filterWeight = min(it.filterWeight, 10 + weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// weights 21..23 and 120
|
||||||
|
weight = getMatchWeight(it.subject, prefix)
|
||||||
|
if (weight != NO_MATCH) {
|
||||||
|
if (weight == 3)
|
||||||
|
weight = 100
|
||||||
|
it.filterWeight = min(it.filterWeight, 20 + weight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// weights 31..33 and 130
|
||||||
|
weight = getMatchWeight(it.body, prefix)
|
||||||
|
if (weight != NO_MATCH) {
|
||||||
|
if (weight == 3)
|
||||||
|
weight = 100
|
||||||
|
it.filterWeight = min(it.filterWeight, 30 + weight)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.filterWeight != NO_MATCH) {
|
||||||
|
it.searchHighlightText = prefix
|
||||||
|
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 {
|
||||||
|
adapter.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
|
||||||
|
with(adapter) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevCount = count
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.ui.modules.messages.utils
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import pl.szczodrzynski.edziennik.R
|
||||||
|
import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding
|
||||||
|
import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
|
||||||
|
|
||||||
|
class SearchTextWatcher(
|
||||||
|
private val b: MessagesListItemSearchBinding,
|
||||||
|
private val filter: MessagesFilter,
|
||||||
|
private val item: MessagesSearch
|
||||||
|
) : TextWatcher {
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
item.searchText = s ?: ""
|
||||||
|
filter.filter(s) { count ->
|
||||||
|
if (s.isNullOrBlank())
|
||||||
|
b.searchLayout.helperText = " "
|
||||||
|
else
|
||||||
|
b.searchLayout.helperText =
|
||||||
|
b.root.context.getString(R.string.messages_search_results, count - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other is SearchTextWatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = b.hashCode()
|
||||||
|
result = 31 * result + filter.hashCode()
|
||||||
|
result = 31 * result + item.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
@ -30,9 +30,13 @@ class MessageViewHolder(
|
|||||||
private const val TAG = "MessageViewHolder"
|
private const val TAG = "MessageViewHolder"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(activity: AppCompatActivity, app: App, item: MessageFull, position: Int, adapter: MessagesAdapter) {
|
override fun onBind(
|
||||||
val manager = app.gradesManager
|
activity: AppCompatActivity,
|
||||||
|
app: App,
|
||||||
|
item: MessageFull,
|
||||||
|
position: Int,
|
||||||
|
adapter: MessagesAdapter
|
||||||
|
) {
|
||||||
b.messageSubject.text = item.subject
|
b.messageSubject.text = item.subject
|
||||||
b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort
|
b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort
|
||||||
b.messageAttachmentImage.isVisible = item.hasAttachments
|
b.messageAttachmentImage.isVisible = item.hasAttachments
|
||||||
@ -55,15 +59,17 @@ class MessageViewHolder(
|
|||||||
b.messageProfileBackground.setImageBitmap(messageInfo.profileImage)
|
b.messageProfileBackground.setImageBitmap(messageInfo.profileImage)
|
||||||
b.messageSender.text = messageInfo.profileName
|
b.messageSender.text = messageInfo.profileName
|
||||||
|
|
||||||
item.searchHighlightText?.let { highlight ->
|
item.searchHighlightText?.toString()?.let { highlight ->
|
||||||
val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity)
|
val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity)
|
||||||
|
|
||||||
b.messageSubject.text = b.messageSubject.text.asSpannable(
|
b.messageSubject.text = b.messageSubject.text.asSpannable(
|
||||||
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
||||||
substring = highlight, ignoreCase = true, ignoreDiacritics = true)
|
substring = highlight, ignoreCase = true, ignoreDiacritics = true
|
||||||
|
)
|
||||||
b.messageSender.text = b.messageSender.text.asSpannable(
|
b.messageSender.text = b.messageSender.text.asSpannable(
|
||||||
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
||||||
substring = highlight, ignoreCase = true, ignoreDiacritics = true)
|
substring = highlight, ignoreCase = true, ignoreDiacritics = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.onItemClick?.let { listener ->
|
adapter.onItemClick?.let { listener ->
|
||||||
|
@ -9,38 +9,43 @@ import android.view.ViewGroup
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
|
import pl.szczodrzynski.edziennik.R
|
||||||
import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding
|
import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
|
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter
|
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
|
import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
|
||||||
|
import pl.szczodrzynski.edziennik.ui.modules.messages.utils.SearchTextWatcher
|
||||||
|
|
||||||
class SearchViewHolder(
|
class SearchViewHolder(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(inflater, parent, false)
|
val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(
|
||||||
|
inflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<MessagesSearch, MessagesAdapter> {
|
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<MessagesSearch, MessagesAdapter> {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SearchViewHolder"
|
private const val TAG = "SearchViewHolder"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(activity: AppCompatActivity, app: App, item: MessagesSearch, position: Int, adapter: MessagesAdapter) {
|
override fun onBind(
|
||||||
b.searchEdit.removeTextChangedListener(adapter.textWatcher)
|
activity: AppCompatActivity,
|
||||||
b.searchEdit.addTextChangedListener(adapter.textWatcher)
|
app: App,
|
||||||
|
item: MessagesSearch,
|
||||||
|
position: Int,
|
||||||
|
adapter: MessagesAdapter
|
||||||
|
) {
|
||||||
|
val watcher = SearchTextWatcher(b, adapter.filter, item)
|
||||||
|
b.searchEdit.removeTextChangedListener(watcher)
|
||||||
|
|
||||||
/*b.searchEdit.setOnKeyboardListener(object : TextInputKeyboardEdit.KeyboardListener {
|
if (adapter.items.isEmpty() || adapter.items.size == adapter.allItems.size)
|
||||||
override fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) {
|
b.searchLayout.helperText = " "
|
||||||
item.isFocused = showing
|
else
|
||||||
}
|
b.searchLayout.helperText =
|
||||||
})*/
|
b.root.context.getString(R.string.messages_search_results, adapter.items.size - 1)
|
||||||
|
|
||||||
/*if (b.searchEdit.text.toString() != item.searchText) {
|
|
||||||
b.searchEdit.setText(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)
|
b.searchEdit.addTextChangedListener(watcher)
|
||||||
|
|
||||||
/*if (item.isFocused && !b.searchEdit.isFocused)
|
|
||||||
b.searchEdit.requestFocus()*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.R
|
|||||||
import pl.szczodrzynski.edziennik.after
|
import pl.szczodrzynski.edziennik.after
|
||||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
|
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED
|
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED
|
||||||
|
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||||
import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog
|
import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog
|
||||||
import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncConfigDialog
|
import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncConfigDialog
|
||||||
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog
|
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog
|
||||||
@ -73,6 +74,13 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
|
|||||||
GradesConfigDialog(activity, reloadOnDismiss = false)
|
GradesConfigDialog(activity, reloadOnDismiss = false)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
util.createActionItem(
|
||||||
|
text = R.string.menu_messages_config,
|
||||||
|
icon = CommunityMaterial.Icon.cmd_calendar_outline
|
||||||
|
) {
|
||||||
|
MessagesConfigDialog(activity, reloadOnDismiss = false)
|
||||||
|
},
|
||||||
|
|
||||||
util.createActionItem(
|
util.createActionItem(
|
||||||
text = R.string.menu_attendance_config,
|
text = R.string.menu_attendance_config,
|
||||||
icon = CommunityMaterial.Icon.cmd_calendar_remove_outline
|
icon = CommunityMaterial.Icon.cmd_calendar_remove_outline
|
||||||
|
73
app/src/main/res/layout/messages_config_dialog.xml
Normal file
73
app/src/main/res/layout/messages_config_dialog.xml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) Kuba Szczodrzyński 2021-4-10.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="config"
|
||||||
|
type="pl.szczodrzynski.edziennik.config.ProfileConfigUI" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/messages_config_compose"
|
||||||
|
android:textAppearance="@style/NavView.TextView.Subtitle" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checked="@={config.messagesGreetingOnCompose}"
|
||||||
|
android:minHeight="32dp"
|
||||||
|
android:text="@string/messages_config_greeting_on_compose" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checked="@={config.messagesGreetingOnReply}"
|
||||||
|
android:minHeight="32dp"
|
||||||
|
android:text="@string/messages_config_greeting_on_reply" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checked="@={config.messagesGreetingOnForward}"
|
||||||
|
android:minHeight="32dp"
|
||||||
|
android:text="@string/messages_config_greeting_on_forward" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/messages_config_greeting_text">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/greetingText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textMultiLine|textShortMessage|textAutoCorrect|textAutoComplete"
|
||||||
|
android:minLines="3"
|
||||||
|
android:maxLines="10"
|
||||||
|
tools:text="\n\nZ poważaniem\nJan Kowalski" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</layout>
|
@ -1444,4 +1444,10 @@
|
|||||||
<string name="legend_event_added_manually">{cmd-clipboard-edit-outline} wydarzenie dodane ręcznie</string>
|
<string name="legend_event_added_manually">{cmd-clipboard-edit-outline} wydarzenie dodane ręcznie</string>
|
||||||
<string name="legend_event_is_done">{cmd-check} oznaczono jako wykonane</string>
|
<string name="legend_event_is_done">{cmd-check} oznaczono jako wykonane</string>
|
||||||
<string name="agenda_config_not_available_yet">Funkcja jeszcze nie jest dostępna.</string>
|
<string name="agenda_config_not_available_yet">Funkcja jeszcze nie jest dostępna.</string>
|
||||||
|
<string name="messages_config_compose">Tworzenie wiadomości</string>
|
||||||
|
<string name="messages_config_greeting_on_compose">Dodaj podpis przy tworzeniu wiadomości</string>
|
||||||
|
<string name="messages_config_greeting_on_reply">Dodaj podpis przy odpowiadaniu na wiadomość</string>
|
||||||
|
<string name="messages_config_greeting_on_forward">Dodaj podpis przy przekazywaniu wiadomości</string>
|
||||||
|
<string name="messages_config_greeting_text">Treść podpisu</string>
|
||||||
|
<string name="menu_messages_config">Ustawienia wiadomości</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user