diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index a07634a1..8d3b59d1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -15,6 +15,7 @@ import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.text.* +import android.text.style.CharacterStyle import android.text.style.ForegroundColorSpan import android.text.style.StrikethroughSpan 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) 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) - if (substring == null) { - spans.forEach { - spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - else if (substring.isNotEmpty()) { - val string = - if (ignoreDiacritics) - this.cleanDiacritics() - else this + substring?.let { substr -> + val string = if (ignoreDiacritics) + this.cleanDiacritics() + else + this + val search = if (ignoreDiacritics) + substr.cleanDiacritics() + else + substr.toString() - var index = string.indexOf(substring, ignoreCase = ignoreCase) - .takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase) - while (index >= 0) { - spans.forEach { - spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + var index = 0 + do { + index = string.indexOf( + string = search, + 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) - .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 } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt index ca22cb76..d40f0dfc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt @@ -49,4 +49,24 @@ class ProfileConfigUI(private val config: ProfileConfig) { var homeCards: List get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() } 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 } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt index 9e52dc81..b3586a79 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt @@ -129,6 +129,9 @@ open class Profile( val isParent get() = accountName != null + val accountOwnerName + get() = accountName ?: studentNameLong + val registerName get() = when (loginStoreType) { LOGIN_TYPE_LIBRUS -> "librus" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt index 3fdcdd0d..96103f38 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt @@ -30,7 +30,7 @@ class MessageFull( @Ignore var filterWeight = 0 @Ignore - var searchHighlightText: String? = null + var searchHighlightText: CharSequence? = null // metadata var seen = false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt new file mode 100644 index 00000000..e42bf61f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt @@ -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" + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt index 30f40c30..e147dea6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt @@ -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.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog import pl.szczodrzynski.edziennik.utils.Anim import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.colorAttr import kotlin.coroutines.CoroutineContext import kotlin.math.min @@ -64,10 +66,20 @@ class MessageFragment : Fragment(), CoroutineScope { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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( IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_window_close).apply { colorAttr(activity, android.R.attr.textColorSecondary) - sizeDp = 16 + sizeDp = 24 } ) b.closeButton.setOnClickListener { activity.navigateUp() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt index f8ccb491..aded9f4a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt @@ -1,11 +1,8 @@ package pl.szczodrzynski.edziennik.ui.modules.messages import android.graphics.Typeface -import android.text.Editable -import android.text.TextWatcher import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.Filter import android.widget.Filterable import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView @@ -13,22 +10,19 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job 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.full.MessageFull 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.utils.MessagesFilter import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.SearchViewHolder -import java.util.* import kotlin.coroutines.CoroutineContext -import kotlin.math.min class MessagesAdapter( - val activity: AppCompatActivity, - val teachers: List, - val onItemClick: ((item: MessageFull) -> Unit)? = null + val activity: AppCompatActivity, + val teachers: List, + val onItemClick: ((item: MessageFull) -> Unit)? = null ) : RecyclerView.Adapter(), CoroutineScope, Filterable { companion object { private const val TAG = "MessagesAdapter" @@ -43,41 +37,10 @@ class MessagesAdapter( override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main - var items = mutableListOf() - var allItems = mutableListOf() + var items = listOf() + var allItems = listOf() val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } - private val comparator by lazy { Comparator { o1: Any, o2: Any -> - if (o1 !is MessageFull || o2 !is MessageFull) - return@Comparator 0 - when { - // standard sorting - o1.filterWeight > o2.filterWeight -> return@Comparator 1 - o1.filterWeight < o2.filterWeight -> return@Comparator -1 - else -> when { - // reversed sorting - o1.addedDate > o2.addedDate -> return@Comparator -1 - o1.addedDate < o2.addedDate -> return@Comparator 1 - else -> return@Comparator 0 - } - } - }} - - val textWatcher by lazy { - object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - getFilter().filter(s.toString()) - } - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - /*items.getOrNull(0)?.let { - if (it is MessagesSearch) { - it.searchText = s?.toString() ?: "" - } - }*/ - } - } - } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -103,138 +66,16 @@ class MessagesAdapter( return when { - holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this) - holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) + holder is MessageViewHolder + && item is MessageFull -> holder.onBind(activity, app, item, position, this) + holder is SearchViewHolder + && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) } } + private val messagesFilter by lazy { + MessagesFilter(this) + } override fun getItemCount() = items.size - override fun getFilter() = filter - private var prevCount = -1 - private val filter by lazy { object : Filter() { - override fun performFiltering(prefix: CharSequence?): FilterResults { - val results = FilterResults() - - if (prevCount == -1) - prevCount = allItems.size - - if (prefix.isNullOrEmpty()) { - allItems.forEach { - if (it is MessageFull) - it.searchHighlightText = null - } - results.values = allItems.toList() - results.count = allItems.size - return results - } - - val items = mutableListOf() - val prefixString = prefix.toString() - - allItems.forEach { - if (it !is MessageFull) { - items.add(it) - return@forEach - } - it.filterWeight = 100 - it.searchHighlightText = null - - var weight: 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 } - // 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 - } + override fun getFilter() = messagesFilter } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt index add36a3c..1849d2f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt @@ -12,7 +12,9 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Message 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.navlib.bottomsheet.items.BottomSheetPrimaryItem import kotlin.coroutines.CoroutineContext class MessagesFragment : Fragment(), CoroutineScope { @@ -100,9 +102,19 @@ class MessagesFragment : Fragment(), CoroutineScope { 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.gainAttentionFAB() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt index 2e4a30b8..32761515 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt @@ -33,6 +33,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { private lateinit var app: App private lateinit var activity: MainActivity private lateinit var b: MessagesListFragmentBinding + private var adapter: MessagesAdapter? = null private val job: Job = Job() override val coroutineContext: CoroutineContext @@ -53,21 +54,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED) var topPosition = arguments.getInt("topPosition", NO_POSITION) var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION) + val searchText = arguments.getString("searchText", "") teachers = withContext(Dispatchers.Default) { app.db.teacherDao().getAllNow(App.profileId) } - val adapter = MessagesAdapter(activity, teachers) { + adapter = MessagesAdapter(activity, teachers) { activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle( "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 - items.forEach { message -> + messages.forEach { message -> message.recipients?.removeAll { it.profileId != message.profileId } message.recipients?.forEach { recipient -> if (recipient.fullName == null) { @@ -77,13 +79,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { } // load & configure the adapter - adapter.items = items.toMutableList() - adapter.items.add(0, MessagesSearch().also { - it.count = items.size + val items = messages.toMutableList() + items.add(0, MessagesSearch().also { + it.searchText = searchText }) - adapter.allItems = adapter.items.toMutableList() + + adapter?.items = items + adapter?.allItems = items + 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 { setHasFixedSize(true) layoutManager = LinearLayoutManager(context) @@ -92,7 +103,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { addOnScrollListener(onScrollListener) } } - adapter.notifyDataSetChanged() + setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty()) (b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager -> @@ -119,10 +130,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { override fun 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( - "topPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition(), - "bottomPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition() + "topPosition" to layoutManager?.findFirstVisibleItemPosition(), + "bottomPosition" to layoutManager?.findLastCompletelyVisibleItemPosition(), + "searchText" to searchItem?.searchText?.toString() )) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt index 051d86bd..091dabef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt @@ -4,6 +4,7 @@ package pl.szczodrzynski.edziennik.ui.modules.messages.compose +import android.annotation.SuppressLint import android.content.Context import android.graphics.Typeface 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.full.MessageFull 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.getProfileImage 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.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.elevateSurface import kotlin.coroutines.CoroutineContext import kotlin.text.replace @@ -67,6 +70,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { override val coroutineContext: CoroutineContext 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() 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 { delay(100) getRecipientList() @@ -290,7 +307,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.recipients.setIllegalCharacterIdentifier { c -> c.toString().matches("[\\n;:_ ]".toRegex()) } - b.recipients.setOnChipRemoveListener { _ -> + b.recipients.setOnChipRemoveListener { b.recipients.setSelection(b.recipients.text.length) } @@ -318,14 +335,15 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { fabExtendedText = getString(R.string.messages_compose_send) fabIcon = CommunityMaterial.Icon3.cmd_send_outline - setFabOnClickListener(View.OnClickListener { + setFabOnClickListener { sendMessage() - }) + } } activity.gainAttentionFAB() } + @SuppressLint("SetTextI18n") private fun updateRecipientList(list: List) { launch { withContext(Dispatchers.Default) { teachers = list.sortedBy { it.fullName }.toMutableList() @@ -344,11 +362,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { val adapter = MessagesComposeSuggestionAdapter(activity, teachers) b.recipients.setAdapter(adapter) + if (profileConfig.messagesGreetingOnCompose) + b.text.setText(greetingText) + handleReplyMessage() handleMailToIntent() }} - private fun handleReplyMessage() { launch { + private fun handleReplyMessage() = launch { val replyMessage = arguments?.getString("message") if (replyMessage != null) { val chipList = mutableListOf() @@ -370,8 +391,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { if (arguments?.getString("type") == "reply") { // add greeting text - span.replace(0, 0, "\n\nZ poważaniem,\n${app.profile.accountName - ?: app.profile.studentNameLong ?: ""}\n\n\n") + if (profileConfig.messagesGreetingOnReply) + span.replace(0, 0, "$greetingText\n\n\n") + else + span.replace(0, 0, "\n\n") teachers.firstOrNull { it.id == msg.senderId }?.let { teacher -> teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName) @@ -379,7 +402,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { } subject = "Re: ${msg.subject}" } 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}" } body = MessagesUtils.htmlToSpannable(activity, msg.body @@ -401,7 +429,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { else { b.recipients.requestFocus() } - }} + } private fun handleMailToIntent() { val teacherId = arguments?.getLong("messageRecipientId") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt index ad206c62..71ed0486 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt @@ -5,7 +5,5 @@ package pl.szczodrzynski.edziennik.ui.modules.messages.models class MessagesSearch { - var isFocused = false - var searchText = "" - var count = 0 + var searchText: CharSequence = "" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt new file mode 100644 index 00000000..2279b600 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt @@ -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 { + + 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 + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt new file mode 100644 index 00000000..202db386 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt @@ -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() + + 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 + } + // 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 + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt new file mode 100644 index 00000000..7103ff29 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt @@ -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 + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt index 62789a0c..a466d08b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt @@ -22,17 +22,21 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils import pl.szczodrzynski.edziennik.utils.models.Date class MessageViewHolder( - inflater: LayoutInflater, - parent: ViewGroup, - val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) ) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "MessageViewHolder" } - override fun onBind(activity: AppCompatActivity, app: App, item: MessageFull, position: Int, adapter: MessagesAdapter) { - val manager = app.gradesManager - + override fun onBind( + activity: AppCompatActivity, + app: App, + item: MessageFull, + position: Int, + adapter: MessagesAdapter + ) { b.messageSubject.text = item.subject b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort b.messageAttachmentImage.isVisible = item.hasAttachments @@ -55,15 +59,17 @@ class MessageViewHolder( b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) b.messageSender.text = messageInfo.profileName - item.searchHighlightText?.let { highlight -> + item.searchHighlightText?.toString()?.let { highlight -> val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) b.messageSubject.text = b.messageSubject.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true) + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true + ) b.messageSender.text = b.messageSender.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true) + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true + ) } adapter.onItemClick?.let { listener -> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt index 5adb1984..a0a7d69c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt @@ -9,38 +9,43 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.ui.modules.messages.utils.SearchTextWatcher class SearchViewHolder( - inflater: LayoutInflater, - parent: ViewGroup, - val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(inflater, parent, false) + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate( + inflater, + parent, + false + ) ) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "SearchViewHolder" } - override fun onBind(activity: AppCompatActivity, app: App, item: MessagesSearch, position: Int, adapter: MessagesAdapter) { - b.searchEdit.removeTextChangedListener(adapter.textWatcher) - b.searchEdit.addTextChangedListener(adapter.textWatcher) + override fun onBind( + activity: AppCompatActivity, + 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 { - override fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) { - item.isFocused = showing - } - })*/ + if (adapter.items.isEmpty() || adapter.items.size == adapter.allItems.size) + b.searchLayout.helperText = " " + 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.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()*/ + b.searchEdit.addTextChangedListener(watcher) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt index 3dacd82d..8d3de849 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.after import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS 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.bell.BellSyncConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog @@ -73,6 +74,13 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { GradesConfigDialog(activity, reloadOnDismiss = false) }, + util.createActionItem( + text = R.string.menu_messages_config, + icon = CommunityMaterial.Icon.cmd_calendar_outline + ) { + MessagesConfigDialog(activity, reloadOnDismiss = false) + }, + util.createActionItem( text = R.string.menu_attendance_config, icon = CommunityMaterial.Icon.cmd_calendar_remove_outline diff --git a/app/src/main/res/layout/messages_config_dialog.xml b/app/src/main/res/layout/messages_config_dialog.xml new file mode 100644 index 00000000..d0205b39 --- /dev/null +++ b/app/src/main/res/layout/messages_config_dialog.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9196508e..31caf032 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1444,4 +1444,10 @@ {cmd-clipboard-edit-outline} wydarzenie dodane ręcznie {cmd-check} oznaczono jako wykonane Funkcja jeszcze nie jest dostępna. + Tworzenie wiadomości + Dodaj podpis przy tworzeniu wiadomości + Dodaj podpis przy odpowiadaniu na wiadomość + Dodaj podpis przy przekazywaniu wiadomości + Treść podpisu + Ustawienia wiadomości