1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-19 20:29:08 -05:00

Add mailbox chooser to messages (#2002)

This commit is contained in:
Mikołaj Pich 2022-11-16 13:46:47 +01:00 committed by GitHub
parent db4f172fb8
commit 51a1097bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 3209 additions and 235 deletions

File diff suppressed because it is too large Load Diff

View File

@ -56,7 +56,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 52
const val VERSION_SCHEMA = 53
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -106,6 +106,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration49(),
Migration50(),
Migration51(),
Migration53(),
)
fun newInstance(

View File

@ -3,12 +3,16 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Mailbox
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface MailboxDao : BaseDao<Mailbox> {
@Query("SELECT * FROM Mailboxes WHERE userLoginId = :userLoginId ")
suspend fun loadAll(userLoginId: Int): List<Mailbox>
@Query("SELECT * FROM Mailboxes WHERE email = :email")
suspend fun loadAll(email: String): List<Mailbox>
@Query("SELECT * FROM Mailboxes WHERE email = :email AND symbol = :symbol AND schoolId = :schoolId")
fun loadAll(email: String, symbol: String, schoolId: String): Flow<List<Mailbox>>
}

View File

@ -16,4 +16,7 @@ interface MessagesDao : BaseDao<Message> {
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>
@Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC")
fun loadAll(folder: Int, email: String): Flow<List<Message>>
}

View File

@ -1,20 +1,27 @@
package io.github.wulkanowy.data.db.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "Mailboxes")
data class Mailbox(
@PrimaryKey
val globalKey: String,
val email: String,
val symbol: String,
val schoolId: String,
val fullName: String,
val userName: String,
val userLoginId: Int,
val studentName: String,
val schoolNameShort: String,
val type: MailboxType,
)
) : java.io.Serializable, Parcelable
enum class MailboxType {
STUDENT,

View File

@ -9,6 +9,9 @@ import java.time.Instant
@Entity(tableName = "Messages")
data class Message(
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "message_global_key")
val messageGlobalKey: String,

View File

@ -0,0 +1,57 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration53 : Migration(52, 53) {
override fun migrate(database: SupportSQLiteDatabase) {
createMailboxTable(database)
recreateMessagesTable(database)
}
private fun createMailboxTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Mailboxes")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Mailboxes` (
`globalKey` TEXT NOT NULL,
`email` TEXT NOT NULL,
`symbol` TEXT NOT NULL,
`schoolId` TEXT NOT NULL,
`fullName` TEXT NOT NULL,
`userName` TEXT NOT NULL,
`studentName` TEXT NOT NULL,
`schoolNameShort` TEXT NOT NULL,
`type` TEXT NOT NULL,
PRIMARY KEY(`globalKey`)
)""".trimIndent()
)
}
private fun recreateMessagesTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Messages")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Messages` (
`email` TEXT NOT NULL,
`message_global_key` TEXT NOT NULL,
`mailbox_key` TEXT NOT NULL,
`message_id` INTEGER NOT NULL,
`correspondents` TEXT NOT NULL,
`subject` TEXT NOT NULL,
`date` INTEGER NOT NULL,
`folder_id` INTEGER NOT NULL,
`unread` INTEGER NOT NULL,
`read_by` INTEGER,
`unread_by` INTEGER,
`has_attachments` INTEGER NOT NULL,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`is_notified` INTEGER NOT NULL,
`content` TEXT NOT NULL,
`sender` TEXT,
`recipients` TEXT
)""".trimIndent()
)
}
}

View File

@ -10,9 +10,11 @@ fun List<SdkMailbox>.mapToEntities(student: Student) = map {
globalKey = it.globalKey,
fullName = it.fullName,
userName = it.userName,
userLoginId = student.userLoginId,
studentName = it.studentName,
schoolNameShort = it.schoolNameShort,
type = MailboxType.valueOf(it.type.name),
email = student.email,
symbol = student.symbol,
schoolId = student.schoolSymbol,
)
}

View File

@ -6,10 +6,13 @@ import io.github.wulkanowy.sdk.pojo.Message as SdkMessage
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkMessage>.mapToEntities(mailbox: Mailbox) = map {
fun List<SdkMessage>.mapToEntities(student: Student, mailbox: Mailbox?, allMailboxes: List<Mailbox>) = map {
Message(
messageGlobalKey = it.globalKey,
mailboxKey = mailbox.globalKey,
mailboxKey = mailbox?.globalKey ?: allMailboxes.find { box ->
box.fullName == it.mailbox
}?.globalKey!!,
email = student.email,
messageId = it.id,
correspondents = it.correspondents,
subject = it.subject.trim(),

View File

@ -1,85 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MailboxRepository @Inject constructor(
private val mailboxDao: MailboxDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
private val cacheKey = "mailboxes"
suspend fun refreshMailboxes(student: Student) {
val new = sdk.init(student).getMailboxes().mapToEntities(student)
val old = mailboxDao.loadAll(student.userLoginId)
mailboxDao.deleteAll(old uniqueSubtract new)
mailboxDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
suspend fun getMailbox(student: Student): Mailbox {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
val mailboxes = mailboxDao.loadAll(student.userLoginId)
val mailbox = mailboxes.filterByStudent(student)
return if (isExpired || mailbox == null) {
refreshMailboxes(student)
val newMailbox = mailboxDao.loadAll(student.userLoginId).filterByStudent(student)
requireNotNull(newMailbox) {
"Mailbox for ${student.userName} - ${student.studentName} not found! Saved mailboxes: $mailboxes"
}
newMailbox
} else mailbox
}
private fun List<Mailbox>.filterByStudent(student: Student): Mailbox? {
val normalizedStudentName = student.studentName.normalizeStudentName()
return find {
it.studentName.normalizeStudentName() == normalizedStudentName
} ?: singleOrNull {
it.studentName.getFirstAndLastPart() == normalizedStudentName.getFirstAndLastPart()
} ?: singleOrNull {
it.studentName.getUnauthorizedVersion() == normalizedStudentName
}
}
private fun String.normalizeStudentName(): String {
return trim().split(" ")
.filter { it.isNotBlank() }
.joinToString(" ") { part ->
part.lowercase().replaceFirstChar { it.uppercase() }
}
}
private fun String.getFirstAndLastPart(): String {
val parts = normalizeStudentName().split(" ")
val endParts = parts.filterIndexed { i, _ ->
i == 0 || parts.size - 1 == i
}
return endParts.joinToString(" ")
}
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.first() + "*".repeat(it.length - 1)
}
}
}

View File

@ -5,6 +5,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.*
@ -15,6 +16,8 @@ import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.utils.AutoRefreshHelper
@ -40,16 +43,18 @@ class MessageRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider,
private val json: Json,
private val mailboxDao: MailboxDao,
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "message"
private val messagesCacheKey = "message"
private val mailboxCacheKey = "mailboxes"
@Suppress("UNUSED_PARAMETER")
fun getMessages(
student: Student,
mailbox: Mailbox,
mailbox: Mailbox?,
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
@ -58,16 +63,20 @@ class MessageRepository @Inject constructor(
isResultEmpty = { it.isEmpty() },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student, folder)
key = getRefreshKey(messagesCacheKey, mailbox, folder)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { messagesDb.loadAll(mailbox.globalKey, folder.id) },
query = {
if (mailbox == null) {
messagesDb.loadAll(folder.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, folder.id)
},
fetch = {
sdk.init(student).getMessages(
folder = Folder.valueOf(folder.name),
mailboxKey = mailbox.globalKey,
).mapToEntities(mailbox)
mailboxKey = mailbox?.globalKey,
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
},
saveFetchResult = { old, new ->
messagesDb.deleteAll(old uniqueSubtract new)
@ -75,7 +84,9 @@ class MessageRepository @Inject constructor(
it.isNotified = !notify
})
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder))
refreshHelper.updateLastRefreshTimestamp(
getRefreshKey(messagesCacheKey, mailbox, folder)
)
}
)
@ -90,7 +101,9 @@ class MessageRepository @Inject constructor(
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
it.message.unread || it.message.content.isBlank()
},
query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
query = {
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
},
fetch = {
sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey, markAsRead)
},
@ -113,8 +126,10 @@ class MessageRepository @Inject constructor(
}
)
fun getMessagesFromDatabase(mailbox: Mailbox): Flow<List<Message>> {
return messagesDb.loadAll(mailbox.globalKey, RECEIVED.id)
fun getMessagesFromDatabase(student: Student, mailbox: Mailbox?): Flow<List<Message>> {
return if (mailbox == null) {
messagesDb.loadAll(RECEIVED.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, RECEIVED.id)
}
suspend fun updateMessages(messages: List<Message>) {
@ -136,7 +151,7 @@ class MessageRepository @Inject constructor(
)
}
suspend fun deleteMessages(student: Student, mailbox: Mailbox, messages: List<Message>) {
suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
val firstMessage = messages.first()
sdk.init(student).deleteMessages(
messages = messages.map { it.messageGlobalKey },
@ -169,6 +184,34 @@ class MessageRepository @Inject constructor(
deleteMessages(student, mailbox, listOf(message))
}
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(mailboxCacheKey, student),
)
it.isEmpty() || isExpired || forceRefresh
},
query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) },
fetch = { sdk.init(student).getMailboxes().mapToEntities(student) },
saveFetchResult = { old, new ->
mailboxDao.deleteAll(old uniqueSubtract new)
mailboxDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(mailboxCacheKey, student))
}
)
suspend fun getMailboxByStudent(student: Student): Mailbox? {
val mailbox = getMailboxByStudentUseCase(student)
return if (mailbox == null) {
getMailboxes(student, forceRefresh = true).toFirstResult()
getMailboxByStudentUseCase(student)
} else mailbox
}
var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_draft))
?.let { json.decodeFromString(it) }

View File

@ -33,9 +33,11 @@ class RecipientRepository @Inject constructor(
suspend fun getRecipients(
student: Student,
mailbox: Mailbox,
type: MailboxType
mailbox: Mailbox?,
type: MailboxType,
): List<Recipient> {
mailbox ?: return emptyList()
val cached = recipientDb.loadAll(type, mailbox.globalKey)
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
@ -47,11 +49,15 @@ class RecipientRepository @Inject constructor(
suspend fun getMessageSender(
student: Student,
mailbox: Mailbox,
message: Message
): List<Recipient> = sdk.init(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)
.mapToEntities(mailbox.globalKey)
mailbox: Mailbox?,
message: Message,
): List<Recipient> {
mailbox ?: return emptyList()
return sdk.init(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)
.mapToEntities(mailbox.globalKey)
}
}

View File

@ -0,0 +1,52 @@
package io.github.wulkanowy.domain.messages
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Student
import javax.inject.Inject
class GetMailboxByStudentUseCase @Inject constructor(
private val mailboxDao: MailboxDao,
) {
suspend operator fun invoke(student: Student): Mailbox? {
return mailboxDao.loadAll(student.email)
.filterByStudent(student)
}
private fun List<Mailbox>.filterByStudent(student: Student): Mailbox? {
val normalizedStudentName = student.studentName.normalizeStudentName()
return find {
it.studentName.normalizeStudentName() == normalizedStudentName
} ?: singleOrNull {
it.studentName.getFirstAndLastPart() == normalizedStudentName.getFirstAndLastPart()
} ?: singleOrNull {
it.studentName.getUnauthorizedVersion() == normalizedStudentName
}
}
private fun String.normalizeStudentName(): String {
return trim().split(" ")
.filter { it.isNotBlank() }
.joinToString(" ") { part ->
part.lowercase().replaceFirstChar { it.uppercase() }
}
}
private fun String.getFirstAndLastPart(): String {
val parts = normalizeStudentName().split(" ")
val endParts = parts.filterIndexed { i, _ ->
i == 0 || parts.size - 1 == i
}
return endParts.joinToString(" ")
}
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.first() + "*".repeat(it.length - 1)
}
}
}

View File

@ -8,7 +8,6 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.NewMessageNotification
@ -12,12 +11,11 @@ import javax.inject.Inject
class MessageWork @Inject constructor(
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val newMessageNotification: NewMessageNotification,
) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val mailbox = mailboxRepository.getMailbox(student)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.getMessages(
student = student,
mailbox = mailbox,
@ -26,7 +24,7 @@ class MessageWork @Inject constructor(
notify = notify
).waitForResult()
messageRepository.getMessagesFromDatabase(mailbox).first()
messageRepository.getMessagesFromDatabase(student, mailbox).first()
.filter { !it.isNotified && it.unread }.let {
if (it.isNotEmpty()) newMessageNotification.notify(it, student)
messageRepository.updateMessages(it.onEach { message -> message.isNotified = true })

View File

@ -1,22 +1,23 @@
package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.RecipientRepository
import io.github.wulkanowy.data.toFirstResult
import javax.inject.Inject
class RecipientWork @Inject constructor(
private val mailboxRepository: MailboxRepository,
private val messageRepository: MessageRepository,
private val recipientRepository: RecipientRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
mailboxRepository.refreshMailboxes(student)
val mailbox = mailboxRepository.getMailbox(student)
recipientRepository.refreshRecipients(student, mailbox, MailboxType.EMPLOYEE)
val mailboxes = messageRepository.getMailboxes(student, forceRefresh = true).toFirstResult()
mailboxes.dataOrNull?.forEach {
recipientRepository.refreshRecipients(student, it, MailboxType.EMPLOYEE)
}
}
}

View File

@ -25,7 +25,6 @@ class DashboardPresenter @Inject constructor(
private val gradeRepository: GradeRepository,
private val semesterRepository: SemesterRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val attendanceSummaryRepository: AttendanceSummaryRepository,
private val timetableRepository: TimetableRepository,
private val homeworkRepository: HomeworkRepository,
@ -228,7 +227,7 @@ class DashboardPresenter @Inject constructor(
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val semester = semesterRepository.getCurrentSemester(student)
val mailbox = mailboxRepository.getMailbox(student)
val mailbox = messageRepository.getMailboxByStudent(student)
val selectedTiles = preferencesRepository.selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null))

View File

@ -19,6 +19,7 @@ val debugMessageItems = listOf(
private fun generateMessage(sender: String, subject: String) = Message(
subject = subject,
messageId = 123,
email = "",
date = Instant.now(),
folderId = 0,
unread = true,

View File

@ -0,0 +1,81 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.databinding.ItemMailboxChooserBinding
import javax.inject.Inject
class MailboxChooserAdapter @Inject constructor() :
ListAdapter<MailboxChooserItem, MailboxChooserAdapter.ItemViewHolder>(Differ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemMailboxChooserBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ItemViewHolder(
private val binding: ItemMailboxChooserBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: MailboxChooserItem) {
with(binding) {
mailboxItemName.text = item.mailbox?.getFirstLine()
?: root.resources.getString(R.string.message_chip_all_mailboxes)
mailboxItemSchool.text = item.mailbox?.getSecondLine()
mailboxItemSchool.isVisible = !item.isAll
root.setOnClickListener { item.onClickListener(item.mailbox) }
}
}
private fun Mailbox.getFirstLine() = buildString {
if (studentName.isNotBlank() && studentName != userName) {
append(studentName)
append(" - ")
}
append(userName)
}
private fun Mailbox.getSecondLine() = buildString {
append(schoolNameShort)
append(" - ")
append(getMailboxType(type))
}
private fun getMailboxType(type: MailboxType): String = when (type) {
MailboxType.STUDENT -> R.string.message_mailbox_type_student
MailboxType.PARENT -> R.string.message_mailbox_type_parent
MailboxType.GUARDIAN -> R.string.message_mailbox_type_guardian
MailboxType.EMPLOYEE -> R.string.message_mailbox_type_employee
MailboxType.UNKNOWN -> null
}.let { it?.let { it1 -> binding.root.resources.getString(it1) }.orEmpty() }
}
private object Differ : ItemCallback<MailboxChooserItem>() {
override fun areItemsTheSame(
oldItem: MailboxChooserItem,
newItem: MailboxChooserItem
): Boolean {
return oldItem.mailbox?.globalKey == newItem.mailbox?.globalKey
}
override fun areContentsTheSame(
oldItem: MailboxChooserItem,
newItem: MailboxChooserItem
): Boolean {
return oldItem == newItem
}
}
}

View File

@ -0,0 +1,75 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.databinding.DialogMailboxChooserBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import javax.inject.Inject
@AndroidEntryPoint
class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(), MailboxChooserView {
@Inject
lateinit var presenter: MailboxChooserPresenter
@Inject
lateinit var mailboxAdapter: MailboxChooserAdapter
companion object {
const val LISTENER_KEY = "mailbox_selected"
const val MAILBOX_KEY = "selected_mailbox"
const val REQUIRED_KEY = "is_mailbox_required"
fun newInstance(mailboxes: List<Mailbox>, isMailboxRequired: Boolean, folder: String) =
MailboxChooserDialog().apply {
arguments = bundleOf(
MAILBOX_KEY to mailboxes.toTypedArray(),
REQUIRED_KEY to isMailboxRequired,
LISTENER_KEY to folder,
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogMailboxChooserBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(
view = this,
requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false),
mailboxes = requireArguments().getParcelableArray(MAILBOX_KEY).orEmpty()
.toList() as List<Mailbox>,
)
}
override fun initView() {
binding.accountQuickDialogRecycler.adapter = mailboxAdapter
}
override fun submitData(items: List<MailboxChooserItem>) {
mailboxAdapter.submitList(items)
}
override fun onMailboxSelected(item: Mailbox?) {
setFragmentResult(
requestKey = requireArguments().getString(LISTENER_KEY).orEmpty(),
result = bundleOf(MAILBOX_KEY to item),
)
dismiss()
}
}

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
data class MailboxChooserItem(
val mailbox: Mailbox? = null,
val isAll: Boolean = false,
val onClickListener: (Mailbox?) -> Unit,
)

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import timber.log.Timber
import javax.inject.Inject
class MailboxChooserPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<MailboxChooserView>(errorHandler, studentRepository) {
fun onAttachView(view: MailboxChooserView, mailboxes: List<Mailbox>, requireMailbox: Boolean) {
super.onAttachView(view)
view.initView()
Timber.i("Mailbox chooser view was initialized")
view.submitData(getMailboxItems(mailboxes, requireMailbox))
}
private fun getMailboxItems(
mailboxes: List<Mailbox>,
requireMailbox: Boolean,
): List<MailboxChooserItem> = buildList {
if (!requireMailbox) {
add(MailboxChooserItem(isAll = true, onClickListener = ::onMailboxSelect))
}
addAll(mailboxes.map {
MailboxChooserItem(mailbox = it, isAll = false, onClickListener = ::onMailboxSelect)
})
}
fun onMailboxSelect(item: Mailbox?) {
view?.onMailboxSelected(item)
}
}

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.ui.base.BaseView
interface MailboxChooserView : BaseView {
fun initView()
fun submitData(items: List<MailboxChooserItem>)
fun onMailboxSelected(item: Mailbox?)
}

View File

@ -6,7 +6,6 @@ import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
@ -21,7 +20,6 @@ class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
@ -187,8 +185,8 @@ class MessagePreviewPresenter @Inject constructor(
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.deleteMessage(student, mailbox, message!!)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox!!, message!!)
}
.onFailure {
retryCallback = { onMessageDelete() }

View File

@ -19,9 +19,13 @@ import androidx.core.text.toHtml
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.databinding.ActivitySendMessageBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.MAILBOX_KEY
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.LISTENER_KEY
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.showSoftInput
@ -100,6 +104,7 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
formSubjectValue = binding.sendMessageSubject.text.toString()
formContentValue =
binding.sendMessageMessageContent.text.toString().parseAsHtml().toString()
binding.sendMessageFrom.setOnClickListener { presenter.onOpenMailboxChooser() }
presenter.onAttachView(
view = this,
@ -107,6 +112,9 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
message = intent.getSerializableExtra(EXTRA_MESSAGE) as? Message,
reply = intent.getSerializableExtra(EXTRA_REPLY) as? Boolean
)
supportFragmentManager.setFragmentResultListener(LISTENER_KEY, this) { _, bundle ->
presenter.onMailboxSelected(bundle.getSerializable(MAILBOX_KEY) as? Mailbox)
}
}
@SuppressLint("ClickableViewAccessibility")
@ -205,6 +213,14 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
}
}
override fun showMailboxChooser(mailboxes: List<Mailbox>) {
MailboxChooserDialog.newInstance(
mailboxes = mailboxes,
isMailboxRequired = true,
folder = LISTENER_KEY,
).show(supportFragmentManager, "chooser")
}
override fun popView() {
finish()
}

View File

@ -1,15 +1,15 @@
package io.github.wulkanowy.ui.modules.message.send
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.repositories.*
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.RecipientRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
@ -28,7 +28,6 @@ class SendMessagePresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val recipientRepository: RecipientRepository,
private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper
@ -36,10 +35,19 @@ class SendMessagePresenter @Inject constructor(
private val messageUpdateChannel = Channel<Unit>()
private var message: Message? = null
private var isReplay: Boolean? = null
private var mailboxes: List<Mailbox> = emptyList()
private var selectedMailbox: Mailbox? = null
fun onAttachView(view: SendMessageView, reason: String?, message: Message?, reply: Boolean?) {
super.onAttachView(view)
view.initView()
initializeSubjectStream()
this.message = message
this.isReplay = reply
Timber.i("Send message view was initialized")
loadData(message, reply)
with(view) {
@ -110,16 +118,31 @@ class SendMessagePresenter @Inject constructor(
return false
}
fun onOpenMailboxChooser() {
view?.showMailboxChooser(mailboxes)
}
fun onMailboxSelected(mailbox: Mailbox?) {
selectedMailbox = mailbox
loadData(message, isReplay)
}
private fun loadData(message: Message?, reply: Boolean?) {
resourceFlow {
val student = studentRepository.getCurrentStudent()
val mailbox = mailboxRepository.getMailbox(student)
if (selectedMailbox == null && mailboxes.isEmpty()) {
selectedMailbox = messageRepository.getMailboxByStudent(student)
mailboxes = messageRepository.getMailboxes(student, false).toFirstResult()
.dataOrNull.orEmpty()
}
Timber.i("Loading recipients started")
val recipients = createChips(
recipients = recipientRepository.getRecipients(
student = student,
mailbox = mailbox,
mailbox = selectedMailbox,
type = MailboxType.EMPLOYEE,
)
)
@ -130,7 +153,7 @@ class SendMessagePresenter @Inject constructor(
message != null && reply == true -> recipientRepository.getMessageSender(
student = student,
message = message,
mailbox = mailbox,
mailbox = selectedMailbox,
)
else -> emptyList()
}.let { createChips(it) }
@ -139,39 +162,42 @@ class SendMessagePresenter @Inject constructor(
messageRecipients.size
)
Triple(mailbox, recipients, messageRecipients)
recipients to messageRecipients
}
.logResourceStatus("load recipients")
.onEach {
when (it) {
is Resource.Loading -> view?.run {
showProgress(true)
showContent(false)
}
is Resource.Success -> it.data.let { (mailbox, recipientChips, selectedRecipientChips) ->
view?.run {
setMailbox(getMailboxName(mailbox))
setRecipients(recipientChips)
if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients(
selectedRecipientChips
)
showContent(true)
}
}
is Resource.Error -> {
view?.showContent(true)
errorHandler.dispatch(it.error)
.onResourceLoading {
view?.run {
showProgress(true)
showContent(false)
}
}
.onResourceNotLoading {
view?.run { showProgress(false) }
}
.onResourceError {
view?.showContent(true)
errorHandler.dispatch(it)
}
.onResourceSuccess {
it.let { (recipientChips, selectedRecipientChips) ->
view?.run {
setMailbox(getMailboxName(selectedMailbox))
setRecipients(recipientChips)
if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients(
selectedRecipientChips
)
showContent(true)
}
}
}.onResourceNotLoading {
view?.run { showProgress(false) }
}.launch()
}
.launch()
}
private fun sendMessage(subject: String, content: String, recipients: List<Recipient>) {
val mailbox = selectedMailbox ?: return
resourceFlow {
val student = studentRepository.getCurrentStudent()
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.sendMessage(
student = student,
subject = subject,
@ -222,18 +248,21 @@ class SendMessagePresenter @Inject constructor(
}
}
private fun getMailboxName(mailbox: Mailbox): String {
private fun getMailboxName(mailbox: Mailbox?): String {
mailbox ?: return ""
// username - accountType [\n student name - ] (school short name)
return buildString {
append(mailbox.userName)
append(" - ")
append(getMailboxType(mailbox.type))
appendLine()
if (mailbox.type == MailboxType.PARENT) {
append(" - ")
append(mailbox.studentName)
append(" - ")
}
append(" - ")
append("(${mailbox.schoolNameShort})")
}
}
@ -267,9 +296,9 @@ class SendMessagePresenter @Inject constructor(
private fun saveDraftMessage() {
messageRepository.draftMessage = MessageDraft(
view?.formRecipientsData!!,
view?.formSubjectValue!!,
view?.formContentValue!!
recipients = view?.formRecipientsData!!,
subject = view?.formSubjectValue!!,
content = view?.formContentValue!!,
)
}

View File

@ -61,4 +61,5 @@ interface SendMessageView : BaseView {
fun getMessageBackupDialogStringWithRecipients(recipients: String): String
fun clearDraft()
fun showMailboxChooser(mailboxes: List<Mailbox>)
}

View File

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.ItemMessageBinding
import io.github.wulkanowy.databinding.ItemMessageChipsBinding
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
@ -19,13 +20,15 @@ import javax.inject.Inject
class MessageTabAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit = { _, _ -> }
lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit
var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit = {}
lateinit var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit
var onHeaderClickListener: (CompoundButton, Boolean) -> Unit = { _, _ -> }
lateinit var onHeaderClickListener: (CompoundButton, Boolean) -> Unit
var onChangesDetectedListener = {}
lateinit var onMailboxClickListener: () -> Unit
lateinit var onChangesDetectedListener: () -> Unit
private var items = mutableListOf<MessageTabDataItem>()
@ -49,12 +52,12 @@ class MessageTabAdapter @Inject constructor() :
val inflater = LayoutInflater.from(parent.context)
return when (MessageItemViewType.values()[viewType]) {
MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false)
)
MessageItemViewType.FILTERS -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false)
)
MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false)
)
}
}
@ -69,6 +72,20 @@ class MessageTabAdapter @Inject constructor() :
val item = items[position] as MessageTabDataItem.FilterHeader
with(holder.binding) {
chipMailbox.text = item.selectedMailbox
?: root.context.getString(R.string.message_chip_all_mailboxes)
chipMailbox.chipBackgroundColor = ColorStateList.valueOf(
if (item.selectedMailbox == null) {
root.context.getCompatColor(R.color.mtrl_choice_chip_background_color)
} else root.context.getThemeAttrColor(android.R.attr.colorPrimary, 64)
)
chipMailbox.setTextColor(
if (item.selectedMailbox == null) {
root.context.getThemeAttrColor(android.R.attr.textColorPrimary)
} else root.context.getThemeAttrColor(android.R.attr.colorPrimary)
)
chipMailbox.setOnClickListener { onMailboxClickListener() }
if (item.onlyUnread == null) {
chipUnread.isVisible = false
} else {
@ -77,6 +94,7 @@ class MessageTabAdapter @Inject constructor() :
chipUnread.setOnCheckedChangeListener(onHeaderClickListener)
}
chipUnread.isEnabled = item.isEnabled
chipAttachments.isEnabled = item.isEnabled
chipAttachments.isChecked = item.onlyWithAttachments
chipAttachments.setOnCheckedChangeListener(onHeaderClickListener)

View File

@ -11,6 +11,7 @@ sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
) : MessageTabDataItem(MessageItemViewType.MESSAGE)
data class FilterHeader(
val selectedMailbox: String?,
val onlyUnread: Boolean?,
val onlyWithAttachments: Boolean,
val isEnabled: Boolean

View File

@ -10,15 +10,18 @@ import android.widget.CompoundButton
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.view.updatePadding
import androidx.fragment.app.setFragmentResultListener
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.databinding.FragmentMessageTabBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
@ -104,6 +107,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
onItemClickListener = presenter::onMessageItemSelected
onLongItemClickListener = presenter::onMessageItemLongSelected
onHeaderClickListener = ::onChipChecked
onMailboxClickListener = presenter::onMailboxFilterSelected
onChangesDetectedListener = ::resetListPosition
}
@ -123,6 +127,12 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
presenter.onMailboxSelected(
mailbox = bundle.getSerializable(MailboxChooserDialog.MAILBOX_KEY) as? Mailbox,
)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -246,6 +256,16 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
)
}
override fun showMailboxChooser(mailboxes: List<Mailbox>) {
(activity as? MainActivity)?.showDialogFragment(
MailboxChooserDialog.newInstance(
mailboxes = mailboxes,
isMailboxRequired = false,
folder = requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!,
)
)
}
override fun hideKeyboard() {
activity?.hideSoftInput()
}

View File

@ -1,9 +1,9 @@
package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
@ -26,7 +26,6 @@ class MessageTabPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<MessageTabView>(errorHandler, studentRepository) {
@ -36,6 +35,9 @@ class MessageTabPresenter @Inject constructor(
private var lastSearchQuery = ""
private var mailboxes: List<Mailbox> = emptyList()
private var selectedMailbox: Mailbox? = null
private var messages = emptyList<Message>()
private val searchChannel = Channel<String>()
@ -122,8 +124,7 @@ class MessageTabPresenter @Inject constructor(
runCatching {
val student = studentRepository.getCurrentStudent(true)
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.deleteMessages(student, mailbox, messageList)
messageRepository.deleteMessages(student, selectedMailbox, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessagesDeleted() }
@ -202,13 +203,28 @@ class MessageTabPresenter @Inject constructor(
}
}
fun onMailboxFilterSelected() {
view?.showMailboxChooser(mailboxes)
}
fun onMailboxSelected(mailbox: Mailbox?) {
selectedMailbox = mailbox
loadData(false)
}
private fun loadData(forceRefresh: Boolean) {
Timber.i("Loading $folder message data started")
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.getMessages(student, mailbox, folder, forceRefresh)
if (selectedMailbox == null && mailboxes.isEmpty()) {
selectedMailbox = messageRepository.getMailboxByStudent(student)
mailboxes = messageRepository.getMailboxes(student, forceRefresh).toFirstResult()
.dataOrNull.orEmpty()
}
messageRepository.getMessages(student, selectedMailbox, folder, forceRefresh)
}
.logResourceStatus("load $folder message")
.onResourceData {
@ -327,7 +343,16 @@ class MessageTabPresenter @Inject constructor(
MessageTabDataItem.FilterHeader(
onlyUnread = onlyUnread.takeIf { folder != MessageFolder.SENT },
onlyWithAttachments = onlyWithAttachments,
isEnabled = !isActionMode
isEnabled = !isActionMode,
selectedMailbox = selectedMailbox?.let {
buildString {
if (it.studentName.isNotBlank() && it.studentName != it.userName) {
append(it.studentName)
append(" - ")
}
append(it.userName)
}
},
)
)

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.base.BaseView
@ -46,4 +47,6 @@ interface MessageTabView : BaseView {
fun showActionMode(show: Boolean)
fun showRecyclerBottomPadding(show: Boolean)
fun showMailboxChooser(mailboxes: List<Mailbox>)
}

View File

@ -4,6 +4,7 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
@ -25,8 +26,8 @@ fun getRefreshKey(name: String, student: Student): String {
return "${name}_${student.userLoginId}"
}
fun getRefreshKey(name: String, student: Student, folder: MessageFolder): String {
return "${name}_${student.id}_${folder.id}"
fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): String {
return "${name}_${mailbox?.globalKey ?: "all"}_${folder.id}"
}
class AutoRefreshHelper @Inject constructor(

View File

@ -55,17 +55,29 @@
android:id="@+id/sendMessageFrom"
android:layout_width="0dp"
android:layout_height="58dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="8dp"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:paddingStart="8dp"
android:paddingEnd="32dp"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/sendMessageFromHint"
app:layout_constraintTop_toTopOf="parent"
tools:text="Jan Kowalski" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:rotation="270"
android:src="@drawable/ic_chevron_left"
app:layout_constraintBottom_toBottomOf="@id/sendMessageFrom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/sendMessageFrom"
app:tint="?android:textColorSecondary"
tools:ignore="ContentDescription" />
<View
android:id="@+id/sendMessageFromDivider"
android:layout_width="match_parent"

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="UselessParent">
<TextView
android:id="@+id/account_quick_dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingLeft="24dp"
android:paddingEnd="24dp"
android:paddingRight="24dp"
android:text="@string/message_mailbox_chooser_title"
android:textSize="20sp"
android:textStyle="bold"
app:firstBaselineToTopHeight="40dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_quick_dialog_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:layout_weight="1"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_mailbox_chooser" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
tools:context=".ui.modules.message.mailboxchooser.MailboxChooserAdapter">
<TextView
android:id="@+id/mailboxItemName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/mailboxItemSchool"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="3dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/mailboxItemName"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,21 +1,30 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<HorizontalScrollView 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"
android:id="@+id/messageChipsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
tools:context=".ui.modules.message.tab.MessageTabAdapter">
<com.google.android.material.chip.ChipGroup
android:id="@+id/messageChipGroup"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip_mailbox"
style="@style/Widget.MaterialComponents.Chip.Action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_chip_all_mailboxes" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_unread"
@ -37,4 +46,4 @@
app:checkedIconEnabled="true"
app:checkedIconTint="@color/mtrl_choice_chip_text_color" />
</com.google.android.material.chip.ChipGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
</HorizontalScrollView>

View File

@ -301,6 +301,7 @@
<string name="message_not_exists">Message does not exist</string>
<string name="message_required_recipients">You need to choose at least 1 recipient</string>
<string name="message_content_min_length">The message content must be at least 3 characters</string>
<string name="message_chip_all_mailboxes">Wszystkie skrzynki</string>
<string name="message_chip_only_unread">Only unread</string>
<string name="message_chip_only_with_attachments">Only with attachments</string>
<string name="message_read">Read: %s</string>
@ -324,6 +325,7 @@
<item quantity="other">%1$d selected</item>
</plurals>
<string name="message_messages_deleted">Messages deleted</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string>
<!--Note-->

View File

@ -27,7 +27,9 @@ fun getMailboxEntity() = Mailbox(
globalKey = "v4",
fullName = "",
userName = "",
userLoginId = 0,
email = "test",
symbol = "powiatwulkanowy",
schoolId = "123456",
studentName = "",
schoolNameShort = "",
type = MailboxType.UNKNOWN,

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories
import android.content.Context
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.Message
@ -10,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.getMailboxEntity
import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk
@ -55,6 +57,12 @@ class MessageRepositoryTest {
@MockK
private lateinit var sharedPrefProvider: SharedPrefProvider
@MockK
private lateinit var mailboxDao: MailboxDao
@MockK
private lateinit var getMailboxByStudentUseCase: GetMailboxByStudentUseCase
private val student = getStudentEntity()
private val mailbox = getMailboxEntity()
@ -74,26 +82,33 @@ class MessageRepositoryTest {
refreshHelper = refreshHelper,
sharedPrefProvider = sharedPrefProvider,
json = Json,
mailboxDao = mailboxDao,
getMailboxByStudentUseCase = getMailboxByStudentUseCase,
)
}
@Test
fun `get messages when fetched completely new message without notify`() = runBlocking {
every { messageDb.loadAll(any(), any()) } returns flowOf(emptyList())
coEvery { mailboxDao.loadAll(any()) } returns listOf(mailbox)
every { messageDb.loadAll(mailbox.globalKey, any()) } returns flowOf(emptyList())
coEvery { sdk.getMessages(Folder.RECEIVED, any()) } returns listOf(
getMessageDto()
getMessageDto().copy(
unreadBy = 5,
readBy = 10,
)
)
coEvery { messageDb.deleteAll(any()) } just Runs
coEvery { messageDb.insertAll(any()) } returns listOf()
repository.getMessages(
val res = repository.getMessages(
student = student,
mailbox = mailbox,
folder = MessageFolder.RECEIVED,
forceRefresh = true,
notify = false,
).toFirstResult().dataOrNull.orEmpty()
).toFirstResult()
assertEquals(null, res.errorOrNull)
coVerify(exactly = 1) { messageDb.deleteAll(withArg { checkEquals(emptyList<Message>()) }) }
coVerify {
messageDb.insertAll(withArg {
@ -187,6 +202,7 @@ class MessageRepositoryTest {
) = Message(
messageGlobalKey = "v4",
mailboxKey = "",
email = "",
correspondents = "",
messageId = messageId,
subject = "",

View File

@ -1,65 +1,52 @@
package io.github.wulkanowy.data.repositories
package io.github.wulkanowy.domain
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.SpyK
import io.mockk.just
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertNull
@OptIn(ExperimentalCoroutinesApi::class)
class MailboxRepositoryTest {
@SpyK
private var sdk = Sdk()
class GetMailboxByStudentUseCaseTest {
@MockK
private lateinit var mailboxDao: MailboxDao
@MockK
private lateinit var refreshHelper: AutoRefreshHelper
private lateinit var systemUnderTest: MailboxRepository
private lateinit var systemUnderTest: GetMailboxByStudentUseCase
@Before
fun setUp() {
MockKAnnotations.init(this)
coEvery { refreshHelper.shouldBeRefreshed(any()) } returns false
coEvery { refreshHelper.updateLastRefreshTimestamp(any()) } just Runs
coEvery { mailboxDao.deleteAll(any()) } just Runs
coEvery { mailboxDao.insertAll(any()) } returns emptyList()
coEvery { mailboxDao.loadAll(any()) } returns emptyList()
coEvery { sdk.getMailboxes() } returns emptyList()
systemUnderTest = MailboxRepository(
mailboxDao = mailboxDao,
sdk = sdk,
refreshHelper = refreshHelper,
)
systemUnderTest = GetMailboxByStudentUseCase(mailboxDao = mailboxDao)
}
@Test(expected = IllegalArgumentException::class)
@Test
fun `get mailbox that doesn't exist`() = runTest {
val student = getStudentEntity(
userName = "Stanisław Kowalski",
studentName = "Jan Kowalski",
)
coEvery { sdk.getMailboxes() } returns emptyList()
coEvery { mailboxDao.loadAll(any()) } returns emptyList()
systemUnderTest.getMailbox(student)
assertNull(systemUnderTest(student))
}
@Test
@ -73,7 +60,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
val selectedMailbox = systemUnderTest.getMailbox(student)
val selectedMailbox = systemUnderTest(student)
assertEquals(expectedMailbox, selectedMailbox)
}
@ -88,7 +75,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
@Test
@ -102,10 +89,10 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
@Test(expected = IllegalArgumentException::class)
@Test
fun `get mailbox for not-unique non-authorized student`() = runTest {
val student = getStudentEntity(
userName = "Stanisław Kowalski",
@ -116,7 +103,7 @@ class MailboxRepositoryTest {
getMailboxEntity("Jan Kurowski"),
)
systemUnderTest.getMailbox(student)
assertNull(systemUnderTest(student))
}
@Test
@ -130,7 +117,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
@Test
@ -144,7 +131,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
@Test
@ -158,7 +145,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
private fun getMailboxEntity(
@ -167,7 +154,9 @@ class MailboxRepositoryTest {
globalKey = "",
fullName = "",
userName = "",
userLoginId = 123,
email = "",
schoolId = "",
symbol = "",
studentName = studentName,
schoolNameShort = "",
type = MailboxType.STUDENT,