Skip to content

Commit f21c93e

Browse files
fix: reusing PagingData crash [WPB-15055][WPB-15079][WPB-15064] πŸ’ (#3778)
Co-authored-by: Yamil Medina <yamilmedina@users.noreply.github.com>
1 parent b711329 commit f21c93e

File tree

7 files changed

+200
-91
lines changed

7 files changed

+200
-91
lines changed

β€Žapp/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.ktβ€Ž

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ import androidx.compose.runtime.setValue
2929

3030
@Composable
3131
fun rememberSearchbarState(
32+
initialIsSearchActive: Boolean = false,
3233
searchQueryTextState: TextFieldState = rememberTextFieldState()
3334
): SearchBarState = rememberSaveable(
34-
saver = SearchBarState.saver(searchQueryTextState)
35+
saver = SearchBarState.saver()
3536
) {
36-
SearchBarState(searchQueryTextState = searchQueryTextState)
37+
SearchBarState(isSearchActive = initialIsSearchActive, searchQueryTextState = searchQueryTextState)
3738
}
3839

3940
class SearchBarState(
@@ -57,14 +58,23 @@ class SearchBarState(
5758
}
5859

5960
companion object {
60-
fun saver(searchQueryTextState: TextFieldState): Saver<SearchBarState, *> = Saver(
61+
fun saver(): Saver<SearchBarState, *> = Saver(
6162
save = {
62-
listOf(it.isSearchActive)
63+
listOf(
64+
it.isSearchActive,
65+
with(TextFieldState.Saver) {
66+
save(it.searchQueryTextState)
67+
}
68+
)
6369
},
6470
restore = {
6571
SearchBarState(
66-
isSearchActive = it[0],
67-
searchQueryTextState = searchQueryTextState
72+
isSearchActive = (it.getOrNull(0) as? Boolean) ?: false,
73+
searchQueryTextState = it.getOrNull(1)?.let {
74+
with(TextFieldState.Saver) {
75+
restore(it)
76+
}
77+
} ?: TextFieldState()
6878
)
6979
}
7080
)

β€Žapp/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.ktβ€Ž

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import com.wire.android.ui.home.conversationslist.common.previewConversationFold
3232
import com.wire.android.ui.home.conversationslist.model.ConversationsSource
3333
import com.wire.android.ui.theme.WireTheme
3434
import com.wire.android.util.ui.PreviewMultipleThemes
35-
import kotlinx.coroutines.flow.flowOf
3635

3736
@HomeNavGraph
3837
@WireDestination
@@ -57,7 +56,7 @@ fun PreviewArchiveEmptyScreen() = WireTheme {
5756
searchBarState = rememberSearchbarState(),
5857
conversationsSource = ConversationsSource.ARCHIVE,
5958
emptyListContent = { ArchiveEmptyContent() },
60-
conversationListViewModel = ConversationListViewModelPreview(flowOf()),
59+
conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(list = listOf())),
6160
)
6261
}
6362

@@ -66,10 +65,10 @@ fun PreviewArchiveEmptyScreen() = WireTheme {
6665
fun PreviewArchiveEmptySearchScreen() = WireTheme {
6766
ConversationsScreenContent(
6867
navigator = rememberNavigator {},
69-
searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")),
68+
searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")),
7069
conversationsSource = ConversationsSource.ARCHIVE,
7170
emptyListContent = { ArchiveEmptyContent() },
72-
conversationListViewModel = ConversationListViewModelPreview(flowOf()),
71+
conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er", list = listOf())),
7372
)
7473
}
7574

@@ -78,7 +77,7 @@ fun PreviewArchiveEmptySearchScreen() = WireTheme {
7877
fun PreviewArchiveScreen() = WireTheme {
7978
ConversationsScreenContent(
8079
navigator = rememberNavigator {},
81-
searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")),
80+
searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")),
8281
conversationsSource = ConversationsSource.ARCHIVE,
8382
emptyListContent = { ArchiveEmptyContent() },
8483
conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er")),

β€Žapp/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.ktβ€Ž

Lines changed: 83 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class ConversationListViewModelPreview(
135135
class ConversationListViewModelImpl @AssistedInject constructor(
136136
@Assisted val conversationsSource: ConversationsSource,
137137
@Assisted private val usePagination: Boolean = BuildConfig.PAGINATED_CONVERSATION_LIST_ENABLED,
138-
dispatcher: DispatcherProvider,
138+
private val dispatcher: DispatcherProvider,
139139
private val updateConversationMutedStatus: UpdateConversationMutedStatusUseCase,
140140
private val getConversationsPaginated: GetConversationsFromSearchUseCase,
141141
private val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase,
@@ -171,6 +171,7 @@ class ConversationListViewModelImpl @AssistedInject constructor(
171171
override val closeBottomSheet = MutableSharedFlow<Unit>()
172172

173173
private val searchQueryFlow: MutableStateFlow<String> = MutableStateFlow("")
174+
private val isSelfUserUnderLegalHoldFlow = MutableSharedFlow<Boolean>(replay = 1)
174175

175176
private val containsNewActivitiesSection = when (conversationsSource) {
176177
ConversationsSource.MAIN,
@@ -185,39 +186,38 @@ class ConversationListViewModelImpl @AssistedInject constructor(
185186
private val conversationsPaginatedFlow: Flow<PagingData<ConversationFolderItem>> = searchQueryFlow
186187
.debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE }
187188
.onStart { emit("") }
189+
.combine(isSelfUserUnderLegalHoldFlow, ::Pair)
188190
.distinctUntilChanged()
189-
.flatMapLatest { searchQuery ->
191+
.flatMapLatest { (searchQuery, isSelfUserUnderLegalHold) ->
190192
getConversationsPaginated(
191193
searchQuery = searchQuery,
192194
fromArchive = conversationsSource == ConversationsSource.ARCHIVE,
193195
conversationFilter = conversationsSource.toFilter(),
194196
onlyInteractionEnabled = false,
195197
newActivitiesOnTop = containsNewActivitiesSection,
196-
).combine(observeLegalHoldStateForSelfUser()) { conversations, selfUserLegalHoldStatus ->
197-
conversations.map {
198-
it.hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus)
199-
}
200-
}.map {
201-
it.insertSeparators { before, after ->
202-
when {
203-
// do not add separators if the list shouldn't show conversations grouped into different folders
204-
!containsNewActivitiesSection -> null
205-
206-
before == null && after != null && after.hasNewActivitiesToShow ->
207-
// list starts with items with "new activities"
208-
ConversationFolder.Predefined.NewActivities
209-
210-
before == null && after != null && !after.hasNewActivitiesToShow ->
211-
// list doesn't contain any items with "new activities"
212-
ConversationFolder.Predefined.Conversations
213-
214-
before != null && before.hasNewActivitiesToShow && after != null && !after.hasNewActivitiesToShow ->
215-
// end of "new activities" section and beginning of "conversations" section
216-
ConversationFolder.Predefined.Conversations
217-
218-
else -> null
198+
).map { pagingData ->
199+
pagingData
200+
.map { it.hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold) }
201+
.insertSeparators { before, after ->
202+
when {
203+
// do not add separators if the list shouldn't show conversations grouped into different folders
204+
!containsNewActivitiesSection -> null
205+
206+
before == null && after != null && after.hasNewActivitiesToShow ->
207+
// list starts with items with "new activities"
208+
ConversationFolder.Predefined.NewActivities
209+
210+
before == null && after != null && !after.hasNewActivitiesToShow ->
211+
// list doesn't contain any items with "new activities"
212+
ConversationFolder.Predefined.Conversations
213+
214+
before != null && before.hasNewActivitiesToShow && after != null && !after.hasNewActivitiesToShow ->
215+
// end of "new activities" section and beginning of "conversations" section
216+
ConversationFolder.Predefined.Conversations
217+
218+
else -> null
219+
}
219220
}
220-
}
221221
}
222222
}
223223
.flowOn(dispatcher.io())
@@ -232,45 +232,59 @@ class ConversationListViewModelImpl @AssistedInject constructor(
232232
private set
233233

234234
init {
235+
observeSelfUserLegalHoldState()
235236
if (!usePagination) {
236-
viewModelScope.launch {
237-
searchQueryFlow
238-
.debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE }
239-
.onStart { emit("") }
240-
.distinctUntilChanged()
241-
.flatMapLatest { searchQuery: String ->
242-
observeConversationListDetailsWithEvents(
243-
fromArchive = conversationsSource == ConversationsSource.ARCHIVE,
244-
conversationFilter = conversationsSource.toFilter()
245-
).combine(observeLegalHoldStateForSelfUser()) { conversations, selfUserLegalHoldStatus ->
246-
conversations.map { conversationDetails ->
247-
conversationDetails.toConversationItem(
248-
userTypeMapper = userTypeMapper,
249-
searchQuery = searchQuery,
250-
selfUserTeamId = observeSelfUser().firstOrNull()?.teamId
251-
).hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus)
252-
} to searchQuery
253-
}
254-
}
255-
.map { (conversationItems, searchQuery) ->
256-
if (searchQuery.isEmpty()) {
257-
conversationItems.withFolders(source = conversationsSource).toImmutableMap()
258-
} else {
259-
searchConversation(
260-
conversationDetails = conversationItems,
261-
searchQuery = searchQuery
262-
).withFolders(source = conversationsSource).toImmutableMap()
263-
}
237+
observeNonPaginatedSearchConversationList()
238+
}
239+
}
240+
241+
private fun observeSelfUserLegalHoldState() {
242+
viewModelScope.launch {
243+
observeLegalHoldStateForSelfUser()
244+
.map { it is LegalHoldStateForSelfUser.Enabled }
245+
.flowOn(dispatcher.io())
246+
.collect { isSelfUserUnderLegalHoldFlow.emit(it) }
247+
}
248+
}
249+
250+
private fun observeNonPaginatedSearchConversationList() {
251+
viewModelScope.launch {
252+
searchQueryFlow
253+
.debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE }
254+
.onStart { emit("") }
255+
.distinctUntilChanged()
256+
.flatMapLatest { searchQuery: String ->
257+
observeConversationListDetailsWithEvents(
258+
fromArchive = conversationsSource == ConversationsSource.ARCHIVE,
259+
conversationFilter = conversationsSource.toFilter()
260+
).combine(isSelfUserUnderLegalHoldFlow) { conversations, isSelfUserUnderLegalHold ->
261+
conversations.map { conversationDetails ->
262+
conversationDetails.toConversationItem(
263+
userTypeMapper = userTypeMapper,
264+
searchQuery = searchQuery,
265+
selfUserTeamId = observeSelfUser().firstOrNull()?.teamId
266+
).hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold)
267+
} to searchQuery
264268
}
265-
.flowOn(dispatcher.io())
266-
.collect {
267-
conversationListState = ConversationListState.NotPaginated(
268-
isLoading = false,
269-
conversations = it,
270-
domain = currentAccount.domain
271-
)
269+
}
270+
.map { (conversationItems, searchQuery) ->
271+
if (searchQuery.isEmpty()) {
272+
conversationItems.withFolders(source = conversationsSource).toImmutableMap()
273+
} else {
274+
searchConversation(
275+
conversationDetails = conversationItems,
276+
searchQuery = searchQuery
277+
).withFolders(source = conversationsSource).toImmutableMap()
272278
}
273-
}
279+
}
280+
.flowOn(dispatcher.io())
281+
.collect {
282+
conversationListState = ConversationListState.NotPaginated(
283+
isLoading = false,
284+
conversations = it,
285+
domain = currentAccount.domain
286+
)
287+
}
274288
}
275289
}
276290

@@ -485,11 +499,13 @@ private fun ConversationsSource.toFilter(): ConversationFilter = when (this) {
485499
is ConversationsSource.FOLDER -> ConversationFilter.Folder(folderId = folderId, folderName = folderName)
486500
}
487501

488-
private fun ConversationItem.hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus: LegalHoldStateForSelfUser) =
489-
// if self user is under legal hold then we shouldn't show legal hold indicator next to every conversation
490-
// the indication is shown in the header of the conversation list for self user in that case and it's enough
491-
when (selfUserLegalHoldStatus) {
492-
is LegalHoldStateForSelfUser.Enabled -> when (this) {
502+
/**
503+
* If self user is under legal hold then we shouldn't show legal hold indicator next to every conversation as in that case
504+
* the legal hold indication is shown in the header of the conversation list for self user in that case and it's enough.
505+
*/
506+
private fun ConversationItem.hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold: Boolean) =
507+
when (isSelfUserUnderLegalHold) {
508+
true -> when (this) {
493509
is ConversationItem.ConnectionConversation -> this.copy(showLegalHoldIndicator = false)
494510
is ConversationItem.GroupConversation -> this.copy(showLegalHoldIndicator = false)
495511
is ConversationItem.PrivateConversation -> this.copy(showLegalHoldIndicator = false)

β€Žapp/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.ktβ€Ž

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ fun ConversationsScreenContent(
9090
lazyListState: LazyListState = rememberLazyListState(),
9191
loadingListContent: @Composable (LazyListState) -> Unit = { ConversationListLoadingContent(it) },
9292
conversationsSource: ConversationsSource = ConversationsSource.MAIN,
93-
initiallyLoaded: Boolean = LocalInspectionMode.current,
9493
conversationListViewModel: ConversationListViewModel = when {
9594
LocalInspectionMode.current -> ConversationListViewModelPreview()
9695
else -> hiltViewModel<ConversationListViewModelImpl, ConversationListViewModelImpl.Factory>(
@@ -191,10 +190,7 @@ fun ConversationsScreenContent(
191190
when (val state = conversationListViewModel.conversationListState) {
192191
is ConversationListState.Paginated -> {
193192
val lazyPagingItems = state.conversations.collectAsLazyPagingItems()
194-
var showLoading by remember(conversationsSource) { mutableStateOf(!initiallyLoaded) }
195-
if (lazyPagingItems.loadState.refresh != LoadState.Loading && showLoading) {
196-
showLoading = false
197-
}
193+
val showLoading = lazyPagingItems.loadState.refresh == LoadState.Loading && lazyPagingItems.itemCount == 0
198194

199195
when {
200196
// when conversation list is not yet fetched, show loading indicator

β€Žapp/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.ktβ€Ž

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import com.wire.android.ui.home.conversationslist.model.ConversationsSource
3434
import com.wire.android.ui.theme.WireTheme
3535
import com.wire.android.util.ui.PreviewMultipleThemes
3636
import com.wire.kalium.logic.data.conversation.ConversationFilter
37-
import kotlinx.coroutines.flow.flowOf
3837

3938
@HomeNavGraph(start = true)
4039
@WireDestination
@@ -70,7 +69,7 @@ fun PreviewAllConversationsEmptyScreen() = WireTheme {
7069
searchBarState = rememberSearchbarState(),
7170
conversationsSource = ConversationsSource.MAIN,
7271
emptyListContent = { ConversationsEmptyContent() },
73-
conversationListViewModel = ConversationListViewModelPreview(flowOf()),
72+
conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(list = listOf())),
7473
)
7574
}
7675

@@ -79,10 +78,10 @@ fun PreviewAllConversationsEmptyScreen() = WireTheme {
7978
fun PreviewAllConversationsEmptySearchScreen() = WireTheme {
8079
ConversationsScreenContent(
8180
navigator = rememberNavigator {},
82-
searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")),
81+
searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")),
8382
conversationsSource = ConversationsSource.MAIN,
8483
emptyListContent = { ConversationsEmptyContent() },
85-
conversationListViewModel = ConversationListViewModelPreview(flowOf()),
84+
conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er", list = listOf())),
8685
)
8786
}
8887

@@ -91,7 +90,7 @@ fun PreviewAllConversationsEmptySearchScreen() = WireTheme {
9190
fun PreviewAllConversationsSearchScreen() = WireTheme {
9291
ConversationsScreenContent(
9392
navigator = rememberNavigator {},
94-
searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")),
93+
searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")),
9594
conversationsSource = ConversationsSource.MAIN,
9695
emptyListContent = { ConversationsEmptyContent() },
9796
conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow("er")),

β€Žapp/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.ktβ€Ž

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import androidx.compose.runtime.snapshots.Snapshot
3030
import androidx.compose.ui.Modifier
3131
import androidx.compose.ui.platform.LocalContext
3232
import androidx.compose.ui.platform.LocalInspectionMode
33+
import androidx.paging.LoadState
34+
import androidx.paging.LoadStates
3335
import androidx.paging.PagingData
3436
import androidx.paging.compose.LazyPagingItems
3537
import androidx.paging.compose.collectAsLazyPagingItems
@@ -235,7 +237,16 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f
235237
fun previewConversationFoldersFlow(
236238
searchQuery: String = "",
237239
list: List<ConversationFolderItem> = previewConversationFolders(searchQuery = searchQuery)
238-
) = flowOf(PagingData.from(list))
240+
) = flowOf(
241+
PagingData.from(
242+
data = list,
243+
sourceLoadStates = LoadStates(
244+
prepend = LoadState.NotLoading(true),
245+
append = LoadState.NotLoading(true),
246+
refresh = LoadState.NotLoading(true),
247+
)
248+
)
249+
)
239250

240251
fun previewConversationFolders(withFolders: Boolean = true, searchQuery: String = "", unreadCount: Int = 3, readCount: Int = 6) =
241252
buildList {

0 commit comments

Comments
Β (0)