mirror of
https://github.com/ProtonMail/android-mail.git
synced 2026-06-14 09:54:47 +00:00
Automatically recover mailbox if user session is null
ET-3788 # Conflicts: # mail-mailbox/presentation/src/main/kotlin/ch/protonmail/android/mailmailbox/presentation/mailbox/MailboxViewModel.kt # mail-mailbox/presentation/src/test/kotlin/ch/protonmail/android/mailmailbox/presentation/MailboxViewModelTest.kt
This commit is contained in:
+23
-3
@@ -100,7 +100,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
@@ -276,9 +279,17 @@ fun MailboxScreen(
|
||||
onClearAll = { viewModel.submit(MailboxViewAction.ClearAll) },
|
||||
onClearAllConfirmed = { viewModel.submit(MailboxViewAction.ClearAllConfirmed) },
|
||||
onClearAllDismissed = { viewModel.submit(MailboxViewAction.ClearAllDismissed) },
|
||||
onSnooze = { viewModel.submit(MailboxViewAction.RequestSnoozeBottomSheet) }
|
||||
onSnooze = { viewModel.submit(MailboxViewAction.RequestSnoozeBottomSheet) },
|
||||
validateUserSession = { viewModel.submit(MailboxViewAction.ValidateUserSession) }
|
||||
)
|
||||
|
||||
val lifecycle = LocalLifecycleOwner.current
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.submit(MailboxViewAction.ValidateUserSession)
|
||||
}
|
||||
}
|
||||
|
||||
mailboxState.bottomSheetState?.let {
|
||||
ConsumableLaunchedEffect(effect = it.bottomSheetVisibilityEffect) { bottomSheetEffect ->
|
||||
when (bottomSheetEffect) {
|
||||
@@ -703,6 +714,7 @@ private fun MailboxSwipeRefresh(
|
||||
val currentViewState = remember(items.loadState, state) {
|
||||
when {
|
||||
state is MailboxListState.Loading -> MailboxScreenState.Loading
|
||||
state is MailboxListState.CouldNotLoadUserSession -> MailboxScreenState.CouldNotLoadUserSession
|
||||
searchMode.isInSearch() -> items.mapToUiStatesInSearch(searchMode, lastViewState)
|
||||
else -> items.mapToUiStates(refreshRequested)
|
||||
}
|
||||
@@ -809,6 +821,7 @@ private fun MailboxSwipeRefresh(
|
||||
modifier = Modifier.testTag(MailboxScreenTestTags.ListProgress)
|
||||
)
|
||||
|
||||
is MailboxScreenState.CouldNotLoadUserSession,
|
||||
is MailboxScreenState.SearchLoadingWithData,
|
||||
is MailboxScreenState.SearchData,
|
||||
is MailboxScreenState.LoadingWithData,
|
||||
@@ -986,6 +999,11 @@ private fun MailboxItemsList(
|
||||
onClick = { items.retry() }
|
||||
)
|
||||
|
||||
is MailboxScreenState.CouldNotLoadUserSession -> AppendError(
|
||||
message = stringResource(id = R.string.mailbox_error_message_user_session),
|
||||
onClick = { actions.validateUserSession() }
|
||||
)
|
||||
|
||||
else -> {
|
||||
Spacer(modifier = Modifier.padding(1.dp))
|
||||
}
|
||||
@@ -1239,7 +1257,8 @@ object MailboxScreen {
|
||||
val onClearAllDismissed: () -> Unit,
|
||||
val onSnooze: () -> Unit,
|
||||
val onActionBarVisibilityChanged: (Boolean) -> Unit,
|
||||
val onCustomizeToolbar: () -> Unit
|
||||
val onCustomizeToolbar: () -> Unit,
|
||||
val validateUserSession: () -> Unit
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -1301,7 +1320,8 @@ object MailboxScreen {
|
||||
onNavigateToUpselling = { _, _ -> },
|
||||
onSnooze = {},
|
||||
onCustomizeToolbar = {},
|
||||
onActionBarVisibilityChanged = {}
|
||||
onActionBarVisibilityChanged = {},
|
||||
validateUserSession = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ import androidx.paging.compose.LazyPagingItems
|
||||
import ch.protonmail.android.mailmailbox.presentation.mailbox.model.MailboxItemUiModel
|
||||
|
||||
sealed class MailboxScreenState {
|
||||
object CouldNotLoadUserSession : MailboxScreenState()
|
||||
object Loading : MailboxScreenState()
|
||||
object Empty : MailboxScreenState()
|
||||
object UnexpectedError : MailboxScreenState()
|
||||
|
||||
+16
-4
@@ -112,7 +112,8 @@ import ch.protonmail.android.mailmessage.presentation.model.bottomsheet.MoveToBo
|
||||
import ch.protonmail.android.mailmessage.presentation.model.bottomsheet.SnoozeSheetState
|
||||
import ch.protonmail.android.mailpagination.domain.usecase.ObservePageInvalidationEvents
|
||||
import ch.protonmail.android.mailsession.domain.repository.EventLoopRepository
|
||||
import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId
|
||||
import ch.protonmail.android.mailsession.domain.usecase.GetUserHasValidSession
|
||||
import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserIdWithValidSession
|
||||
import ch.protonmail.android.mailsettings.domain.model.ToolbarActionsRefreshSignal
|
||||
import ch.protonmail.android.mailsettings.domain.usecase.ObserveFolderColorSettings
|
||||
import ch.protonmail.android.mailsettings.domain.usecase.ObserveSwipeActionsPreference
|
||||
@@ -143,7 +144,6 @@ import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.util.kotlin.DispatcherProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.map
|
||||
|
||||
@HiltViewModel
|
||||
@SuppressWarnings("LongParameterList", "TooManyFunctions", "LargeClass")
|
||||
@@ -151,8 +151,9 @@ class MailboxViewModel @Inject constructor(
|
||||
@AppScope private val appScope: CoroutineScope,
|
||||
private val mailboxPagerFactory: MailboxPagerFactory,
|
||||
private val getCurrentViewModeForLabel: GetCurrentViewModeForLabel,
|
||||
observePrimaryUserId: ObservePrimaryUserId,
|
||||
private val observePrimaryUserIdWithValidSession: ObservePrimaryUserIdWithValidSession,
|
||||
private val observeMailLabels: ObserveMailLabels,
|
||||
private val getUserHasValidSession: GetUserHasValidSession,
|
||||
private val observeSwipeActionsPreference: ObserveSwipeActionsPreference,
|
||||
private val observeSelectedMailLabelId: ObserveSelectedMailLabelId,
|
||||
private val observeLoadedMailLabelId: ObserveLoadedMailLabelId,
|
||||
@@ -193,7 +194,7 @@ class MailboxViewModel @Inject constructor(
|
||||
private val eventLoopRepository: EventLoopRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val primaryUserId = observePrimaryUserId().filterNotNull()
|
||||
private val primaryUserId = observePrimaryUserIdWithValidSession().filterNotNull()
|
||||
private val mutableState = MutableStateFlow(initialState)
|
||||
private val itemIds = Collections.synchronizedList(mutableListOf<String>())
|
||||
private val folderColorSettings = primaryUserId.flatMapLatest {
|
||||
@@ -370,6 +371,7 @@ class MailboxViewModel @Inject constructor(
|
||||
is MailboxViewAction.RequestSnoozeBottomSheet -> requestSnoozeBottomSheet(viewAction)
|
||||
is MailboxViewAction.SignalMoveToCompleted -> handleMoveToCompleted(viewAction)
|
||||
is MailboxViewAction.SignalLabelAsCompleted -> handleLabelAsCompleted(viewAction)
|
||||
is MailboxViewAction.ValidateUserSession -> handleValidateUserSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,6 +437,7 @@ class MailboxViewModel @Inject constructor(
|
||||
when (state.value.mailboxListState) {
|
||||
is MailboxListState.Data.SelectionMode -> handleItemClickInSelectionMode(item)
|
||||
is MailboxListState.Data.ViewMode -> handleItemClickInViewMode(item)
|
||||
is MailboxListState.CouldNotLoadUserSession,
|
||||
is MailboxListState.Loading -> {
|
||||
Timber.d("Loading state can't handle item clicks")
|
||||
}
|
||||
@@ -1152,6 +1155,15 @@ class MailboxViewModel @Inject constructor(
|
||||
mutableState.value = state
|
||||
}
|
||||
|
||||
// A user can be logged in but not have a valid user session, in this case the app can't function. User sessions
|
||||
// are cached, but in rare cases such as migration RUST will not have even cached the session and therefore network
|
||||
// connection is obligatory and we MUST load the session before the app can recover
|
||||
private suspend fun handleValidateUserSession() {
|
||||
if (state.value.mailboxListState is MailboxListState.Loading && !getUserHasValidSession()) {
|
||||
emitNewStateFrom(MailboxEvent.CouldNotLoadUserSession)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<MailboxState>.observeUnreadFilterState() =
|
||||
this.map { it.unreadFilterState as? UnreadFilterState.Data }
|
||||
.mapNotNull { it?.isFilterEnabled }
|
||||
|
||||
+1
@@ -77,6 +77,7 @@ sealed interface MailboxListState {
|
||||
}
|
||||
|
||||
data object Loading : MailboxListState
|
||||
data object CouldNotLoadUserSession : MailboxListState
|
||||
|
||||
companion object {
|
||||
const val maxItemSelectionLimit = 100
|
||||
|
||||
+2
-1
@@ -171,10 +171,11 @@ internal sealed interface MailboxViewAction : MailboxOperation {
|
||||
AffectingBottomSheet,
|
||||
AffectingTopAppBar,
|
||||
AffectingBottomAppBar
|
||||
object ValidateUserSession : MailboxViewAction
|
||||
}
|
||||
|
||||
internal sealed interface MailboxEvent : MailboxOperation {
|
||||
|
||||
object CouldNotLoadUserSession : MailboxEvent, AffectingMailboxList
|
||||
data class LabelAsConfirmed(
|
||||
val viewMode: ViewMode,
|
||||
val itemCount: Int,
|
||||
|
||||
+5
@@ -89,6 +89,7 @@ class MailboxListReducer @Inject constructor(
|
||||
is MailboxEvent.AttachmentReadyEvent -> reduceAttachmentReady(operation, currentState)
|
||||
is MailboxEvent.AttachmentErrorEvent -> reduceAttachmentDownloadError(currentState)
|
||||
is MailboxEvent.PaginatorInvalidated -> reducePaginatorInvalidated(operation, currentState)
|
||||
is MailboxEvent.CouldNotLoadUserSession -> reduceCouldNotLoadUserSession()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +244,7 @@ class MailboxListReducer @Inject constructor(
|
||||
): MailboxListState.Data {
|
||||
val currentMailLabel = operation.selectedLabel
|
||||
return when (currentState) {
|
||||
is MailboxListState.CouldNotLoadUserSession,
|
||||
is MailboxListState.Loading -> MailboxListState.Data.ViewMode(
|
||||
currentMailLabel,
|
||||
openItemEffect = Effect.empty(),
|
||||
@@ -274,6 +276,7 @@ class MailboxListReducer @Inject constructor(
|
||||
): MailboxListState.Data {
|
||||
val currentMailLabel = operation.selectedLabel
|
||||
return when (currentState) {
|
||||
is MailboxListState.CouldNotLoadUserSession,
|
||||
is MailboxListState.Loading -> MailboxListState.Data.ViewMode(
|
||||
currentMailLabel,
|
||||
openItemEffect = Effect.empty(),
|
||||
@@ -557,4 +560,6 @@ class MailboxListReducer @Inject constructor(
|
||||
|
||||
else -> currentState
|
||||
}
|
||||
|
||||
private fun reduceCouldNotLoadUserSession(): MailboxListState = MailboxListState.CouldNotLoadUserSession
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<string name="mailbox_default_sender">(No Sender)</string>
|
||||
<string name="mailbox_error_message_offline">You are offline, unable to retrieve items</string>
|
||||
<string name="mailbox_error_message_generic">Loading mailbox failed</string>
|
||||
<string name="mailbox_error_message_user_session">Unable to load session. Please check your connection and try again.</string>
|
||||
<string name="mailbox_error_message_unexpected">Something went unexpectedly wrong</string>
|
||||
<string name="mailbox_is_empty_title">No messages</string>
|
||||
<string name="trash_is_empty_title">Nothing in Trash</string>
|
||||
|
||||
+71
-4
@@ -110,7 +110,8 @@ import ch.protonmail.android.mailmessage.presentation.model.bottomsheet.SnoozeSh
|
||||
import ch.protonmail.android.mailpagination.domain.model.PageInvalidationEvent
|
||||
import ch.protonmail.android.mailpagination.domain.usecase.ObservePageInvalidationEvents
|
||||
import ch.protonmail.android.mailsession.domain.repository.EventLoopRepository
|
||||
import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId
|
||||
import ch.protonmail.android.mailsession.domain.usecase.GetUserHasValidSession
|
||||
import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserIdWithValidSession
|
||||
import ch.protonmail.android.mailsettings.domain.model.FolderColorSettings
|
||||
import ch.protonmail.android.mailsettings.domain.model.SwipeActionsPreference
|
||||
import ch.protonmail.android.mailsettings.domain.model.ToolbarActionsRefreshSignal
|
||||
@@ -175,7 +176,7 @@ internal class MailboxViewModelTest {
|
||||
private val actionUiModelMapper = ActionUiModelMapper()
|
||||
private val swipeActionsMapper = SwipeActionsMapper()
|
||||
|
||||
private val observePrimaryUserId = mockk<ObservePrimaryUserId> {
|
||||
private val observePrimaryUserId = mockk<ObservePrimaryUserIdWithValidSession> {
|
||||
every { this@mockk.invoke() } returns flowOf(userId)
|
||||
}
|
||||
|
||||
@@ -244,6 +245,9 @@ internal class MailboxViewModelTest {
|
||||
private val deleteMessages = mockk<DeleteMessages>()
|
||||
private val starMessages = mockk<StarMessages>()
|
||||
private val starConversations = mockk<StarConversations>()
|
||||
private val getUserHasValidSession = mockk<GetUserHasValidSession> {
|
||||
coEvery { this@mockk.invoke() } returns true
|
||||
}
|
||||
private val unStarMessages = mockk<UnStarMessages>()
|
||||
private val unStarConversations = mockk<UnStarConversations>()
|
||||
private val getBottomSheetActions = mockk<GetBottomSheetActions>()
|
||||
@@ -302,7 +306,7 @@ internal class MailboxViewModelTest {
|
||||
appScope = scope,
|
||||
mailboxPagerFactory = pagerFactory,
|
||||
getCurrentViewModeForLabel = getCurrentViewModeForLabel,
|
||||
observePrimaryUserId = observePrimaryUserId,
|
||||
observePrimaryUserIdWithValidSession = observePrimaryUserId,
|
||||
observeMailLabels = observeMailLabels,
|
||||
observeSwipeActionsPreference = observeSwipeActionsPreference,
|
||||
observeSelectedMailLabelId = observeSelectedMailLabelId,
|
||||
@@ -341,7 +345,8 @@ internal class MailboxViewModelTest {
|
||||
observeViewModeChanged = observeViewModeChanged,
|
||||
toolbarRefreshSignal = toolbarRefreshSignal,
|
||||
terminateConversationPaginator = terminateConversationPaginator,
|
||||
eventLoopRepository = eventLoopRepository
|
||||
eventLoopRepository = eventLoopRepository,
|
||||
getUserHasValidSession = getUserHasValidSession
|
||||
)
|
||||
}
|
||||
|
||||
@@ -922,6 +927,68 @@ internal class MailboxViewModelTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when loading and validateUserSession is false then emit CouldNotLoadUserSession`() = runTest {
|
||||
// Given
|
||||
coEvery { getUserHasValidSession() } returns false
|
||||
coEvery { observeUnreadCounters(userId = any()) } returns emptyFlow()
|
||||
coEvery { observeMailLabels(userId = any()) } returns emptyFlow()
|
||||
|
||||
// When
|
||||
mailboxViewModel.state.test {
|
||||
// Then
|
||||
val actual = awaitItem()
|
||||
val expected = MailboxState(
|
||||
mailboxListState = MailboxListState.Loading,
|
||||
topAppBarState = MailboxTopAppBarState.Loading,
|
||||
unreadFilterState = UnreadFilterState.Loading,
|
||||
bottomAppBarState = BottomBarState.Data.Hidden(emptyList<ActionUiModel>().toImmutableList()),
|
||||
deleteDialogState = DeleteDialogState.Hidden,
|
||||
clearAllDialogState = DeleteDialogState.Hidden,
|
||||
bottomSheetState = null,
|
||||
actionResult = Effect.empty(),
|
||||
error = Effect.empty()
|
||||
)
|
||||
|
||||
// when
|
||||
mailboxViewModel.submit(MailboxViewAction.ValidateUserSession)
|
||||
assertEquals(expected, actual)
|
||||
awaitItem()
|
||||
verify { mailboxReducer.newStateFrom(actual, MailboxEvent.CouldNotLoadUserSession) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when loading and validateUserSession is true then do not emit CouldNotLoadUserSession`() = runTest {
|
||||
// Given
|
||||
coEvery { getUserHasValidSession() } returns true
|
||||
coEvery { observeUnreadCounters(userId = any()) } returns emptyFlow()
|
||||
coEvery { observeMailLabels(userId = any()) } returns emptyFlow()
|
||||
|
||||
// When
|
||||
mailboxViewModel.state.test {
|
||||
// Then
|
||||
val actual = awaitItem()
|
||||
val expected = MailboxState(
|
||||
mailboxListState = MailboxListState.Loading,
|
||||
topAppBarState = MailboxTopAppBarState.Loading,
|
||||
unreadFilterState = UnreadFilterState.Loading,
|
||||
bottomAppBarState = BottomBarState.Data.Hidden(emptyList<ActionUiModel>().toImmutableList()),
|
||||
deleteDialogState = DeleteDialogState.Hidden,
|
||||
clearAllDialogState = DeleteDialogState.Hidden,
|
||||
bottomSheetState = null,
|
||||
actionResult = Effect.empty(),
|
||||
error = Effect.empty()
|
||||
)
|
||||
|
||||
// when
|
||||
mailboxViewModel.submit(MailboxViewAction.ValidateUserSession)
|
||||
assertEquals(expected, actual)
|
||||
coVerify(exactly = 1) { getUserHasValidSession.invoke() }
|
||||
verify(exactly = 0) { mailboxReducer.newStateFrom(actual, MailboxEvent.CouldNotLoadUserSession) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when avatar click action is submitted to remove item from selection, new state is created and emitted`() =
|
||||
runTest {
|
||||
|
||||
+45
@@ -167,6 +167,21 @@ internal class MailboxListReducerTest(
|
||||
avatarImagesUiModel = AvatarImagesUiModel.Empty
|
||||
)
|
||||
),
|
||||
TestInput(
|
||||
currentState = MailboxListState.CouldNotLoadUserSession,
|
||||
operation = MailboxEvent.SelectedLabelChanged(MailLabelTestData.spamSystemLabel),
|
||||
expectedState = MailboxListState.Data.ViewMode(
|
||||
currentMailLabel = MailLabelTestData.spamSystemLabel,
|
||||
openItemEffect = Effect.empty(),
|
||||
scrollToMailboxTop = Effect.empty(),
|
||||
refreshErrorEffect = Effect.empty(),
|
||||
refreshRequested = false,
|
||||
swipeActions = null,
|
||||
searchState = MailboxSearchStateSampleData.NotSearching,
|
||||
shouldShowFab = true,
|
||||
avatarImagesUiModel = AvatarImagesUiModel.Empty
|
||||
)
|
||||
),
|
||||
TestInput(
|
||||
currentState = MailboxListState.Loading,
|
||||
operation = MailboxEvent.NewLabelSelected(MailLabelTestData.customLabelOne, UNREAD_COUNT),
|
||||
@@ -182,6 +197,21 @@ internal class MailboxListReducerTest(
|
||||
avatarImagesUiModel = AvatarImagesUiModel.Empty
|
||||
)
|
||||
),
|
||||
TestInput(
|
||||
currentState = MailboxListState.CouldNotLoadUserSession,
|
||||
operation = MailboxEvent.NewLabelSelected(MailLabelTestData.customLabelOne, UNREAD_COUNT),
|
||||
expectedState = MailboxListState.Data.ViewMode(
|
||||
currentMailLabel = MailLabelTestData.customLabelOne,
|
||||
openItemEffect = Effect.empty(),
|
||||
scrollToMailboxTop = Effect.empty(),
|
||||
refreshErrorEffect = Effect.empty(),
|
||||
refreshRequested = false,
|
||||
swipeActions = null,
|
||||
searchState = MailboxSearchStateSampleData.NotSearching,
|
||||
shouldShowFab = true,
|
||||
avatarImagesUiModel = AvatarImagesUiModel.Empty
|
||||
)
|
||||
),
|
||||
TestInput(
|
||||
currentState = MailboxListState.Loading,
|
||||
operation = MailboxEvent.NewLabelSelected(MailLabelTestData.spamSystemLabel, UNREAD_COUNT),
|
||||
@@ -1365,6 +1395,21 @@ internal class MailboxListReducerTest(
|
||||
avatarImagesUiModel = AvatarImagesUiModel.Empty,
|
||||
areAllItemsSelected = false
|
||||
)
|
||||
),
|
||||
TestInput(
|
||||
currentState = MailboxListState.Data.ViewMode(
|
||||
currentMailLabel = MailLabelTestData.customLabelOne,
|
||||
openItemEffect = Effect.empty(),
|
||||
scrollToMailboxTop = Effect.empty(),
|
||||
refreshErrorEffect = Effect.empty(),
|
||||
refreshRequested = true,
|
||||
swipeActions = null,
|
||||
searchState = MailboxSearchStateSampleData.NotSearching,
|
||||
shouldShowFab = true,
|
||||
avatarImagesUiModel = AvatarImagesUiModel.Empty
|
||||
),
|
||||
operation = MailboxEvent.CouldNotLoadUserSession,
|
||||
expectedState = MailboxListState.CouldNotLoadUserSession
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+21
-7
@@ -40,8 +40,10 @@ import ch.protonmail.android.mailsession.domain.model.UserSettings
|
||||
import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository
|
||||
import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
@@ -64,6 +66,7 @@ class UserSessionRepositoryImpl @Inject constructor(
|
||||
|
||||
// Cache to store MailUserSession per UserId
|
||||
private val userSessionCache = mutableMapOf<UserId, MailUserSessionWrapper>()
|
||||
private val userSessionUpdatedTrigger = MutableSharedFlow<UserId>(replay = 1)
|
||||
|
||||
private suspend fun getStoredAccount(userId: UserId) = mailSession.getAccount(userId.toLocalUserId())
|
||||
|
||||
@@ -123,19 +126,30 @@ class UserSessionRepositoryImpl @Inject constructor(
|
||||
userSessionCache.remove(userId)
|
||||
}
|
||||
|
||||
override fun observeUserSessionAvailable(userId: UserId): Flow<UserId> = userSessionUpdatedTrigger.filter {
|
||||
it == userId
|
||||
}.distinctUntilChanged()
|
||||
|
||||
override suspend fun getUserSession(userId: UserId): MailUserSessionWrapper? {
|
||||
// Return cached session if it exists
|
||||
userSessionCache[userId]?.let { return it }
|
||||
userSessionCache[userId]?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
// Create and store session if not in cache
|
||||
val session = getStoredAccount(userId).getOrNull()?.let { account ->
|
||||
mailSession.getAccountSessions(account).getOrNull()?.firstOrNull()
|
||||
val storedAccount = getStoredAccount(userId).getOrNull()
|
||||
val session = storedAccount?.let { account ->
|
||||
val accountSessions = mailSession.getAccountSessions(account).getOrNull()
|
||||
accountSessions?.firstOrNull()
|
||||
}
|
||||
val userContext = session?.let { mailSession.userContextFromSession(it) }?.getOrNull()
|
||||
if (userContext != null) {
|
||||
userSessionCache[userId] = userContext
|
||||
// throws network error
|
||||
val userContext = session?.let { mailSession.userContextFromSession(it) }
|
||||
userContext?.getOrNull()?.let {
|
||||
userSessionUpdatedTrigger.emit(userId)
|
||||
userSessionCache[userId] = it
|
||||
return it
|
||||
}
|
||||
return userContext
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun getUserSettings(userId: UserId): UserSettings? {
|
||||
|
||||
+27
@@ -209,6 +209,33 @@ class UserSessionRepositoryImplTest {
|
||||
coVerify(exactly = 1) { mailSession.userContextFromSession(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `caching of a new user session will trigger observer`() = runTest {
|
||||
// Given
|
||||
val userId = UserIdTestData.userId
|
||||
val userId1 = UserIdTestData.userId1
|
||||
val expectedMailUserSession = mockk<MailUserSessionWrapper>()
|
||||
val mailSession = mailSessionWithUserSessionStored(expectedMailUserSession)
|
||||
coEvery { mailSessionRepository.getMailSession() } returns mailSession
|
||||
|
||||
|
||||
// When
|
||||
userSessionRepository.getUserSession(userId)
|
||||
// Then
|
||||
userSessionRepository.observeUserSessionAvailable(userId).test {
|
||||
assertEquals(userId, awaitItem())
|
||||
}
|
||||
|
||||
// When
|
||||
userSessionRepository.getUserSession(userId1)
|
||||
|
||||
// Then
|
||||
userSessionRepository.observeUserSessionAvailable(userId1).test {
|
||||
assertEquals(userId1, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun mailSessionWithNoUserSessionsStored() = mockk<MailSessionWrapper> {
|
||||
coEvery { getAccount(any()) } returns DataError.Local.NoDataCached.left()
|
||||
coEvery { getAccounts() } returns emptyList<StoredAccount>().right()
|
||||
|
||||
+2
@@ -68,6 +68,8 @@ interface UserSessionRepository {
|
||||
suspend fun setAutoLockPinCode(autoLockPin: AutoLockPin): Either<SetAutoLockPinError, Unit>
|
||||
|
||||
suspend fun setBiometricAppProtection(): Either<DataError, Unit>
|
||||
|
||||
fun observeUserSessionAvailable(userId: UserId): Flow<UserId>
|
||||
}
|
||||
|
||||
fun UserSessionRepository.onAccountState(state: AccountState, initialState: Boolean = true): Flow<Account> =
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.mailsession.domain.usecase
|
||||
|
||||
import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetUserHasValidSession @Inject constructor(
|
||||
private val observePrimaryUserId: ObservePrimaryUserId,
|
||||
private val userSessionRepository: UserSessionRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): Boolean {
|
||||
val userId = observePrimaryUserId().filterNotNull().first()
|
||||
val session = userSessionRepository.getUserSession(userId)
|
||||
return session != null
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.mailsession.domain.usecase
|
||||
|
||||
import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import javax.inject.Inject
|
||||
|
||||
class ObservePrimaryUserIdWithValidSession @Inject constructor(
|
||||
private val observePrimaryUserId: ObservePrimaryUserId,
|
||||
private val userSessionRepository: UserSessionRepository
|
||||
) {
|
||||
|
||||
operator fun invoke(): Flow<UserId?> = observePrimaryUserId()
|
||||
.filterNotNull()
|
||||
.flatMapLatest { userId ->
|
||||
userSessionRepository.observeUserSessionAvailable(userId)
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.mailsession.domain.usecase
|
||||
|
||||
import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository
|
||||
import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper
|
||||
import ch.protonmail.android.testdata.user.UserIdTestData
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class GetUserHasValidSessionTest {
|
||||
|
||||
private val userSessionRepository = mockk<UserSessionRepository>()
|
||||
|
||||
private val observePrimaryUserId = ObservePrimaryUserId(userSessionRepository)
|
||||
private val getUserHasValidSession = GetUserHasValidSession(observePrimaryUserId, userSessionRepository)
|
||||
|
||||
@Test
|
||||
fun `given no user session then false`() = runTest {
|
||||
// Given
|
||||
every { userSessionRepository.observePrimaryUserId() } returns flowOf(UserIdTestData.userId)
|
||||
coEvery { userSessionRepository.getUserSession(UserIdTestData.userId) } returns null
|
||||
|
||||
// Then
|
||||
assertEquals(false, getUserHasValidSession())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given user session then true`() = runTest {
|
||||
// Given
|
||||
every { userSessionRepository.observePrimaryUserId() } returns flowOf(UserIdTestData.userId)
|
||||
coEvery { userSessionRepository.getUserSession(UserIdTestData.userId) } returns MailUserSessionWrapper(
|
||||
mockk()
|
||||
)
|
||||
|
||||
// Then
|
||||
assertEquals(true, getUserHasValidSession())
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.mailsession.domain.usecase
|
||||
|
||||
import app.cash.turbine.test
|
||||
import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository
|
||||
import ch.protonmail.android.testdata.user.UserIdTestData
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ObservePrimaryUserIdWithValidSessionTest {
|
||||
|
||||
private val userSessionRepository = mockk<UserSessionRepository>()
|
||||
|
||||
private val observePrimaryUserId = ObservePrimaryUserId(userSessionRepository)
|
||||
|
||||
|
||||
@Test
|
||||
fun `given userId and session available then return flow of UserId`() = runTest {
|
||||
// Given
|
||||
every { userSessionRepository.observePrimaryUserId() } returns flowOf(null, UserIdTestData.userId)
|
||||
coEvery { userSessionRepository.observeUserSessionAvailable(UserIdTestData.userId) } returns
|
||||
flowOf(UserIdTestData.userId)
|
||||
|
||||
// Then
|
||||
ObservePrimaryUserIdWithValidSession(observePrimaryUserId, userSessionRepository).invoke().test {
|
||||
assertEquals(UserIdTestData.userId, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given null userId then valid session is never returned for UserId`() = runTest {
|
||||
// Given
|
||||
every { userSessionRepository.observePrimaryUserId() } returns flowOf(null)
|
||||
coEvery { userSessionRepository.observeUserSessionAvailable(UserIdTestData.userId) } returns
|
||||
flowOf(UserIdTestData.userId)
|
||||
|
||||
// Then
|
||||
ObservePrimaryUserIdWithValidSession(observePrimaryUserId, userSessionRepository).invoke().test {
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given valid session is never returned for UserId`() = runTest {
|
||||
// Given
|
||||
every { userSessionRepository.observePrimaryUserId() } returns flowOf(null, UserIdTestData.userId)
|
||||
coEvery { userSessionRepository.observeUserSessionAvailable(UserIdTestData.userId) } returns
|
||||
flowOf()
|
||||
|
||||
// Then
|
||||
ObservePrimaryUserIdWithValidSession(observePrimaryUserId, userSessionRepository).invoke().test {
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -30,6 +30,7 @@ import ch.protonmail.android.mailsession.domain.model.UserSettings
|
||||
import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository
|
||||
import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.network.domain.session.SessionId
|
||||
@@ -95,4 +96,6 @@ internal class FakeUserSessionRepository(
|
||||
override suspend fun setBiometricAppProtection(): Either<DataError, Unit> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun observeUserSessionAvailable(userId: UserId): Flow<UserId> = observePrimaryUserId().filterNotNull()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user