Skip to content

Commit

Permalink
feat: conversation list pagination [WPB-9433] (#3509)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk authored Oct 16, 2024
1 parent d26ae20 commit 9984c00
Show file tree
Hide file tree
Showing 30 changed files with 1,076 additions and 732 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUs
import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationReceiptModeUseCase
import com.wire.kalium.logic.feature.conversation.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery
import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase
import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase
import com.wire.kalium.logic.feature.conversation.guestroomlink.ObserveGuestRoomLinkUseCase
Expand Down Expand Up @@ -306,4 +307,9 @@ class ConversationModule {
@Provides
fun provideGetConversationProtocolInfoUseCase(conversationScope: ConversationScope): GetConversationProtocolInfoUseCase =
conversationScope.getConversationProtocolInfo

@ViewModelScoped
@Provides
fun provideGetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(conversationScope: ConversationScope) =
conversationScope.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery
}
184 changes: 184 additions & 0 deletions app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program 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.
*
* This program 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 this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.mapper

import com.wire.android.model.ImageAsset.UserAvatarAsset
import com.wire.android.model.NameBasedAvatar
import com.wire.android.model.UserAvatarData
import com.wire.android.ui.home.conversations.model.UILastMessageContent
import com.wire.android.ui.home.conversationslist.model.BadgeEventType
import com.wire.android.ui.home.conversationslist.model.BlockState
import com.wire.android.ui.home.conversationslist.model.ConversationInfo
import com.wire.android.ui.home.conversationslist.model.ConversationItem
import com.wire.android.ui.home.conversationslist.showLegalHoldIndicator
import com.wire.android.util.ui.WireSessionImageLoader
import com.wire.kalium.logic.data.conversation.ConversationDetails.Connection
import com.wire.kalium.logic.data.conversation.ConversationDetails.Group
import com.wire.kalium.logic.data.conversation.ConversationDetails.OneOne
import com.wire.kalium.logic.data.conversation.ConversationDetails.Self
import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents
import com.wire.kalium.logic.data.conversation.MutedConversationStatus
import com.wire.kalium.logic.data.conversation.UnreadEventCount
import com.wire.kalium.logic.data.message.UnreadEventType
import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.UserAvailabilityStatus

@Suppress("LongMethod")
fun ConversationDetailsWithEvents.toConversationItem(
wireSessionImageLoader: WireSessionImageLoader,
userTypeMapper: UserTypeMapper,
searchQuery: String,
): ConversationItem = when (val conversationDetails = this.conversationDetails) {
is Group -> {
ConversationItem.GroupConversation(
groupName = conversationDetails.conversation.name.orEmpty(),
conversationId = conversationDetails.conversation.id,
mutedStatus = conversationDetails.conversation.mutedStatus,
isLegalHold = conversationDetails.conversation.legalHoldStatus.showLegalHoldIndicator(),
lastMessageContent = lastMessage.toUIPreview(unreadEventCount),
badgeEventType = parseConversationEventType(
mutedStatus = conversationDetails.conversation.mutedStatus,
unreadEventCount = unreadEventCount
),
hasOnGoingCall = conversationDetails.hasOngoingCall && conversationDetails.isSelfUserMember,
isSelfUserCreator = conversationDetails.isSelfUserCreator,
isSelfUserMember = conversationDetails.isSelfUserMember,
teamId = conversationDetails.conversation.teamId,
selfMemberRole = conversationDetails.selfRole,
isArchived = conversationDetails.conversation.archived,
mlsVerificationStatus = conversationDetails.conversation.mlsVerificationStatus,
proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus,
hasNewActivitiesToShow = hasNewActivitiesToShow,
searchQuery = searchQuery,
)
}

is OneOne -> {
ConversationItem.PrivateConversation(
userAvatarData = UserAvatarData(
asset = conversationDetails.otherUser.previewPicture?.let { UserAvatarAsset(wireSessionImageLoader, it) },
availabilityStatus = conversationDetails.otherUser.availabilityStatus,
connectionState = conversationDetails.otherUser.connectionStatus,
nameBasedAvatar = NameBasedAvatar(conversationDetails.otherUser.name, conversationDetails.otherUser.accentId)
),
conversationInfo = ConversationInfo(
name = conversationDetails.otherUser.name.orEmpty(),
membership = userTypeMapper.toMembership(conversationDetails.userType),
isSenderUnavailable = conversationDetails.otherUser.isUnavailableUser
),
conversationId = conversationDetails.conversation.id,
mutedStatus = conversationDetails.conversation.mutedStatus,
isLegalHold = conversationDetails.conversation.legalHoldStatus.showLegalHoldIndicator(),
lastMessageContent = lastMessage.toUIPreview(unreadEventCount),
badgeEventType = parsePrivateConversationEventType(
conversationDetails.otherUser.connectionStatus,
conversationDetails.otherUser.deleted,
parseConversationEventType(
mutedStatus = conversationDetails.conversation.mutedStatus,
unreadEventCount = unreadEventCount
)
),
userId = conversationDetails.otherUser.id,
blockingState = conversationDetails.otherUser.BlockState,
teamId = conversationDetails.otherUser.teamId,
isArchived = conversationDetails.conversation.archived,
mlsVerificationStatus = conversationDetails.conversation.mlsVerificationStatus,
proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus,
hasNewActivitiesToShow = hasNewActivitiesToShow,
searchQuery = searchQuery,
)
}

is Connection -> {
ConversationItem.ConnectionConversation(
userAvatarData = UserAvatarData(
asset = conversationDetails.otherUser?.previewPicture?.let { UserAvatarAsset(wireSessionImageLoader, it) },
availabilityStatus = conversationDetails.otherUser?.availabilityStatus ?: UserAvailabilityStatus.NONE,
nameBasedAvatar = NameBasedAvatar(conversationDetails.otherUser?.name, conversationDetails.otherUser?.accentId ?: -1)
),
conversationInfo = ConversationInfo(
name = conversationDetails.otherUser?.name.orEmpty(),
membership = userTypeMapper.toMembership(conversationDetails.userType),
isSenderUnavailable = conversationDetails.otherUser?.isUnavailableUser ?: true
),
lastMessageContent = UILastMessageContent.Connection(
connectionState = conversationDetails.connection.status,
userId = conversationDetails.connection.qualifiedToId
),
badgeEventType = parseConnectionEventType(conversationDetails.connection.status),
conversationId = conversationDetails.conversation.id,
mutedStatus = conversationDetails.conversation.mutedStatus,
hasNewActivitiesToShow = hasNewActivitiesToShow,
searchQuery = searchQuery,
)
}

is Self -> {
throw IllegalArgumentException("Self conversations should not be visible to the user.")
}

else -> {
throw IllegalArgumentException("$this conversations should not be visible to the user.")
}
}

private fun parseConnectionEventType(connectionState: ConnectionState) =
if (connectionState == ConnectionState.SENT) {
BadgeEventType.SentConnectRequest
} else {
BadgeEventType.ReceivedConnectionRequest
}

private fun parsePrivateConversationEventType(
connectionState: ConnectionState,
isDeleted: Boolean,
eventType: BadgeEventType
) =
if (connectionState == ConnectionState.BLOCKED) {
BadgeEventType.Blocked
} else if (isDeleted) {
BadgeEventType.Deleted
} else {
eventType
}

private fun parseConversationEventType(
mutedStatus: MutedConversationStatus,
unreadEventCount: UnreadEventCount
): BadgeEventType = when (mutedStatus) {
MutedConversationStatus.AllMuted -> BadgeEventType.None
MutedConversationStatus.OnlyMentionsAndRepliesAllowed ->
when {
unreadEventCount.containsKey(UnreadEventType.MENTION) -> BadgeEventType.UnreadMention
unreadEventCount.containsKey(UnreadEventType.REPLY) -> BadgeEventType.UnreadReply
unreadEventCount.containsKey(UnreadEventType.MISSED_CALL) -> BadgeEventType.MissedCall
else -> BadgeEventType.None
}

else -> {
val unreadMessagesCount = unreadEventCount.values.sum()
when {
unreadEventCount.containsKey(UnreadEventType.KNOCK) -> BadgeEventType.Knock
unreadEventCount.containsKey(UnreadEventType.MISSED_CALL) -> BadgeEventType.MissedCall
unreadEventCount.containsKey(UnreadEventType.MENTION) -> BadgeEventType.UnreadMention
unreadEventCount.containsKey(UnreadEventType.REPLY) -> BadgeEventType.UnreadReply
unreadMessagesCount > 0 -> BadgeEventType.UnreadMessage(unreadMessagesCount)
else -> BadgeEventType.None
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import com.wire.android.ui.common.topappbar.search.rememberSearchbarState
import com.wire.android.ui.home.HomeStateHolder
import com.wire.android.ui.home.conversationslist.ConversationListViewModelPreview
import com.wire.android.ui.home.conversationslist.ConversationsScreenContent
import com.wire.android.ui.home.conversationslist.common.previewConversationFolders
import com.wire.android.ui.home.conversationslist.common.previewConversationFoldersFlow
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.collections.immutable.persistentMapOf
import kotlinx.coroutines.flow.flowOf

@HomeNavGraph
@WireDestination
Expand All @@ -56,7 +56,7 @@ fun PreviewArchiveEmptyScreen() = WireTheme {
searchBarState = rememberSearchbarState(),
conversationsSource = ConversationsSource.ARCHIVE,
emptyListContent = { ArchiveEmptyContent() },
conversationListViewModel = ConversationListViewModelPreview(persistentMapOf()),
conversationListViewModel = ConversationListViewModelPreview(flowOf()),
)
}

Expand All @@ -68,7 +68,7 @@ fun PreviewArchiveEmptySearchScreen() = WireTheme {
searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")),
conversationsSource = ConversationsSource.ARCHIVE,
emptyListContent = { ArchiveEmptyContent() },
conversationListViewModel = ConversationListViewModelPreview(persistentMapOf(), "er"),
conversationListViewModel = ConversationListViewModelPreview(flowOf()),
)
}

Expand All @@ -80,6 +80,6 @@ fun PreviewArchiveScreen() = WireTheme {
searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")),
conversationsSource = ConversationsSource.ARCHIVE,
emptyListContent = { ArchiveEmptyContent() },
conversationListViewModel = ConversationListViewModelPreview(previewConversationFolders(), "er"),
conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er")),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -51,7 +52,7 @@ import com.wire.android.ui.home.newconversation.model.Contact
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.extension.FolderType
import com.wire.android.util.extension.folderWithElements
import com.wire.android.util.ui.KeepOnTopWhenNotScrolled
import com.wire.android.util.ui.keepOnTopWhenNotScrolled
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.UserId
Expand Down Expand Up @@ -184,7 +185,9 @@ private fun SearchResult(
}
}

KeepOnTopWhenNotScrolled(lazyListState)
SideEffect {
keepOnTopWhenNotScrolled(lazyListState)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program 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.
*
* This program 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 this program. If not, see http://www.gnu.org/licenses/.
*/

package com.wire.android.ui.home.conversations.usecase

import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.map
import com.wire.android.mapper.UserTypeMapper
import com.wire.android.mapper.toConversationItem
import com.wire.android.ui.home.conversationslist.model.ConversationItem
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.WireSessionImageLoader
import com.wire.kalium.logic.data.conversation.ConversationQueryConfig
import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class GetConversationsFromSearchUseCase @Inject constructor(
private val useCase: GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase,
private val wireSessionImageLoader: WireSessionImageLoader,
private val userTypeMapper: UserTypeMapper,
private val dispatchers: DispatcherProvider,
) {
suspend operator fun invoke(
searchQuery: String = "",
fromArchive: Boolean = false,
newActivitiesOnTop: Boolean = false,
onlyInteractionEnabled: Boolean = false,
): Flow<PagingData<ConversationItem>> {
val pagingConfig = PagingConfig(
pageSize = PAGE_SIZE,
prefetchDistance = PREFETCH_DISTANCE,
initialLoadSize = INITIAL_LOAD_SIZE,
enablePlaceholders = true,
)
return useCase(
queryConfig = ConversationQueryConfig(
searchQuery = searchQuery,
fromArchive = fromArchive,
newActivitiesOnTop = newActivitiesOnTop,
onlyInteractionEnabled = onlyInteractionEnabled
),
pagingConfig = pagingConfig,
startingOffset = 0L,
).map { pagingData ->
pagingData.map {
it.toConversationItem(wireSessionImageLoader, userTypeMapper, searchQuery)
}
}.flowOn(dispatchers.io())
}

private companion object {
const val PAGE_SIZE = 20
const val INITIAL_LOAD_SIZE = 60
const val PREFETCH_DISTANCE = 5
}
}
Loading

0 comments on commit 9984c00

Please sign in to comment.