Merge pull request #39 from szkolny-eu/feature/messages-fixes

[Messages] UI updates and fixes
This commit is contained in:
Kuba Szczodrzyński 2021-04-14 22:45:44 +02:00 committed by GitHub
commit 99021f6b3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 582 additions and 250 deletions

View File

@ -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) this.cleanDiacritics()
} else
} this
else if (substring.isNotEmpty()) { val search = if (ignoreDiacritics)
val string = substr.cleanDiacritics()
if (ignoreDiacritics) else
this.cleanDiacritics() substr.toString()
else this
var index = string.indexOf(substring, ignoreCase = ignoreCase) var index = 0
.takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase) do {
while (index >= 0) { index = string.indexOf(
spans.forEach { string = search,
spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) startIndex = index,
ignoreCase = ignoreCase
)
if (index >= 0) {
spans.forEach {
spannable.setSpan(
CharacterStyle.wrap(it),
index,
index + substring.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
index += substring.length.coerceAtLeast(1)
} }
index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) } while (index >= 0)
.takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
} } ?: spans.forEach {
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
return spannable return spannable
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,22 +10,19 @@ 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,
val teachers: List<Teacher>, val teachers: List<Teacher>,
val onItemClick: ((item: MessageFull) -> Unit)? = null val onItemClick: ((item: MessageFull) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope, Filterable { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope, Filterable {
companion object { companion object {
private const val TAG = "MessagesAdapter" private const val TAG = "MessagesAdapter"
@ -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
}
} }

View File

@ -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()

View File

@ -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) {
b.list.adapter = adapter if (searchText.isNotBlank())
adapter?.filter?.filter(searchText) {
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()
)) ))
} }
} }

View File

@ -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 {
span.replace(0, 0, "\n\n") // add greeting text
if (profileConfig.messagesGreetingOnForward)
span.replace(0, 0, "$greetingText\n\n\n")
else
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")

View File

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

View File

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

View File

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

View File

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

View File

@ -22,17 +22,21 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
class MessageViewHolder( class MessageViewHolder(
inflater: LayoutInflater, inflater: LayoutInflater,
parent: ViewGroup, parent: ViewGroup,
val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<MessageFull, MessagesAdapter> { ) : RecyclerView.ViewHolder(b.root), BindableViewHolder<MessageFull, MessagesAdapter> {
companion object { companion object {
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 ->

View File

@ -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)
b.searchEdit.setText(item.searchText)
/*if (b.searchEdit.text.toString() != item.searchText) { b.searchEdit.addTextChangedListener(watcher)
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

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

View 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>

View File

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