diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt index a2718e5719..b0840f9b02 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.feature.asset.UpdateAudioMessageNormalizedLoudnessU import com.wire.kalium.logic.feature.asset.upload.ScheduleNewAssetMessageUseCase import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase import com.wire.kalium.logic.feature.message.DeleteMessageUseCase +import com.wire.kalium.logic.feature.message.FetchOlderNomadMessagesByConversationUseCase import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase import com.wire.kalium.logic.feature.message.GetNotificationsUseCase import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesByConversationUseCase @@ -55,6 +56,7 @@ import com.wire.kalium.logic.feature.message.draft.GetMessageDraftUseCase import com.wire.kalium.logic.feature.message.draft.RemoveMessageDraftUseCase import com.wire.kalium.logic.feature.message.draft.SaveMessageDraftUseCase import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase +import com.wire.kalium.logic.feature.message.fetchOlderMessagesByConversationId import com.wire.kalium.logic.feature.message.getPaginatedFlowOfAssetMessageByConversationId import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesByConversation import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesBySearchQueryAndConversation @@ -177,6 +179,11 @@ class MessageModule { fun provideGetPaginatedMessagesUseCase(messageScope: MessageScope): GetPaginatedFlowOfMessagesByConversationUseCase = messageScope.getPaginatedFlowOfMessagesByConversation + @ViewModelScoped + @Provides + fun provideFetchOlderMessagesUseCase(messageScope: MessageScope): FetchOlderNomadMessagesByConversationUseCase = + messageScope.fetchOlderMessagesByConversationId + @ViewModelScoped @Provides fun provideGetImageAssetMessagesByConversationUseCase(messageScope: MessageScope): GetImageAssetMessagesForConversationUseCase = diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index b3987b3a38..8cb424fc12 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.conversations -import com.wire.android.navigation.annotation.app.WireRootDestination import android.annotation.SuppressLint import android.content.Context import android.net.Uri @@ -82,6 +81,14 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey +import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.GroupConversationDetailsScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.ImagesPreviewScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.MediaGalleryScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.MessageDetailsScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.SelfUserProfileScreenDestination +import com.ramcosta.composedestinations.generated.sketch.destinations.DrawingCanvasScreenDestination import com.ramcosta.composedestinations.result.NavResult.Canceled import com.ramcosta.composedestinations.result.NavResult.Value import com.ramcosta.composedestinations.result.OpenResultRecipient @@ -92,7 +99,6 @@ import com.wire.android.R import com.wire.android.appLogger import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.feature.analytics.model.AnalyticsEvent -import com.ramcosta.composedestinations.generated.sketch.destinations.DrawingCanvasScreenDestination import com.wire.android.feature.sketch.model.DrawingCanvasNavArgs import com.wire.android.feature.sketch.model.DrawingCanvasNavBackArgs import com.wire.android.mapper.MessageDateTimeGroup @@ -101,6 +107,7 @@ import com.wire.android.model.SnackBarMessage import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator +import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.ui.calling.getOutgoingCallIntent import com.wire.android.ui.calling.ongoing.getOngoingCallIntent import com.wire.android.ui.common.HandleActions @@ -128,13 +135,6 @@ import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.snackbar.SwipeableSnackbar import com.wire.android.ui.common.spacers.HorizontalSpace import com.wire.android.ui.common.visbility.rememberVisibilityState -import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.GroupConversationDetailsScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.ImagesPreviewScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.MediaGalleryScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.MessageDetailsScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.SelfUserProfileScreenDestination import com.wire.android.ui.emoji.EmojiPickerBottomSheet import com.wire.android.ui.home.conversations.AuthorHeaderHelper.rememberShouldHaveSmallBottomPadding import com.wire.android.ui.home.conversations.AuthorHeaderHelper.rememberShouldShowHeader @@ -182,10 +182,10 @@ import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography import com.wire.android.util.DateAndTimeParsers import com.wire.android.util.normalizeLink +import com.wire.android.util.openDownloadFolder import com.wire.android.util.serverDate import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText -import com.wire.android.util.openDownloadFolder import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode @@ -492,8 +492,8 @@ fun ConversationScreen( navigator.navigate(NavigationCommand(SelfUserProfileScreenDestination)) } else { (conversationInfoViewState.conversationDetailsData as? ConversationDetailsData.Group)?.conversationId.let { - navigator.navigate(NavigationCommand(OtherUserProfileScreenDestination(mentionUserId, it))) - } + navigator.navigate(NavigationCommand(OtherUserProfileScreenDestination(mentionUserId, it))) + } } } }, @@ -661,6 +661,9 @@ fun ConversationScreen( ) }, currentTimeInMillisFlow = conversationMessagesViewModel.currentTimeInMillisFlow, + onReachedOldestMessage = { + conversationMessagesViewModel.fetchOlderMessagesIfNeeded() + }, onAttachmentClick = messageAttachmentsViewModel::onAttachmentClicked, onAttachmentMenuClick = messageAttachmentsViewModel::onAttachmentMenuClicked, isWireCellsEnabled = conversationInfoViewModel.conversationInfoViewState.isWireCellEnabled, @@ -928,6 +931,7 @@ private fun ConversationScreen( onAttachmentClick: (AttachmentDraftUi) -> Unit, onAttachmentMenuClick: (AttachmentDraftUi) -> Unit, currentTimeInMillisFlow: Flow = flow { }, + onReachedOldestMessage: () -> Unit = {}, isWireCellsEnabled: Boolean = false, ) { val context = LocalContext.current @@ -1025,6 +1029,7 @@ private fun ConversationScreen( onLinkClick = onLinkClick, onNavigateToReplyOriginalMessage = onNavigateToReplyOriginalMessage, currentTimeInMillisFlow = currentTimeInMillisFlow, + onReachedOldestMessage = onReachedOldestMessage, openDrawingCanvas = openDrawingCanvas, onAttachmentClick = onAttachmentClick, onAttachmentMenuClick = onAttachmentMenuClick, @@ -1113,6 +1118,7 @@ private fun ConversationScreenContent( onAttachmentClick: (AttachmentDraftUi) -> Unit, onAttachmentMenuClick: (AttachmentDraftUi) -> Unit, currentTimeInMillisFlow: Flow = flow {}, + onReachedOldestMessage: () -> Unit = {}, showHistoryLoadingIndicator: Boolean = false, isBubbleUiEnabled: Boolean = false, isWireCellsEnabled: Boolean = false, @@ -1159,6 +1165,7 @@ private fun ConversationScreenContent( selectedMessageId = selectedMessageId, interactionAvailability = messageComposerStateHolder.messageComposerViewState.value.interactionAvailability, currentTimeInMillisFlow = currentTimeInMillisFlow, + onReachedOldestMessage = onReachedOldestMessage, showHistoryLoadingIndicator = showHistoryLoadingIndicator, isBubbleUiEnabled = isBubbleUiEnabled, isWireCellsEnabled = isWireCellsEnabled, @@ -1244,9 +1251,11 @@ fun MessageList( showHistoryLoadingIndicator: Boolean = false, isBubbleUiEnabled: Boolean = false, isWireCellsEnabled: Boolean = false, + onReachedOldestMessage: () -> Unit = {}, ) { val prevItemCount = remember { mutableStateOf(lazyPagingMessages.itemCount) } val readLastMessageAtStartTriggered = remember { mutableStateOf(false) } + val shouldTriggerOldestMessageFetch = remember { mutableStateOf(true) } val currentTime by currentTimeInMillisFlow.collectAsState(initial = System.currentTimeMillis()) LaunchedEffect(lazyPagingMessages.itemCount) { @@ -1282,6 +1291,18 @@ fun MessageList( } } + LaunchedEffect(lazyListState.isScrollInProgress, lazyPagingMessages.itemCount) { + if (!lazyListState.isScrollInProgress && lazyPagingMessages.itemCount > 0) { + val reachedOldest = !lazyListState.canScrollForward + if (reachedOldest && shouldTriggerOldestMessageFetch.value) { + onReachedOldestMessage() + shouldTriggerOldestMessageFetch.value = false // triggered only once, to avoid multiple calls for the same end of the list + } else if (!reachedOldest) { + shouldTriggerOldestMessageFetch.value = true + } + } + } + Box( contentAlignment = Alignment.BottomEnd, modifier = modifier diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt index 1f1d1eddc2..944ff37303 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.R import com.wire.android.appLogger import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer @@ -39,7 +40,6 @@ import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.model.UIQuotedMessage import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.startFileShareIntent @@ -62,6 +62,7 @@ import com.wire.kalium.logic.feature.conversation.ClearUsersTypingEventsUseCase import com.wire.kalium.logic.feature.conversation.GetConversationUnreadEventsCountUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.message.DeleteMessageUseCase +import com.wire.kalium.logic.feature.message.FetchOlderNomadMessagesByConversationUseCase import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePositionUseCase import com.wire.kalium.logic.feature.message.ToggleReactionUseCase @@ -100,6 +101,7 @@ class ConversationMessagesViewModel @Inject constructor( private val fileManager: FileManager, private val dispatchers: DispatcherProvider, private val getMessageForConversation: GetMessagesForConversationUseCase, + private val fetchOlderNomadMessages: FetchOlderNomadMessagesByConversationUseCase, private val toggleReaction: ToggleReactionUseCase, private val resetSession: ResetSessionUseCase, private val audioMessagePlayer: ConversationAudioMessagePlayer, @@ -220,6 +222,12 @@ class ConversationMessagesViewModel @Inject constructor( handleSelectedSearchedMessageHighlighting() } + fun fetchOlderMessagesIfNeeded() { + viewModelScope.launch { + fetchOlderNomadMessages(conversationId) + } + } + private suspend fun handleSelectedSearchedMessageHighlighting() { viewModelScope.launch { delay(3.seconds) @@ -431,6 +439,7 @@ class ConversationMessagesViewModel @Inject constructor( private companion object { const val DEFAULT_ASSET_NAME = "Wire File" const val CURRENT_TIME_REFRESH_WINDOW_IN_MILLIS: Long = 60_000 + const val REMOTE_PAGE_SIZE = 20 } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt index 5ba03e5f45..3e12dc8639 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.home.conversations.messages import androidx.lifecycle.SavedStateHandle import androidx.paging.PagingData +import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.media.audiomessage.AudioSpeed @@ -31,7 +32,6 @@ import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.FileManager import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.data.asset.AttachmentType @@ -51,6 +51,7 @@ import com.wire.kalium.logic.feature.conversation.ClearUsersTypingEventsUseCase import com.wire.kalium.logic.feature.conversation.GetConversationUnreadEventsCountUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.message.DeleteMessageUseCase +import com.wire.kalium.logic.feature.message.FetchOlderNomadMessagesByConversationUseCase import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePositionUseCase import com.wire.kalium.logic.feature.message.MessageOperationResult @@ -83,6 +84,9 @@ class ConversationMessagesViewModelArrangement { @MockK lateinit var getMessagesForConversationUseCase: GetMessagesForConversationUseCase + @MockK + lateinit var fetchOlderNomadMessagesByConversationUseCase: FetchOlderNomadMessagesByConversationUseCase + @MockK lateinit var getMessageById: GetMessageByIdUseCase @@ -136,6 +140,7 @@ class ConversationMessagesViewModelArrangement { fileManager, TestDispatcherProvider(), getMessagesForConversationUseCase, + fetchOlderNomadMessagesByConversationUseCase, toggleReaction, resetSession, conversationAudioMessagePlayer, @@ -155,6 +160,7 @@ class ConversationMessagesViewModelArrangement { coEvery { toggleReaction(any(), any(), any()) } returns ToggleReactionResult.Success coEvery { observeConversationDetails(any()) } returns flowOf() coEvery { getMessagesForConversationUseCase(any(), any()) } returns messagesChannel.consumeAsFlow() + coEvery { fetchOlderNomadMessagesByConversationUseCase(any(), any()) } returns Unit coEvery { getConversationUnreadEventsCount(any()) } returns GetConversationUnreadEventsCountUseCase.Result.Success(0L) coEvery { updateAssetMessageDownloadStatus(any(), any(), any()) } returns UpdateTransferStatusResult.Success coEvery { clearUsersTypingEvents() } returns Unit diff --git a/kalium b/kalium index 83f70d55a7..5237d17fc3 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 83f70d55a711418a993b8af63d58e438ba3281f3 +Subproject commit 5237d17fc3a9ac3027f22a7172f6af8205664509