Merge branch 'develop' into feature/attendance-excuse-whole-day

This commit is contained in:
Mikołaj Pich 2024-02-29 23:13:12 +01:00
commit f8c9122686
32 changed files with 2839 additions and 91 deletions

4
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: wulkanowy
custom: https://www.paypal.com/paypalme/wulkanowy

View File

@ -187,7 +187,7 @@ huaweiPublish {
ext { ext {
work_manager = "2.9.0" work_manager = "2.9.0"
android_hilt = "1.1.0" android_hilt = "1.2.0"
room = "2.6.1" room = "2.6.1"
chucker = "4.0.0" chucker = "4.0.0"
mockk = "1.13.9" mockk = "1.13.9"
@ -246,7 +246,7 @@ dependencies {
implementation 'com.github.Faierbel:slf4j-timber:2.0' implementation 'com.github.Faierbel:slf4j-timber:2.0'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2' implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'io.coil-kt:coil:2.5.0' implementation 'io.coil-kt:coil:2.6.0'
implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'com.fredporciuncula:flow-preferences:1.9.1'

File diff suppressed because it is too large Load Diff

View File

@ -54,5 +54,9 @@
{ {
"displayName": "Antoni Paduch", "displayName": "Antoni Paduch",
"githubUsername": "janAte1" "githubUsername": "janAte1"
},
{
"displayName": "Kamil Wąsik",
"githubUsername": "JestemKamil"
} }
] ]

View File

@ -254,6 +254,10 @@ internal class DataModule {
@Provides @Provides
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
@Singleton
@Provides
fun provideMutesDao(database: AppDatabase) = database.mutedMessageSendersDao
@Singleton @Singleton
@Provides @Provides
fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao

View File

@ -25,6 +25,7 @@ import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
import io.github.wulkanowy.data.db.dao.NoteDao import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.NotificationDao import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.db.dao.RecipientDao
@ -56,6 +57,7 @@ import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.MutedMessageSender
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
@ -157,6 +159,7 @@ import javax.inject.Singleton
SchoolAnnouncement::class, SchoolAnnouncement::class,
Notification::class, Notification::class,
AdminMessage::class, AdminMessage::class,
MutedMessageSender::class,
GradeDescriptive::class, GradeDescriptive::class,
], ],
autoMigrations = [ autoMigrations = [
@ -169,6 +172,7 @@ import javax.inject.Singleton
AutoMigration(from = 56, to = 57, spec = Migration57::class), AutoMigration(from = 56, to = 57, spec = Migration57::class),
AutoMigration(from = 57, to = 58, spec = Migration58::class), AutoMigration(from = 57, to = 58, spec = Migration58::class),
AutoMigration(from = 58, to = 59), AutoMigration(from = 58, to = 59),
AutoMigration(from = 59, to = 60),
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -177,7 +181,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 59 const val VERSION_SCHEMA = 60
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -303,5 +307,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract val adminMessagesDao: AdminMessageDao abstract val adminMessagesDao: AdminMessageDao
abstract val mutedMessageSendersDao: MutedMessageSendersDao
abstract val gradeDescriptiveDao: GradeDescriptiveDao abstract val gradeDescriptiveDao: GradeDescriptiveDao
} }

View File

@ -5,15 +5,23 @@ import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface MessagesDao : BaseDao<Message> { interface MessagesDao : BaseDao<Message> {
@Transaction @Transaction
@Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey") @Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?> fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?>
@Transaction
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadMessagesWithMutedAuthor(mailboxKey: String, folder: Int): Flow<List<MessageWithMutedAuthor>>
@Transaction
@Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC")
fun loadMessagesWithMutedAuthor(folder: Int, email: String): Flow<List<MessageWithMutedAuthor>>
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC") @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>> fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>

View File

@ -0,0 +1,20 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.MutedMessageSender
@Dao
interface MutedMessageSendersDao : BaseDao<MutedMessageSender> {
@Query("SELECT COUNT(*) FROM MutedMessageSenders WHERE author = :author")
suspend fun checkMute(author: String): Boolean
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertMute(mute: MutedMessageSender): Long
@Query("DELETE FROM MutedMessageSenders WHERE author = :author")
suspend fun deleteMute(author: String)
}

View File

@ -2,11 +2,15 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Relation import androidx.room.Relation
import java.io.Serializable
data class MessageWithAttachment( data class MessageWithAttachment(
@Embedded @Embedded
val message: Message, val message: Message,
@Relation(parentColumn = "message_global_key", entityColumn = "message_global_key") @Relation(parentColumn = "message_global_key", entityColumn = "message_global_key")
val attachments: List<MessageAttachment> val attachments: List<MessageAttachment>,
)
@Relation(parentColumn = "correspondents", entityColumn = "author")
val mutedMessageSender: MutedMessageSender?,
) : Serializable

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded
import androidx.room.Relation
data class MessageWithMutedAuthor(
@Embedded
val message: Message,
@Relation(parentColumn = "correspondents", entityColumn = "author")
val mutedMessageSender: MutedMessageSender?,
)

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity(tableName = "MutedMessageSenders")
data class MutedMessageSender(
@ColumnInfo(name = "author")
val author: String,
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -8,9 +8,12 @@ import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import io.github.wulkanowy.data.db.entities.MutedMessageSender
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
@ -42,6 +45,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class MessageRepository @Inject constructor( class MessageRepository @Inject constructor(
private val messagesDb: MessagesDao, private val messagesDb: MessagesDao,
private val mutedMessageSendersDao: MutedMessageSendersDao,
private val messageAttachmentDao: MessageAttachmentDao, private val messageAttachmentDao: MessageAttachmentDao,
private val sdk: Sdk, private val sdk: Sdk,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
@ -51,7 +55,6 @@ class MessageRepository @Inject constructor(
private val mailboxDao: MailboxDao, private val mailboxDao: MailboxDao,
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase, private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val messagesCacheKey = "message" private val messagesCacheKey = "message"
@ -63,7 +66,7 @@ class MessageRepository @Inject constructor(
folder: MessageFolder, folder: MessageFolder,
forceRefresh: Boolean, forceRefresh: Boolean,
notify: Boolean = false, notify: Boolean = false,
): Flow<Resource<List<Message>>> = networkBoundResource( ): Flow<Resource<List<MessageWithMutedAuthor>>> = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() }, isResultEmpty = { it.isEmpty() },
shouldFetch = { shouldFetch = {
@ -74,8 +77,8 @@ class MessageRepository @Inject constructor(
}, },
query = { query = {
if (mailbox == null) { if (mailbox == null) {
messagesDb.loadAll(folder.id, student.email) messagesDb.loadMessagesWithMutedAuthor(folder.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, folder.id) } else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id)
}, },
fetch = { fetch = {
sdk.init(student).getMessages( sdk.init(student).getMessages(
@ -83,10 +86,12 @@ class MessageRepository @Inject constructor(
mailboxKey = mailbox?.globalKey, mailboxKey = mailbox?.globalKey,
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email)) ).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
}, },
saveFetchResult = { old, new -> saveFetchResult = { oldWithAuthors, new ->
val old = oldWithAuthors.map { it.message }
messagesDb.deleteAll(old uniqueSubtract new) messagesDb.deleteAll(old uniqueSubtract new)
messagesDb.insertAll((new uniqueSubtract old).onEach { messagesDb.insertAll((new uniqueSubtract old).onEach {
it.isNotified = !notify val muted = isMuted(it.correspondents)
it.isNotified = !notify || muted
}) })
refreshHelper.updateLastRefreshTimestamp( refreshHelper.updateLastRefreshTimestamp(
@ -106,9 +111,7 @@ class MessageRepository @Inject constructor(
Timber.d("Message content in db empty: ${it.message.content.isBlank()}") Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
(it.message.unread && markAsRead) || it.message.content.isBlank() (it.message.unread && markAsRead) || it.message.content.isBlank()
}, },
query = { query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
},
fetch = { fetch = {
sdk.init(student).getMessageDetails( sdk.init(student).getMessageDetails(
messageKey = it!!.message.messageGlobalKey, messageKey = it!!.message.messageGlobalKey,
@ -236,4 +239,18 @@ class MessageRepository @Inject constructor(
context.getString(R.string.pref_key_message_draft), context.getString(R.string.pref_key_message_draft),
value?.let { json.encodeToString(it) } value?.let { json.encodeToString(it) }
) )
suspend fun isMuted(author: String): Boolean {
return mutedMessageSendersDao.checkMute(author)
}
suspend fun muteMessage(author: String) {
if (isMuted(author)) return
mutedMessageSendersDao.insertMute(MutedMessageSender(author))
}
suspend fun unmuteMessage(author: String) {
if (!isMuted(author)) return
mutedMessageSendersDao.deleteMute(author)
}
} }

View File

@ -304,6 +304,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh forceRefresh = forceRefresh
) )
} }
.mapResourceData { it.map { messageWithAuthor -> messageWithAuthor.message } }
.onResourceError { errorHandler.dispatch(it) } .onResourceError { errorHandler.dispatch(it) }
.takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess .takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess

View File

@ -50,12 +50,15 @@ class MessagePreviewAdapter @Inject constructor() :
ViewType.MESSAGE.id -> MessageViewHolder( ViewType.MESSAGE.id -> MessageViewHolder(
ItemMessagePreviewBinding.inflate(inflater, parent, false) ItemMessagePreviewBinding.inflate(inflater, parent, false)
) )
ViewType.DIVIDER.id -> DividerViewHolder( ViewType.DIVIDER.id -> DividerViewHolder(
ItemMessageDividerBinding.inflate(inflater, parent, false) ItemMessageDividerBinding.inflate(inflater, parent, false)
) )
ViewType.ATTACHMENT.id -> AttachmentViewHolder( ViewType.ATTACHMENT.id -> AttachmentViewHolder(
ItemMessageAttachmentBinding.inflate(inflater, parent, false) ItemMessageAttachmentBinding.inflate(inflater, parent, false)
) )
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
@ -66,6 +69,7 @@ class MessagePreviewAdapter @Inject constructor() :
holder, holder,
requireNotNull(messageWithAttachment).message requireNotNull(messageWithAttachment).message
) )
is AttachmentViewHolder -> bindAttachment( is AttachmentViewHolder -> bindAttachment(
holder, holder,
requireNotNull(messageWithAttachment).attachments[position - 2] requireNotNull(messageWithAttachment).attachments[position - 2]
@ -82,9 +86,11 @@ class MessagePreviewAdapter @Inject constructor() :
recipientCount > 1 -> { recipientCount > 1 -> {
context.getString(R.string.message_read_by, message.readBy, recipientCount) context.getString(R.string.message_read_by, message.readBy, recipientCount)
} }
message.readBy == 1 || (isReceived && !message.unread) -> { message.readBy == 1 || (isReceived && !message.unread) -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes)) context.getString(R.string.message_read, context.getString(R.string.all_yes))
} }
else -> context.getString(R.string.message_read, context.getString(R.string.all_no)) else -> context.getString(R.string.message_read, context.getString(R.string.all_no))
} }

View File

@ -50,12 +50,20 @@ class MessagePreviewFragment :
private var menuPrintButton: MenuItem? = null private var menuPrintButton: MenuItem? = null
private var menuMuteButton: MenuItem? = null
override val titleStringId: Int override val titleStringId: Int
get() = R.string.message_title get() = R.string.message_title
override val deleteMessageSuccessString: String override val deleteMessageSuccessString: String
get() = getString(R.string.message_delete_success) get() = getString(R.string.message_delete_success)
override val muteMessageSuccessString: String
get() = getString(R.string.message_mute_success)
override val unmuteMessageSuccessString: String
get() = getString(R.string.message_unmute_success)
override val messageNoSubjectString: String override val messageNoSubjectString: String
get() = getString(R.string.message_no_subject) get() = getString(R.string.message_no_subject)
@ -106,6 +114,7 @@ class MessagePreviewFragment :
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete) menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare) menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint) menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
menuMuteButton = menu.findItem(R.id.messagePreviewMenuMute)
presenter.onCreateOptionsMenu() presenter.onCreateOptionsMenu()
menu.findItem(R.id.mainMenuAccount).isVisible = false menu.findItem(R.id.mainMenuAccount).isVisible = false
@ -118,6 +127,7 @@ class MessagePreviewFragment :
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete() R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
R.id.messagePreviewMenuShare -> presenter.onShare() R.id.messagePreviewMenuShare -> presenter.onShare()
R.id.messagePreviewMenuPrint -> presenter.onPrint() R.id.messagePreviewMenuPrint -> presenter.onPrint()
R.id.messagePreviewMenuMute -> presenter.onMute()
else -> false else -> false
} }
} }
@ -129,6 +139,11 @@ class MessagePreviewFragment :
} }
} }
override fun updateMuteToggleButton(isMuted: Boolean) {
menuMuteButton?.setTitle(if (isMuted) R.string.message_unmute else R.string.message_mute)
}
override fun showProgress(show: Boolean) { override fun showProgress(show: Boolean) {
binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE
} }
@ -143,6 +158,7 @@ class MessagePreviewFragment :
menuDeleteButton?.isVisible = show menuDeleteButton?.isVisible = show
menuShareButton?.isVisible = show menuShareButton?.isVisible = show
menuPrintButton?.isVisible = show menuPrintButton?.isVisible = show
menuMuteButton?.isVisible = show && isReplayable
} }
override fun setDeletedOptionsLabels() { override fun setDeletedOptionsLabels() {
@ -213,7 +229,7 @@ class MessagePreviewFragment :
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(MESSAGE_ID_KEY, presenter.message) outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }

View File

@ -5,7 +5,7 @@ import androidx.core.text.parseAsHtml
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -26,9 +26,7 @@ class MessagePreviewPresenter @Inject constructor(
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) { ) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
var message: Message? = null var messageWithAttachments: MessageWithAttachment? = null
var attachments: List<MessageAttachment>? = null
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
@ -38,7 +36,6 @@ class MessagePreviewPresenter @Inject constructor(
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError errorHandler.showErrorMessage = ::showErrorViewOnError
this.message = message
loadData(requireNotNull(message)) loadData(requireNotNull(message))
} }
@ -66,13 +63,12 @@ class MessagePreviewPresenter @Inject constructor(
.logResourceStatus("message ${messageToLoad.messageId} preview") .logResourceStatus("message ${messageToLoad.messageId} preview")
.onResourceData { .onResourceData {
if (it != null) { if (it != null) {
message = it.message messageWithAttachments = it
attachments = it.attachments
view?.apply { view?.apply {
setMessageWithAttachment(it) setMessageWithAttachment(it)
showContent(true) showContent(true)
initOptions() initOptions()
updateMuteToggleButton(isMuted = it.mutedMessageSender != null)
if (preferencesRepository.isIncognitoMode && it.message.unread) { if (preferencesRepository.isIncognitoMode && it.message.unread) {
showMessage(R.string.message_incognito_description) showMessage(R.string.message_incognito_description)
} }
@ -83,8 +79,7 @@ class MessagePreviewPresenter @Inject constructor(
popView() popView()
} }
} }
} }.onResourceSuccess {
.onResourceSuccess {
if (it != null) { if (it != null) {
analytics.logEvent( analytics.logEvent(
"load_item", "load_item",
@ -92,31 +87,28 @@ class MessagePreviewPresenter @Inject constructor(
"length" to it.message.content.length "length" to it.message.content.length
) )
} }
} }.onResourceNotLoading { view?.showProgress(false) }.onResourceError {
.onResourceNotLoading { view?.showProgress(false) }
.onResourceError {
retryCallback = { onMessageLoadRetry(messageToLoad) } retryCallback = { onMessageLoadRetry(messageToLoad) }
errorHandler.dispatch(it) errorHandler.dispatch(it)
} }.launch()
.launch()
} }
fun onReply(): Boolean { fun onReply(): Boolean {
return if (message != null) { return if (messageWithAttachments?.message != null) {
view?.openMessageReply(message) view?.openMessageReply(messageWithAttachments?.message)
true true
} else false } else false
} }
fun onForward(): Boolean { fun onForward(): Boolean {
return if (message != null) { return if (messageWithAttachments?.message != null) {
view?.openMessageForward(message) view?.openMessageForward(messageWithAttachments?.message)
true true
} else false } else false
} }
fun onShare(): Boolean { fun onShare(): Boolean {
val message = message ?: return false val message = messageWithAttachments?.message ?: return false
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() } val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val text = buildString { val text = buildString {
@ -129,13 +121,15 @@ class MessagePreviewPresenter @Inject constructor(
appendLine(message.content.parseAsHtml()) appendLine(message.content.parseAsHtml())
if (!attachments.isNullOrEmpty()) { if (!messageWithAttachments?.attachments.isNullOrEmpty()) {
appendLine() appendLine()
appendLine("Załączniki:") appendLine("Załączniki:")
append(attachments.orEmpty().joinToString(separator = "\n") { attachment -> append(
"${attachment.filename}: ${attachment.url}" messageWithAttachments?.attachments.orEmpty()
}) .joinToString(separator = "\n") { attachment ->
"${attachment.filename}: ${attachment.url}"
})
} }
} }
@ -148,7 +142,7 @@ class MessagePreviewPresenter @Inject constructor(
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun onPrint(): Boolean { fun onPrint(): Boolean {
val message = message ?: return false val message = messageWithAttachments?.message ?: return false
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() } val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val dateString = message.date.toFormattedString("yyyy-MM-dd HH:mm:ss") val dateString = message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")
@ -159,8 +153,7 @@ class MessagePreviewPresenter @Inject constructor(
append("<div><h4>Od</h4>${message.sender}</div>") append("<div><h4>Od</h4>${message.sender}</div>")
append("<div><h4>DO</h4>${message.recipients}</div>") append("<div><h4>DO</h4>${message.recipients}</div>")
} }
val messageContent = "<p>${message.content}</p>" val messageContent = "<p>${message.content}</p>".replace(Regex("[\\n\\r]{2,}"), "</p><p>")
.replace(Regex("[\\n\\r]{2,}"), "</p><p>")
.replace(Regex("[\\n\\r]"), "<br>") .replace(Regex("[\\n\\r]"), "<br>")
val jobName = buildString { val jobName = buildString {
@ -171,9 +164,7 @@ class MessagePreviewPresenter @Inject constructor(
} }
view?.apply { view?.apply {
val html = printHTML val html = printHTML.replace("%SUBJECT%", subject).replace("%CONTENT%", messageContent)
.replace("%SUBJECT%", subject)
.replace("%CONTENT%", messageContent)
.replace("%INFO%", infoContent) .replace("%INFO%", infoContent)
printDocument(html, jobName) printDocument(html, jobName)
} }
@ -182,7 +173,7 @@ class MessagePreviewPresenter @Inject constructor(
} }
private fun deleteMessage() { private fun deleteMessage() {
message ?: return messageWithAttachments?.message ?: return
view?.run { view?.run {
showContent(false) showContent(false)
@ -191,24 +182,22 @@ class MessagePreviewPresenter @Inject constructor(
showErrorView(false) showErrorView(false)
} }
Timber.i("Delete message ${message?.messageGlobalKey}") Timber.i("Delete message ${messageWithAttachments?.message?.messageGlobalKey}")
presenterScope.launch { presenterScope.launch {
runCatching { runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true) val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = messageRepository.getMailboxByStudent(student) val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox, message!!) messageRepository.deleteMessage(student, mailbox, messageWithAttachments?.message!!)
}.onFailure {
retryCallback = { onMessageDelete() }
errorHandler.dispatch(it)
}.onSuccess {
view?.run {
showMessage(deleteMessageSuccessString)
popView()
}
} }
.onFailure {
retryCallback = { onMessageDelete() }
errorHandler.dispatch(it)
}
.onSuccess {
view?.run {
showMessage(deleteMessageSuccessString)
popView()
}
}
view?.showProgress(false) view?.showProgress(false)
} }
@ -232,10 +221,10 @@ class MessagePreviewPresenter @Inject constructor(
private fun initOptions() { private fun initOptions() {
view?.apply { view?.apply {
showOptions( showOptions(
show = message != null, show = messageWithAttachments?.message != null,
isReplayable = message?.folderId != MessageFolder.SENT.id, isReplayable = messageWithAttachments?.message?.folderId != MessageFolder.SENT.id,
) )
message?.let { messageWithAttachments?.message?.let {
when (it.folderId == MessageFolder.TRASHED.id) { when (it.folderId == MessageFolder.TRASHED.id) {
true -> setDeletedOptionsLabels() true -> setDeletedOptionsLabels()
false -> setNotDeletedOptionsLabels() false -> setNotDeletedOptionsLabels()
@ -248,4 +237,29 @@ class MessagePreviewPresenter @Inject constructor(
fun onCreateOptionsMenu() { fun onCreateOptionsMenu() {
initOptions() initOptions()
} }
fun onMute(): Boolean {
val message = messageWithAttachments?.message ?: return false
val isMuted = messageWithAttachments?.mutedMessageSender != null
presenterScope.launch {
runCatching {
when (isMuted) {
true -> {
messageRepository.unmuteMessage(message.correspondents)
view?.run { showMessage(unmuteMessageSuccessString) }
}
false -> {
messageRepository.muteMessage(message.correspondents)
view?.run { showMessage(muteMessageSuccessString) }
}
}
}.onFailure {
errorHandler.dispatch(it)
}
}
view?.updateMuteToggleButton(isMuted)
return true
}
} }

View File

@ -9,6 +9,10 @@ interface MessagePreviewView : BaseView {
val deleteMessageSuccessString: String val deleteMessageSuccessString: String
val muteMessageSuccessString: String
val unmuteMessageSuccessString: String
val messageNoSubjectString: String val messageNoSubjectString: String
val printHTML: String val printHTML: String
@ -19,6 +23,8 @@ interface MessagePreviewView : BaseView {
fun setMessageWithAttachment(item: MessageWithAttachment) fun setMessageWithAttachment(item: MessageWithAttachment)
fun updateMuteToggleButton(isMuted: Boolean)
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
fun showContent(show: Boolean) fun showContent(show: Boolean)

View File

@ -18,8 +18,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject import javax.inject.Inject
class MessageTabAdapter @Inject constructor() : class MessageTabAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit
@ -52,10 +51,11 @@ class MessageTabAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (MessageItemViewType.values()[viewType]) { return when (MessageItemViewType.entries[viewType]) {
MessageItemViewType.FILTERS -> HeaderViewHolder( MessageItemViewType.FILTERS -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false) ItemMessageChipsBinding.inflate(inflater, parent, false)
) )
MessageItemViewType.MESSAGE -> ItemViewHolder( MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false) ItemMessageBinding.inflate(inflater, parent, false)
) )
@ -137,7 +137,12 @@ class MessageTabAdapter @Inject constructor() :
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor)) ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor))
isVisible = message.hasAttachments isVisible = message.hasAttachments
} }
messageItemUnreadIndicator.isVisible = message.unread messageItemUnreadIndicator.isVisible = message.unread || item.isMuted
when (item.isMuted) {
true -> messageItemUnreadIndicator.setImageResource(R.drawable.ic_notifications_off)
else -> messageItemUnreadIndicator.setImageResource(R.drawable.ic_circle_notification)
}
root.setOnClickListener { root.setOnClickListener {
holder.bindingAdapterPosition.let { holder.bindingAdapterPosition.let {
@ -165,8 +170,7 @@ class MessageTabAdapter @Inject constructor() :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
private class MessageTabDiffUtil( private class MessageTabDiffUtil(
private val old: List<MessageTabDataItem>, private val old: List<MessageTabDataItem>, private val new: List<MessageTabDataItem>
private val new: List<MessageTabDataItem>
) : DiffUtil.Callback() { ) : DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size override fun getOldListSize(): Int = old.size

View File

@ -6,6 +6,7 @@ sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
data class MessageItem( data class MessageItem(
val message: Message, val message: Message,
val isMuted: Boolean,
val isSelected: Boolean, val isSelected: Boolean,
val isActionMode: Boolean val isActionMode: Boolean
) : MessageTabDataItem(MessageItemViewType.MESSAGE) ) : MessageTabDataItem(MessageItemViewType.MESSAGE)

View File

@ -4,6 +4,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
@ -39,7 +40,7 @@ class MessageTabPresenter @Inject constructor(
private var mailboxes: List<Mailbox> = emptyList() private var mailboxes: List<Mailbox> = emptyList()
private var selectedMailbox: Mailbox? = null private var selectedMailbox: Mailbox? = null
private var messages = emptyList<Message>() private var messages = emptyList<MessageWithMutedAuthor>()
private val searchChannel = Channel<String>() private val searchChannel = Channel<String>()
@ -141,7 +142,7 @@ class MessageTabPresenter @Inject constructor(
} }
fun onActionModeSelectCheckAll() { fun onActionModeSelectCheckAll() {
val messagesToSelect = getFilteredData() val messagesToSelect = getFilteredData().map { it.message }
val isAllSelected = messagesToDelete.containsAll(messagesToSelect) val isAllSelected = messagesToDelete.containsAll(messagesToSelect)
if (isAllSelected) { if (isAllSelected) {
@ -188,7 +189,7 @@ class MessageTabPresenter @Inject constructor(
view?.showActionMode(false) view?.showActionMode(false)
} }
val filteredData = getFilteredData() val filteredData = getFilteredData().map { it.message }
view?.run { view?.run {
updateActionModeTitle(messagesToDelete.size) updateActionModeTitle(messagesToDelete.size)
@ -320,25 +321,31 @@ class MessageTabPresenter @Inject constructor(
} }
} }
private fun getFilteredData(): List<Message> { private fun getFilteredData(): List<MessageWithMutedAuthor> {
if (lastSearchQuery.trim().isEmpty()) { if (lastSearchQuery.trim().isEmpty()) {
val sortedMessages = messages.sortedByDescending { it.date } val sortedMessages = messages.sortedByDescending { it.message.date }
return when { return when {
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread } it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments } }
(onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
else -> sortedMessages else -> sortedMessages
} }
} else { } else {
val sortedMessages = messages val sortedMessages = messages
.map { it to calculateMatchRatio(it, lastSearchQuery) } .map { it to calculateMatchRatio(it.message, lastSearchQuery) }
.sortedWith(compareBy<Pair<Message, Int>> { -it.second }.thenByDescending { it.first.date }) .sortedWith(compareBy<Pair<MessageWithMutedAuthor, Int>> { -it.second }.thenByDescending { it.first.message.date })
.filter { it.second > 6000 } .filter { it.second > 6000 }
.map { it.first } .map { it.first }
return when { return when {
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread } it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments } }
(onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
else -> sortedMessages else -> sortedMessages
} }
} }
@ -367,8 +374,9 @@ class MessageTabPresenter @Inject constructor(
addAll(data.map { message -> addAll(data.map { message ->
MessageTabDataItem.MessageItem( MessageTabDataItem.MessageItem(
message = message, message = message.message,
isSelected = messagesToDelete.any { it.messageGlobalKey == message.messageGlobalKey }, isMuted = message.mutedMessageSender != null,
isSelected = messagesToDelete.any { it.messageGlobalKey == message.message.messageGlobalKey },
isActionMode = isActionMode isActionMode = isActionMode
) )
}) })

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorPrimary" />
<size
android:width="10dp"
android:height="10dp" />
</shape>

View File

@ -0,0 +1,5 @@
<vector android:height="17dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="17dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,18.69L7.84,6.14 5.27,3.49 4,4.76l2.8,2.8v0.01c-0.52,0.99 -0.8,2.16 -0.8,3.42v5l-2,2v1h13.73l2,2L21,19.72l-1,-1.03zM12,22c1.11,0 2,-0.89 2,-2h-4c0,1.11 0.89,2 2,2zM18,14.68L18,11c0,-3.08 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68c-0.15,0.03 -0.29,0.08 -0.42,0.12 -0.1,0.03 -0.2,0.07 -0.3,0.11h-0.01c-0.01,0 -0.01,0 -0.02,0.01 -0.23,0.09 -0.46,0.2 -0.68,0.31 0,0 -0.01,0 -0.01,0.01L18,14.68z"/>
</vector>

View File

@ -81,9 +81,9 @@
<ImageView <ImageView
android:id="@+id/messageItemUnreadIndicator" android:id="@+id/messageItemUnreadIndicator"
android:layout_width="10dp" android:layout_width="wrap_content"
android:layout_height="10dp" android:layout_height="wrap_content"
android:src="@drawable/ic_circle" android:src="@drawable/ic_circle_notification"
app:layout_constraintBottom_toBottomOf="@id/messageItemDate" app:layout_constraintBottom_toBottomOf="@id/messageItemDate"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/messageItemDate" app:layout_constraintTop_toTopOf="@id/messageItemDate"

View File

@ -36,4 +36,11 @@
android:title="@string/message_move_to_trash" android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium" app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuMute"
android:icon="@drawable/ic_settings_notifications"
android:orderInCategory="1"
android:title="@string/message_mute"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
</menu> </menu>

View File

@ -854,6 +854,7 @@
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Žádné internetové připojení</string> <string name="error_no_internet">Žádné internetové připojení</string>
<string name="error_invalid_device_datetime">Vyskytla se chyba. Zkontrolujte hodiny svého zařízení</string> <string name="error_invalid_device_datetime">Vyskytla se chyba. Zkontrolujte hodiny svého zařízení</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
<string name="error_timeout">Nelze se připojit ke deníku. Servery mohou být přetíženy. Prosím zkuste to znovu později</string> <string name="error_timeout">Nelze se připojit ke deníku. Servery mohou být přetíženy. Prosím zkuste to znovu později</string>
<string name="error_login_failed">Načítání dat se nezdařilo. Prosím zkuste to znovu později</string> <string name="error_login_failed">Načítání dat se nezdařilo. Prosím zkuste to znovu později</string>
<string name="error_password_change_required">Je vyžadována změna hesla pro deník</string> <string name="error_password_change_required">Je vyžadována změna hesla pro deník</string>

View File

@ -760,6 +760,7 @@
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Keine Internetverbindung</string> <string name="error_no_internet">Keine Internetverbindung</string>
<string name="error_invalid_device_datetime">Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr</string> <string name="error_invalid_device_datetime">Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
<string name="error_timeout">Registrierungsverbindung fehlgeschlagen. Server können überlastet sein. Bitte versuchen Sie es später noch einmal</string> <string name="error_timeout">Registrierungsverbindung fehlgeschlagen. Server können überlastet sein. Bitte versuchen Sie es später noch einmal</string>
<string name="error_login_failed">Das Laden der Daten ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal</string> <string name="error_login_failed">Das Laden der Daten ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal</string>
<string name="error_password_change_required">Passwortänderung für Registrierung erforderlich</string> <string name="error_password_change_required">Passwortänderung für Registrierung erforderlich</string>

View File

@ -854,6 +854,7 @@
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Brak połączenia z internetem</string> <string name="error_no_internet">Brak połączenia z internetem</string>
<string name="error_invalid_device_datetime">Wystąpił błąd. Sprawdź poprawność daty w urządzeniu</string> <string name="error_invalid_device_datetime">Wystąpił błąd. Sprawdź poprawność daty w urządzeniu</string>
<string name="error_account_inactive">Konto jest nieaktywne. Spróbuj zalogować się ponownie</string>
<string name="error_timeout">Nie udało się połączyć z dziennikiem. Serwery mogą być przeciążone. Spróbuj ponownie później</string> <string name="error_timeout">Nie udało się połączyć z dziennikiem. Serwery mogą być przeciążone. Spróbuj ponownie później</string>
<string name="error_login_failed">Ładowanie danych nie powiodło się. Spróbuj ponownie później</string> <string name="error_login_failed">Ładowanie danych nie powiodło się. Spróbuj ponownie później</string>
<string name="error_password_change_required">Wymagana zmiana hasła do dziennika</string> <string name="error_password_change_required">Wymagana zmiana hasła do dziennika</string>

View File

@ -854,6 +854,7 @@
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Интернет-соединение отсутствует</string> <string name="error_no_internet">Интернет-соединение отсутствует</string>
<string name="error_invalid_device_datetime">Произошла ошибка. Проверьте время на вашем устройстве</string> <string name="error_invalid_device_datetime">Произошла ошибка. Проверьте время на вашем устройстве</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
<string name="error_timeout">Не удалось подключиться к дневнику. Возможно, сервера перегружены, повторите попытку позже</string> <string name="error_timeout">Не удалось подключиться к дневнику. Возможно, сервера перегружены, повторите попытку позже</string>
<string name="error_login_failed">Не удалось загрузить данные, повторите попытку позже</string> <string name="error_login_failed">Не удалось загрузить данные, повторите попытку позже</string>
<string name="error_password_change_required">Необходимо изменить пароль дневника</string> <string name="error_password_change_required">Необходимо изменить пароль дневника</string>

View File

@ -854,6 +854,7 @@
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Žiadne internetové pripojenie</string> <string name="error_no_internet">Žiadne internetové pripojenie</string>
<string name="error_invalid_device_datetime">Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia</string> <string name="error_invalid_device_datetime">Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
<string name="error_timeout">Nedá sa pripojiť ku denníku. Servery môžu byť preťažené. Prosím skúste to znova neskôr</string> <string name="error_timeout">Nedá sa pripojiť ku denníku. Servery môžu byť preťažené. Prosím skúste to znova neskôr</string>
<string name="error_login_failed">Načítanie údajov zlyhalo. Skúste neskôr prosím</string> <string name="error_login_failed">Načítanie údajov zlyhalo. Skúste neskôr prosím</string>
<string name="error_password_change_required">Je vyžadovaná zmena hesla pre denník</string> <string name="error_password_change_required">Je vyžadovaná zmena hesla pre denník</string>

View File

@ -854,6 +854,7 @@
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Немає з\'єднання з інтернетом</string> <string name="error_no_internet">Немає з\'єднання з інтернетом</string>
<string name="error_invalid_device_datetime">Сталася помилка. Перевірте годинник пристрою</string> <string name="error_invalid_device_datetime">Сталася помилка. Перевірте годинник пристрою</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
<string name="error_timeout">Помилка підключення до щоденнику. Сервери можуть бути перевантажені, спробуйте пізніше</string> <string name="error_timeout">Помилка підключення до щоденнику. Сервери можуть бути перевантажені, спробуйте пізніше</string>
<string name="error_login_failed">Помилка завантаження даних, спробуйте пізніше</string> <string name="error_login_failed">Помилка завантаження даних, спробуйте пізніше</string>
<string name="error_password_change_required">Необхідна зміна пароля щоденника</string> <string name="error_password_change_required">Необхідна зміна пароля щоденника</string>

View File

@ -866,4 +866,10 @@
<string name="error_feature_disabled">Feature disabled by your school</string> <string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string> <string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>
<string name="error_field_required">This field is required</string> <string name="error_field_required">This field is required</string>
<!-- Mute system -->
<string name="message_mute">Mute</string>
<string name="message_unmute">Unmute</string>
<string name="message_mute_success">You have muted this user</string>
<string name="message_unmute_success">You have unmuted this user</string>
</resources> </resources>

View File

@ -6,8 +6,10 @@ import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.MutedMessageSender
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.data.toFirstResult
@ -19,9 +21,16 @@ import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.Status import io.github.wulkanowy.utils.Status
import io.github.wulkanowy.utils.status import io.github.wulkanowy.utils.status
import io.mockk.* import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.checkEquals
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.SpyK import io.mockk.impl.annotations.SpyK
import io.mockk.just
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
@ -45,6 +54,9 @@ class MessageRepositoryTest {
@MockK @MockK
private lateinit var messageDb: MessagesDao private lateinit var messageDb: MessagesDao
@MockK
private lateinit var mutesDb: MutedMessageSendersDao
@MockK @MockK
private lateinit var messageAttachmentDao: MessageAttachmentDao private lateinit var messageAttachmentDao: MessageAttachmentDao
@ -73,9 +85,22 @@ class MessageRepositoryTest {
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
every { refreshHelper.shouldBeRefreshed(any()) } returns false every { refreshHelper.shouldBeRefreshed(any()) } returns false
coEvery { mutesDb.checkMute(any()) } returns false
coEvery {
messageDb.loadMessagesWithMutedAuthor(
mailboxKey = any(),
folder = any()
)
} returns flowOf(emptyList())
coEvery {
messageDb.loadMessagesWithMutedAuthor(
folder = any(),
email = any()
)
} returns flowOf(emptyList())
repository = MessageRepository( repository = MessageRepository(
messagesDb = messageDb, messagesDb = messageDb,
mutedMessageSendersDao = mutesDb,
messageAttachmentDao = messageAttachmentDao, messageAttachmentDao = messageAttachmentDao,
sdk = sdk, sdk = sdk,
context = context, context = context,
@ -131,7 +156,11 @@ class MessageRepositoryTest {
@Test @Test
fun `get message when content already in db`() { fun `get message when content already in db`() {
val testMessage = getMessageEntity(123, "Test", false) val testMessage = getMessageEntity(123, "Test", false)
val messageWithAttachment = MessageWithAttachment(testMessage, emptyList()) val messageWithAttachment = MessageWithAttachment(
testMessage,
emptyList(),
MutedMessageSender("Jan Kowalski - P - (WULKANOWY)")
)
coEvery { messageDb.loadMessageWithAttachment("v4") } returns flowOf( coEvery { messageDb.loadMessageWithAttachment("v4") } returns flowOf(
messageWithAttachment messageWithAttachment
@ -149,8 +178,16 @@ class MessageRepositoryTest {
val testMessage = getMessageEntity(123, "", true) val testMessage = getMessageEntity(123, "", true)
val testMessageWithContent = testMessage.copy().apply { content = "Test" } val testMessageWithContent = testMessage.copy().apply { content = "Test" }
val mWa = MessageWithAttachment(testMessage, emptyList()) val mWa = MessageWithAttachment(
val mWaWithContent = MessageWithAttachment(testMessageWithContent, emptyList()) testMessage,
emptyList(),
MutedMessageSender("Jan Kowalski - P - (WULKANOWY)")
)
val mWaWithContent = MessageWithAttachment(
testMessageWithContent,
emptyList(),
MutedMessageSender("Jan Kowalski - P - (WULKANOWY)")
)
coEvery { coEvery {
messageDb.loadMessageWithAttachment("v4") messageDb.loadMessageWithAttachment("v4")