mirror of
https://github.com/ProtonMail/android-mail.git
synced 2026-05-15 09:50:40 +00:00
Refactor message cursor handling into dedicated repository
Solution: - Add MessageCursorRepository to manage cursor lifecycle - Delegate getConversationCursor to the new repository - Limit RustMessageListQuery to reusing the active paginator only - First try to reuse Mailbox scroller to create a cursor - Fall back to a separate cursor scroller scoped to the repository ET-4801
This commit is contained in:
+6
@@ -29,6 +29,7 @@ import ch.protonmail.android.mailmessage.data.local.RustMessageListQueryImpl
|
||||
import ch.protonmail.android.mailmessage.data.local.RustRsvpEventDataSource
|
||||
import ch.protonmail.android.mailmessage.data.local.RustRsvpEventDataSourceImpl
|
||||
import ch.protonmail.android.mailmessage.data.repository.InMemoryAvatarImageStateRepositoryImpl
|
||||
import ch.protonmail.android.mailmessage.data.repository.MessageCursorRepositoryImpl
|
||||
import ch.protonmail.android.mailmessage.data.repository.PreviousScheduleSendTimeInMemoryRepository
|
||||
import ch.protonmail.android.mailmessage.data.repository.RsvpEventRepositoryImpl
|
||||
import ch.protonmail.android.mailmessage.data.repository.RustMessageActionRepository
|
||||
@@ -37,6 +38,7 @@ import ch.protonmail.android.mailmessage.data.repository.RustMessageRepositoryIm
|
||||
import ch.protonmail.android.mailmessage.domain.repository.InMemoryAvatarImageStateRepository
|
||||
import ch.protonmail.android.mailmessage.domain.repository.MessageActionRepository
|
||||
import ch.protonmail.android.mailmessage.domain.repository.MessageBodyRepository
|
||||
import ch.protonmail.android.mailmessage.domain.repository.MessageCursorRepository
|
||||
import ch.protonmail.android.mailmessage.domain.repository.MessageRepository
|
||||
import ch.protonmail.android.mailmessage.domain.repository.PreviousScheduleSendTimeRepository
|
||||
import ch.protonmail.android.mailmessage.domain.repository.RsvpEventRepository
|
||||
@@ -87,6 +89,10 @@ object MailMessageModule {
|
||||
@Singleton
|
||||
fun bindMessageRepository(impl: RustMessageRepositoryImpl): MessageRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMessageCursorRepository(impl: MessageCursorRepositoryImpl): MessageCursorRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindsInMemoryAvatarImageStateRepository(
|
||||
|
||||
-10
@@ -19,18 +19,14 @@
|
||||
package ch.protonmail.android.mailmessage.data.local
|
||||
|
||||
import arrow.core.Either
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelAsAction
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalMessageId
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalMessageMetadata
|
||||
import ch.protonmail.android.mailcommon.data.mapper.RemoteMessageId
|
||||
import ch.protonmail.android.mailcommon.data.wrapper.ConversationCursor
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import ch.protonmail.android.mailcommon.domain.model.UndoSendError
|
||||
import ch.protonmail.android.mailcommon.domain.model.UndoableOperation
|
||||
import ch.protonmail.android.maillabel.domain.model.LabelId
|
||||
import ch.protonmail.android.mailmessage.domain.model.MessageId
|
||||
import ch.protonmail.android.mailmessage.domain.model.MessageScrollerFetchNewStatus
|
||||
import ch.protonmail.android.mailmessage.domain.model.PreviousScheduleSendTime
|
||||
@@ -63,12 +59,6 @@ interface RustMessageDataSource {
|
||||
|
||||
suspend fun observeMessage(userId: UserId, messageId: LocalMessageId): Flow<Either<DataError, LocalMessageMetadata>>
|
||||
|
||||
suspend fun getConversationCursor(
|
||||
firstPage: LocalConversationId,
|
||||
userId: UserId,
|
||||
labelId: LabelId
|
||||
): Either<ConversationCursorError, ConversationCursor>
|
||||
|
||||
suspend fun getSenderImage(
|
||||
userId: UserId,
|
||||
address: String,
|
||||
|
||||
-41
@@ -20,22 +20,17 @@ package ch.protonmail.android.mailmessage.data.local
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelAsAction
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalMessageId
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalMessageMetadata
|
||||
import ch.protonmail.android.mailcommon.data.mapper.RemoteMessageId
|
||||
import ch.protonmail.android.mailcommon.data.wrapper.ConversationCursor
|
||||
import ch.protonmail.android.mailcommon.domain.annotation.MissingRustApi
|
||||
import ch.protonmail.android.mailcommon.domain.coroutines.IODispatcher
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError.InvalidState
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import ch.protonmail.android.mailcommon.domain.model.UndoSendError
|
||||
import ch.protonmail.android.mailcommon.domain.model.UndoableOperation
|
||||
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
|
||||
import ch.protonmail.android.maillabel.domain.model.LabelId
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toLocalMessageId
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toMessageScrollerFetchNewStatus
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toPreviousScheduleSendTime
|
||||
@@ -62,7 +57,6 @@ import ch.protonmail.android.mailmessage.data.usecase.RustUnstarMessages
|
||||
import ch.protonmail.android.mailmessage.domain.model.MessageId
|
||||
import ch.protonmail.android.mailmessage.domain.model.MessageScrollerFetchNewStatus
|
||||
import ch.protonmail.android.mailmessage.domain.model.PreviousScheduleSendTime
|
||||
import ch.protonmail.android.mailmessage.domain.model.toConversationCursorError
|
||||
import ch.protonmail.android.mailpagination.domain.model.PageKey
|
||||
import ch.protonmail.android.mailpagination.domain.model.PaginationError
|
||||
import ch.protonmail.android.mailsession.data.usecase.ExecuteWithUserSession
|
||||
@@ -169,41 +163,6 @@ class RustMessageDataSourceImpl @Inject constructor(
|
||||
return rustMessageQuery.observeMessage(session, messageId).flowOn(ioDispatcher)
|
||||
}
|
||||
|
||||
override suspend fun getConversationCursor(
|
||||
firstPage: LocalConversationId,
|
||||
userId: UserId,
|
||||
labelId: LabelId
|
||||
): Either<ConversationCursorError, ConversationCursor> = withContext(ioDispatcher) {
|
||||
// we are relying on the exiting pager being open already
|
||||
val result = rustMessageListQuery.getCursor(userId = userId, conversationId = firstPage, labelId = labelId)
|
||||
?: initializeCursor(userId, labelId, firstPage).apply {
|
||||
Timber.d("rust-message cursor unable to get cursor, retrieving conversations and retrying")
|
||||
}
|
||||
|
||||
return@withContext when {
|
||||
result == null -> InvalidState.left()
|
||||
else -> {
|
||||
result.mapLeft {
|
||||
it.toConversationCursorError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initializeCursor(
|
||||
userId: UserId,
|
||||
labelId: LabelId,
|
||||
firstPage: LocalConversationId
|
||||
) = rustMessageListQuery.getMessages(userId, PageKey.DefaultPageKey().copy(labelId = labelId))
|
||||
.onLeft {
|
||||
Timber.e("rust-message cursor unable to recover and get conversations")
|
||||
}
|
||||
.fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { rustMessageListQuery.getCursor(userId, labelId, firstPage) }
|
||||
)
|
||||
|
||||
|
||||
override suspend fun getSenderImage(
|
||||
userId: UserId,
|
||||
address: String,
|
||||
|
||||
+2
-2
@@ -39,10 +39,10 @@ interface RustMessageListQuery {
|
||||
|
||||
suspend fun updateShowSpamTrashFilter(showSpamTrash: Boolean)
|
||||
|
||||
suspend fun getCursor(
|
||||
suspend fun getCursorFromActivePaginator(
|
||||
userId: UserId,
|
||||
labelId: LabelId,
|
||||
conversationId: LocalConversationId
|
||||
firstPage: LocalConversationId
|
||||
): Either<PaginationError, MailMessageCursorWrapper>?
|
||||
|
||||
fun observeScrollerFetchNewStatus(): Flow<MessageScrollerStatusUpdate>
|
||||
|
||||
+24
-32
@@ -23,7 +23,6 @@ import arrow.core.left
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
|
||||
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
|
||||
import ch.protonmail.android.maillabel.data.wrapper.MailboxWrapper
|
||||
import ch.protonmail.android.maillabel.domain.model.LabelId
|
||||
import ch.protonmail.android.mailmessage.data.MessageRustCoroutineScope
|
||||
@@ -134,43 +133,39 @@ class RustMessageListQueryImpl @Inject constructor(
|
||||
return deferred.await()
|
||||
}
|
||||
|
||||
override suspend fun getCursor(
|
||||
override suspend fun getCursorFromActivePaginator(
|
||||
userId: UserId,
|
||||
labelId: LabelId,
|
||||
conversationId: LocalConversationId
|
||||
firstPage: LocalConversationId
|
||||
): Either<PaginationError, MailMessageCursorWrapper>? {
|
||||
val pageDescriptor = PageDescriptor.Default(userId = userId, labelId = labelId)
|
||||
|
||||
val initError = paginatorMutex.withLock {
|
||||
if (shouldInitPaginatorForCursor(pageDescriptor)) {
|
||||
Timber.d(
|
||||
"rust-message-query: Initializing paginator for getCursor with page desc=%s",
|
||||
pageDescriptor
|
||||
)
|
||||
val mailbox = rustMailboxFactory.create(userId, labelId.toLocalLabelId()).getOrNull()
|
||||
if (mailbox == null) {
|
||||
Timber.e(
|
||||
"rust-message-query: Unable to create mailbox for userId=%s and labelId=%s",
|
||||
userId,
|
||||
labelId
|
||||
)
|
||||
PaginationError.Other(DataError.Local.IllegalStateError).left()
|
||||
} else {
|
||||
initPaginator(pageDescriptor, mailbox)
|
||||
return paginatorMutex.withLock {
|
||||
val state = paginatorState
|
||||
when {
|
||||
state == null -> {
|
||||
Timber.d("rust-message-query: No active paginator to reuse for cursor")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"rust-message-query: Reusing existing paginator for getCursor with page desc=%s",
|
||||
pageDescriptor
|
||||
)
|
||||
null
|
||||
|
||||
state.pageDescriptor != pageDescriptor -> {
|
||||
Timber.d(
|
||||
"rust-message-query: Active paginator cannot be reused for cursor. active=%s requested=%s",
|
||||
state.pageDescriptor,
|
||||
pageDescriptor
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.d(
|
||||
"rust-message-query: Reusing active mailbox paginator for cursor, scrollerId=%s",
|
||||
state.paginatorWrapper.getScrollerId()
|
||||
)
|
||||
state.paginatorWrapper.getCursor(firstPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (initError != null) return initError
|
||||
|
||||
return paginatorState?.paginatorWrapper?.getCursor(conversationId)
|
||||
}
|
||||
|
||||
override fun observeScrollerFetchNewStatus(): Flow<MessageScrollerStatusUpdate> =
|
||||
@@ -283,9 +278,6 @@ class RustMessageListQueryImpl @Inject constructor(
|
||||
paginatorState?.pageDescriptor != pageDescriptor ||
|
||||
pageKey.pageToLoad == PageToLoad.First
|
||||
|
||||
private fun shouldInitPaginatorForCursor(pageDescriptor: PageDescriptor): Boolean =
|
||||
paginatorState == null || paginatorState?.pageDescriptor != pageDescriptor
|
||||
|
||||
private fun destroy() {
|
||||
if (paginatorState == null) {
|
||||
Timber.d("rust-message-query: no paginator to destroy")
|
||||
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailmessage.data.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
|
||||
import ch.protonmail.android.mailcommon.data.repository.RustConversationCursorImpl
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
|
||||
import ch.protonmail.android.mailcommon.domain.model.CursorId
|
||||
import ch.protonmail.android.mailcommon.domain.repository.ConversationCursor
|
||||
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
|
||||
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
|
||||
import ch.protonmail.android.maillabel.data.wrapper.MailboxWrapper
|
||||
import ch.protonmail.android.maillabel.domain.model.LabelId
|
||||
import ch.protonmail.android.mailmessage.data.local.RustMessageListQuery
|
||||
import ch.protonmail.android.mailmessage.data.local.debugTypeName
|
||||
import ch.protonmail.android.mailmessage.data.usecase.CreateRustMessagesPaginator
|
||||
import ch.protonmail.android.mailmessage.data.wrapper.MailMessageCursorWrapper
|
||||
import ch.protonmail.android.mailmessage.data.wrapper.MessagePaginatorWrapper
|
||||
import ch.protonmail.android.mailmessage.domain.model.toConversationCursorError
|
||||
import ch.protonmail.android.mailmessage.domain.repository.MessageCursorRepository
|
||||
import ch.protonmail.android.mailsnooze.data.mapper.toLocalConversationId
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import timber.log.Timber
|
||||
import uniffi.mail_uniffi.MessageScrollerLiveQueryCallback
|
||||
import uniffi.mail_uniffi.MessageScrollerUpdate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class MessageCursorRepositoryImpl @Inject constructor(
|
||||
private val rustMailboxFactory: RustMailboxFactory,
|
||||
private val createRustMessagesPaginator: CreateRustMessagesPaginator,
|
||||
private val rustMessageListQuery: RustMessageListQuery
|
||||
) : MessageCursorRepository {
|
||||
|
||||
private var cursorPaginatorState: CursorPaginatorState? = null
|
||||
private val cursorPaginatorMutex = Mutex()
|
||||
|
||||
override suspend fun getCursor(
|
||||
firstPage: CursorId,
|
||||
userId: UserId,
|
||||
labelId: LabelId
|
||||
): Either<ConversationCursorError, ConversationCursor> {
|
||||
val firstPageId = firstPage.messageId?.toLocalConversationId()
|
||||
?: firstPage.conversationId.toLocalConversationId()
|
||||
|
||||
return getCursorWrapper(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = firstPageId
|
||||
).map { cursorWrapper ->
|
||||
RustConversationCursorImpl(firstPage, cursorWrapper)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor lookup strategy:
|
||||
*
|
||||
* 1. Try to obtain the cursor from the active mailbox paginator.
|
||||
* 2. If mailbox paginator reuse is unavailable or fails, fall back to a dedicated
|
||||
* cursor paginator managed by this repository.
|
||||
*/
|
||||
private suspend fun getCursorWrapper(
|
||||
userId: UserId,
|
||||
labelId: LabelId,
|
||||
firstPage: LocalConversationId
|
||||
): Either<ConversationCursorError, MailMessageCursorWrapper> {
|
||||
when (
|
||||
val mailboxCursorResult = rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = firstPage
|
||||
)
|
||||
) {
|
||||
null -> {
|
||||
Timber.d(
|
||||
"message-cursor-repository: No reusable mailbox paginator cursor, falling back"
|
||||
)
|
||||
}
|
||||
|
||||
is Either.Right -> {
|
||||
Timber.d("message-cursor-repository: Reused mailbox paginator cursor")
|
||||
if (hasCursorPaginator()) destroyCursorPaginator()
|
||||
return mailboxCursorResult
|
||||
}
|
||||
|
||||
is Either.Left -> {
|
||||
Timber.w(
|
||||
"message-cursor-repository: Failed to get cursor from active mailbox paginator, " +
|
||||
"falling back. error=%s",
|
||||
mailboxCursorResult.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return getCursorFromCursorPaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = firstPage
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getCursorFromCursorPaginator(
|
||||
userId: UserId,
|
||||
labelId: LabelId,
|
||||
firstPage: LocalConversationId
|
||||
): Either<ConversationCursorError, MailMessageCursorWrapper> {
|
||||
val pageDescriptor = PageDescriptor(userId = userId, labelId = labelId)
|
||||
|
||||
val initError = cursorPaginatorMutex.withLock {
|
||||
val state = cursorPaginatorState
|
||||
if (state == null || state.pageDescriptor != pageDescriptor) {
|
||||
Timber.d(
|
||||
"message-cursor-repository: Initializing cursor paginator for pageDescriptor=%s",
|
||||
pageDescriptor
|
||||
)
|
||||
|
||||
val mailbox = rustMailboxFactory.create(userId, labelId.toLocalLabelId()).getOrNull()
|
||||
if (mailbox == null) {
|
||||
Timber.e(
|
||||
"message-cursor-repository: Unable to create mailbox for userId=%s and labelId=%s",
|
||||
userId,
|
||||
labelId
|
||||
)
|
||||
ConversationCursorError.InvalidState
|
||||
} else {
|
||||
initCursorPaginator(pageDescriptor, mailbox)
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"message-cursor-repository: Reusing existing cursor paginator, scrollerId=%s",
|
||||
state.paginatorWrapper.getScrollerId()
|
||||
)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (initError != null) return initError.left()
|
||||
|
||||
return cursorPaginatorMutex.withLock {
|
||||
cursorPaginatorState?.paginatorWrapper?.getCursor(firstPage)?.mapLeft {
|
||||
it.toConversationCursorError()
|
||||
} ?: ConversationCursorError.InvalidState.left()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initCursorPaginator(
|
||||
pageDescriptor: PageDescriptor,
|
||||
mailbox: MailboxWrapper
|
||||
): ConversationCursorError? {
|
||||
destroyCursorPaginator()
|
||||
|
||||
return createRustMessagesPaginator(
|
||||
mailbox = mailbox,
|
||||
callback = object : MessageScrollerLiveQueryCallback {
|
||||
override fun onUpdate(update: MessageScrollerUpdate) {
|
||||
Timber.d(
|
||||
"message-cursor-repository: Cursor paginator update=%s",
|
||||
update.debugTypeName()
|
||||
)
|
||||
}
|
||||
}
|
||||
).fold(
|
||||
ifLeft = { error ->
|
||||
Timber.e(
|
||||
"message-cursor-repository: Failed to create cursor paginator. error=%s",
|
||||
error
|
||||
)
|
||||
ConversationCursorError.Other(error)
|
||||
},
|
||||
ifRight = { paginator ->
|
||||
Timber.d(
|
||||
"message-cursor-repository: Cursor paginator created, scrollerId=%s",
|
||||
paginator.getScrollerId()
|
||||
)
|
||||
cursorPaginatorState = CursorPaginatorState(
|
||||
paginatorWrapper = paginator,
|
||||
pageDescriptor = pageDescriptor
|
||||
)
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun destroyCursorPaginator() {
|
||||
val state = cursorPaginatorState ?: run {
|
||||
Timber.d("message-cursor-repository: No cursor paginator to destroy")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d(
|
||||
"message-cursor-repository: Destroying cursor paginator, scrollerId=%s",
|
||||
state.paginatorWrapper.getScrollerId()
|
||||
)
|
||||
state.paginatorWrapper.disconnect()
|
||||
cursorPaginatorState = null
|
||||
}
|
||||
|
||||
private fun hasCursorPaginator(): Boolean = cursorPaginatorState != null
|
||||
|
||||
private fun String.toLocalConversationId() = LocalConversationId(this.toULong())
|
||||
|
||||
private data class CursorPaginatorState(
|
||||
val paginatorWrapper: MessagePaginatorWrapper,
|
||||
val pageDescriptor: PageDescriptor
|
||||
)
|
||||
|
||||
private data class PageDescriptor(
|
||||
val userId: UserId,
|
||||
val labelId: LabelId
|
||||
)
|
||||
}
|
||||
+5
-16
@@ -20,8 +20,6 @@ package ch.protonmail.android.mailmessage.data.repository
|
||||
|
||||
import java.io.File
|
||||
import arrow.core.Either
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
|
||||
import ch.protonmail.android.mailcommon.data.repository.RustConversationCursorImpl
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
|
||||
import ch.protonmail.android.mailcommon.domain.model.CursorId
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
@@ -31,7 +29,6 @@ import ch.protonmail.android.mailcommon.domain.repository.UndoRepository
|
||||
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
|
||||
import ch.protonmail.android.maillabel.domain.model.LabelId
|
||||
import ch.protonmail.android.mailmessage.data.local.RustMessageDataSource
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toLocalConversationId
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toLocalMessageId
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toMessage
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toRemoteMessageId
|
||||
@@ -41,6 +38,7 @@ import ch.protonmail.android.mailmessage.domain.model.MessageScrollerFetchNewSta
|
||||
import ch.protonmail.android.mailmessage.domain.model.PreviousScheduleSendTime
|
||||
import ch.protonmail.android.mailmessage.domain.model.RemoteMessageId
|
||||
import ch.protonmail.android.mailmessage.domain.model.SenderImage
|
||||
import ch.protonmail.android.mailmessage.domain.repository.MessageCursorRepository
|
||||
import ch.protonmail.android.mailmessage.domain.repository.MessageRepository
|
||||
import ch.protonmail.android.mailpagination.domain.model.PageKey
|
||||
import ch.protonmail.android.mailpagination.domain.model.PaginationError
|
||||
@@ -55,7 +53,8 @@ import javax.inject.Inject
|
||||
@Suppress("TooManyFunctions")
|
||||
class RustMessageRepositoryImpl @Inject constructor(
|
||||
private val rustMessageDataSource: RustMessageDataSource,
|
||||
private val undoRepository: UndoRepository
|
||||
private val undoRepository: UndoRepository,
|
||||
private val messageCursorRepository: MessageCursorRepository
|
||||
) : MessageRepository {
|
||||
|
||||
override suspend fun updateShowSpamTrashFilter(showSpamTrash: Boolean) =
|
||||
@@ -110,16 +109,8 @@ class RustMessageRepositoryImpl @Inject constructor(
|
||||
firstPage: CursorId,
|
||||
userId: UserId,
|
||||
labelId: LabelId
|
||||
): Either<ConversationCursorError, ConversationCursor> = rustMessageDataSource
|
||||
.getConversationCursor(
|
||||
firstPage = firstPage.messageId?.toLocalConversationId()
|
||||
?: firstPage.conversationId.toLocalConversationId(),
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
)
|
||||
.map {
|
||||
RustConversationCursorImpl(firstPage, it)
|
||||
}
|
||||
): Either<ConversationCursorError, ConversationCursor> =
|
||||
messageCursorRepository.getCursor(firstPage, userId, labelId)
|
||||
|
||||
override suspend fun moveTo(
|
||||
userId: UserId,
|
||||
@@ -194,8 +185,6 @@ class RustMessageRepositoryImpl @Inject constructor(
|
||||
override suspend fun blockSender(userId: UserId, email: String): Either<DataError, Unit> =
|
||||
rustMessageDataSource.blockSender(userId, email)
|
||||
|
||||
private fun String.toLocalConversationId() = LocalConversationId(this.toULong())
|
||||
|
||||
override suspend fun isMessageSenderBlocked(userId: UserId, messageId: MessageId): Either<DataError, Boolean> =
|
||||
rustMessageDataSource.isMessageSenderBlocked(userId, messageId.toLocalMessageId())
|
||||
}
|
||||
|
||||
+73
-8
@@ -24,7 +24,6 @@ import ch.protonmail.android.mailcommon.domain.model.ConversationId
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import ch.protonmail.android.mailcommon.domain.sample.UserIdSample
|
||||
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
|
||||
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
|
||||
import ch.protonmail.android.maillabel.data.wrapper.MailboxWrapper
|
||||
import ch.protonmail.android.maillabel.domain.model.SystemLabelId
|
||||
import ch.protonmail.android.mailmessage.data.local.RustMessageListQueryImpl.Companion.NONE_FOLLOWUP_GRACE_MS
|
||||
@@ -621,14 +620,12 @@ class RustMessageListQueryImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursor returns cursor from existing paginator without reinitializing`() = runTest {
|
||||
fun `getCursorFromActivePaginator returns cursor from existing paginator without reinitializing`() = runTest {
|
||||
// Given
|
||||
val userId = UserIdSample.Primary
|
||||
val labelId = SystemLabelId.Inbox.labelId
|
||||
val conversationId = ConversationId("111").toLocalConversationId()
|
||||
val expectedCursor = mockk<MailMessageCursorWrapper>()
|
||||
|
||||
val mailbox = mockk<MailboxWrapper>()
|
||||
val callbackSlot = slot<MessageScrollerLiveQueryCallback>()
|
||||
val paginator = mockk<MessagePaginatorWrapper> {
|
||||
coEvery { nextPage() } coAnswers {
|
||||
@@ -656,7 +653,6 @@ class RustMessageListQueryImplTest {
|
||||
)
|
||||
} returns paginator.right()
|
||||
|
||||
// Initialize paginator first so getCursor can reuse it
|
||||
rustMessageListQuery.getMessages(
|
||||
userId = userId,
|
||||
pageKey = PageKey.DefaultPageKey(
|
||||
@@ -666,10 +662,10 @@ class RustMessageListQueryImplTest {
|
||||
)
|
||||
|
||||
// When
|
||||
val actual = rustMessageListQuery.getCursor(
|
||||
val actual = rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
conversationId = conversationId
|
||||
firstPage = conversationId
|
||||
)
|
||||
|
||||
// Then
|
||||
@@ -681,7 +677,76 @@ class RustMessageListQueryImplTest {
|
||||
callback = any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { rustMailboxFactory.create(userId, labelId.toLocalLabelId()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursorFromActivePaginator returns null when there is no active paginator`() = runTest {
|
||||
// Given
|
||||
val labelId = SystemLabelId.Inbox.labelId
|
||||
val conversationId = ConversationId("111").toLocalConversationId()
|
||||
|
||||
// When
|
||||
val actual = rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = conversationId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertEquals(null, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursorFromActivePaginator returns null when active paginator label does not match`() = runTest {
|
||||
// Given
|
||||
val activeLabelId = SystemLabelId.Inbox.labelId
|
||||
val requestedLabelId = SystemLabelId.Archive.labelId
|
||||
val conversationId = ConversationId("111").toLocalConversationId()
|
||||
|
||||
val callbackSlot = slot<MessageScrollerLiveQueryCallback>()
|
||||
val paginator = mockk<MessagePaginatorWrapper> {
|
||||
coEvery { nextPage() } coAnswers {
|
||||
callbackSlot.captured.onUpdate(
|
||||
MessageScrollerUpdate.List(
|
||||
MessageScrollerListUpdate.Append(
|
||||
items = expectedMessages,
|
||||
scrollerId = DefaultScrollerId
|
||||
)
|
||||
)
|
||||
)
|
||||
Unit.right()
|
||||
}
|
||||
coEvery { filterUnread(false) } just Runs
|
||||
coEvery { showSpamAndTrash(false) } just Runs
|
||||
every { getScrollerId() } returns DefaultScrollerId
|
||||
}
|
||||
|
||||
coEvery { rustMailboxFactory.create(userId) } returns mailbox.right()
|
||||
coEvery {
|
||||
createRustMessagesPaginator(
|
||||
mailbox = mailbox,
|
||||
callback = capture(callbackSlot)
|
||||
)
|
||||
} returns paginator.right()
|
||||
|
||||
rustMessageListQuery.getMessages(
|
||||
userId = userId,
|
||||
pageKey = PageKey.DefaultPageKey(
|
||||
labelId = activeLabelId,
|
||||
pageToLoad = PageToLoad.First
|
||||
)
|
||||
)
|
||||
|
||||
// When
|
||||
val actual = rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = requestedLabelId,
|
||||
firstPage = conversationId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertEquals(null, actual)
|
||||
coVerify(exactly = 0) { paginator.getCursor(any()) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailmessage.data.repository
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationId
|
||||
import ch.protonmail.android.mailcommon.domain.model.CursorId
|
||||
import ch.protonmail.android.mailcommon.domain.model.CursorResult
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import ch.protonmail.android.mailcommon.domain.sample.UserIdSample
|
||||
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
|
||||
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
|
||||
import ch.protonmail.android.maillabel.data.wrapper.MailboxWrapper
|
||||
import ch.protonmail.android.maillabel.domain.model.SystemLabelId
|
||||
import ch.protonmail.android.mailmessage.data.local.RustMessageListQuery
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toLocalConversationId
|
||||
import ch.protonmail.android.mailmessage.data.usecase.CreateRustMessagesPaginator
|
||||
import ch.protonmail.android.mailmessage.data.wrapper.MailMessageCursorWrapper
|
||||
import ch.protonmail.android.mailmessage.data.wrapper.MessagePaginatorWrapper
|
||||
import ch.protonmail.android.mailpagination.domain.model.PaginationError
|
||||
import ch.protonmail.android.test.utils.rule.MainDispatcherRule
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.slot
|
||||
import org.junit.Rule
|
||||
import uniffi.mail_uniffi.MessageScrollerLiveQueryCallback
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.text.toULong
|
||||
|
||||
internal class MessageCursorRepositoryImplTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val rustMailboxFactory = mockk<RustMailboxFactory>()
|
||||
private val createRustMessagesPaginator = mockk<CreateRustMessagesPaginator>()
|
||||
private val rustMessageListQuery = mockk<RustMessageListQuery>()
|
||||
|
||||
private val repository = MessageCursorRepositoryImpl(
|
||||
rustMailboxFactory = rustMailboxFactory,
|
||||
createRustMessagesPaginator = createRustMessagesPaginator,
|
||||
rustMessageListQuery = rustMessageListQuery
|
||||
)
|
||||
|
||||
private val cursorWrapper = mockk<MailMessageCursorWrapper> {
|
||||
coEvery { nextPage() } returns CursorResult.Cursor(ConversationId("101"))
|
||||
every { previousPage() } returns CursorResult.Cursor(ConversationId("99"))
|
||||
every { goForwards() } just Runs
|
||||
every { goBackwards() } just Runs
|
||||
every { disconnect() } just Runs
|
||||
}
|
||||
|
||||
private val secondCursorWrapper = mockk<MailMessageCursorWrapper> {
|
||||
coEvery { nextPage() } returns CursorResult.Cursor(ConversationId("201"))
|
||||
every { previousPage() } returns CursorResult.Cursor(ConversationId("199"))
|
||||
every { goForwards() } just Runs
|
||||
every { goBackwards() } just Runs
|
||||
every { disconnect() } just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursor reuses active mailbox paginator cursor when available`() = runTest {
|
||||
// Given
|
||||
val userId = UserIdSample.Primary
|
||||
val labelId = SystemLabelId.Inbox.labelId
|
||||
val firstPage = CursorId(ConversationId("100"), "102")
|
||||
val messageId = LocalConversationId("102".toULong())
|
||||
|
||||
coEvery {
|
||||
rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = messageId
|
||||
)
|
||||
} returns cursorWrapper.right()
|
||||
|
||||
// When
|
||||
val actual = repository.getCursor(
|
||||
firstPage = firstPage,
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertTrue(actual.isRight())
|
||||
assertEquals(
|
||||
ConversationId("100"),
|
||||
(actual.getOrNull()?.current as? CursorResult.Cursor)?.conversationId
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = messageId
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { rustMailboxFactory.create(any(), any()) }
|
||||
coVerify(exactly = 0) { createRustMessagesPaginator(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursor falls back to dedicated cursor paginator when active mailbox paginator returns null`() = runTest {
|
||||
// Given
|
||||
val userId = UserIdSample.Primary
|
||||
val labelId = SystemLabelId.Inbox.labelId
|
||||
val firstPage = CursorId(ConversationId("100"), null)
|
||||
val conversationId = firstPage.conversationId.toLocalConversationId()
|
||||
val mailbox = mockk<MailboxWrapper>()
|
||||
val callbackSlot = slot<MessageScrollerLiveQueryCallback>()
|
||||
|
||||
val paginator = mockk<MessagePaginatorWrapper> {
|
||||
every { getScrollerId() } returns "cursor-scroller-id"
|
||||
coEvery { disconnect() } just Runs
|
||||
coEvery { getCursor(conversationId) } returns cursorWrapper.right()
|
||||
}
|
||||
|
||||
coEvery {
|
||||
rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = conversationId
|
||||
)
|
||||
} returns null
|
||||
|
||||
coEvery {
|
||||
rustMailboxFactory.create(userId, labelId.toLocalLabelId())
|
||||
} returns mailbox.right()
|
||||
|
||||
coEvery {
|
||||
createRustMessagesPaginator(
|
||||
mailbox = mailbox,
|
||||
callback = capture(callbackSlot)
|
||||
)
|
||||
} returns paginator.right()
|
||||
|
||||
// When
|
||||
val actual = repository.getCursor(
|
||||
firstPage = firstPage,
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertTrue(actual.isRight())
|
||||
assertEquals(
|
||||
ConversationId("100"),
|
||||
(actual.getOrNull()?.current as? CursorResult.Cursor)?.conversationId
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
rustMailboxFactory.create(userId, labelId.toLocalLabelId())
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
createRustMessagesPaginator(
|
||||
mailbox = mailbox,
|
||||
callback = any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) { paginator.getCursor(conversationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursor falls back to dedicated cursor paginator when active mailbox paginator returns error`() = runTest {
|
||||
// Given
|
||||
val userId = UserIdSample.Primary
|
||||
val labelId = SystemLabelId.Inbox.labelId
|
||||
val firstPage = CursorId(ConversationId("100"), null)
|
||||
val conversationId = firstPage.conversationId.toLocalConversationId()
|
||||
val mailbox = mockk<MailboxWrapper>()
|
||||
val callbackSlot = slot<MessageScrollerLiveQueryCallback>()
|
||||
|
||||
val paginator = mockk<MessagePaginatorWrapper> {
|
||||
every { getScrollerId() } returns "cursor-scroller-id"
|
||||
coEvery { disconnect() } just Runs
|
||||
coEvery { getCursor(conversationId) } returns cursorWrapper.right()
|
||||
}
|
||||
|
||||
coEvery {
|
||||
rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = conversationId
|
||||
)
|
||||
} returns PaginationError.Other(DataError.Local.IllegalStateError).left()
|
||||
|
||||
coEvery {
|
||||
rustMailboxFactory.create(userId, labelId.toLocalLabelId())
|
||||
} returns mailbox.right()
|
||||
|
||||
coEvery {
|
||||
createRustMessagesPaginator(
|
||||
mailbox = mailbox,
|
||||
callback = capture(callbackSlot)
|
||||
)
|
||||
} returns paginator.right()
|
||||
|
||||
// When
|
||||
val actual = repository.getCursor(
|
||||
firstPage = firstPage,
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertTrue(actual.isRight())
|
||||
coVerify(exactly = 1) {
|
||||
rustMailboxFactory.create(userId, labelId.toLocalLabelId())
|
||||
}
|
||||
coVerify(exactly = 1) { paginator.getCursor(conversationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursor returns InvalidState when fallback mailbox cannot be created`() = runTest {
|
||||
// Given
|
||||
val userId = UserIdSample.Primary
|
||||
val labelId = SystemLabelId.Inbox.labelId
|
||||
val firstPage = CursorId(ConversationId("100"), null)
|
||||
val conversationId = firstPage.conversationId.toLocalConversationId()
|
||||
|
||||
coEvery {
|
||||
rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = conversationId
|
||||
)
|
||||
} returns null
|
||||
|
||||
coEvery {
|
||||
rustMailboxFactory.create(userId, labelId.toLocalLabelId())
|
||||
} returns DataError.Local.Unknown.left()
|
||||
|
||||
// When
|
||||
val actual = repository.getCursor(
|
||||
firstPage = firstPage,
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertEquals(ConversationCursorError.InvalidState.left(), actual)
|
||||
coVerify(exactly = 0) { createRustMessagesPaginator(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursor reuses existing dedicated cursor paginator for same user and label`() = runTest {
|
||||
// Given
|
||||
val userId = UserIdSample.Primary
|
||||
val labelId = SystemLabelId.Inbox.labelId
|
||||
val firstPage = CursorId(ConversationId("100"), null)
|
||||
val secondPage = CursorId(ConversationId("200"), null)
|
||||
val firstConversationId = firstPage.conversationId.toLocalConversationId()
|
||||
val secondConversationId = secondPage.conversationId.toLocalConversationId()
|
||||
val mailbox = mockk<MailboxWrapper>()
|
||||
val callbackSlot = slot<MessageScrollerLiveQueryCallback>()
|
||||
|
||||
val paginator = mockk<MessagePaginatorWrapper> {
|
||||
every { getScrollerId() } returns "cursor-scroller-id"
|
||||
coEvery { disconnect() } just Runs
|
||||
coEvery { getCursor(firstConversationId) } returns cursorWrapper.right()
|
||||
coEvery { getCursor(secondConversationId) } returns secondCursorWrapper.right()
|
||||
}
|
||||
|
||||
coEvery {
|
||||
rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = labelId,
|
||||
firstPage = any()
|
||||
)
|
||||
} returns null
|
||||
|
||||
coEvery {
|
||||
rustMailboxFactory.create(userId, labelId.toLocalLabelId())
|
||||
} returns mailbox.right()
|
||||
|
||||
coEvery {
|
||||
createRustMessagesPaginator(
|
||||
mailbox = mailbox,
|
||||
callback = capture(callbackSlot)
|
||||
)
|
||||
} returns paginator.right()
|
||||
|
||||
// When
|
||||
val firstResult = repository.getCursor(
|
||||
firstPage = firstPage,
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
)
|
||||
val secondResult = repository.getCursor(
|
||||
firstPage = secondPage,
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertTrue(firstResult.isRight())
|
||||
assertTrue(secondResult.isRight())
|
||||
coVerify(exactly = 1) {
|
||||
createRustMessagesPaginator(
|
||||
mailbox = mailbox,
|
||||
callback = any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) { paginator.getCursor(firstConversationId) }
|
||||
coVerify(exactly = 1) { paginator.getCursor(secondConversationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCursor recreates dedicated cursor paginator when label changes`() = runTest {
|
||||
// Given
|
||||
val userId = UserIdSample.Primary
|
||||
val firstLabelId = SystemLabelId.Inbox.labelId
|
||||
val secondLabelId = SystemLabelId.Archive.labelId
|
||||
val firstPage = CursorId(ConversationId("100"), null)
|
||||
val secondPage = CursorId(ConversationId("200"), null)
|
||||
val firstConversationId = firstPage.conversationId.toLocalConversationId()
|
||||
val secondConversationId = secondPage.conversationId.toLocalConversationId()
|
||||
|
||||
val firstMailbox = mockk<MailboxWrapper>()
|
||||
val secondMailbox = mockk<MailboxWrapper>()
|
||||
val firstCallbackSlot = slot<MessageScrollerLiveQueryCallback>()
|
||||
val secondCallbackSlot = slot<MessageScrollerLiveQueryCallback>()
|
||||
|
||||
val firstPaginator = mockk<MessagePaginatorWrapper> {
|
||||
every { getScrollerId() } returns "cursor-scroller-id-1"
|
||||
coEvery { disconnect() } just Runs
|
||||
coEvery { getCursor(firstConversationId) } returns cursorWrapper.right()
|
||||
}
|
||||
|
||||
val secondPaginator = mockk<MessagePaginatorWrapper> {
|
||||
every { getScrollerId() } returns "cursor-scroller-id-2"
|
||||
coEvery { disconnect() } just Runs
|
||||
coEvery { getCursor(secondConversationId) } returns secondCursorWrapper.right()
|
||||
}
|
||||
|
||||
coEvery {
|
||||
rustMessageListQuery.getCursorFromActivePaginator(
|
||||
userId = userId,
|
||||
labelId = any(),
|
||||
firstPage = any()
|
||||
)
|
||||
} returns null
|
||||
|
||||
coEvery {
|
||||
rustMailboxFactory.create(userId, firstLabelId.toLocalLabelId())
|
||||
} returns firstMailbox.right()
|
||||
coEvery {
|
||||
rustMailboxFactory.create(userId, secondLabelId.toLocalLabelId())
|
||||
} returns secondMailbox.right()
|
||||
|
||||
coEvery {
|
||||
createRustMessagesPaginator(
|
||||
mailbox = firstMailbox,
|
||||
callback = capture(firstCallbackSlot)
|
||||
)
|
||||
} returns firstPaginator.right()
|
||||
coEvery {
|
||||
createRustMessagesPaginator(
|
||||
mailbox = secondMailbox,
|
||||
callback = capture(secondCallbackSlot)
|
||||
)
|
||||
} returns secondPaginator.right()
|
||||
|
||||
// When
|
||||
val firstResult = repository.getCursor(
|
||||
firstPage = firstPage,
|
||||
userId = userId,
|
||||
labelId = firstLabelId
|
||||
)
|
||||
val secondResult = repository.getCursor(
|
||||
firstPage = secondPage,
|
||||
userId = userId,
|
||||
labelId = secondLabelId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertTrue(firstResult.isRight())
|
||||
assertTrue(secondResult.isRight())
|
||||
coVerify(exactly = 1) { firstPaginator.disconnect() }
|
||||
coVerify(exactly = 1) { firstPaginator.getCursor(firstConversationId) }
|
||||
coVerify(exactly = 1) { secondPaginator.getCursor(secondConversationId) }
|
||||
}
|
||||
}
|
||||
+16
-10
@@ -23,13 +23,12 @@ import app.cash.turbine.test
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import ch.protonmail.android.mailcommon.data.repository.RustConversationCursorImpl
|
||||
import ch.protonmail.android.mailcommon.data.wrapper.ConversationCursor
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationId
|
||||
import ch.protonmail.android.mailcommon.domain.model.CursorId
|
||||
import ch.protonmail.android.mailcommon.domain.model.CursorResult
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import ch.protonmail.android.mailcommon.domain.model.UndoableOperation
|
||||
import ch.protonmail.android.mailcommon.domain.repository.ConversationCursor
|
||||
import ch.protonmail.android.mailcommon.domain.repository.UndoRepository
|
||||
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
|
||||
import ch.protonmail.android.maillabel.domain.model.SystemLabelId
|
||||
@@ -40,11 +39,13 @@ import ch.protonmail.android.mailmessage.data.mapper.toMessage
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toMessageId
|
||||
import ch.protonmail.android.mailmessage.data.mapper.toRemoteMessageId
|
||||
import ch.protonmail.android.mailmessage.domain.model.SenderImage
|
||||
import ch.protonmail.android.mailmessage.domain.repository.MessageCursorRepository
|
||||
import ch.protonmail.android.mailpagination.domain.model.PageKey
|
||||
import ch.protonmail.android.testdata.message.rust.LocalMessageIdSample
|
||||
import ch.protonmail.android.testdata.message.rust.LocalMessageTestData
|
||||
import ch.protonmail.android.testdata.message.rust.RemoteMessageIdSample
|
||||
import ch.protonmail.android.testdata.user.UserIdTestData
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
@@ -55,7 +56,6 @@ import junit.framework.TestCase.assertNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import uniffi.mail_uniffi.Id
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
@@ -64,9 +64,10 @@ internal class RustMessageRepositoryImplTest {
|
||||
|
||||
private val rustMessageDataSource = mockk<RustMessageDataSource>()
|
||||
private val undoRepository = mockk<UndoRepository>()
|
||||
private val messageCursorRepository = mockk<MessageCursorRepository>()
|
||||
private val labelId = LabelIdSample.RustLabel1
|
||||
private val userId = UserId("userId")
|
||||
private val repository = RustMessageRepositoryImpl(rustMessageDataSource, undoRepository)
|
||||
private val repository = RustMessageRepositoryImpl(rustMessageDataSource, undoRepository, messageCursorRepository)
|
||||
|
||||
@Test
|
||||
fun `getLocalMessages should return list of messages`() = runTest {
|
||||
@@ -181,12 +182,17 @@ internal class RustMessageRepositoryImplTest {
|
||||
fun `when getConversationCursor returns a cursor with the first messsageId`() = runTest {
|
||||
// Given
|
||||
val conversationCursor = mockk<ConversationCursor> {
|
||||
every { previousPage() } returns CursorResult.Cursor(ConversationId("200"))
|
||||
coEvery { nextPage() } returns CursorResult.Cursor(ConversationId("300"))
|
||||
every { current } returns CursorResult.Cursor(ConversationId("100"))
|
||||
every { previous } returns CursorResult.Cursor(ConversationId("200"))
|
||||
every { next } returns CursorResult.Cursor(ConversationId("300"))
|
||||
coEvery { moveForward() } just Runs
|
||||
coEvery { moveBackward() } just Runs
|
||||
coEvery { invalidatePrevious() } just Runs
|
||||
every { close() } just Runs
|
||||
}
|
||||
val firstPage = Id(100.toULong())
|
||||
val firstPage = CursorId(conversationId = ConversationId("99"), messageId = "100")
|
||||
coEvery {
|
||||
rustMessageDataSource.getConversationCursor(
|
||||
messageCursorRepository.getCursor(
|
||||
firstPage = firstPage,
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
@@ -196,14 +202,14 @@ internal class RustMessageRepositoryImplTest {
|
||||
|
||||
// When
|
||||
val result = repository.getConversationCursor(
|
||||
firstPage = CursorId(ConversationId("100"), null),
|
||||
firstPage = firstPage,
|
||||
userId = userId,
|
||||
labelId = labelId
|
||||
)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isRight())
|
||||
assertTrue(result.getOrNull() is RustConversationCursorImpl)
|
||||
assertTrue(result.getOrNull() is ConversationCursor)
|
||||
assertEquals("100", (result.getOrNull()?.current as? CursorResult.Cursor)?.conversationId?.id)
|
||||
}
|
||||
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailmessage.domain.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
|
||||
import ch.protonmail.android.mailcommon.domain.model.CursorId
|
||||
import ch.protonmail.android.mailcommon.domain.repository.ConversationCursor
|
||||
import ch.protonmail.android.maillabel.domain.model.LabelId
|
||||
import me.proton.core.domain.entity.UserId
|
||||
|
||||
interface MessageCursorRepository {
|
||||
suspend fun getCursor(
|
||||
firstPage: CursorId,
|
||||
userId: UserId,
|
||||
labelId: LabelId
|
||||
): Either<ConversationCursorError, ConversationCursor>
|
||||
}
|
||||
Reference in New Issue
Block a user