[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.
This commit is contained in:
Kuba Szczodrzyński 2021-10-10 19:21:50 +02:00 committed by GitHub
parent 50ae767fcd
commit 1a543814f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 728 additions and 494 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
adapter.setAllItems(
if (eventTypeId != null)
events.filter { it.type == eventTypeId }
else
events
events,
)
if (b.eventsView.adapter == null) {
b.eventsView.adapter = adapter

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -61,6 +61,7 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope {
showTime = true,
showSubject = true,
markAsSeen = true,
isReversed = homeworkDate == HomeworkDate.PAST,
onItemClick = {
EventDetailsDialog(
activity,
@ -76,35 +77,34 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope {
}
)
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 }
}

View File

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

View File

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

View File

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

View File

@ -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.messageSubject.text = adapter.highlightSearchText(
item = item,
text = item.subject,
color = colorHighlight
)
b.messageSender.text = b.messageSender.text.asSpannable(
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
substring = highlight, ignoreCase = true, ignoreDiacritics = true
b.messageSender.text = adapter.highlightSearchText(
item = item,
text = messageInfo.profileName ?: "",
color = colorHighlight
)
}
adapter.onItemClick?.let { listener ->
b.root.onClick { listener(item) }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +50,7 @@ class MessagesFilter(
if (prefix.isNullOrBlank()) {
allItems.forEach {
if (it is MessageFull)
it.searchPriority = NO_MATCH
it.searchHighlightText = null
}
results.values = allItems.toList()
@ -62,67 +58,50 @@ class MessagesFilter(
return results
}
val items = mutableListOf<Any>()
val newItems = allItems.mapNotNull { item ->
if (item is SearchField) {
return@mapNotNull item
}
item.searchPriority = NO_MATCH
item.searchHighlightText = null
allItems.forEach {
if (it !is MessageFull) {
items.add(it)
return@forEach
}
it.filterWeight = NO_MATCH
it.searchHighlightText = null
// get all keyword sets from the entity
val searchKeywords = item.searchKeywords
// a temporary variable for the loops below
var matchWeight: Int
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)
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)
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
}
// weights 31..33 and 130
weight = getMatchWeight(it.body, prefix)
if (weight != NO_MATCH) {
if (weight == 3)
weight = 100
it.filterWeight = min(it.filterWeight, 30 + weight)
}
if (it.filterWeight != NO_MATCH) {
it.searchHighlightText = prefix
items.add(it)
}
}
Collections.sort(items, comparator)
results.values = items
results.count = items.size
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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