forked from github/wulkanowy-mirror
Add mute message senders (#2415)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
This commit is contained in:
parent
1ab300d74f
commit
7a4032dda4
2527
app/schemas/io.github.wulkanowy.data.db.AppDatabase/60.json
Normal file
2527
app/schemas/io.github.wulkanowy.data.db.AppDatabase/60.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -54,5 +54,9 @@
|
||||
{
|
||||
"displayName": "Antoni Paduch",
|
||||
"githubUsername": "janAte1"
|
||||
},
|
||||
{
|
||||
"displayName": "Kamil Wąsik",
|
||||
"githubUsername": "JestemKamil"
|
||||
}
|
||||
]
|
||||
|
@ -254,6 +254,10 @@ internal class DataModule {
|
||||
@Provides
|
||||
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideMutesDao(database: AppDatabase) = database.mutedMessageSendersDao
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao
|
||||
|
@ -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.MessagesDao
|
||||
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.NotificationDao
|
||||
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.MessageAttachment
|
||||
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.Notification
|
||||
import io.github.wulkanowy.data.db.entities.Recipient
|
||||
@ -157,6 +159,7 @@ import javax.inject.Singleton
|
||||
SchoolAnnouncement::class,
|
||||
Notification::class,
|
||||
AdminMessage::class,
|
||||
MutedMessageSender::class,
|
||||
GradeDescriptive::class,
|
||||
],
|
||||
autoMigrations = [
|
||||
@ -169,6 +172,7 @@ import javax.inject.Singleton
|
||||
AutoMigration(from = 56, to = 57, spec = Migration57::class),
|
||||
AutoMigration(from = 57, to = 58, spec = Migration58::class),
|
||||
AutoMigration(from = 58, to = 59),
|
||||
AutoMigration(from = 59, to = 60),
|
||||
],
|
||||
version = AppDatabase.VERSION_SCHEMA,
|
||||
exportSchema = true
|
||||
@ -177,7 +181,7 @@ import javax.inject.Singleton
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
companion object {
|
||||
const val VERSION_SCHEMA = 59
|
||||
const val VERSION_SCHEMA = 60
|
||||
|
||||
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
|
||||
Migration2(),
|
||||
@ -303,5 +307,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract val adminMessagesDao: AdminMessageDao
|
||||
|
||||
abstract val mutedMessageSendersDao: MutedMessageSendersDao
|
||||
|
||||
abstract val gradeDescriptiveDao: GradeDescriptiveDao
|
||||
}
|
||||
|
@ -5,15 +5,23 @@ import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
|
||||
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface MessagesDao : BaseDao<Message> {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
|
||||
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")
|
||||
fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -2,11 +2,15 @@ package io.github.wulkanowy.data.db.entities
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import java.io.Serializable
|
||||
|
||||
data class MessageWithAttachment(
|
||||
@Embedded
|
||||
val message: Message,
|
||||
|
||||
@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
|
||||
|
@ -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?,
|
||||
)
|
@ -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
|
||||
}
|
@ -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.MessageAttachmentDao
|
||||
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.Message
|
||||
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.Student
|
||||
import io.github.wulkanowy.data.enums.MessageFolder
|
||||
@ -42,6 +45,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class MessageRepository @Inject constructor(
|
||||
private val messagesDb: MessagesDao,
|
||||
private val mutedMessageSendersDao: MutedMessageSendersDao,
|
||||
private val messageAttachmentDao: MessageAttachmentDao,
|
||||
private val sdk: Sdk,
|
||||
@ApplicationContext private val context: Context,
|
||||
@ -51,7 +55,6 @@ class MessageRepository @Inject constructor(
|
||||
private val mailboxDao: MailboxDao,
|
||||
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private val messagesCacheKey = "message"
|
||||
@ -63,7 +66,7 @@ class MessageRepository @Inject constructor(
|
||||
folder: MessageFolder,
|
||||
forceRefresh: Boolean,
|
||||
notify: Boolean = false,
|
||||
): Flow<Resource<List<Message>>> = networkBoundResource(
|
||||
): Flow<Resource<List<MessageWithMutedAuthor>>> = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = { it.isEmpty() },
|
||||
shouldFetch = {
|
||||
@ -74,8 +77,8 @@ class MessageRepository @Inject constructor(
|
||||
},
|
||||
query = {
|
||||
if (mailbox == null) {
|
||||
messagesDb.loadAll(folder.id, student.email)
|
||||
} else messagesDb.loadAll(mailbox.globalKey, folder.id)
|
||||
messagesDb.loadMessagesWithMutedAuthor(folder.id, student.email)
|
||||
} else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student).getMessages(
|
||||
@ -83,10 +86,12 @@ class MessageRepository @Inject constructor(
|
||||
mailboxKey = mailbox?.globalKey,
|
||||
).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.insertAll((new uniqueSubtract old).onEach {
|
||||
it.isNotified = !notify
|
||||
val muted = isMuted(it.correspondents)
|
||||
it.isNotified = !notify || muted
|
||||
})
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(
|
||||
@ -106,9 +111,7 @@ class MessageRepository @Inject constructor(
|
||||
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
|
||||
(it.message.unread && markAsRead) || it.message.content.isBlank()
|
||||
},
|
||||
query = {
|
||||
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
|
||||
},
|
||||
query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
|
||||
fetch = {
|
||||
sdk.init(student).getMessageDetails(
|
||||
messageKey = it!!.message.messageGlobalKey,
|
||||
@ -236,4 +239,18 @@ class MessageRepository @Inject constructor(
|
||||
context.getString(R.string.pref_key_message_draft),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -304,6 +304,7 @@ class DashboardPresenter @Inject constructor(
|
||||
forceRefresh = forceRefresh
|
||||
)
|
||||
}
|
||||
.mapResourceData { it.map { messageWithAuthor -> messageWithAuthor.message } }
|
||||
.onResourceError { errorHandler.dispatch(it) }
|
||||
.takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
|
||||
|
||||
|
@ -50,12 +50,15 @@ class MessagePreviewAdapter @Inject constructor() :
|
||||
ViewType.MESSAGE.id -> MessageViewHolder(
|
||||
ItemMessagePreviewBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
ViewType.DIVIDER.id -> DividerViewHolder(
|
||||
ItemMessageDividerBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
ViewType.ATTACHMENT.id -> AttachmentViewHolder(
|
||||
ItemMessageAttachmentBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
@ -66,6 +69,7 @@ class MessagePreviewAdapter @Inject constructor() :
|
||||
holder,
|
||||
requireNotNull(messageWithAttachment).message
|
||||
)
|
||||
|
||||
is AttachmentViewHolder -> bindAttachment(
|
||||
holder,
|
||||
requireNotNull(messageWithAttachment).attachments[position - 2]
|
||||
@ -82,9 +86,11 @@ class MessagePreviewAdapter @Inject constructor() :
|
||||
recipientCount > 1 -> {
|
||||
context.getString(R.string.message_read_by, message.readBy, recipientCount)
|
||||
}
|
||||
|
||||
message.readBy == 1 || (isReceived && !message.unread) -> {
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -50,12 +50,20 @@ class MessagePreviewFragment :
|
||||
|
||||
private var menuPrintButton: MenuItem? = null
|
||||
|
||||
private var menuMuteButton: MenuItem? = null
|
||||
|
||||
override val titleStringId: Int
|
||||
get() = R.string.message_title
|
||||
|
||||
override val deleteMessageSuccessString: String
|
||||
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
|
||||
get() = getString(R.string.message_no_subject)
|
||||
|
||||
@ -106,6 +114,7 @@ class MessagePreviewFragment :
|
||||
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
|
||||
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
|
||||
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
|
||||
menuMuteButton = menu.findItem(R.id.messagePreviewMenuMute)
|
||||
presenter.onCreateOptionsMenu()
|
||||
|
||||
menu.findItem(R.id.mainMenuAccount).isVisible = false
|
||||
@ -118,6 +127,7 @@ class MessagePreviewFragment :
|
||||
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
|
||||
R.id.messagePreviewMenuShare -> presenter.onShare()
|
||||
R.id.messagePreviewMenuPrint -> presenter.onPrint()
|
||||
R.id.messagePreviewMenuMute -> presenter.onMute()
|
||||
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) {
|
||||
binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE
|
||||
}
|
||||
@ -143,6 +158,7 @@ class MessagePreviewFragment :
|
||||
menuDeleteButton?.isVisible = show
|
||||
menuShareButton?.isVisible = show
|
||||
menuPrintButton?.isVisible = show
|
||||
menuMuteButton?.isVisible = show && isReplayable
|
||||
}
|
||||
|
||||
override fun setDeletedOptionsLabels() {
|
||||
@ -213,7 +229,7 @@ class MessagePreviewFragment :
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putSerializable(MESSAGE_ID_KEY, presenter.message)
|
||||
outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import androidx.core.text.parseAsHtml
|
||||
import io.github.wulkanowy.R
|
||||
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.db.entities.MessageWithAttachment
|
||||
import io.github.wulkanowy.data.enums.MessageFolder
|
||||
import io.github.wulkanowy.data.repositories.MessageRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
@ -26,9 +26,7 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
private val analytics: AnalyticsHelper
|
||||
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
|
||||
|
||||
var message: Message? = null
|
||||
|
||||
var attachments: List<MessageAttachment>? = null
|
||||
var messageWithAttachments: MessageWithAttachment? = null
|
||||
|
||||
private lateinit var lastError: Throwable
|
||||
|
||||
@ -38,7 +36,6 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
super.onAttachView(view)
|
||||
view.initView()
|
||||
errorHandler.showErrorMessage = ::showErrorViewOnError
|
||||
this.message = message
|
||||
loadData(requireNotNull(message))
|
||||
}
|
||||
|
||||
@ -66,13 +63,12 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
.logResourceStatus("message ${messageToLoad.messageId} preview")
|
||||
.onResourceData {
|
||||
if (it != null) {
|
||||
message = it.message
|
||||
attachments = it.attachments
|
||||
messageWithAttachments = it
|
||||
view?.apply {
|
||||
setMessageWithAttachment(it)
|
||||
showContent(true)
|
||||
initOptions()
|
||||
|
||||
updateMuteToggleButton(isMuted = it.mutedMessageSender != null)
|
||||
if (preferencesRepository.isIncognitoMode && it.message.unread) {
|
||||
showMessage(R.string.message_incognito_description)
|
||||
}
|
||||
@ -83,8 +79,7 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
popView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onResourceSuccess {
|
||||
}.onResourceSuccess {
|
||||
if (it != null) {
|
||||
analytics.logEvent(
|
||||
"load_item",
|
||||
@ -92,31 +87,28 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
"length" to it.message.content.length
|
||||
)
|
||||
}
|
||||
}
|
||||
.onResourceNotLoading { view?.showProgress(false) }
|
||||
.onResourceError {
|
||||
}.onResourceNotLoading { view?.showProgress(false) }.onResourceError {
|
||||
retryCallback = { onMessageLoadRetry(messageToLoad) }
|
||||
errorHandler.dispatch(it)
|
||||
}
|
||||
.launch()
|
||||
}.launch()
|
||||
}
|
||||
|
||||
fun onReply(): Boolean {
|
||||
return if (message != null) {
|
||||
view?.openMessageReply(message)
|
||||
return if (messageWithAttachments?.message != null) {
|
||||
view?.openMessageReply(messageWithAttachments?.message)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
fun onForward(): Boolean {
|
||||
return if (message != null) {
|
||||
view?.openMessageForward(message)
|
||||
return if (messageWithAttachments?.message != null) {
|
||||
view?.openMessageForward(messageWithAttachments?.message)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
fun onShare(): Boolean {
|
||||
val message = message ?: return false
|
||||
val message = messageWithAttachments?.message ?: return false
|
||||
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
|
||||
|
||||
val text = buildString {
|
||||
@ -129,13 +121,15 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
|
||||
appendLine(message.content.parseAsHtml())
|
||||
|
||||
if (!attachments.isNullOrEmpty()) {
|
||||
if (!messageWithAttachments?.attachments.isNullOrEmpty()) {
|
||||
appendLine()
|
||||
appendLine("Załączniki:")
|
||||
|
||||
append(attachments.orEmpty().joinToString(separator = "\n") { attachment ->
|
||||
"${attachment.filename}: ${attachment.url}"
|
||||
})
|
||||
append(
|
||||
messageWithAttachments?.attachments.orEmpty()
|
||||
.joinToString(separator = "\n") { attachment ->
|
||||
"${attachment.filename}: ${attachment.url}"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +142,7 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun onPrint(): Boolean {
|
||||
val message = message ?: return false
|
||||
val message = messageWithAttachments?.message ?: return false
|
||||
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
|
||||
|
||||
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>DO</h4>${message.recipients}</div>")
|
||||
}
|
||||
val messageContent = "<p>${message.content}</p>"
|
||||
.replace(Regex("[\\n\\r]{2,}"), "</p><p>")
|
||||
val messageContent = "<p>${message.content}</p>".replace(Regex("[\\n\\r]{2,}"), "</p><p>")
|
||||
.replace(Regex("[\\n\\r]"), "<br>")
|
||||
|
||||
val jobName = buildString {
|
||||
@ -171,9 +164,7 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
view?.apply {
|
||||
val html = printHTML
|
||||
.replace("%SUBJECT%", subject)
|
||||
.replace("%CONTENT%", messageContent)
|
||||
val html = printHTML.replace("%SUBJECT%", subject).replace("%CONTENT%", messageContent)
|
||||
.replace("%INFO%", infoContent)
|
||||
printDocument(html, jobName)
|
||||
}
|
||||
@ -182,7 +173,7 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun deleteMessage() {
|
||||
message ?: return
|
||||
messageWithAttachments?.message ?: return
|
||||
|
||||
view?.run {
|
||||
showContent(false)
|
||||
@ -191,24 +182,22 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
showErrorView(false)
|
||||
}
|
||||
|
||||
Timber.i("Delete message ${message?.messageGlobalKey}")
|
||||
Timber.i("Delete message ${messageWithAttachments?.message?.messageGlobalKey}")
|
||||
|
||||
presenterScope.launch {
|
||||
runCatching {
|
||||
val student = studentRepository.getCurrentStudent(decryptPass = true)
|
||||
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)
|
||||
}
|
||||
@ -232,10 +221,10 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
private fun initOptions() {
|
||||
view?.apply {
|
||||
showOptions(
|
||||
show = message != null,
|
||||
isReplayable = message?.folderId != MessageFolder.SENT.id,
|
||||
show = messageWithAttachments?.message != null,
|
||||
isReplayable = messageWithAttachments?.message?.folderId != MessageFolder.SENT.id,
|
||||
)
|
||||
message?.let {
|
||||
messageWithAttachments?.message?.let {
|
||||
when (it.folderId == MessageFolder.TRASHED.id) {
|
||||
true -> setDeletedOptionsLabels()
|
||||
false -> setNotDeletedOptionsLabels()
|
||||
@ -248,4 +237,29 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
fun onCreateOptionsMenu() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,10 @@ interface MessagePreviewView : BaseView {
|
||||
|
||||
val deleteMessageSuccessString: String
|
||||
|
||||
val muteMessageSuccessString: String
|
||||
|
||||
val unmuteMessageSuccessString: String
|
||||
|
||||
val messageNoSubjectString: String
|
||||
|
||||
val printHTML: String
|
||||
@ -19,6 +23,8 @@ interface MessagePreviewView : BaseView {
|
||||
|
||||
fun setMessageWithAttachment(item: MessageWithAttachment)
|
||||
|
||||
fun updateMuteToggleButton(isMuted: Boolean)
|
||||
|
||||
fun showProgress(show: Boolean)
|
||||
|
||||
fun showContent(show: Boolean)
|
||||
|
@ -18,8 +18,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessageTabAdapter @Inject constructor() :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
class MessageTabAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
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 {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
|
||||
return when (MessageItemViewType.values()[viewType]) {
|
||||
return when (MessageItemViewType.entries[viewType]) {
|
||||
MessageItemViewType.FILTERS -> HeaderViewHolder(
|
||||
ItemMessageChipsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
MessageItemViewType.MESSAGE -> ItemViewHolder(
|
||||
ItemMessageBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
@ -137,7 +137,12 @@ class MessageTabAdapter @Inject constructor() :
|
||||
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor))
|
||||
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 {
|
||||
holder.bindingAdapterPosition.let {
|
||||
@ -165,8 +170,7 @@ class MessageTabAdapter @Inject constructor() :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
private class MessageTabDiffUtil(
|
||||
private val old: List<MessageTabDataItem>,
|
||||
private val new: List<MessageTabDataItem>
|
||||
private val old: List<MessageTabDataItem>, private val new: List<MessageTabDataItem>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int = old.size
|
||||
|
@ -6,6 +6,7 @@ sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
|
||||
|
||||
data class MessageItem(
|
||||
val message: Message,
|
||||
val isMuted: Boolean,
|
||||
val isSelected: Boolean,
|
||||
val isActionMode: Boolean
|
||||
) : MessageTabDataItem(MessageItemViewType.MESSAGE)
|
||||
|
@ -4,6 +4,7 @@ import io.github.wulkanowy.R
|
||||
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.db.entities.MessageWithMutedAuthor
|
||||
import io.github.wulkanowy.data.enums.MessageFolder
|
||||
import io.github.wulkanowy.data.repositories.MessageRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
@ -39,7 +40,7 @@ class MessageTabPresenter @Inject constructor(
|
||||
private var mailboxes: List<Mailbox> = emptyList()
|
||||
private var selectedMailbox: Mailbox? = null
|
||||
|
||||
private var messages = emptyList<Message>()
|
||||
private var messages = emptyList<MessageWithMutedAuthor>()
|
||||
|
||||
private val searchChannel = Channel<String>()
|
||||
|
||||
@ -141,7 +142,7 @@ class MessageTabPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
fun onActionModeSelectCheckAll() {
|
||||
val messagesToSelect = getFilteredData()
|
||||
val messagesToSelect = getFilteredData().map { it.message }
|
||||
val isAllSelected = messagesToDelete.containsAll(messagesToSelect)
|
||||
|
||||
if (isAllSelected) {
|
||||
@ -188,7 +189,7 @@ class MessageTabPresenter @Inject constructor(
|
||||
view?.showActionMode(false)
|
||||
}
|
||||
|
||||
val filteredData = getFilteredData()
|
||||
val filteredData = getFilteredData().map { it.message }
|
||||
|
||||
view?.run {
|
||||
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()) {
|
||||
val sortedMessages = messages.sortedByDescending { it.date }
|
||||
val sortedMessages = messages.sortedByDescending { it.message.date }
|
||||
return when {
|
||||
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
|
||||
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
|
||||
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
|
||||
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
|
||||
it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
|
||||
}
|
||||
|
||||
(onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
|
||||
onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
|
||||
else -> sortedMessages
|
||||
}
|
||||
} else {
|
||||
val sortedMessages = messages
|
||||
.map { it to calculateMatchRatio(it, lastSearchQuery) }
|
||||
.sortedWith(compareBy<Pair<Message, Int>> { -it.second }.thenByDescending { it.first.date })
|
||||
.map { it to calculateMatchRatio(it.message, lastSearchQuery) }
|
||||
.sortedWith(compareBy<Pair<MessageWithMutedAuthor, Int>> { -it.second }.thenByDescending { it.first.message.date })
|
||||
.filter { it.second > 6000 }
|
||||
.map { it.first }
|
||||
return when {
|
||||
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
|
||||
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
|
||||
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
|
||||
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
|
||||
it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
|
||||
}
|
||||
|
||||
(onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
|
||||
onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
|
||||
else -> sortedMessages
|
||||
}
|
||||
}
|
||||
@ -367,8 +374,9 @@ class MessageTabPresenter @Inject constructor(
|
||||
|
||||
addAll(data.map { message ->
|
||||
MessageTabDataItem.MessageItem(
|
||||
message = message,
|
||||
isSelected = messagesToDelete.any { it.messageGlobalKey == message.messageGlobalKey },
|
||||
message = message.message,
|
||||
isMuted = message.mutedMessageSender != null,
|
||||
isSelected = messagesToDelete.any { it.messageGlobalKey == message.message.messageGlobalKey },
|
||||
isActionMode = isActionMode
|
||||
)
|
||||
})
|
||||
|
10
app/src/main/res/drawable/ic_circle_notification.xml
Normal file
10
app/src/main/res/drawable/ic_circle_notification.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_notifications_off.xml
Normal file
5
app/src/main/res/drawable/ic_notifications_off.xml
Normal 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>
|
@ -81,9 +81,9 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageItemUnreadIndicator"
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:src="@drawable/ic_circle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_circle_notification"
|
||||
app:layout_constraintBottom_toBottomOf="@id/messageItemDate"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/messageItemDate"
|
||||
|
@ -36,4 +36,11 @@
|
||||
android:title="@string/message_move_to_trash"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
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>
|
||||
|
@ -864,4 +864,10 @@
|
||||
<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_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>
|
||||
|
@ -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.MessageAttachmentDao
|
||||
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.MessageWithAttachment
|
||||
import io.github.wulkanowy.data.db.entities.MutedMessageSender
|
||||
import io.github.wulkanowy.data.enums.MessageFolder
|
||||
import io.github.wulkanowy.data.errorOrNull
|
||||
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.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.SpyK
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toList
|
||||
@ -45,6 +54,9 @@ class MessageRepositoryTest {
|
||||
@MockK
|
||||
private lateinit var messageDb: MessagesDao
|
||||
|
||||
@MockK
|
||||
private lateinit var mutesDb: MutedMessageSendersDao
|
||||
|
||||
@MockK
|
||||
private lateinit var messageAttachmentDao: MessageAttachmentDao
|
||||
|
||||
@ -73,9 +85,22 @@ class MessageRepositoryTest {
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
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(
|
||||
messagesDb = messageDb,
|
||||
mutedMessageSendersDao = mutesDb,
|
||||
messageAttachmentDao = messageAttachmentDao,
|
||||
sdk = sdk,
|
||||
context = context,
|
||||
@ -131,7 +156,11 @@ class MessageRepositoryTest {
|
||||
@Test
|
||||
fun `get message when content already in db`() {
|
||||
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(
|
||||
messageWithAttachment
|
||||
@ -149,8 +178,16 @@ class MessageRepositoryTest {
|
||||
val testMessage = getMessageEntity(123, "", true)
|
||||
val testMessageWithContent = testMessage.copy().apply { content = "Test" }
|
||||
|
||||
val mWa = MessageWithAttachment(testMessage, emptyList())
|
||||
val mWaWithContent = MessageWithAttachment(testMessageWithContent, emptyList())
|
||||
val mWa = MessageWithAttachment(
|
||||
testMessage,
|
||||
emptyList(),
|
||||
MutedMessageSender("Jan Kowalski - P - (WULKANOWY)")
|
||||
)
|
||||
val mWaWithContent = MessageWithAttachment(
|
||||
testMessageWithContent,
|
||||
emptyList(),
|
||||
MutedMessageSender("Jan Kowalski - P - (WULKANOWY)")
|
||||
)
|
||||
|
||||
coEvery {
|
||||
messageDb.loadMessageWithAttachment("v4")
|
||||
|
Loading…
Reference in New Issue
Block a user