From 1a543814f4cd967fa7b0f1a0832ee370ce8b6523 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= <kuba@szczodrzynski.pl>
Date: Sun, 10 Oct 2021 19:21:50 +0200
Subject: [PATCH] [UI] Implement homework searching. (#93)

* [Messages] Create message type checking methods.

* [UI] Refactor messages searching to a separate module.

* [Refactor] Move dialogs.event to modules package.

* [Refactor] Move classes from modules.messages to separate packages.

* [Homework] Implement searching homework lists.

* [Homework] Fix highlighting search query in addedBy text.

* [Homework] Workaround IconicsTextView discarding span data.

* [Messages] Make attachments searchable.

* [Events] Show icons for events with attachments.

* [Homework] Workaround IconicsTextView discarding span data, again.

* [Search] Fix serialization crashes with searchable models.

* [Messages] Fix searching in HTML body.
---
 .../szczodrzynski/edziennik/MainActivity.kt   |   6 +-
 .../messages/LibrusMessagesSendMessage.kt     |   2 +-
 .../data/web/MobidziennikWebGetAttachment.kt  |   2 +-
 .../data/web/MobidziennikWebGetMessage.kt     |   6 +-
 .../data/web/MobidziennikWebSendMessage.kt    |   3 +-
 .../hebe/VulcanHebeMessagesChangeStatus.kt    |   3 +-
 .../vulcan/data/hebe/VulcanHebeSendMessage.kt |   2 +-
 .../edziennik/data/db/entity/Event.kt         |   3 +
 .../edziennik/data/db/entity/Message.kt       |   9 ++
 .../edziennik/data/db/full/EventFull.kt       |  44 +++++-
 .../edziennik/data/db/full/MessageFull.kt     |  53 ++++++-
 .../edziennik/ui/dialogs/day/DayDialog.kt     |  18 +--
 .../ui/dialogs/event/EventListAdapter.kt      | 126 -----------------
 .../ui/dialogs/sync/SyncViewListDialog.kt     |   2 +-
 .../dialogs/timetable/LessonDetailsDialog.kt  |   8 +-
 .../ui/modules/agenda/AgendaFragment.kt       |   2 +-
 .../modules/agenda/AgendaFragmentDefault.kt   |   2 +-
 .../event/EventDetailsDialog.kt               |   2 +-
 .../ui/modules/event/EventListAdapter.kt      |  71 ++++++++++
 .../event/EventManualDialog.kt                |   2 +-
 .../ui/modules/event/EventViewHolder.kt       | 133 ++++++++++++++++++
 .../ui/modules/home/cards/HomeEventsCard.kt   |   8 +-
 .../ui/modules/homework/HomeworkFragment.kt   |   2 +-
 .../modules/homework/HomeworkListFragment.kt  |  86 +++++------
 .../ui/modules/messages/MessagesAdapter.kt    |  86 -----------
 .../ui/modules/messages/MessagesUtils.kt      |   5 +-
 .../compose/MessagesComposeFragment.kt        |   4 +-
 .../{viewholder => list}/MessageViewHolder.kt |  41 +++---
 .../modules/messages/list/MessagesAdapter.kt  |  50 +++++++
 .../messages/{ => list}/MessagesFragment.kt   |   2 +-
 .../{ => list}/MessagesListFragment.kt        |  26 +---
 .../modules/messages/models/MessagesSearch.kt |   9 --
 .../messages/{ => single}/MessageFragment.kt  |  14 +-
 .../messages/utils/MessagesComparator.kt      |  28 ----
 .../ui/modules/search/SearchField.kt          |  21 +++
 .../SearchFilter.kt}                          |  91 +++++-------
 .../utils => search}/SearchTextWatcher.kt     |  11 +-
 .../viewholder => search}/SearchViewHolder.kt |  24 +---
 .../edziennik/ui/modules/search/Searchable.kt |  24 ++++
 .../ui/modules/search/SearchableAdapter.kt    | 133 ++++++++++++++++++
 .../ui/modules/timetable/TimetableFragment.kt |   2 +-
 .../edziennik/utils/managers/EventManager.kt  |   3 +-
 .../utils/managers/MessageManager.kt          |   6 +-
 app/src/main/res/layout/event_list_item.xml   |  40 ++++--
 .../main/res/layout/messages_list_item.xml    |   7 +-
 ...s_list_item_search.xml => search_item.xml} |   0
 46 files changed, 728 insertions(+), 494 deletions(-)
 delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/{dialogs => modules}/event/EventDetailsDialog.kt (99%)
 create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventListAdapter.kt
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/{dialogs => modules}/event/EventManualDialog.kt (99%)
 create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventViewHolder.kt
 delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/{viewholder => list}/MessageViewHolder.kt (64%)
 create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesAdapter.kt
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/{ => list}/MessagesFragment.kt (98%)
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/{ => list}/MessagesListFragment.kt (83%)
 delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/{ => single}/MessageFragment.kt (95%)
 delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt
 create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchField.kt
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/{messages/utils/MessagesFilter.kt => search/SearchFilter.kt} (52%)
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/{messages/utils => search}/SearchTextWatcher.kt (75%)
 rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/{messages/viewholder => search}/SearchViewHolder.kt (50%)
 create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/Searchable.kt
 create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchableAdapter.kt
 rename app/src/main/res/layout/{messages_list_item_search.xml => search_item.xml} (100%)

diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
index 48b872e4..b27af5bb 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
@@ -52,7 +52,6 @@ import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileConfigDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
 import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment
@@ -64,15 +63,16 @@ import pl.szczodrzynski.edziennik.ui.modules.debug.DebugFragment
 import pl.szczodrzynski.edziennik.ui.modules.debug.LabFragment
 import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog
 import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
+import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment
 import pl.szczodrzynski.edziennik.ui.modules.grades.GradesListFragment
 import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment
 import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
 import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment
 import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
-import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment
-import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
 import pl.szczodrzynski.edziennik.ui.modules.messages.compose.MessagesComposeFragment
+import pl.szczodrzynski.edziennik.ui.modules.messages.list.MessagesFragment
+import pl.szczodrzynski.edziennik.ui.modules.messages.single.MessageFragment
 import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment
 import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
 import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt
index d568135e..e388f6fd 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt
@@ -48,7 +48,7 @@ class LibrusMessagesSendMessage(override val data: DataLibrus,
             }
 
             LibrusMessagesGetList(data, type = Message.TYPE_SENT, lastSync = null) {
-                val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.id == id }
+                val message = data.messageList.firstOrNull { it.isSent && it.id == id }
                 val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id }
                 val event = MessageSentEvent(data.profileId, message, message?.addedDate)
 
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt
index 52561a81..b8c6644f 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt
@@ -28,7 +28,7 @@ class MobidziennikWebGetAttachment(override val data: DataMobidziennik,
         val targetFile = File(Utils.getStorageDir(), attachmentName)
 
         val typeUrl = when (owner) {
-            is Message -> if (owner.type == Message.TYPE_SENT)
+            is Message -> if (owner.isSent)
                 "dziennik/wiadwyslana/?id="
             else
                 "dziennik/wiadodebrana/?id="
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt
index 542e4488..b23182ba 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt
@@ -10,8 +10,6 @@ import pl.szczodrzynski.edziennik.data.api.Regexes
 import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
 import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
 import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
-import pl.szczodrzynski.edziennik.data.db.entity.Message
-import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
 import pl.szczodrzynski.edziennik.data.db.entity.Metadata
 import pl.szczodrzynski.edziennik.data.db.full.MessageFull
 import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull
@@ -31,7 +29,7 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik,
     }
 
     init {
-        val typeUrl = if (message.type == Message.TYPE_SENT)
+        val typeUrl = if (message.isSent)
             "wiadwyslana"
         else
             "wiadodebrana"
@@ -46,7 +44,7 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik,
 
             val body = content.select(".wiadomosc_tresc").first()
 
-            if (message.type == TYPE_RECEIVED) {
+            if (message.isReceived) {
                 var readDate = System.currentTimeMillis()
                 Regexes.MOBIDZIENNIK_MESSAGE_READ_DATE.find(body.html())?.let {
                     val date = Date(
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt
index 89178b8b..b5a6c5ee 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt
@@ -9,7 +9,6 @@ import pl.szczodrzynski.edziennik.data.api.POST
 import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
 import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
 import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
-import pl.szczodrzynski.edziennik.data.db.entity.Message
 import pl.szczodrzynski.edziennik.data.db.entity.Metadata
 import pl.szczodrzynski.edziennik.data.db.entity.Teacher
 
@@ -43,7 +42,7 @@ class MobidziennikWebSendMessage(override val data: DataMobidziennik,
 
             // TODO create MobidziennikWebMessagesSent and replace this
             MobidziennikWebMessagesAll(data, null) {
-                val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject }
+                val message = data.messageList.firstOrNull { it.isSent && it.subject == subject }
                 val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id }
                 val event = MessageSentEvent(data.profileId, message, message?.addedDate)
 
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt
index ff5dbe4c..a5be03b9 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt
@@ -10,7 +10,6 @@ import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS
 import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
 import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
 import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
-import pl.szczodrzynski.edziennik.data.db.entity.Message
 import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
 import pl.szczodrzynski.edziennik.data.db.entity.Metadata
 import pl.szczodrzynski.edziennik.data.db.full.MessageFull
@@ -48,7 +47,7 @@ class VulcanHebeMessagesChangeStatus(
                 messageObject.seen = true
             }
 
-            if (messageObject.type != Message.TYPE_SENT) {
+            if (!messageObject.isSent) {
                 val messageRecipientObject = MessageRecipient(
                     profileId,
                     -1,
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt
index c6a02ae7..2e7b1491 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt
@@ -87,7 +87,7 @@ class VulcanHebeSendMessage(
             }
 
             VulcanHebeMessages(data, null) {
-                val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject }
+                val message = data.messageList.firstOrNull { it.isSent && it.subject == subject }
                 val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId }
                 val event = MessageSentEvent(data.profileId, message, message?.addedDate)
 
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt
index 4d7d4450..928f0931 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt
@@ -10,6 +10,7 @@ import androidx.room.Index
 import com.google.gson.annotations.SerializedName
 import pl.szczodrzynski.edziennik.MINUTE
 import pl.szczodrzynski.edziennik.data.db.full.EventFull
+import pl.szczodrzynski.edziennik.isNotNullNorEmpty
 import pl.szczodrzynski.edziennik.utils.models.Date
 import pl.szczodrzynski.edziennik.utils.models.Time
 import java.util.*
@@ -97,6 +98,8 @@ open class Event(
      * or the topic contains the body already.
      */
     var homeworkBody: String? = null
+    val hasAttachments
+        get() = attachmentIds.isNotNullNorEmpty()
     var attachmentIds: MutableList<Long>? = null
     var attachmentNames: MutableList<String>? = null
 
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt
index c115c3d3..927e6d1d 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt
@@ -43,6 +43,15 @@ open class Message(
     @ColumnInfo(name = "messageIsPinned")
     var isStarred: Boolean = false
 
+    val isReceived
+        get() = type == TYPE_RECEIVED
+    val isSent
+        get() = type == TYPE_SENT
+    val isDeleted
+        get() = type == TYPE_DELETED
+    val isDraft
+        get() = type == TYPE_DRAFT
+
     var hasAttachments = false // if the attachments are not yet downloaded but we already know there are some
         get() = field || attachmentIds.isNotNullNorEmpty()
     var attachmentIds: MutableList<Long>? = null
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt
index 86a5ab92..c68ecedd 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt
@@ -3,8 +3,10 @@
  */
 package pl.szczodrzynski.edziennik.data.db.full
 
+import androidx.room.Ignore
 import pl.szczodrzynski.edziennik.data.db.entity.Event
 import pl.szczodrzynski.edziennik.data.db.entity.Metadata
+import pl.szczodrzynski.edziennik.ui.modules.search.Searchable
 import pl.szczodrzynski.edziennik.utils.models.Date
 import pl.szczodrzynski.edziennik.utils.models.Time
 
@@ -16,7 +18,7 @@ class EventFull(
         profileId, id, date, time,
         topic, color, type,
         teacherId, subjectId, teamId, addedDate
-) {
+), Searchable<EventFull> {
     constructor(event: Event, metadata: Metadata? = null) : this(
             event.profileId, event.id, event.date, event.time,
             event.topic, event.color, event.type,
@@ -46,6 +48,46 @@ class EventFull(
     var teamName: String? = null
     var teamCode: String? = null
 
+    @Ignore
+    @Transient
+    override var searchPriority = 0
+
+    @Ignore
+    @Transient
+    override var searchHighlightText: String? = null
+
+    @delegate:Ignore
+    @delegate:Transient
+    override val searchKeywords by lazy {
+        listOf(
+            listOf(topic, homeworkBody),
+            attachmentNames,
+            listOf(subjectLongName),
+            listOf(teacherName),
+            listOf(sharedByName),
+        )
+    }
+
+    override fun compareTo(other: Searchable<*>): Int {
+        if (other !is EventFull)
+            return 0
+        return when {
+            // ascending sorting
+            searchPriority > other.searchPriority -> 1
+            searchPriority < other.searchPriority -> -1
+            // ascending sorting
+            date > other.date -> 1
+            date < other.date -> -1
+            // ascending sorting
+            (time?.value ?: 0) > (other.time?.value ?: 0) -> 1
+            (time?.value ?: 0) < (other.time?.value ?: 0) -> -1
+            // ascending sorting
+            addedDate > other.addedDate -> 1
+            addedDate < other.addedDate -> -1
+            else -> 0
+        }
+    }
+
     // metadata
     var seen = false
     var notified = false
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 3f912711..534ad11c 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt
@@ -3,10 +3,12 @@
  */
 package pl.szczodrzynski.edziennik.data.db.full
 
+import androidx.core.text.HtmlCompat
 import androidx.room.Ignore
 import androidx.room.Relation
 import pl.szczodrzynski.edziennik.data.db.entity.Message
 import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
+import pl.szczodrzynski.edziennik.ui.modules.search.Searchable
 
 class MessageFull(
         profileId: Int, id: Long, type: Int,
@@ -16,7 +18,7 @@ class MessageFull(
         profileId, id, type,
         subject, body, senderId,
         addedDate
-) {
+), Searchable<MessageFull> {
     var senderName: String? = null
     @Relation(parentColumn = "messageId", entityColumn = "messageId", entity = MessageRecipient::class)
     var recipients: MutableList<MessageRecipientFull>? = null
@@ -27,11 +29,56 @@ class MessageFull(
         return this
     }
 
+    @delegate:Ignore
+    @delegate:Transient
+    val bodyHtml by lazy {
+        body?.let {
+            HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY)
+        }
+    }
+
+
     @Ignore
-    var filterWeight = 0
+    @Transient
+    override var searchPriority = 0
+
     @Ignore
-    var searchHighlightText: CharSequence? = null
+    @Transient
+    override var searchHighlightText: String? = null
+
+    @delegate:Ignore
+    @delegate:Transient
+    override val searchKeywords by lazy {
+        listOf(
+            when {
+                isSent -> recipients?.map { it.fullName }
+                else -> listOf(senderName)
+            },
+            listOf(subject),
+            listOf(bodyHtml?.toString()),
+            attachmentNames,
+        )
+    }
+
+    override fun compareTo(other: Searchable<*>): Int {
+        if (other !is MessageFull)
+            return 0
+        return when {
+            // ascending sorting
+            searchPriority > other.searchPriority -> 1
+            searchPriority < other.searchPriority -> -1
+            // descending sorting (1. true, 2. false)
+            isStarred && !other.isStarred -> -1
+            !isStarred && other.isStarred -> 1
+            // descending sorting
+            addedDate > other.addedDate -> -1
+            addedDate < other.addedDate -> 1
+            else -> 0
+        }
+    }
+
     @Ignore
+    @Transient
     var readByEveryone = true
 
     // metadata
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt
index 74ba0030..55850a0e 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt
@@ -14,15 +14,15 @@ import kotlinx.coroutines.*
 import pl.szczodrzynski.edziennik.*
 import pl.szczodrzynski.edziennik.data.db.entity.Lesson
 import pl.szczodrzynski.edziennik.databinding.DialogDayBinding
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog
 import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEvent
 import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEventRenderer
 import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent
 import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer
+import pl.szczodrzynski.edziennik.ui.modules.event.EventDetailsDialog
+import pl.szczodrzynski.edziennik.ui.modules.event.EventListAdapter
+import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog
 import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
 import pl.szczodrzynski.edziennik.utils.models.Date
 import pl.szczodrzynski.edziennik.utils.models.Time
@@ -161,7 +161,7 @@ class DayDialog(
         b.teacherAbsenceFrame.isVisible = teacherAbsences.isNotEmpty()
 
         adapter = EventListAdapter(
-                activity,
+                activity = activity,
                 showWeekDay = false,
                 showDate = false,
                 showType = true,
@@ -188,10 +188,12 @@ class DayDialog(
         )
 
         app.db.eventDao().getAllByDate(profileId, date).observe(activity) { events ->
-            adapter.items = if (eventTypeId != null)
-                events.filter { it.type == eventTypeId }
-            else
-                events
+            adapter.setAllItems(
+                if (eventTypeId != null)
+                    events.filter { it.type == eventTypeId }
+                else
+                    events,
+            )
 
             if (b.eventsView.adapter == null) {
                 b.eventsView.adapter = adapter
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt
deleted file mode 100644
index 9bb199ea..00000000
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (c) Kacper Ziubryniewicz 2019-11-30
- */
-
-package pl.szczodrzynski.edziennik.ui.dialogs.event
-
-import android.content.Context
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import pl.szczodrzynski.edziennik.*
-import pl.szczodrzynski.edziennik.data.db.full.EventFull
-import pl.szczodrzynski.edziennik.databinding.EventListItemBinding
-import pl.szczodrzynski.edziennik.utils.models.Date
-import pl.szczodrzynski.edziennik.utils.models.Week
-import kotlin.coroutines.CoroutineContext
-
-class EventListAdapter(
-        val context: Context,
-        val simpleMode: Boolean = false,
-        val showWeekDay: Boolean = false,
-        val showDate: Boolean = false,
-        val showType: Boolean = true,
-        val showTime: Boolean = true,
-        val showSubject: Boolean = true,
-        val markAsSeen: Boolean = true,
-        val onItemClick: ((event: EventFull) -> Unit)? = null,
-        val onEventEditClick: ((event: EventFull) -> Unit)? = null
-) : RecyclerView.Adapter<EventListAdapter.ViewHolder>(), CoroutineScope {
-
-    private val app = context.applicationContext as App
-    private val manager
-        get() = app.eventManager
-
-    private val job = Job()
-    override val coroutineContext: CoroutineContext
-        get() = job + Dispatchers.Main
-
-    var items = listOf<EventFull>()
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
-        val inflater = LayoutInflater.from(parent.context)
-        val view = EventListItemBinding.inflate(inflater, parent, false)
-        return ViewHolder(view)
-    }
-
-    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
-        val event = items[position]
-        val b = holder.b
-        val manager = app.eventManager
-
-        b.root.onClick {
-            onItemClick?.invoke(event)
-            if (!event.seen) {
-                manager.markAsSeen(event)
-            }
-            if (event.showAsUnseen == true) {
-                event.showAsUnseen = false
-                notifyItemChanged(event)
-            }
-        }
-
-        val bullet = " • "
-
-        b.simpleMode = simpleMode
-
-        manager.setEventTopic(b.topic, event, showType = false)
-        b.topic.maxLines = if (simpleMode) 2 else 3
-
-        b.details.text = mutableListOf<CharSequence?>(
-                if (showWeekDay) Week.getFullDayName(event.date.weekDay) else null,
-                if (showDate) event.date.getRelativeString(context, 7) ?: event.date.formattedStringShort else null,
-                if (showType) event.typeName else null,
-                if (showTime) event.time?.stringHM ?: app.getString(R.string.event_all_day) else null,
-                if (showSubject) event.subjectLongName else null
-        ).concat(bullet)
-
-        b.addedBy.setText(
-                when (event.sharedBy) {
-                    null -> when {
-                        event.addedManually -> R.string.event_list_added_by_self_format
-                        event.teacherName == null -> R.string.event_list_added_by_unknown_format
-                        else -> R.string.event_list_added_by_format
-                    }
-                    "self" -> R.string.event_list_shared_by_self_format
-                    else -> R.string.event_list_shared_by_format
-                },
-                Date.fromMillis(event.addedDate).formattedString,
-                event.sharedByName ?: event.teacherName ?: "",
-                event.teamName?.let { bullet+it } ?: ""
-        )
-
-        b.typeColor.background?.setTintColor(event.eventColor)
-        b.typeColor.isVisible = showType
-
-        b.editButton.isVisible = !simpleMode && event.addedManually && !event.isDone
-        b.editButton.onClick {
-            onEventEditClick?.invoke(event)
-        }
-        b.editButton.attachToastHint(R.string.hint_edit_event)
-
-        if (event.showAsUnseen == null)
-            event.showAsUnseen = !event.seen
-
-        b.unread.isVisible = event.showAsUnseen == true
-        if (markAsSeen && !event.seen) {
-            manager.markAsSeen(event)
-        }
-    }
-
-    private fun notifyItemChanged(model: Any) {
-        startCoroutineTimer(1000L, 0L) {
-            val index = items.indexOf(model)
-            if (index != -1)
-                notifyItemChanged(index)
-        }
-    }
-
-    override fun getItemCount() = items.size
-
-    class ViewHolder(val b: EventListItemBinding) : RecyclerView.ViewHolder(b.root)
-}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt
index b9cbd0a0..96a72cb5 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt
@@ -14,7 +14,7 @@ import pl.szczodrzynski.edziennik.MainActivity
 import pl.szczodrzynski.edziennik.R
 import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
 import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
-import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
+import pl.szczodrzynski.edziennik.ui.modules.messages.list.MessagesFragment
 import kotlin.coroutines.CoroutineContext
 
 class SyncViewListDialog(
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt
index 2df41c36..5662b1a0 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt
@@ -26,10 +26,10 @@ import pl.szczodrzynski.edziennik.data.db.full.LessonFull
 import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
 import pl.szczodrzynski.edziennik.onClick
 import pl.szczodrzynski.edziennik.setText
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceDetailsDialog
+import pl.szczodrzynski.edziennik.ui.modules.event.EventDetailsDialog
+import pl.szczodrzynski.edziennik.ui.modules.event.EventListAdapter
+import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
 import pl.szczodrzynski.edziennik.utils.BetterLink
 import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
@@ -228,7 +228,7 @@ class LessonDetailsDialog(
         )
 
         app.db.eventDao().getAllByDateTime(lesson.profileId, lessonDate, lessonTime).observe(activity, Observer { events ->
-            adapter.items = events
+            adapter.setAllItems(events)
             if (b.eventsView.adapter == null) {
                 b.eventsView.adapter = adapter
                 b.eventsView.apply {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt
index 8607217a..949e36cc 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt
@@ -28,7 +28,7 @@ import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding
 import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding
 import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
+import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog
 import pl.szczodrzynski.edziennik.utils.Themes
 import pl.szczodrzynski.edziennik.utils.models.Date
 import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt
index 9167fb9a..68240e0b 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt
@@ -22,7 +22,6 @@ import pl.szczodrzynski.edziennik.MainActivity
 import pl.szczodrzynski.edziennik.data.db.full.EventFull
 import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding
 import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog
 import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEvent
@@ -33,6 +32,7 @@ import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesE
 import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEventRenderer
 import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent
 import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer
+import pl.szczodrzynski.edziennik.ui.modules.event.EventDetailsDialog
 import pl.szczodrzynski.edziennik.utils.models.Date
 import java.util.*
 
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt
similarity index 99%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt
index 3c4be7bd..ae848edb 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt
@@ -2,7 +2,7 @@
  * Copyright (c) Kuba Szczodrzyński 2019-12-18.
  */
 
-package pl.szczodrzynski.edziennik.ui.dialogs.event
+package pl.szczodrzynski.edziennik.ui.modules.event
 
 import android.content.ActivityNotFoundException
 import android.content.Intent
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventListAdapter.kt
new file mode 100644
index 00000000..40de3145
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventListAdapter.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) Kacper Ziubryniewicz 2019-11-30
+ */
+
+package pl.szczodrzynski.edziennik.ui.modules.event
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import pl.szczodrzynski.edziennik.App
+import pl.szczodrzynski.edziennik.data.db.full.EventFull
+import pl.szczodrzynski.edziennik.startCoroutineTimer
+import pl.szczodrzynski.edziennik.ui.modules.search.SearchableAdapter
+import kotlin.coroutines.CoroutineContext
+
+class EventListAdapter(
+    val activity: AppCompatActivity,
+    val simpleMode: Boolean = false,
+    val showWeekDay: Boolean = false,
+    val showDate: Boolean = false,
+    val showType: Boolean = true,
+    val showTime: Boolean = true,
+    val showSubject: Boolean = true,
+    val markAsSeen: Boolean = true,
+    isReversed: Boolean = false,
+    val onItemClick: ((event: EventFull) -> Unit)? = null,
+    val onEventEditClick: ((event: EventFull) -> Unit)? = null,
+) : SearchableAdapter<EventFull>(isReversed), CoroutineScope {
+    companion object {
+        private const val TAG = "EventListAdapter"
+        private const val ITEM_TYPE_EVENT = 0
+    }
+
+    private val app = activity.applicationContext as App
+    private val manager
+        get() = app.eventManager
+
+    private val job = Job()
+    override val coroutineContext: CoroutineContext
+        get() = job + Dispatchers.Main
+
+    override fun getItemViewType(item: EventFull) = ITEM_TYPE_EVENT
+
+    override fun onBindViewHolder(
+        holder: RecyclerView.ViewHolder,
+        position: Int,
+        item: EventFull,
+    ) {
+        if (holder !is EventViewHolder)
+            return
+        holder.onBind(activity, app, item, position, this)
+    }
+
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup,
+        viewType: Int,
+    ) = EventViewHolder(inflater, parent)
+
+    internal fun notifyItemChanged(model: Any) {
+        startCoroutineTimer(1000L, 0L) {
+            val index = items.indexOf(model)
+            if (index != -1)
+                notifyItemChanged(index)
+        }
+    }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt
similarity index 99%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt
index da684520..5385ee96 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt
@@ -2,7 +2,7 @@
  * Copyright (c) Kuba Szczodrzyński 2019-11-12.
  */
 
-package pl.szczodrzynski.edziennik.ui.dialogs.event
+package pl.szczodrzynski.edziennik.ui.modules.event
 
 import android.view.View
 import android.widget.Toast
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventViewHolder.kt
new file mode 100644
index 00000000..8eb4f638
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventViewHolder.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2021-10-10.
+ */
+
+package pl.szczodrzynski.edziennik.ui.modules.event
+
+import android.text.SpannableString
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import com.mikepenz.iconics.utils.buildIconics
+import pl.szczodrzynski.edziennik.*
+import pl.szczodrzynski.edziennik.data.db.full.EventFull
+import pl.szczodrzynski.edziennik.databinding.EventListItemBinding
+import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
+import pl.szczodrzynski.edziennik.utils.models.Date
+import pl.szczodrzynski.edziennik.utils.models.Week
+
+class EventViewHolder(
+    inflater: LayoutInflater,
+    parent: ViewGroup,
+    val b: EventListItemBinding = EventListItemBinding.inflate(inflater, parent, false),
+) : RecyclerView.ViewHolder(b.root), BindableViewHolder<EventFull, EventListAdapter> {
+    companion object {
+        private const val TAG = "EventViewHolder"
+    }
+
+    override fun onBind(
+        activity: AppCompatActivity,
+        app: App,
+        item: EventFull,
+        position: Int,
+        adapter: EventListAdapter,
+    ) {
+        val manager = app.eventManager
+
+        b.root.onClick {
+            adapter.onItemClick?.invoke(item)
+            if (!item.seen) {
+                manager.markAsSeen(item)
+            }
+            if (item.showAsUnseen == true) {
+                item.showAsUnseen = false
+                adapter.notifyItemChanged(item)
+            }
+        }
+
+        val bullet = " • "
+        val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity)
+
+        b.simpleMode = adapter.simpleMode
+
+        manager.setEventTopic(b.topic, item, showType = false)
+        b.topic.text = SpannableString(
+            adapter.highlightSearchText(
+                item = item,
+                text = b.topic.text,
+                color = colorHighlight
+            )
+        ).buildIconics()
+        b.topic.maxLines = if (adapter.simpleMode) 2 else 3
+
+        b.details.text = mutableListOf(
+            if (adapter.showWeekDay)
+                Week.getFullDayName(item.date.weekDay)
+            else null,
+            if (adapter.showDate)
+                item.date.getRelativeString(activity, 7) ?: item.date.formattedStringShort
+            else null,
+            if (adapter.showType)
+                item.typeName
+            else null,
+            if (adapter.showTime)
+                item.time?.stringHM ?: app.getString(R.string.event_all_day)
+            else null,
+            if (adapter.showSubject)
+                adapter.highlightSearchText(
+                    item = item,
+                    text = item.subjectLongName ?: "",
+                    color = colorHighlight
+                )
+            else null,
+        ).concat(bullet)
+
+        val addedBy = item.sharedByName ?: item.teacherName ?: ""
+        b.addedBy.setText(
+            when (item.sharedBy) {
+                null -> when {
+                    item.addedManually -> R.string.event_list_added_by_self_format
+                    item.teacherName == null -> R.string.event_list_added_by_unknown_format
+                    else -> R.string.event_list_added_by_format
+                }
+                "self" -> R.string.event_list_shared_by_self_format
+                else -> R.string.event_list_shared_by_format
+            },
+            /* 1$ */
+            Date.fromMillis(item.addedDate).formattedString,
+            /* 2$ */
+            addedBy,
+            /* 3$ */
+            item.teamName?.let { bullet + it } ?: "",
+        )
+        val addedBySpanned = adapter.highlightSearchText(
+            item = item,
+            text = addedBy,
+            color = colorHighlight
+        )
+        b.addedBy.text = SpannableString(
+            b.addedBy.text.replace(addedBy, addedBySpanned)
+        ).buildIconics()
+
+        b.attachmentIcon.isVisible = item.hasAttachments
+
+        b.typeColor.background?.setTintColor(item.eventColor)
+        b.typeColor.isVisible = adapter.showType
+
+        b.editButton.isVisible = !adapter.simpleMode && item.addedManually && !item.isDone
+        b.editButton.onClick {
+            adapter.onEventEditClick?.invoke(item)
+        }
+        b.editButton.attachToastHint(R.string.hint_edit_event)
+
+        if (item.showAsUnseen == null)
+            item.showAsUnseen = !item.seen
+
+        b.unread.isVisible = item.showAsUnseen == true
+        if (adapter.markAsSeen && !item.seen) {
+            manager.markAsSeen(item)
+        }
+    }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt
index 9a831c0f..f92e86b8 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt
@@ -22,9 +22,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile
 import pl.szczodrzynski.edziennik.databinding.CardHomeEventsBinding
 import pl.szczodrzynski.edziennik.dp
 import pl.szczodrzynski.edziennik.onClick
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
+import pl.szczodrzynski.edziennik.ui.modules.event.EventDetailsDialog
+import pl.szczodrzynski.edziennik.ui.modules.event.EventListAdapter
+import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard
 import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter
 import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
@@ -82,7 +82,7 @@ class HomeEventsCard(
         )
 
         app.db.eventDao().getNearestNotDone(profile.id, Date.getToday(), 4).observe(activity, Observer { events ->
-            adapter.items = events
+            adapter.setAllItems(events)
             if (b.eventsView.adapter == null) {
                 b.eventsView.adapter = adapter
                 b.eventsView.apply {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt
index c8a62def..54763fe7 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt
@@ -20,8 +20,8 @@ import pl.szczodrzynski.edziennik.*
 import pl.szczodrzynski.edziennik.data.db.entity.Event
 import pl.szczodrzynski.edziennik.data.db.entity.Metadata
 import pl.szczodrzynski.edziennik.databinding.HomeworkFragmentBinding
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter
+import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog
 import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
 import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
 import kotlin.coroutines.CoroutineContext
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt
index 1852c340..8c1e8b7e 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt
@@ -13,10 +13,10 @@ import kotlinx.coroutines.Job
 import pl.szczodrzynski.edziennik.*
 import pl.szczodrzynski.edziennik.data.db.entity.Event
 import pl.szczodrzynski.edziennik.databinding.HomeworkListFragmentBinding
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
+import pl.szczodrzynski.edziennik.ui.modules.event.EventDetailsDialog
+import pl.szczodrzynski.edziennik.ui.modules.event.EventListAdapter
+import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog
 import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
 import pl.szczodrzynski.edziennik.utils.models.Date
 import kotlin.coroutines.CoroutineContext
@@ -54,57 +54,57 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope {
         }
 
         val adapter = EventListAdapter(
-                activity,
-                showWeekDay = true,
-                showDate = true,
-                showType = false,
-                showTime = true,
-                showSubject = true,
-                markAsSeen = true,
-                onItemClick = {
-                    EventDetailsDialog(
-                            activity,
-                            it
-                    )
-                },
-                onEventEditClick = {
-                    EventManualDialog(
-                            activity,
-                            it.profileId,
-                            editingEvent = it
-                    )
-                }
+            activity,
+            showWeekDay = true,
+            showDate = true,
+            showType = false,
+            showTime = true,
+            showSubject = true,
+            markAsSeen = true,
+            isReversed = homeworkDate == HomeworkDate.PAST,
+            onItemClick = {
+                EventDetailsDialog(
+                    activity,
+                    it
+                )
+            },
+            onEventEditClick = {
+                EventManualDialog(
+                    activity,
+                    it.profileId,
+                    editingEvent = it
+                )
+            }
         )
 
-        app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { items ->
+        app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { events ->
             if (!isAdded) return@Observer
 
-            // load & configure the adapter
-            adapter.items = items
-            if (items.isNotNullNorEmpty() && b.list.adapter == null) {
-                b.list.adapter = adapter
+            // show/hide relevant views
+            setSwipeToRefresh(events.isEmpty())
+            b.progressBar.isVisible = false
+            b.list.isVisible = events.isNotEmpty()
+            b.noData.isVisible = events.isEmpty()
+            if (events.isEmpty()) {
+                return@Observer
+            }
+
+            // apply the new event list
+            adapter.setAllItems(events, addSearchField = true)
+
+            // configure the adapter & recycler view
+            if (b.list.adapter == null) {
                 b.list.apply {
                     setHasFixedSize(true)
-                    layoutManager = LinearLayoutManager(context).apply {
-                        reverseLayout = homeworkDate == HomeworkDate.PAST
-                        stackFromEnd = homeworkDate == HomeworkDate.PAST
-                    }
+                    layoutManager = LinearLayoutManager(context)
                     addItemDecoration(SimpleDividerItemDecoration(context))
                     addOnScrollListener(onScrollListener)
+                    this.adapter = adapter
                 }
             }
-            adapter.notifyDataSetChanged()
-            setSwipeToRefresh(items.isNullOrEmpty())
 
-            // show/hide relevant views
-            b.progressBar.isVisible = false
-            if (items.isNullOrEmpty()) {
-                b.list.isVisible = false
-                b.noData.isVisible = true
-            } else {
-                b.list.isVisible = true
-                b.noData.isVisible = false
-            }
+            // reapply the filter
+            adapter.getSearchField()?.applyTo(adapter)
         })
     }; return true }
 }
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
deleted file mode 100644
index 16faa588..00000000
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package pl.szczodrzynski.edziennik.ui.modules.messages
-
-import android.graphics.Typeface
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import android.widget.Filterable
-import androidx.appcompat.app.AppCompatActivity
-import androidx.recyclerview.widget.RecyclerView
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import pl.szczodrzynski.edziennik.App
-import pl.szczodrzynski.edziennik.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 kotlin.coroutines.CoroutineContext
-
-class MessagesAdapter(
-    val activity: AppCompatActivity,
-    val teachers: List<Teacher>,
-    val onItemClick: ((item: MessageFull) -> Unit)? = null,
-    val onStarClick: ((item: MessageFull) -> Unit)? = null,
-) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope, Filterable {
-    companion object {
-        private const val TAG = "MessagesAdapter"
-        private const val ITEM_TYPE_MESSAGE = 0
-        private const val ITEM_TYPE_SEARCH = 1
-    }
-
-    private val app = activity.applicationContext as App
-    // optional: place the manager here
-    internal val manager
-        get() = app.messageManager
-
-    private val job = Job()
-    override val coroutineContext: CoroutineContext
-        get() = job + Dispatchers.Main
-
-    // mutable var changed by the filter
-    var items = listOf<Any>()
-    // mutable list managed by the fragment
-    val allItems = mutableListOf<Any>()
-    val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) }
-    val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
-        val inflater = LayoutInflater.from(parent.context)
-        return when (viewType) {
-            ITEM_TYPE_MESSAGE -> MessageViewHolder(inflater, parent)
-            ITEM_TYPE_SEARCH -> SearchViewHolder(inflater, parent)
-            else -> throw IllegalArgumentException("Incorrect viewType")
-        }
-    }
-
-    override fun getItemViewType(position: Int): Int {
-        return when (items[position]) {
-            is MessageFull -> ITEM_TYPE_MESSAGE
-            is MessagesSearch -> ITEM_TYPE_SEARCH
-            else -> throw IllegalArgumentException("Incorrect viewType")
-        }
-    }
-
-    @Suppress("DEPRECATION")
-    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
-        val item = items[position]
-        if (holder !is BindableViewHolder<*, *>)
-            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)
-        }
-    }
-
-    private val messagesFilter by lazy {
-        MessagesFilter(this)
-    }
-    override fun getItemCount() = items.size
-    override fun getFilter() = messagesFilter
-}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt
index 07a0769a..d61604d8 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt
@@ -9,7 +9,6 @@ import android.text.Spanned
 import androidx.core.graphics.ColorUtils
 import pl.szczodrzynski.edziennik.App
 import pl.szczodrzynski.edziennik.R
-import pl.szczodrzynski.edziennik.data.db.entity.Message
 import pl.szczodrzynski.edziennik.data.db.full.MessageFull
 import pl.szczodrzynski.edziennik.fixName
 import pl.szczodrzynski.edziennik.getNameInitials
@@ -123,10 +122,10 @@ object MessagesUtils {
     fun getMessageInfo(app: App, message: MessageFull, diameterDp: Int, textSizeBigDp: Int, textSizeMediumDp: Int, textSizeSmallDp: Int): MessageInfo {
         var profileImage: Bitmap? = null
         var profileName: String? = null
-        if (message.type == Message.TYPE_RECEIVED || message.type == Message.TYPE_DELETED) {
+        if (message.isReceived || message.isDeleted) {
             profileName = message.senderName?.fixName()
             profileImage = getProfileImage(diameterDp, textSizeBigDp, textSizeMediumDp, textSizeSmallDp, 1, profileName)
-        } else if (message.type == Message.TYPE_SENT || message.type == Message.TYPE_DRAFT && message.recipients != null) {
+        } else if (message.isSent || message.isDraft && message.recipients != null) {
             when (val count = message.recipients?.size ?: 0) {
                 0 -> {
                     profileName = app.getString(R.string.messages_draft_title)
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 fcba191b..bd468ea7 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
@@ -37,7 +37,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message
 import pl.szczodrzynski.edziennik.data.db.entity.Teacher
 import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
 import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
-import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
+import pl.szczodrzynski.edziennik.ui.modules.messages.list.MessagesFragment
 import pl.szczodrzynski.edziennik.utils.Themes
 import pl.szczodrzynski.edziennik.utils.managers.MessageManager.UIConfig
 import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig
@@ -398,7 +398,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
         b.recipients.setAdapter(adapter)
 
         val message = manager.fillWithBundle(uiConfig, arguments)
-        if (message != null && message.type == Message.TYPE_DRAFT) {
+        if (message != null && message.isDraft) {
             draftMessageId = message.id
             if (discardDraftItem != null)
                 activity.bottomSheet.addItemAt(2, discardDraftItem!!)
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/list/MessageViewHolder.kt
similarity index 64%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessageViewHolder.kt
index f59bee59..230992c2 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/list/MessageViewHolder.kt
@@ -2,29 +2,24 @@
  * Copyright (c) Kuba Szczodrzyński 2020-4-5.
  */
 
-package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder
+package pl.szczodrzynski.edziennik.ui.modules.messages.list
 
-import android.graphics.Typeface
-import android.text.style.BackgroundColorSpan
-import android.text.style.StyleSpan
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.isVisible
 import androidx.recyclerview.widget.RecyclerView
 import pl.szczodrzynski.edziennik.*
-import pl.szczodrzynski.edziennik.data.db.entity.Message
 import pl.szczodrzynski.edziennik.data.db.full.MessageFull
 import pl.szczodrzynski.edziennik.databinding.MessagesListItemBinding
 import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
-import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter
 import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
 import pl.szczodrzynski.edziennik.utils.models.Date
 
 class MessageViewHolder(
     inflater: LayoutInflater,
     parent: ViewGroup,
-    val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false)
+    val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false),
 ) : RecyclerView.ViewHolder(b.root), BindableViewHolder<MessageFull, MessagesAdapter> {
     companion object {
         private const val TAG = "MessageViewHolder"
@@ -35,16 +30,14 @@ class MessageViewHolder(
         app: App,
         item: MessageFull,
         position: Int,
-        adapter: MessagesAdapter
+        adapter: MessagesAdapter,
     ) {
-        b.messageSubject.text = item.subject
         b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort
         b.messageAttachmentImage.isVisible = item.hasAttachments
 
-        val text = item.body?.take(200) ?: ""
-        b.messageBody.text = MessagesUtils.htmlToSpannable(activity, text)
+        b.messageBody.text = item.bodyHtml?.take(200)
 
-        val isRead = item.type == Message.TYPE_SENT || item.type == Message.TYPE_DRAFT || item.seen
+        val isRead = item.isSent || item.isDraft || item.seen
         val typeface = if (isRead) adapter.typefaceNormal else adapter.typefaceBold
         val style = if (isRead) R.style.NavView_TextView_Small else R.style.NavView_TextView_Normal
         // set text styles
@@ -62,20 +55,18 @@ class MessageViewHolder(
 
         val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12)
         b.messageProfileBackground.setImageBitmap(messageInfo.profileImage)
-        b.messageSender.text = messageInfo.profileName
 
-        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
-            )
-            b.messageSender.text = b.messageSender.text.asSpannable(
-                StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
-                substring = highlight, ignoreCase = true, ignoreDiacritics = true
-            )
-        }
+        val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity)
+        b.messageSubject.text = adapter.highlightSearchText(
+            item = item,
+            text = item.subject,
+            color = colorHighlight
+        )
+        b.messageSender.text = adapter.highlightSearchText(
+            item = item,
+            text = messageInfo.profileName ?: "",
+            color = colorHighlight
+        )
 
         adapter.onItemClick?.let { listener ->
             b.root.onClick { listener(item) }
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesAdapter.kt
new file mode 100644
index 00000000..1061ac86
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesAdapter.kt
@@ -0,0 +1,50 @@
+package pl.szczodrzynski.edziennik.ui.modules.messages.list
+
+import android.graphics.Typeface
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.RecyclerView
+import pl.szczodrzynski.edziennik.App
+import pl.szczodrzynski.edziennik.data.db.entity.Teacher
+import pl.szczodrzynski.edziennik.data.db.full.MessageFull
+import pl.szczodrzynski.edziennik.ui.modules.search.SearchableAdapter
+
+class MessagesAdapter(
+    val activity: AppCompatActivity,
+    val teachers: List<Teacher>,
+    val onItemClick: ((item: MessageFull) -> Unit)? = null,
+    val onStarClick: ((item: MessageFull) -> Unit)? = null,
+) : SearchableAdapter<MessageFull>() {
+    companion object {
+        private const val TAG = "MessagesAdapter"
+        private const val ITEM_TYPE_MESSAGE = 0
+    }
+
+    private val app = activity.applicationContext as App
+
+    // optional: place the manager here
+    internal val manager
+        get() = app.messageManager
+
+    val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) }
+    val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
+
+    override fun getItemViewType(item: MessageFull) = ITEM_TYPE_MESSAGE
+
+    override fun onBindViewHolder(
+        holder: RecyclerView.ViewHolder,
+        position: Int,
+        item: MessageFull,
+    ) {
+        if (holder !is MessageViewHolder)
+            return
+        holder.onBind(activity, app, item, position, this)
+    }
+
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup,
+        viewType: Int,
+    ) = MessageViewHolder(inflater, parent)
+}
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/list/MessagesFragment.kt
similarity index 98%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesFragment.kt
index d35ec67a..faf0e3ee 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/list/MessagesFragment.kt
@@ -1,4 +1,4 @@
-package pl.szczodrzynski.edziennik.ui.modules.messages
+package pl.szczodrzynski.edziennik.ui.modules.messages.list
 
 import android.os.Bundle
 import android.view.LayoutInflater
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/list/MessagesListFragment.kt
similarity index 83%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesListFragment.kt
index 314aee31..19190e58 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/list/MessagesListFragment.kt
@@ -2,7 +2,7 @@
  * Copyright (c) Kuba Szczodrzyński 2020-4-4.
  */
 
-package pl.szczodrzynski.edziennik.ui.modules.messages
+package pl.szczodrzynski.edziennik.ui.modules.messages.list
 
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -17,10 +17,8 @@ import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_MESSAGES_COMPOSE
 import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_MESSAGES_DETAILS
 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.databinding.MessagesListFragmentBinding
 import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
-import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
 import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
 import kotlin.coroutines.CoroutineContext
 
@@ -63,7 +61,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
 
         adapter = MessagesAdapter(activity, teachers, onItemClick = {
             val (target, args) =
-                if (it.type == Message.TYPE_DRAFT) {
+                if (it.isDraft) {
                     TARGET_MESSAGES_COMPOSE to Bundle("message" to app.gson.toJson(it))
                 } else {
                     TARGET_MESSAGES_DETAILS to Bundle("messageId" to it.id)
@@ -98,17 +96,8 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
                 return@Observer
             }
 
-            if (adapter.allItems.isEmpty()) {
-                // items empty - add the search field
-                adapter.allItems += MessagesSearch().also {
-                    it.searchText = searchText ?: ""
-                }
-            } else {
-                // items not empty - remove all messages
-                adapter.allItems.removeAll { it is MessageFull }
-            }
-            // add all messages
-            adapter.allItems.addAll(messages)
+            // apply the new message list
+            adapter.setAllItems(messages, searchText, addSearchField = true)
 
             // configure the adapter & recycler view
             if (b.list.adapter == null) {
@@ -125,8 +114,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
             val layoutManager = (b.list.layoutManager as? LinearLayoutManager) ?: return@Observer
 
             // reapply the filter
-            val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
-            adapter.filter.filter(searchText ?: searchItem?.searchText) {
+            adapter.getSearchField()?.applyTo(adapter) {
                 // restore the previously saved scroll position
                 recyclerViewState?.let {
                     layoutManager.onRestoreInstanceState(it)
@@ -141,11 +129,11 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
         if (!isAdded || !this::adapter.isInitialized)
             return
         val layoutManager = (b.list.layoutManager as? LinearLayoutManager)
-        val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
+        val searchField = adapter.getSearchField()
 
         onPageDestroy?.invoke(position, Bundle(
             "recyclerViewState" to layoutManager?.onSaveInstanceState(),
-            "searchText" to searchItem?.searchText?.toString()
+            "searchText" to searchField?.searchText?.toString()
         ))
     }
 }
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
deleted file mode 100644
index 71ed0486..00000000
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * Copyright (c) Kuba Szczodrzyński 2020-4-5.
- */
-
-package pl.szczodrzynski.edziennik.ui.modules.messages.models
-
-class MessagesSearch {
-    var searchText: CharSequence = ""
-}
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/single/MessageFragment.kt
similarity index 95%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/single/MessageFragment.kt
index 430fe0f0..ad86f287 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/single/MessageFragment.kt
@@ -2,7 +2,7 @@
  * Copyright (c) Kuba Szczodrzyński 2019-11-12.
  */
 
-package pl.szczodrzynski.edziennik.ui.modules.messages
+package pl.szczodrzynski.edziennik.ui.modules.messages.single
 
 import android.os.Bundle
 import android.text.Html
@@ -25,11 +25,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
 import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
 import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
 import pl.szczodrzynski.edziennik.data.db.entity.LoginStore.Companion.LOGIN_TYPE_IDZIENNIK
-import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED
-import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
 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.ui.modules.messages.MessagesUtils
+import pl.szczodrzynski.edziennik.ui.modules.messages.list.MessagesFragment
 import pl.szczodrzynski.edziennik.utils.Anim
 import pl.szczodrzynski.edziennik.utils.BetterLink
 import pl.szczodrzynski.edziennik.utils.models.Date
@@ -188,7 +188,7 @@ class MessageFragment : Fragment(), CoroutineScope {
 
         if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_VULCAN) {
             // vulcan: change message status or download attachments
-            if (message.type == TYPE_RECEIVED && !message.seen || message.attachmentIds == null) {
+            if ((message.isReceived || message.isDeleted) && !message.seen || message.attachmentIds == null) {
                 EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
                 return
             }
@@ -214,9 +214,9 @@ class MessageFragment : Fragment(), CoroutineScope {
 
         manager.setStarIcon(b.messageStar, message)
 
-        b.replyButton.isVisible = message.type == TYPE_RECEIVED || message.type == TYPE_DELETED
-        b.deleteButton.isVisible = message.type == TYPE_RECEIVED
-        if (message.type == TYPE_RECEIVED || message.type == TYPE_DELETED) {
+        b.replyButton.isVisible = message.isReceived || message.isDeleted
+        b.deleteButton.isVisible = message.isReceived
+        if (message.isReceived || message.isDeleted) {
             activity.navView.apply {
                 bottomBar.apply {
                     fabEnable = true
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
deleted file mode 100644
index 43497aa1..00000000
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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 {
-            // descending sorting (1. true, 2. false)
-            o1.isStarred && !o2.isStarred -> -1
-            !o1.isStarred && o2.isStarred -> 1
-            // ascending sorting
-            o1.filterWeight > o2.filterWeight -> 1
-            o1.filterWeight < o2.filterWeight -> -1
-            // descending 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/search/SearchField.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchField.kt
new file mode 100644
index 00000000..00f7b49d
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchField.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2020-4-5.
+ */
+
+package pl.szczodrzynski.edziennik.ui.modules.search
+
+import android.widget.Filter
+
+class SearchField(
+    var searchText: CharSequence = "",
+) : Searchable<SearchField> {
+
+    override val searchKeywords = emptyList<List<String>>()
+    override var searchPriority = 0
+    override var searchHighlightText: String? = null
+    override fun compareTo(other: Searchable<*>) = 0
+
+    fun applyTo(adapter: SearchableAdapter<*>, listener: Filter.FilterListener? = null) {
+        adapter.filter.filter(searchText, listener)
+    }
+}
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/search/SearchFilter.kt
similarity index 52%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchFilter.kt
index 202db386..ffbcbf45 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchFilter.kt
@@ -2,24 +2,20 @@
  * Copyright (c) Kuba Szczodrzyński 2021-4-14.
  */
 
-package pl.szczodrzynski.edziennik.ui.modules.messages.utils
+package pl.szczodrzynski.edziennik.ui.modules.search
 
 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
+class SearchFilter<T : Searchable<T>>(
+    private val adapter: SearchableAdapter<T>,
 ) : Filter() {
     companion object {
         private const val NO_MATCH = 1000
     }
 
-    private val comparator = MessagesComparator()
     private var prevCount = -1
 
     private val allItems
@@ -54,75 +50,58 @@ class MessagesFilter(
 
         if (prefix.isNullOrBlank()) {
             allItems.forEach {
-                if (it is MessageFull)
-                    it.searchHighlightText = null
+                it.searchPriority = NO_MATCH
+                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
+        val newItems = allItems.mapNotNull { item ->
+            if (item is SearchField) {
+                return@mapNotNull item
             }
-            it.filterWeight = NO_MATCH
-            it.searchHighlightText = null
+            item.searchPriority = NO_MATCH
+            item.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)
+            // get all keyword sets from the entity
+            val searchKeywords = item.searchKeywords
+            // a temporary variable for the loops below
+            var matchWeight: Int
+
+            searchKeywords.forEachIndexed { priority, keywords ->
+                keywords ?: return@forEachIndexed
+                keywords.forEach { keyword ->
+                    matchWeight = getMatchWeight(keyword, prefix)
+                    if (matchWeight != NO_MATCH) {
+                        // a match not at the word start boundary should be least prioritized
+                        if (matchWeight == 3)
+                            matchWeight = 100
+                        item.searchPriority = min(item.searchPriority, priority * 10 + matchWeight)
                     }
                 }
-            } 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)
+            if (item.searchPriority != NO_MATCH) {
+                // the adapter is reversed, the search priority also should be
+                if (adapter.isReversed)
+                    item.searchPriority *= -1
+                item.searchHighlightText = prefix.toString()
+                return@mapNotNull item
             }
+            return@mapNotNull null
         }
 
-        Collections.sort(items, comparator)
-        results.values = items
-        results.count = items.size
+        results.values = newItems.sorted()
+        results.count = newItems.size
         return results
     }
 
     override fun publishResults(constraint: CharSequence?, results: FilterResults) {
         results.values?.let {
-            adapter.items = it as MutableList<Any>
+            @Suppress("UNCHECKED_CAST") // yes I know it's checked.
+            adapter.setFilteredItems(it as List<T>)
         }
         // do not re-bind the search box
         val count = results.count - 1
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/search/SearchTextWatcher.kt
similarity index 75%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchTextWatcher.kt
index 7103ff29..f37b1760 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchTextWatcher.kt
@@ -2,18 +2,17 @@
  * Copyright (c) Kuba Szczodrzyński 2021-4-14.
  */
 
-package pl.szczodrzynski.edziennik.ui.modules.messages.utils
+package pl.szczodrzynski.edziennik.ui.modules.search
 
 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
+import pl.szczodrzynski.edziennik.databinding.SearchItemBinding
 
 class SearchTextWatcher(
-    private val b: MessagesListItemSearchBinding,
-    private val filter: MessagesFilter,
-    private val item: MessagesSearch
+    private val b: SearchItemBinding,
+    private val filter: SearchFilter<*>,
+    private val item: SearchField,
 ) : TextWatcher {
 
     override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
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/search/SearchViewHolder.kt
similarity index 50%
rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt
rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchViewHolder.kt
index a0a7d69c..06a38ab7 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/search/SearchViewHolder.kt
@@ -2,40 +2,28 @@
  * Copyright (c) Kuba Szczodrzyński 2020-4-5.
  */
 
-package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder
+package pl.szczodrzynski.edziennik.ui.modules.search
 
 import android.view.LayoutInflater
 import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatActivity
 import androidx.recyclerview.widget.RecyclerView
-import pl.szczodrzynski.edziennik.App
 import pl.szczodrzynski.edziennik.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
+import pl.szczodrzynski.edziennik.databinding.SearchItemBinding
 
 class SearchViewHolder(
     inflater: LayoutInflater,
     parent: ViewGroup,
-    val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(
+    val b: SearchItemBinding = SearchItemBinding.inflate(
         inflater,
         parent,
         false
-    )
-) : RecyclerView.ViewHolder(b.root), BindableViewHolder<MessagesSearch, MessagesAdapter> {
+    ),
+) : RecyclerView.ViewHolder(b.root) {
     companion object {
         private const val TAG = "SearchViewHolder"
     }
 
-    override fun onBind(
-        activity: AppCompatActivity,
-        app: App,
-        item: MessagesSearch,
-        position: Int,
-        adapter: MessagesAdapter
-    ) {
+    internal fun bind(item: SearchField, adapter: SearchableAdapter<*>) {
         val watcher = SearchTextWatcher(b, adapter.filter, item)
         b.searchEdit.removeTextChangedListener(watcher)
 
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/Searchable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/Searchable.kt
new file mode 100644
index 00000000..072d6db5
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/Searchable.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2021-10-10.
+ */
+
+package pl.szczodrzynski.edziennik.ui.modules.search
+
+interface Searchable<in T> : Comparable<Searchable<*>> {
+
+    /**
+     * A prioritized list of keywords sets. First items are of the highest priority.
+     * Items within a keyword set have the same priority.
+     */
+    val searchKeywords: List<List<String?>?>
+
+    /**
+     * A priority assigned by [SearchFilter]. Lower numbers mean a higher priority.
+     */
+    var searchPriority: Int
+
+    /**
+     * The text to be highlighted when filtering.
+     */
+    var searchHighlightText: String?
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchableAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchableAdapter.kt
new file mode 100644
index 00000000..7f9cc103
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchableAdapter.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2021-10-10.
+ */
+
+package pl.szczodrzynski.edziennik.ui.modules.search
+
+import android.text.SpannableStringBuilder
+import android.text.style.BackgroundColorSpan
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.Filterable
+import androidx.recyclerview.widget.RecyclerView
+import pl.szczodrzynski.edziennik.asSpannable
+import pl.szczodrzynski.edziennik.utils.span.BoldSpan
+
+abstract class SearchableAdapter<T : Searchable<T>>(
+    val isReversed: Boolean = false,
+) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), Filterable {
+    companion object {
+        const val ITEM_TYPE_SEARCH = 2137
+    }
+
+    /**
+     * A mutable list managed by [setAllItems].
+     * Items are never displayed straight from this list.
+     * Items in this list are always sorted according to their
+     * natural order, with the [SearchField] preceding any other.
+     */
+    val allItems = mutableListOf<T>()
+
+    /**
+     * A mutable var changed by the [SearchFilter].
+     * This list is the only direct source of displayed items.
+     * Items in this list may be in reverse order ([isReversed]), with the [SearchField]
+     * still as the first item.
+     */
+    var items = listOf<T>()
+        private set
+
+    /**
+     * Set [items] as the currently displayed item list. The [items] are first
+     * sorted appropriately to the [isReversed] property.
+     */
+    internal fun setFilteredItems(items: List<T>) {
+        this.items = if (isReversed)
+            items.sortedDescending() // the sort is stable - SearchField should stay at the top
+        else
+            items.sorted()
+    }
+
+    /**
+     * Put [items] to the sorted, unfiltered data source.
+     *
+     * @param searchText the text to fill the [SearchField] with, by default
+     * @param addSearchField whether searching should be enabled and visible
+     */
+    fun setAllItems(items: List<T>, searchText: String? = null, addSearchField: Boolean = false) {
+        if (allItems.isEmpty()) {
+            // items empty - add the search field
+            if (addSearchField) {
+                @Suppress("UNCHECKED_CAST") // what ???
+                allItems += SearchField(searchText ?: "") as T
+            }
+        } else {
+            // items not empty - remove all except the search field
+            allItems.removeAll { it !is SearchField }
+        }
+        // add all new items
+        allItems.addAll(items.sorted())
+        // show all items if searching is disabled
+        if (!addSearchField) {
+            setFilteredItems(allItems)
+        }
+    }
+
+    /**
+     * Return the search field in this adapter's list, or null if not found.
+     */
+    fun getSearchField(): SearchField? {
+        return allItems.filterIsInstance<SearchField>().firstOrNull()
+    }
+
+    fun highlightSearchText(item: T, text: CharSequence, color: Int): CharSequence {
+        if (item.searchHighlightText == null)
+            return SpannableStringBuilder(text)
+        return text.asSpannable(
+            BoldSpan(),
+            BackgroundColorSpan(color),
+            substring = item.searchHighlightText,
+            ignoreCase = true,
+            ignoreDiacritics = true,
+        )
+    }
+
+    final override fun onCreateViewHolder(
+        parent: ViewGroup,
+        viewType: Int,
+    ): RecyclerView.ViewHolder {
+        val inflater = LayoutInflater.from(parent.context)
+        return when (viewType) {
+            ITEM_TYPE_SEARCH -> SearchViewHolder(inflater, parent)
+            else -> onCreateViewHolder(inflater, parent, viewType)
+        }
+    }
+
+    final override fun getItemViewType(position: Int): Int {
+        return when (val item = items[position]) {
+            is SearchField -> ITEM_TYPE_SEARCH
+            else -> getItemViewType(item)
+        }
+    }
+
+    final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+        val item = items[position]
+        if (holder is SearchViewHolder && item is SearchField) {
+            holder.bind(item, this)
+        } else {
+            onBindViewHolder(holder, position, item)
+        }
+    }
+
+    abstract fun getItemViewType(item: T): Int
+    abstract fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, item: T)
+    abstract fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup,
+        viewType: Int,
+    ): RecyclerView.ViewHolder
+
+    private val filter = SearchFilter(this)
+    override fun getItemCount() = items.size
+    override fun getFilter() = filter
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt
index 85332fe1..13d3ae2c 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt
@@ -26,8 +26,8 @@ import pl.szczodrzynski.edziennik.R
 import pl.szczodrzynski.edziennik.data.db.entity.Metadata
 import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
 import pl.szczodrzynski.edziennik.getSchoolYearConstrains
-import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
 import pl.szczodrzynski.edziennik.ui.dialogs.timetable.GenerateBlockTimetableDialog
+import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog
 import pl.szczodrzynski.edziennik.utils.models.Date
 import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
 import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt
index f5b7faa9..6594aa01 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt
@@ -4,6 +4,7 @@
 
 package pl.szczodrzynski.edziennik.utils.managers
 
+import android.widget.TextView
 import androidx.core.view.isVisible
 import com.mikepenz.iconics.IconicsDrawable
 import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
@@ -39,7 +40,7 @@ class EventManager(val app: App) : CoroutineScope {
     }
 
     fun setEventTopic(
-        title: IconicsTextView,
+        title: TextView,
         event: EventFull,
         showType: Boolean = true,
         doneIconColor: Int? = null
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt
index 600353a6..c72a770f 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt
@@ -95,12 +95,12 @@ class MessageManager(private val app: App) {
                 recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: ""
 
             // unset the readByEveryone flag
-            if (recipient.readDate < 1 && message.type == Message.TYPE_SENT)
+            if (recipient.readDate < 1 && message.isSent)
                 message.readByEveryone = false
         }
 
         // store the account name as sender for sent messages
-        if (message.type == Message.TYPE_SENT && message.senderName == null) {
+        if (message.isSent && message.senderName == null) {
             message.senderName = app.profile.accountName ?: app.profile.studentNameLong
         }
 
@@ -193,7 +193,7 @@ class MessageManager(private val app: App) {
         else null
 
         when {
-            message != null && message.type == Message.TYPE_DRAFT -> {
+            message != null && message.isDraft -> {
                 fillWithDraftMessage(config, message)
             }
             message != null -> {
diff --git a/app/src/main/res/layout/event_list_item.xml b/app/src/main/res/layout/event_list_item.xml
index a69b74c0..e353c837 100644
--- a/app/src/main/res/layout/event_list_item.xml
+++ b/app/src/main/res/layout/event_list_item.xml
@@ -2,12 +2,14 @@
 <!--
   ~ Copyright (c) Kuba Szczodrzyński 2019-12-15.
   -->
-
-<layout xmlns:tools="http://schemas.android.com/tools"
-    xmlns:android="http://schemas.android.com/apk/res/android">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <data>
-        <import type="android.view.View"/>
+
+        <import type="android.view.View" />
+
         <variable
             name="simpleMode"
             type="Boolean" />
@@ -16,9 +18,9 @@
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:padding="8dp"
+        android:background="?selectableItemBackground"
         android:orientation="vertical"
-        android:background="?selectableItemBackground">
+        android:padding="8dp">
 
         <LinearLayout
             android:layout_width="match_parent"
@@ -33,14 +35,24 @@
                 android:layout_marginRight="8dp"
                 android:background="@drawable/unread_red_circle" />
 
+            <com.mikepenz.iconics.view.IconicsImageView
+                android:id="@+id/attachmentIcon"
+                android:layout_width="16dp"
+                android:layout_height="16dp"
+                android:layout_marginEnd="8dp"
+                android:layout_marginRight="8dp"
+                app:iiv_color="?android:textColorSecondary"
+                app:iiv_icon="cmd-attachment"
+                tools:background="@tools:sample/avatars[4]" />
+
             <TextView
                 android:id="@+id/details"
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_weight="1"
+                android:maxLines="2"
                 android:textAppearance="@style/NavView.TextView.Helper"
                 android:textSize="16sp"
-                android:maxLines="2"
                 tools:text="sprawdzian • 9:05 • historia i społeczeństwo" />
 
             <View
@@ -48,10 +60,9 @@
                 android:layout_width="12dp"
                 android:layout_height="12dp"
                 android:layout_marginHorizontal="4dp"
-                android:visibility="gone"
                 android:background="@drawable/unread_red_circle"
-                tools:visibility="visible"/>
-
+                android:visibility="gone"
+                tools:visibility="visible" />
         </LinearLayout>
 
         <LinearLayout
@@ -60,7 +71,7 @@
             android:gravity="center_vertical"
             android:orientation="horizontal">
 
-            <com.mikepenz.iconics.view.IconicsTextView
+            <TextView
                 android:id="@+id/topic"
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
@@ -69,7 +80,6 @@
                 android:maxLines="3"
                 android:textAppearance="@style/NavView.TextView.Medium"
                 tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia. Nie wiem co się dzieje w tym roku nie będzie już religii w szkołach podstawowych w Polsce i Europie zachodniej Afryki" />
-
             <!-- cmd_pencil_outline -->
             <com.google.android.material.button.MaterialButton
                 android:id="@+id/editButton"
@@ -85,13 +95,13 @@
                 tools:visibility="visible" />
         </LinearLayout>
 
-        <com.mikepenz.iconics.view.IconicsTextView
+        <TextView
             android:id="@+id/addedBy"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:textAppearance="@style/NavView.TextView.Helper"
-            android:singleLine="true"
             android:ellipsize="middle"
+            android:singleLine="true"
+            android:textAppearance="@style/NavView.TextView.Helper"
             android:visibility="@{simpleMode ? View.GONE : View.VISIBLE}"
             tools:text="Udostępniono 10 grudnia przez Ktoś Z Twojej Klasy • 2B3T" />
     </LinearLayout>
diff --git a/app/src/main/res/layout/messages_list_item.xml b/app/src/main/res/layout/messages_list_item.xml
index fc38ad0d..ab365ba0 100644
--- a/app/src/main/res/layout/messages_list_item.xml
+++ b/app/src/main/res/layout/messages_list_item.xml
@@ -114,13 +114,10 @@
 
         <com.mikepenz.iconics.view.IconicsImageView
             android:id="@+id/messageAttachmentImage"
-            android:layout_width="wrap_content"
-            android:layout_height="0dp"
+            android:layout_width="16dp"
+            android:layout_height="16dp"
             android:layout_marginEnd="4dp"
             android:layout_marginRight="4dp"
-            android:adjustViewBounds="true"
-            android:paddingVertical="2dp"
-            android:scaleType="fitCenter"
             app:iiv_color="?android:textColorSecondary"
             app:iiv_icon="cmd-attachment"
             app:layout_constraintBottom_toBottomOf="@+id/messageDate"
diff --git a/app/src/main/res/layout/messages_list_item_search.xml b/app/src/main/res/layout/search_item.xml
similarity index 100%
rename from app/src/main/res/layout/messages_list_item_search.xml
rename to app/src/main/res/layout/search_item.xml