Skip to content

Commit

Permalink
feat: Add pagination in searched messages result screen (WPB-5498) (#…
Browse files Browse the repository at this point in the history
…2489)

Co-authored-by: MohamadJaara <[email protected]>
  • Loading branch information
alexandreferris and MohamadJaara authored Dec 1, 2023
1 parent b51377f commit 5aa2802
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 191 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ dependencies {
testImplementation(libs.turbine)
testImplementation(libs.okio.fakeFileSystem)
testRuntimeOnly(libs.junit5.engine)
testImplementation(libs.androidx.paging.testing)

// Acceptance/Functional tests dependencies
androidTestImplementation(libs.androidx.test.runner)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase
import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase
import com.wire.kalium.logic.feature.asset.UpdateAssetMessageDownloadStatusUseCase
import com.wire.kalium.logic.feature.message.DeleteMessageUseCase
import com.wire.kalium.logic.feature.message.GetConversationMessagesFromSearchQueryUseCase
import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase
import com.wire.kalium.logic.feature.message.GetNotificationsUseCase
import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesByConversationUseCase
import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase
import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePositionUseCase
import com.wire.kalium.logic.feature.message.MarkMessagesAsNotifiedUseCase
import com.wire.kalium.logic.feature.message.MessageScope
Expand All @@ -45,6 +45,7 @@ import com.wire.kalium.logic.feature.message.ToggleReactionUseCase
import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase
import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase
import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesByConversation
import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesBySearchQueryAndConversation
import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
Expand Down Expand Up @@ -152,8 +153,10 @@ class MessageModule {

@ViewModelScoped
@Provides
fun provideGetConversationMessagesFromSearchQueryUseCase(messageScope: MessageScope): GetConversationMessagesFromSearchQueryUseCase =
messageScope.getConversationMessagesFromSearchQuery
fun provideGetPaginatedFlowOfMessagesBySearchQueryAndConversation(
messageScope: MessageScope
): GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase =
messageScope.getPaginatedFlowOfMessagesBySearchQueryAndConversation

@ViewModelScoped
@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class MessageMapper @Inject constructor(
private val userTypeMapper: UserTypeMapper,
private val messageContentMapper: MessageContentMapper,
private val isoFormatter: ISOFormatter,
// TODO(qol): a message mapper should not depend on a UI related component
private val wireSessionImageLoader: WireSessionImageLoader
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,36 @@
package com.wire.android.ui.home.conversations.search.messages

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.home.conversations.MessageItem
import com.wire.android.ui.home.conversations.info.ConversationDetailsData
import com.wire.android.ui.home.conversations.mock.mockMessageWithText
import com.wire.android.ui.home.conversations.model.UIMessage
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.ui.PreviewMultipleThemes
import kotlinx.coroutines.flow.flowOf

@Composable
fun SearchConversationMessagesResultsScreen(
searchResult: List<UIMessage>,
lazyPagingMessages: LazyPagingItems<UIMessage>,
searchQuery: String = "",
onMessageClick: (messageId: String) -> Unit
) {
LazyColumn {
items(searchResult) { message ->
items(
count = lazyPagingMessages.itemCount,
key = lazyPagingMessages.itemKey { it.header.messageId },
contentType = lazyPagingMessages.itemContentType { it }
) { index ->
val message: UIMessage = lazyPagingMessages[index]
?: return@items // We can draw a placeholder here, as we fetch the next page of messages

when (message) {
is UIMessage.Regular -> {
MessageItem(
Expand Down Expand Up @@ -70,10 +82,14 @@ fun SearchConversationMessagesResultsScreen(
fun previewSearchConversationMessagesResultsScreen() {
WireTheme {
SearchConversationMessagesResultsScreen(
searchResult = listOf(
mockMessageWithText,
mockMessageWithText,
),
lazyPagingMessages = flowOf(
PagingData.from(
listOf<UIMessage>(
mockMessageWithText.copy(header = mockMessageWithText.header.copy(messageId = "1")),
mockMessageWithText.copy(header = mockMessageWithText.header.copy(messageId = "2")),
)
)
).collectAsLazyPagingItems(),
onMessageClick = {}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.wire.android.R
Expand All @@ -35,6 +37,7 @@ import com.wire.android.ui.common.topappbar.search.SearchTopBar
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.home.conversations.ConversationNavArgs
import com.wire.android.ui.home.conversations.model.UIMessage
import kotlinx.coroutines.flow.Flow

@RootNavGraph
@Destination(
Expand Down Expand Up @@ -63,9 +66,7 @@ fun SearchConversationMessagesScreen(
content = {
SearchConversationMessagesResultContent(
searchQuery = searchQuery.text,
noneSearchSucceed = isEmptyResult,
searchResult = searchResult,
isLoading = isLoading,
onMessageClick = { messageId ->
navigator.navigate(
NavigationCommand(
Expand All @@ -91,22 +92,22 @@ fun SearchConversationMessagesScreen(
@Composable
fun SearchConversationMessagesResultContent(
searchQuery: String,
noneSearchSucceed: Boolean,
searchResult: List<UIMessage>,
isLoading: Boolean,
searchResult: Flow<PagingData<UIMessage>>,
onMessageClick: (messageId: String) -> Unit
) {
val lazyPagingMessages = searchResult.collectAsLazyPagingItems()

if (searchQuery.isEmpty()) {
SearchConversationMessagesEmptyScreen()
} else {
if (noneSearchSucceed && !isLoading) {
SearchConversationMessagesNoResultsScreen()
} else {
if (lazyPagingMessages.itemCount > 0) {
SearchConversationMessagesResultsScreen(
searchResult = searchResult,
lazyPagingMessages = lazyPagingMessages,
searchQuery = searchQuery,
onMessageClick = onMessageClick
)
} else {
SearchConversationMessagesNoResultsScreen()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
package com.wire.android.ui.home.conversations.search.messages

import androidx.compose.ui.text.input.TextFieldValue
import androidx.paging.PagingData
import com.wire.android.ui.home.conversations.model.UIMessage
import com.wire.kalium.logic.data.id.ConversationId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow

data class SearchConversationMessagesState(
val conversationId: ConversationId,
val searchQuery: TextFieldValue = TextFieldValue(""),
val searchResult: ImmutableList<UIMessage> = persistentListOf(),
val isEmptyResult: Boolean = false,
val searchResult: Flow<PagingData<UIMessage>> = emptyFlow(),
val isLoading: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@ import androidx.lifecycle.viewmodel.compose.saveable
import com.wire.android.ui.home.conversations.search.SearchPeopleViewModel
import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase
import com.wire.android.ui.navArgs
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.functional.onSuccess
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class SearchConversationMessagesViewModel @Inject constructor(
private val getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase,
private val dispatchers: DispatcherProvider,
savedStateHandle: SavedStateHandle
) : ViewModel() {

Expand All @@ -63,32 +65,35 @@ class SearchConversationMessagesViewModel @Inject constructor(
private val mutableSearchQueryFlow = MutableStateFlow(searchConversationMessagesState.searchQuery.text)

init {
viewModelScope.launch {
mutableSearchQueryFlow
.debounce(SearchPeopleViewModel.DEFAULT_SEARCH_QUERY_DEBOUNCE)
.collectLatest { searchTerm ->
val messagesResultFlow = mutableSearchQueryFlow
.onEach {
searchConversationMessagesState = searchConversationMessagesState.copy(
isLoading = true
)
}
.debounce(SearchPeopleViewModel.DEFAULT_SEARCH_QUERY_DEBOUNCE)
.flatMapConcat { searchTerm ->
getSearchMessagesForConversation(
searchTerm = searchTerm,
conversationId = conversationId,
lastReadIndex = 0
).onEach {
searchConversationMessagesState = searchConversationMessagesState.copy(
isLoading = true
isLoading = false
)

getSearchMessagesForConversation(
searchTerm = searchTerm,
conversationId = conversationId
).onSuccess { uiMessages ->
searchConversationMessagesState = searchConversationMessagesState.copy(
searchResult = uiMessages.toPersistentList(),
isEmptyResult = uiMessages.isEmpty(),
isLoading = false
)
}
}
}
}.flowOn(dispatchers.io())
}
searchConversationMessagesState = searchConversationMessagesState.copy(
searchResult = messagesResultFlow
)
}

fun searchQueryChanged(searchQuery: TextFieldValue) {
val textQueryChanged = searchConversationMessagesState.searchQuery.text != searchQuery.text
// we set the state with a searchQuery, immediately to update the UI first
searchConversationMessagesState = searchConversationMessagesState.copy(searchQuery = searchQuery)
searchConversationMessagesState = searchConversationMessagesState.copy(
searchQuery = searchQuery
)
if (textQueryChanged && searchQuery.text.isNotBlank()) {
viewModelScope.launch {
mutableSearchQueryFlow.emit(searchQuery.text.trim())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,72 @@
*/
package com.wire.android.ui.home.conversations.usecase

import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.flatMap
import com.wire.android.mapper.MessageMapper
import com.wire.android.ui.home.conversations.model.UIMessage
import com.wire.kalium.logic.CoreFailure
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase
import com.wire.kalium.logic.feature.message.GetConversationMessagesFromSearchQueryUseCase
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.map
import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject
import kotlin.math.max

class GetConversationMessagesFromSearchUseCase @Inject constructor(
private val getConversationMessagesFromSearch: GetConversationMessagesFromSearchQueryUseCase,
private val getMessagesSearch: GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase,
private val observeMemberDetailsByIds: ObserveUserListByIdUseCase,
private val messageMapper: MessageMapper
private val messageMapper: MessageMapper,
private val dispatchers: DispatcherProvider
) {

/**
* This operation combines messages searched from a conversation and its respective user to UI
* @param searchQuery The search term used to define which messages will be returned.
* @param searchTerm The search term used to define which messages will be returned.
* @param conversationId The conversation ID that it will look for messages in.
* @return A [Either<CoreFailure, List<UIMessage>>] indicating the success of the operation.
*/
suspend operator fun invoke(
searchTerm: String,
conversationId: ConversationId
): Either<CoreFailure, List<UIMessage>> =
conversationId: ConversationId,
lastReadIndex: Int
): Flow<PagingData<UIMessage>> {
val pagingConfig = PagingConfig(
pageSize = PAGE_SIZE,
prefetchDistance = PREFETCH_DISTANCE,
initialLoadSize = INITIAL_LOAD_SIZE
)

if (searchTerm.length >= MINIMUM_CHARACTERS_TO_SEARCH) {
getConversationMessagesFromSearch(
return getMessagesSearch(
searchQuery = searchTerm,
conversationId = conversationId
).map { foundMessages ->
foundMessages.flatMap { messageItem ->
observeMemberDetailsByIds(
userIdList = messageMapper.memberIdList(
messages = foundMessages
)
).map { usersList ->
messageMapper.toUIMessage(
userList = usersList,
message = messageItem
)?.let { listOf(it) } ?: emptyList()
}.first()
conversationId = conversationId,
pagingConfig = pagingConfig,
startingOffset = max(0, lastReadIndex - PREFETCH_DISTANCE)
).map { pagingData ->
pagingData.flatMap { messageItem ->
observeMemberDetailsByIds(messageMapper.memberIdList(listOf(messageItem)))
.mapLatest { usersList ->
messageMapper.toUIMessage(usersList, messageItem)?.let { listOf(it) }
?: emptyList()
}.first()
}
}
}.flowOn(dispatchers.io())
} else {
Either.Right(value = listOf())
return flowOf(PagingData.empty<UIMessage>()).flowOn(dispatchers.io())
}
}

private companion object {
const val MINIMUM_CHARACTERS_TO_SEARCH = 1
const val PAGE_SIZE = 20
const val INITIAL_LOAD_SIZE = 20
const val PREFETCH_DISTANCE = 30
}
}
Loading

0 comments on commit 5aa2802

Please sign in to comment.