From 7c7645c16870e8708b6a1153615c3e1937c1c4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 18 Dec 2024 16:01:34 +0100 Subject: [PATCH 1/6] fix: reusing PagingData crash [WPB-15055][WPB-15079][WPB-15064] --- .../common/topappbar/search/SearchBarState.kt | 23 ++- .../android/ui/home/archive/ArchiveScreen.kt | 9 +- .../ConversationListViewModel.kt | 167 ++++++++++-------- .../ConversationsScreenContent.kt | 6 +- .../all/AllConversationsScreen.kt | 9 +- kalium | 2 +- 6 files changed, 118 insertions(+), 98 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt index d87a1193371..f9881b24a31 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt @@ -29,11 +29,12 @@ import androidx.compose.runtime.setValue @Composable fun rememberSearchbarState( + initialIsSearchActive: Boolean = false, searchQueryTextState: TextFieldState = rememberTextFieldState() ): SearchBarState = rememberSaveable( - saver = SearchBarState.saver(searchQueryTextState) + saver = SearchBarState.saver() ) { - SearchBarState(searchQueryTextState = searchQueryTextState) + SearchBarState(isSearchActive = initialIsSearchActive, searchQueryTextState = searchQueryTextState) } class SearchBarState( @@ -57,16 +58,26 @@ class SearchBarState( } companion object { - fun saver(searchQueryTextState: TextFieldState): Saver = Saver( + fun saver(): Saver = Saver( save = { - listOf(it.isSearchActive) + listOf( + it.isSearchActive, + with(TextFieldState.Saver) { + save(it.searchQueryTextState) + } + ) }, restore = { SearchBarState( - isSearchActive = it[0], - searchQueryTextState = searchQueryTextState + isSearchActive = it[0] as Boolean, + searchQueryTextState = it[1]?.let { + with(TextFieldState.Saver) { + restore(it) + } + } ?: TextFieldState() ) } ) } } + diff --git a/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt index 597f2d26390..07a1c578927 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt @@ -32,7 +32,6 @@ import com.wire.android.ui.home.conversationslist.common.previewConversationFold import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes -import kotlinx.coroutines.flow.flowOf @HomeNavGraph @WireDestination @@ -57,7 +56,7 @@ fun PreviewArchiveEmptyScreen() = WireTheme { searchBarState = rememberSearchbarState(), conversationsSource = ConversationsSource.ARCHIVE, emptyListContent = { ArchiveEmptyContent() }, - conversationListViewModel = ConversationListViewModelPreview(flowOf()), + conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(list = listOf())), ) } @@ -66,10 +65,10 @@ fun PreviewArchiveEmptyScreen() = WireTheme { fun PreviewArchiveEmptySearchScreen() = WireTheme { ConversationsScreenContent( navigator = rememberNavigator {}, - searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")), + searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")), conversationsSource = ConversationsSource.ARCHIVE, emptyListContent = { ArchiveEmptyContent() }, - conversationListViewModel = ConversationListViewModelPreview(flowOf()), + conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er", list = listOf())), ) } @@ -78,7 +77,7 @@ fun PreviewArchiveEmptySearchScreen() = WireTheme { fun PreviewArchiveScreen() = WireTheme { ConversationsScreenContent( navigator = rememberNavigator {}, - searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")), + searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")), conversationsSource = ConversationsSource.ARCHIVE, emptyListContent = { ArchiveEmptyContent() }, conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er")), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index 3ec4d460b94..b41ad3af523 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData +import androidx.paging.cachedIn import androidx.paging.insertSeparators import androidx.paging.map import com.wire.android.BuildConfig @@ -126,7 +127,7 @@ class ConversationListViewModelPreview( class ConversationListViewModelImpl @AssistedInject constructor( @Assisted val conversationsSource: ConversationsSource, @Assisted private val usePagination: Boolean = BuildConfig.PAGINATED_CONVERSATION_LIST_ENABLED, - dispatcher: DispatcherProvider, + private val dispatcher: DispatcherProvider, private val updateConversationMutedStatus: UpdateConversationMutedStatusUseCase, private val getConversationsPaginated: GetConversationsFromSearchUseCase, private val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase, @@ -161,6 +162,7 @@ class ConversationListViewModelImpl @AssistedInject constructor( override val closeBottomSheet = MutableSharedFlow() private val searchQueryFlow: MutableStateFlow = MutableStateFlow("") + private val isSelfUserUnderLegalHoldFlow = MutableSharedFlow(replay = 1) private val containsNewActivitiesSection = when (conversationsSource) { ConversationsSource.MAIN, @@ -174,94 +176,105 @@ class ConversationListViewModelImpl @AssistedInject constructor( private val conversationsPaginatedFlow: Flow> = searchQueryFlow .debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE } .onStart { emit("") } + .combine(isSelfUserUnderLegalHoldFlow, ::Pair) .distinctUntilChanged() - .flatMapLatest { searchQuery -> + .flatMapLatest { (searchQuery, isSelfUserUnderLegalHold) -> getConversationsPaginated( searchQuery = searchQuery, fromArchive = conversationsSource == ConversationsSource.ARCHIVE, conversationFilter = conversationsSource.toFilter(), onlyInteractionEnabled = false, newActivitiesOnTop = containsNewActivitiesSection, - ).combine(observeLegalHoldStateForSelfUser()) { conversations, selfUserLegalHoldStatus -> - conversations.map { - it.hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus) - } - }.map { - it.insertSeparators { before, after -> - when { - // do not add separators if the list shouldn't show conversations grouped into different folders - !containsNewActivitiesSection -> null - - before == null && after != null && after.hasNewActivitiesToShow -> - // list starts with items with "new activities" - ConversationFolder.Predefined.NewActivities - - before == null && after != null && !after.hasNewActivitiesToShow -> - // list doesn't contain any items with "new activities" - ConversationFolder.Predefined.Conversations - - before != null && before.hasNewActivitiesToShow && after != null && !after.hasNewActivitiesToShow -> - // end of "new activities" section and beginning of "conversations" section - ConversationFolder.Predefined.Conversations - - else -> null + ).map { pagingData -> + pagingData + .map { it.hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold) } + .insertSeparators { before, after -> + when { + // do not add separators if the list shouldn't show conversations grouped into different folders + !containsNewActivitiesSection -> null + + before == null && after != null && after.hasNewActivitiesToShow -> + // list starts with items with "new activities" + ConversationFolder.Predefined.NewActivities + + before == null && after != null && !after.hasNewActivitiesToShow -> + // list doesn't contain any items with "new activities" + ConversationFolder.Predefined.Conversations + + before != null && before.hasNewActivitiesToShow && after != null && !after.hasNewActivitiesToShow -> + // end of "new activities" section and beginning of "conversations" section + ConversationFolder.Predefined.Conversations + + else -> null + } } - } } } .flowOn(dispatcher.io()) + .cachedIn(viewModelScope) - private var notPaginatedConversationListState by mutableStateOf(ConversationListState.NotPaginated()) - override val conversationListState: ConversationListState - get() = if (usePagination) { - ConversationListState.Paginated( - conversations = conversationsPaginatedFlow, - domain = currentAccount.domain - ) - } else { - notPaginatedConversationListState + override var conversationListState by mutableStateOf( + when (usePagination) { + true -> ConversationListState.Paginated(conversations = conversationsPaginatedFlow, domain = currentAccount.domain) + false -> ConversationListState.NotPaginated() } + ) + private set init { + observeSelfUserLegalHoldState() if (!usePagination) { - viewModelScope.launch { - searchQueryFlow - .debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE } - .onStart { emit("") } - .distinctUntilChanged() - .flatMapLatest { searchQuery: String -> - observeConversationListDetailsWithEvents( - fromArchive = conversationsSource == ConversationsSource.ARCHIVE, - conversationFilter = conversationsSource.toFilter() - ).combine(observeLegalHoldStateForSelfUser()) { conversations, selfUserLegalHoldStatus -> - conversations.map { conversationDetails -> - conversationDetails.toConversationItem( - userTypeMapper = userTypeMapper, - searchQuery = searchQuery, - selfUserTeamId = observeSelfUser().firstOrNull()?.teamId - ).hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus) - } to searchQuery - } - } - .map { (conversationItems, searchQuery) -> - if (searchQuery.isEmpty()) { - conversationItems.withFolders(source = conversationsSource).toImmutableMap() - } else { - searchConversation( - conversationDetails = conversationItems, - searchQuery = searchQuery - ).withFolders(source = conversationsSource).toImmutableMap() - } + observeNonPaginatedSearchConversationList() + } + } + + private fun observeSelfUserLegalHoldState() { + viewModelScope.launch { + observeLegalHoldStateForSelfUser() + .map { it is LegalHoldStateForSelfUser.Enabled } + .flowOn(dispatcher.io()) + .collect { isSelfUserUnderLegalHoldFlow.emit(it) } + } + } + + private fun observeNonPaginatedSearchConversationList() { + viewModelScope.launch { + searchQueryFlow + .debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE } + .onStart { emit("") } + .distinctUntilChanged() + .flatMapLatest { searchQuery: String -> + observeConversationListDetailsWithEvents( + fromArchive = conversationsSource == ConversationsSource.ARCHIVE, + conversationFilter = conversationsSource.toFilter() + ).combine(isSelfUserUnderLegalHoldFlow) { conversations, isSelfUserUnderLegalHold -> + conversations.map { conversationDetails -> + conversationDetails.toConversationItem( + userTypeMapper = userTypeMapper, + searchQuery = searchQuery, + selfUserTeamId = observeSelfUser().firstOrNull()?.teamId + ).hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold) + } to searchQuery } - .flowOn(dispatcher.io()) - .collect { - notPaginatedConversationListState = notPaginatedConversationListState.copy( - isLoading = false, - conversations = it, - domain = currentAccount.domain - ) + } + .map { (conversationItems, searchQuery) -> + if (searchQuery.isEmpty()) { + conversationItems.withFolders(source = conversationsSource).toImmutableMap() + } else { + searchConversation( + conversationDetails = conversationItems, + searchQuery = searchQuery + ).withFolders(source = conversationsSource).toImmutableMap() } - } + } + .flowOn(dispatcher.io()) + .collect { + conversationListState = ConversationListState.NotPaginated( + isLoading = false, + conversations = it, + domain = currentAccount.domain + ) + } } } @@ -446,11 +459,13 @@ private fun ConversationsSource.toFilter(): ConversationFilter = when (this) { ConversationsSource.ONE_ON_ONE -> ConversationFilter.ONE_ON_ONE } -private fun ConversationItem.hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus: LegalHoldStateForSelfUser) = - // if self user is under legal hold then we shouldn't show legal hold indicator next to every conversation - // the indication is shown in the header of the conversation list for self user in that case and it's enough - when (selfUserLegalHoldStatus) { - is LegalHoldStateForSelfUser.Enabled -> when (this) { +/** + * If self user is under legal hold then we shouldn't show legal hold indicator next to every conversation as in that case + * the legal hold indication is shown in the header of the conversation list for self user in that case and it's enough. + */ +private fun ConversationItem.hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold: Boolean) = + when (isSelfUserUnderLegalHold) { + true -> when (this) { is ConversationItem.ConnectionConversation -> this.copy(showLegalHoldIndicator = false) is ConversationItem.GroupConversation -> this.copy(showLegalHoldIndicator = false) is ConversationItem.PrivateConversation -> this.copy(showLegalHoldIndicator = false) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt index 244fc837067..bdcce5f78e3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt @@ -88,7 +88,6 @@ fun ConversationsScreenContent( lazyListState: LazyListState = rememberLazyListState(), loadingListContent: @Composable (LazyListState) -> Unit = { ConversationListLoadingContent(it) }, conversationsSource: ConversationsSource = ConversationsSource.MAIN, - initiallyLoaded: Boolean = LocalInspectionMode.current, conversationListViewModel: ConversationListViewModel = when { LocalInspectionMode.current -> ConversationListViewModelPreview() else -> hiltViewModel( @@ -189,10 +188,7 @@ fun ConversationsScreenContent( when (val state = conversationListViewModel.conversationListState) { is ConversationListState.Paginated -> { val lazyPagingItems = state.conversations.collectAsLazyPagingItems() - var showLoading by remember { mutableStateOf(!initiallyLoaded) } - if (lazyPagingItems.loadState.refresh != LoadState.Loading && showLoading) { - showLoading = false - } + val showLoading = lazyPagingItems.loadState.refresh == LoadState.Loading && lazyPagingItems.itemCount == 0 when { // when conversation list is not yet fetched, show loading indicator diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt index 3c7da0a4825..f02ba56f1b4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt @@ -33,7 +33,6 @@ import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.conversation.ConversationFilter -import kotlinx.coroutines.flow.flowOf @HomeNavGraph(start = true) @WireDestination @@ -103,7 +102,7 @@ fun PreviewAllConversationsEmptyScreen() = WireTheme { searchBarState = rememberSearchbarState(), conversationsSource = ConversationsSource.MAIN, emptyListContent = { ConversationsEmptyContent() }, - conversationListViewModel = ConversationListViewModelPreview(flowOf()), + conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(list = listOf())), ) } @@ -112,10 +111,10 @@ fun PreviewAllConversationsEmptyScreen() = WireTheme { fun PreviewAllConversationsEmptySearchScreen() = WireTheme { ConversationsScreenContent( navigator = rememberNavigator {}, - searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")), + searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")), conversationsSource = ConversationsSource.MAIN, emptyListContent = { ConversationsEmptyContent() }, - conversationListViewModel = ConversationListViewModelPreview(flowOf()), + conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er", list = listOf())), ) } @@ -124,7 +123,7 @@ fun PreviewAllConversationsEmptySearchScreen() = WireTheme { fun PreviewAllConversationsSearchScreen() = WireTheme { ConversationsScreenContent( navigator = rememberNavigator {}, - searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")), + searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")), conversationsSource = ConversationsSource.MAIN, emptyListContent = { ConversationsEmptyContent() }, conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow("er")), diff --git a/kalium b/kalium index da1e7ebe24d..1ea1358de16 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit da1e7ebe24dc6db2081071e28169154b31f99096 +Subproject commit 1ea1358de164581456ca3ef6b61f88d6bc99215a From 455ee0874139ae6f476dedaa99a7b124f3770fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 18 Dec 2024 16:15:04 +0100 Subject: [PATCH 2/6] fix: add LoadStates to preview PagingData --- .../conversationslist/common/ConversationList.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt index 730ef25eaf7..46d962ca34d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt @@ -30,6 +30,8 @@ import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode +import androidx.paging.LoadState +import androidx.paging.LoadStates import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -234,7 +236,15 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f fun previewConversationFoldersFlow( searchQuery: String = "", list: List = previewConversationFolders(searchQuery = searchQuery) -) = flowOf(PagingData.from(list)) +) = flowOf(PagingData.from( + data = list, + sourceLoadStates = LoadStates( + prepend = LoadState.NotLoading(true), + append = LoadState.NotLoading(true), + refresh = LoadState.NotLoading(true), + ) +) +) fun previewConversationFolders(withFolders: Boolean = true, searchQuery: String = "", unreadCount: Int = 3, readCount: Int = 6) = buildList { From 3351627116c0738886e12cbc604ff10b6558114d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 18 Dec 2024 16:38:23 +0100 Subject: [PATCH 3/6] add tests for the PagingData flow --- .../ConversationListViewModelTest.kt | 82 ++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt index a1a0fc548fa..f52ec2a58ad 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt @@ -61,6 +61,9 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -233,6 +236,77 @@ class ConversationListViewModelTest { coVerify(exactly = 1) { arrangement.unblockUser(userId) } } + @Test + fun `given cached PagingData, when self user legal hold changes, then should call paginated use case again`() = + runTest(dispatcherProvider.main()) { + // given + val conversations = listOf( + TestConversationItem.CONNECTION.copy(conversationId = ConversationId("conn_1", "")), + TestConversationItem.PRIVATE.copy(conversationId = ConversationId("private_1", "")), + TestConversationItem.GROUP.copy(conversationId = ConversationId("group_1", "")), + ).associateBy { it.conversationId } + val selfUserLegalHoldStateFlow = MutableSharedFlow() + val (arrangement, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.MAIN) + .withConversationsPaginated(conversations.values.toList()) + .withSelfUserLegalHoldStateFlow(selfUserLegalHoldStateFlow) + .arrange() + advanceUntilIdle() + + (conversationListViewModel.conversationListState as ConversationListState.Paginated).conversations.test { + // initial legal hold state + selfUserLegalHoldStateFlow.emit(LegalHoldStateForSelfUser.Disabled) + advanceUntilIdle() + + // use case is called initially + coVerify(exactly = 1) { + arrangement.getConversationsPaginated(any(), any(), any(), any(), any()) + } + + // when legal hold state is changed + selfUserLegalHoldStateFlow.emit(LegalHoldStateForSelfUser.Enabled) + advanceUntilIdle() + + // then use case should be called again (in total 2 executions) to create new PagingData + coVerify(exactly = 2) { + arrangement.getConversationsPaginated(any(), any(), any(), any(), any()) + } + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given cached PagingData, when observing twice, then paginated use case should not be called again`() = + runTest(dispatcherProvider.main()) { + // given + val conversations = listOf( + TestConversationItem.CONNECTION.copy(conversationId = ConversationId("conn_1", "")), + TestConversationItem.PRIVATE.copy(conversationId = ConversationId("private_1", "")), + TestConversationItem.GROUP.copy(conversationId = ConversationId("group_1", "")), + ).associateBy { it.conversationId } + val (arrangement, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.MAIN) + .withConversationsPaginated(conversations.values.toList()) + .withSelfUserLegalHoldState(LegalHoldStateForSelfUser.Disabled) + .arrange() + advanceUntilIdle() + + // flow is collected first time + (conversationListViewModel.conversationListState as ConversationListState.Paginated).conversations.first() + + // use case is called initially + coVerify(exactly = 1) { + arrangement.getConversationsPaginated(any(), any(), any(), any(), any()) + } + + // flow is collected second time + (conversationListViewModel.conversationListState as ConversationListState.Paginated).conversations.first() + + // use case should NOT be called again, there should be still only one call + coVerify(exactly = 1) { + arrangement.getConversationsPaginated(any(), any(), any(), any(), any()) + } + } + inner class Arrangement(val conversationsSource: ConversationsSource = ConversationsSource.MAIN) { @MockK lateinit var updateConversationMutedStatus: UpdateConversationMutedStatusUseCase @@ -321,8 +395,12 @@ class ConversationListViewModelTest { ) } - fun withSelfUserLegalHoldState(LegalHoldStateForSelfUser: LegalHoldStateForSelfUser) = apply { - coEvery { observeLegalHoldStateForSelfUserUseCase() } returns flowOf(LegalHoldStateForSelfUser) + fun withSelfUserLegalHoldState(legalHoldStateForSelfUser: LegalHoldStateForSelfUser) = apply { + coEvery { observeLegalHoldStateForSelfUserUseCase() } returns flowOf(legalHoldStateForSelfUser) + } + + fun withSelfUserLegalHoldStateFlow(legalHoldStateForSelfUserFlow: Flow) = apply { + coEvery { observeLegalHoldStateForSelfUserUseCase() } returns legalHoldStateForSelfUserFlow } fun arrange() = this to ConversationListViewModelImpl( From 94fccf7047dd94fe2391fea77d0b33cec070f357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Wed, 18 Dec 2024 17:19:30 +0100 Subject: [PATCH 4/6] fix detekt --- .../ui/common/topappbar/search/SearchBarState.kt | 1 - .../conversationslist/common/ConversationList.kt | 15 ++++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt index f9881b24a31..8f841ffde93 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt @@ -80,4 +80,3 @@ class SearchBarState( ) } } - diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt index 46d962ca34d..cb2c1a2dddd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt @@ -236,15 +236,16 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f fun previewConversationFoldersFlow( searchQuery: String = "", list: List = previewConversationFolders(searchQuery = searchQuery) -) = flowOf(PagingData.from( - data = list, - sourceLoadStates = LoadStates( - prepend = LoadState.NotLoading(true), - append = LoadState.NotLoading(true), - refresh = LoadState.NotLoading(true), +) = flowOf( + PagingData.from( + data = list, + sourceLoadStates = LoadStates( + prepend = LoadState.NotLoading(true), + append = LoadState.NotLoading(true), + refresh = LoadState.NotLoading(true), + ) ) ) -) fun previewConversationFolders(withFolders: Boolean = true, searchQuery: String = "", unreadCount: Int = 3, readCount: Int = 6) = buildList { From f72e1bbf67217d531268794eb7f979635caf85c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 30 Dec 2024 13:42:06 +0100 Subject: [PATCH 5/6] update kalium ref --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 1ea1358de16..dc70e05c41c 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 1ea1358de164581456ca3ef6b61f88d6bc99215a +Subproject commit dc70e05c41c2eea2e7cc4a6a64795ac6cc36a15e From a67ed50b2731a8e5cbe6b989e71769dc5c5a8a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 30 Dec 2024 13:45:04 +0100 Subject: [PATCH 6/6] make SearchBarState restoring safer --- .../wire/android/ui/common/topappbar/search/SearchBarState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt index 8f841ffde93..8646ca95f5c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt @@ -69,8 +69,8 @@ class SearchBarState( }, restore = { SearchBarState( - isSearchActive = it[0] as Boolean, - searchQueryTextState = it[1]?.let { + isSearchActive = (it.getOrNull(0) as? Boolean) ?: false, + searchQueryTextState = it.getOrNull(1)?.let { with(TextFieldState.Saver) { restore(it) }