Update readBy and unreadBy fields during message list fetch (#1452)

This commit is contained in:
Mikołaj Pich 2021-08-29 15:40:28 +02:00 committed by GitHub
parent 2f43b6e552
commit 57d11e825b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 205 additions and 34 deletions

View File

@ -4,10 +4,12 @@ import android.content.Context
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
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.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.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
@ -48,22 +50,54 @@ class MessageRepository @Inject constructor(
private val cacheKey = "message" private val cacheKey = "message"
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( fun getMessages(
student: Student, semester: Semester,
folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false
): Flow<Resource<List<Message>>> = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) }, shouldFetch = {
it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(
getRefreshKey(cacheKey, student, folder)
)
},
query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) }, fetch = {
sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now())
.mapToEntities(student)
},
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
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 it.isNotified = !notify
}) })
messagesDb.updateAll(getMessagesWithReadByChange(old, new, !notify))
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder))
} }
) )
fun getMessage(student: Student, message: Message, markAsRead: Boolean = false) = networkBoundResource( private fun getMessagesWithReadByChange(
old: List<Message>, new: List<Message>,
setNotified: Boolean
): List<Message> {
val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) }
val newMeta = new.map { Triple(it, it.readBy, it.unreadBy) }
val updatedItems = newMeta uniqueSubtract oldMeta
return updatedItems.map {
val oldItem = old.find { item -> item.messageId == it.first.messageId }
it.first.apply {
id = oldItem?.id ?: 0
isNotified = oldItem?.isNotified ?: setNotified
content = oldItem?.content.orEmpty()
}
}
}
fun getMessage(
student: Student, message: Message, markAsRead: Boolean = false
): Flow<Resource<MessageWithAttachment?>> = networkBoundResource(
shouldFetch = { shouldFetch = {
checkNotNull(it, { "This message no longer exist!" }) checkNotNull(it, { "This message no longer exist!" })
Timber.d("Message content in db empty: ${it.message.content.isEmpty()}") Timber.d("Message content in db empty: ${it.message.content.isEmpty()}")
@ -71,7 +105,12 @@ class MessageRepository @Inject constructor(
}, },
query = { messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId) }, query = { messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId) },
fetch = { fetch = {
sdk.init(student).getMessageDetails(it!!.message.messageId, message.folderId, markAsRead, message.realId).let { details -> sdk.init(student).getMessageDetails(
messageId = it!!.message.messageId,
folderId = message.folderId,
read = markAsRead,
id = message.realId
).let { details ->
details.content to details.attachments.mapToEntities() details.content to details.attachments.mapToEntities()
} }
}, },
@ -95,26 +134,34 @@ class MessageRepository @Inject constructor(
return messagesDb.updateAll(messages) return messagesDb.updateAll(messages)
} }
suspend fun sendMessage(student: Student, subject: String, content: String, recipients: List<Recipient>): SentMessage { suspend fun sendMessage(
return sdk.init(student).sendMessage( student: Student, subject: String, content: String,
subject = subject, recipients: List<Recipient>
content = content, ): SentMessage = sdk.init(student).sendMessage(
recipients = recipients.mapFromEntities() subject = subject,
) content = content,
} recipients = recipients.mapFromEntities()
)
suspend fun deleteMessage(student: Student, message: Message) { suspend fun deleteMessage(student: Student, message: Message) {
val isDeleted = sdk.init(student).deleteMessages(listOf(message.messageId), message.folderId) val isDeleted = sdk.init(student).deleteMessages(
messages = listOf(message.messageId), message.folderId
)
if (message.folderId != MessageFolder.TRASHED.id) { if (message.folderId != MessageFolder.TRASHED.id && isDeleted) {
if (isDeleted) messagesDb.updateAll(listOf(message.copy(folderId = MessageFolder.TRASHED.id).apply { val deletedMessage = message.copy(folderId = MessageFolder.TRASHED.id).apply {
id = message.id id = message.id
content = message.content content = message.content
})) }
messagesDb.updateAll(listOf(deletedMessage))
} else messagesDb.deleteAll(listOf(message)) } else messagesDb.deleteAll(listOf(message))
} }
var draftMessage: MessageDraft? var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))
set(value) = sharedPrefProvider.putString(context.getString(R.string.pref_key_message_send_draft), value?.let { MessageDraftJsonAdapter(moshi).toJson(it) }) ?.let { MessageDraftJsonAdapter(moshi).fromJson(it) }
set(value) = sharedPrefProvider.putString(
context.getString(R.string.pref_key_message_send_draft),
value?.let { MessageDraftJsonAdapter(moshi).toJson(it) }
)
} }

View File

@ -8,19 +8,25 @@ 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.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.enums.MessageFolder
import io.github.wulkanowy.getSemesterEntity
import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.sdk.pojo.MessageDetails import io.github.wulkanowy.sdk.pojo.MessageDetails
import io.github.wulkanowy.sdk.pojo.Sender
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.toFirstResult import io.github.wulkanowy.utils.toFirstResult
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.Runs import io.mockk.Runs
import io.mockk.checkEquals
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every 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.just
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -29,6 +35,7 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import java.net.UnknownHostException import java.net.UnknownHostException
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.test.assertTrue
class MessageRepositoryTest { class MessageRepositoryTest {
@ -52,7 +59,9 @@ class MessageRepositoryTest {
private val student = getStudentEntity() private val student = getStudentEntity()
private lateinit var messageRepository: MessageRepository private val semester = getSemesterEntity()
private lateinit var repository: MessageRepository
@MockK @MockK
private lateinit var moshi: Moshi private lateinit var moshi: Moshi
@ -62,15 +71,92 @@ class MessageRepositoryTest {
MockKAnnotations.init(this) MockKAnnotations.init(this)
every { refreshHelper.isShouldBeRefreshed(any()) } returns false every { refreshHelper.isShouldBeRefreshed(any()) } returns false
messageRepository = MessageRepository(messageDb, messageAttachmentDao, sdk, context, refreshHelper, sharedPrefProvider, moshi) repository = MessageRepository(
messagesDb = messageDb,
messageAttachmentDao = messageAttachmentDao,
sdk = sdk,
context = context,
refreshHelper = refreshHelper,
sharedPrefProvider = sharedPrefProvider,
moshi = moshi,
)
}
@Test
fun `get messages when read by values was changed on already read message`() = runBlocking {
every { messageDb.loadAll(any(), any()) } returns flow {
val dbMessage = getMessageEntity(3, "", false).apply {
unreadBy = 10
readBy = 5
isNotified = true
}
emit(listOf(dbMessage))
}
coEvery { sdk.getMessages(Folder.RECEIVED, any(), any()) } returns listOf(
getMessageDto(messageId = 3, content = "", unread = false).copy(
unreadBy = 5,
readBy = 10,
)
)
coEvery { messageDb.deleteAll(any()) } just Runs
coEvery { messageDb.insertAll(any()) } returns listOf()
repository.getMessages(
student = student,
semester = semester,
folder = MessageFolder.RECEIVED,
forceRefresh = true,
notify = true, // all new messages will be marked as not notified
).toFirstResult().data.orEmpty()
coVerify(exactly = 1) { messageDb.deleteAll(emptyList()) }
coVerify(exactly = 1) { messageDb.insertAll(emptyList()) }
coVerify(exactly = 1) {
messageDb.updateAll(withArg {
assertEquals(1, it.size)
assertEquals(5, it.single().unreadBy)
assertEquals(10, it.single().readBy)
})
}
}
@Test
fun `get messages when fetched completely new message without notify`() = runBlocking {
every { messageDb.loadAll(any(), any()) } returns flowOf(emptyList())
coEvery { sdk.getMessages(Folder.RECEIVED, any(), any()) } returns listOf(
getMessageDto(messageId = 4, content = "Test", unread = true).copy(
unreadBy = 5,
readBy = 10,
)
)
coEvery { messageDb.deleteAll(any()) } just Runs
coEvery { messageDb.insertAll(any()) } returns listOf()
repository.getMessages(
student = student,
semester = semester,
folder = MessageFolder.RECEIVED,
forceRefresh = true,
notify = false,
).toFirstResult().data.orEmpty()
coVerify(exactly = 1) { messageDb.deleteAll(withArg { checkEquals(emptyList<Message>()) }) }
coVerify {
messageDb.insertAll(withArg {
assertEquals(4, it.single().messageId)
assertTrue(it.single().isNotified)
})
}
} }
@Test(expected = NoSuchElementException::class) @Test(expected = NoSuchElementException::class)
fun `throw error when message is not in the db`() { fun `throw error when message is not in the db`() {
val testMessage = getMessageEntity(1, "", false) val testMessage = getMessageEntity(1, "", false)
coEvery { messageDb.loadMessageWithAttachment(1, 1) } throws NoSuchElementException("No message in database") coEvery {
messageDb.loadMessageWithAttachment(1, 1)
} throws NoSuchElementException("No message in database")
runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } runBlocking { repository.getMessage(student, testMessage).toFirstResult() }
} }
@Test @Test
@ -78,9 +164,11 @@ class MessageRepositoryTest {
val testMessage = getMessageEntity(123, "Test", false) val testMessage = getMessageEntity(123, "Test", false)
val messageWithAttachment = MessageWithAttachment(testMessage, emptyList()) val messageWithAttachment = MessageWithAttachment(testMessage, emptyList())
coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returns flowOf(messageWithAttachment) coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returns flowOf(
messageWithAttachment
)
val res = runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } val res = runBlocking { repository.getMessage(student, testMessage).toFirstResult() }
assertEquals(null, res.error) assertEquals(null, res.error)
assertEquals(Status.SUCCESS, res.status) assertEquals(Status.SUCCESS, res.status)
@ -95,12 +183,24 @@ class MessageRepositoryTest {
val mWa = MessageWithAttachment(testMessage, emptyList()) val mWa = MessageWithAttachment(testMessage, emptyList())
val mWaWithContent = MessageWithAttachment(testMessageWithContent, emptyList()) val mWaWithContent = MessageWithAttachment(testMessageWithContent, emptyList())
coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returnsMany listOf(flowOf(mWa), flowOf(mWaWithContent)) coEvery {
coEvery { sdk.getMessageDetails(testMessage.messageId, 1, false, testMessage.realId) } returns MessageDetails("Test", emptyList()) messageDb.loadMessageWithAttachment(
1,
testMessage.messageId
)
} returnsMany listOf(flowOf(mWa), flowOf(mWaWithContent))
coEvery {
sdk.getMessageDetails(
messageId = testMessage.messageId,
folderId = 1,
read = false,
id = testMessage.realId
)
} returns MessageDetails("Test", emptyList())
coEvery { messageDb.updateAll(any()) } just Runs coEvery { messageDb.updateAll(any()) } just Runs
coEvery { messageAttachmentDao.insertAttachments(any()) } returns listOf(1) coEvery { messageAttachmentDao.insertAttachments(any()) } returns listOf(1)
val res = runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } val res = runBlocking { repository.getMessage(student, testMessage).toFirstResult() }
assertEquals(null, res.error) assertEquals(null, res.error)
assertEquals(Status.SUCCESS, res.status) assertEquals(Status.SUCCESS, res.status)
@ -112,18 +212,22 @@ class MessageRepositoryTest {
fun `get message when content in db is empty and there is no internet connection`() { fun `get message when content in db is empty and there is no internet connection`() {
val testMessage = getMessageEntity(123, "", false) val testMessage = getMessageEntity(123, "", false)
coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } throws UnknownHostException() coEvery {
messageDb.loadMessageWithAttachment(1, testMessage.messageId)
} throws UnknownHostException()
runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } runBlocking { repository.getMessage(student, testMessage).toFirstResult() }
} }
@Test(expected = UnknownHostException::class) @Test(expected = UnknownHostException::class)
fun `get message when content in db is empty, unread and there is no internet connection`() { fun `get message when content in db is empty, unread and there is no internet connection`() {
val testMessage = getMessageEntity(123, "", true) val testMessage = getMessageEntity(123, "", true)
coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } throws UnknownHostException() coEvery {
messageDb.loadMessageWithAttachment(1, testMessage.messageId)
} throws UnknownHostException()
runBlocking { messageRepository.getMessage(student, testMessage).toList()[1] } runBlocking { repository.getMessage(student, testMessage).toList()[1] }
} }
private fun getMessageEntity( private fun getMessageEntity(
@ -135,10 +239,10 @@ class MessageRepositoryTest {
realId = 1, realId = 1,
messageId = messageId, messageId = messageId,
sender = "", sender = "",
senderId = 1, senderId = 0,
recipient = "", recipient = "Wielu adresatów",
subject = "", subject = "",
date = LocalDateTime.now(), date = LocalDateTime.MAX,
folderId = 1, folderId = 1,
unread = unread, unread = unread,
removed = false, removed = false,
@ -148,4 +252,24 @@ class MessageRepositoryTest {
unreadBy = 1 unreadBy = 1
readBy = 1 readBy = 1
} }
private fun getMessageDto(
messageId: Int,
content: String,
unread: Boolean,
) = io.github.wulkanowy.sdk.pojo.Message(
id = 1,
messageId = messageId,
sender = Sender("", "", 0, 0, 0, ""),
recipients = listOf(),
subject = "",
content = content,
date = LocalDateTime.MAX,
folderId = 1,
unread = unread,
unreadBy = 0,
readBy = 0,
removed = false,
hasAttachments = false,
)
} }