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 f60d1179c00..88b613385f7 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 @@ -21,8 +21,9 @@ import com.wire.android.di.CurrentAccount import com.wire.android.di.KaliumCoreLogic import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.asset.GetAssetMessagesForConversationUseCase +import com.wire.kalium.logic.feature.asset.GetImageAssetMessagesForConversationUseCase import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import com.wire.kalium.logic.feature.asset.GetPaginatedFlowOfAssetMessageByConversationIdUseCase import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase import com.wire.kalium.logic.feature.asset.UpdateAssetMessageDownloadStatusUseCase import com.wire.kalium.logic.feature.message.DeleteMessageUseCase @@ -42,6 +43,7 @@ import com.wire.kalium.logic.feature.message.SendTextMessageUseCase 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.getPaginatedFlowOfAssetMessageByConversationId import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesByConversation import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesBySearchQueryAndConversation import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase @@ -154,8 +156,15 @@ class MessageModule { @ViewModelScoped @Provides - fun provideGetAssetMessagesUseCase(messageScope: MessageScope): GetAssetMessagesForConversationUseCase = - messageScope.getAssetMessagesByConversation + fun provideGetImageAssetMessagesByConversationUseCase(messageScope: MessageScope): GetImageAssetMessagesForConversationUseCase = + messageScope.getImageAssetMessagesByConversation + + @ViewModelScoped + @Provides + fun provideGetPaginatedFlowOfAssetMessageByConversationId( + messageScope: MessageScope + ): GetPaginatedFlowOfAssetMessageByConversationIdUseCase = + messageScope.getPaginatedFlowOfAssetMessageByConversationId @ViewModelScoped @Provides diff --git a/app/src/main/kotlin/com/wire/android/ui/common/Extensions.kt b/app/src/main/kotlin/com/wire/android/ui/common/Extensions.kt index d4df9a98b35..b4cbc61010a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/Extensions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/Extensions.kt @@ -50,11 +50,20 @@ import com.google.accompanist.placeholder.shimmer import com.wire.android.R import com.wire.android.model.ClickBlockParams import com.wire.android.model.Clickable +import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.LocalSyncStateObserver +import com.wire.kalium.logic.data.message.Message import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import java.time.format.TextStyle +import java.util.Locale import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -145,3 +154,28 @@ fun Flow.collectAsStateLifecycleAware( fun StateFlow.collectAsStateLifecycleAware( context: CoroutineContext = EmptyCoroutineContext ): State = collectAsStateLifecycleAware(value, context) + +fun monthYearHeader(month: Int, year: Int): String { + val currentYear = Instant.fromEpochMilliseconds(System.currentTimeMillis()).toLocalDateTime( + TimeZone.currentSystemDefault()).year + val monthYearInstant = LocalDateTime(year = year, monthNumber = month, 1, 0, 0, 0) + + val monthName = monthYearInstant.month.getDisplayName(TextStyle.FULL_STANDALONE, Locale.getDefault()) + return if (year == currentYear) { + // If it's the current year, display only the month name + monthName + } else { + // If it's not the current year, display both the month name and the year + "$monthName $year" + } +} + +fun List.toImageAssetGroupedByMonthAndYear(timeZone: TimeZone) = this.groupBy { asset -> + val localDateTime = asset.time.toLocalDateTime(timeZone) + monthYearHeader(year = localDateTime.year, month = localDateTime.monthNumber) +} + +fun List.toGenericAssetGroupedByMonthAndYear(timeZone: TimeZone) = this.groupBy { message -> + val localDateTime = message.date.toInstant().toLocalDateTime(timeZone) + monthYearHeader(year = localDateTime.year, month = localDateTime.monthNumber) +} 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 a48e60191a1..2e65ed2c5bf 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 @@ -775,7 +775,7 @@ private fun ConversationScreenContent( } @Composable -private fun SnackBarMessage( +fun SnackBarMessage( composerMessages: SharedFlow, conversationMessages: SharedFlow ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt index a74f09767ea..cccda75d043 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt @@ -26,11 +26,12 @@ import androidx.lifecycle.viewModelScope import com.wire.android.mapper.UIAssetMapper import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.usecase.GetAssetMessagesFromConversationUseCase 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.data.message.Message -import com.wire.kalium.logic.feature.asset.GetAssetMessagesForConversationUseCase +import com.wire.kalium.logic.feature.asset.GetImageAssetMessagesForConversationUseCase import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.MessageAssetResult import dagger.hilt.android.lifecycle.HiltViewModel @@ -44,7 +45,8 @@ import javax.inject.Inject class ConversationAssetMessagesViewModel @Inject constructor( override val savedStateHandle: SavedStateHandle, private val dispatchers: DispatcherProvider, - private val getAssets: GetAssetMessagesForConversationUseCase, + private val getImageMessages: GetImageAssetMessagesForConversationUseCase, + private val getAssetMessages: GetAssetMessagesFromConversationUseCase, private val getPrivateAsset: GetMessageAssetUseCase, private val assetMapper: UIAssetMapper, ) : SavedStateViewModel(savedStateHandle) { @@ -60,21 +62,33 @@ class ConversationAssetMessagesViewModel @Inject constructor( private var currentOffset: Int = 0 init { + loadImages() loadAssets() } + private fun loadAssets() = viewModelScope.launch { + val assetsResult = getAssetMessages.invoke( + conversationId = conversationId, + initialOffset = 0 + ) + + viewState = viewState.copy( + assetMessages = assetsResult + ) + } + fun continueLoading(shouldContinue: Boolean) { if (shouldContinue) { if (!continueLoading) { continueLoading = true - loadAssets() + loadImages() } } else { continueLoading = false } } - private fun loadAssets() = viewModelScope.launch { + private fun loadImages() = viewModelScope.launch { if (isLoading) { return@launch } @@ -82,7 +96,7 @@ class ConversationAssetMessagesViewModel @Inject constructor( try { while (continueLoading) { val uiAssetList = withContext(dispatchers.io()) { - getAssets.invoke( + getImageMessages.invoke( conversationId = conversationId, limit = BATCH_SIZE, offset = currentOffset @@ -90,7 +104,7 @@ class ConversationAssetMessagesViewModel @Inject constructor( } // imitate loading new asset batch - viewState = viewState.copy(messages = viewState.messages.plus(uiAssetList.map { + viewState = viewState.copy(imageMessages = viewState.imageMessages.plus(uiAssetList.map { it.copy( downloadStatus = if (it.assetPath == null && it.downloadStatus != Message.DownloadStatus.FAILED_DOWNLOAD) { Message.DownloadStatus.DOWNLOAD_IN_PROGRESS @@ -117,7 +131,7 @@ class ConversationAssetMessagesViewModel @Inject constructor( currentOffset += BATCH_SIZE viewState = viewState.copy( - messages = viewState.messages.dropLast(uiMessages.size).plus(uiMessages).toImmutableList(), + imageMessages = viewState.imageMessages.dropLast(uiMessages.size).plus(uiMessages).toImmutableList(), ) } else { continueLoading = false diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewState.kt index 6dc6832f9c4..eb8fef595e4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewState.kt @@ -21,11 +21,16 @@ package com.wire.android.ui.home.conversations.media import androidx.compose.runtime.Stable +import androidx.paging.PagingData import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage +import com.wire.android.ui.home.conversations.usecase.UIPagingItem import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow @Stable data class ConversationAssetMessagesViewState( - val messages: ImmutableList = persistentListOf() + val imageMessages: ImmutableList = persistentListOf(), + val assetMessages: Flow> = emptyFlow() ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt index a9c6c57de4c..d0d495a445e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt @@ -44,6 +44,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R +import com.wire.android.media.audiomessage.AudioState import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation @@ -56,7 +57,10 @@ import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.destinations.MediaGalleryScreenDestination -import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage +import com.wire.android.ui.home.conversations.DownloadedAssetDialog +import com.wire.android.ui.home.conversations.MessageComposerViewModel +import com.wire.android.ui.home.conversations.SnackBarMessage +import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewModel import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.ui.PreviewMultipleThemes @@ -69,9 +73,13 @@ import kotlinx.coroutines.launch style = PopUpNavigationAnimation::class ) @Composable -fun ConversationMediaScreen(navigator: Navigator) { - val viewModel: ConversationAssetMessagesViewModel = hiltViewModel() - val state: ConversationAssetMessagesViewState = viewModel.viewState +fun ConversationMediaScreen( + navigator: Navigator, + conversationAssetMessagesViewModel: ConversationAssetMessagesViewModel = hiltViewModel(), + conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel(), + messageComposerViewModel: MessageComposerViewModel = hiltViewModel() +) { + val state: ConversationAssetMessagesViewState = conversationAssetMessagesViewModel.viewState Content( state = state, @@ -89,8 +97,23 @@ fun ConversationMediaScreen(navigator: Navigator) { ) }, continueAssetLoading = { shouldContinue -> - viewModel.continueLoading(shouldContinue) - } + conversationAssetMessagesViewModel.continueLoading(shouldContinue) + }, + onAssetItemClicked = conversationMessagesViewModel::downloadOrFetchAssetAndShowDialog, + audioMessagesState = conversationMessagesViewModel.conversationViewState.audioMessagesState, + onAudioItemClicked = conversationMessagesViewModel::audioClick, + ) + + DownloadedAssetDialog( + downloadedAssetDialogState = conversationMessagesViewModel.conversationViewState.downloadedAssetDialogState, + onSaveFileToExternalStorage = conversationMessagesViewModel::downloadAssetExternally, + onOpenFileWithExternalApp = conversationMessagesViewModel::downloadAndOpenAsset, + hideOnAssetDownloadedDialog = conversationMessagesViewModel::hideOnAssetDownloadedDialog + ) + + SnackBarMessage( + messageComposerViewModel.infoMessage, + conversationMessagesViewModel.infoMessage ) } @@ -100,7 +123,10 @@ private fun Content( state: ConversationAssetMessagesViewState, onNavigationPressed: () -> Unit = {}, onImageFullScreenMode: (conversationId: ConversationId, messageId: String, isSelfAsset: Boolean) -> Unit, - continueAssetLoading: (shouldContinue: Boolean) -> Unit + continueAssetLoading: (shouldContinue: Boolean) -> Unit, + audioMessagesState: Map = emptyMap(), + onAudioItemClicked: (String) -> Unit, + onAssetItemClicked: (String) -> Unit ) { val scope = rememberCoroutineScope() val lazyListStates: List = ConversationMediaScreenTabItem.entries.map { rememberLazyListState() } @@ -139,12 +165,17 @@ private fun Content( .padding(padding) ) { pageIndex -> when (ConversationMediaScreenTabItem.entries[pageIndex]) { - ConversationMediaScreenTabItem.PICTURES -> PicturesContent( - uiAssetMessageList = state.messages, + ConversationMediaScreenTabItem.PICTURES -> ImageAssetsContent( + groupedImageMessageList = state.imageMessages, onImageFullScreenMode = onImageFullScreenMode, continueAssetLoading = continueAssetLoading ) - ConversationMediaScreenTabItem.FILES -> FilesContent() + ConversationMediaScreenTabItem.FILES -> FileAssetsContent( + groupedAssetMessageList = state.assetMessages, + audioMessagesState = audioMessagesState, + onAudioItemClicked = onAudioItemClicked, + onAssetItemClicked = onAssetItemClicked + ) } } @@ -157,32 +188,6 @@ private fun Content( } } -@Composable -private fun PicturesContent( - uiAssetMessageList: List, - onImageFullScreenMode: (conversationId: ConversationId, messageId: String, isSelfAsset: Boolean) -> Unit, - continueAssetLoading: (shouldContinue: Boolean) -> Unit -) { - if (uiAssetMessageList.isEmpty()) { - EmptyMediaContentScreen( - text = stringResource(R.string.label_conversation_pictures_empty) - ) - } else { - AssetGrid( - uiAssetMessageList = uiAssetMessageList, - onImageFullScreenMode = onImageFullScreenMode, - continueAssetLoading = continueAssetLoading - ) - } -} - -@Composable -private fun FilesContent() { - EmptyMediaContentScreen( - text = stringResource(R.string.label_conversation_files_empty) - ) -} - enum class ConversationMediaScreenTabItem(@StringRes override val titleResId: Int) : TabItem { PICTURES(R.string.label_conversation_pictures), FILES(R.string.label_conversation_files); @@ -194,8 +199,10 @@ fun previewConversationMediaScreenEmptyContent() { WireTheme { Content( state = ConversationAssetMessagesViewState(), - onImageFullScreenMode = {_, _, _ -> }, - continueAssetLoading = {} + onImageFullScreenMode = { _, _, _ -> }, + continueAssetLoading = { }, + onAudioItemClicked = { }, + onAssetItemClicked = { } ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt new file mode 100644 index 00000000000..c2163d31333 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt @@ -0,0 +1,135 @@ +/* + * Wire + * Copyright (C) 2023 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.media + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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.R +import com.wire.android.media.audiomessage.AudioState +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.home.conversations.MessageItem +import com.wire.android.ui.home.conversations.info.ConversationDetailsData +import com.wire.android.ui.home.conversations.model.UIMessage +import com.wire.android.ui.home.conversations.usecase.UIPagingItem +import com.wire.android.ui.home.conversationslist.common.FolderHeader +import com.wire.android.ui.theme.wireColorScheme +import kotlinx.coroutines.flow.Flow + +@Composable +fun FileAssetsContent( + groupedAssetMessageList: Flow>, + audioMessagesState: Map = emptyMap(), + onAudioItemClicked: (String) -> Unit, + onAssetItemClicked: (String) -> Unit +) { + val lazyPagingMessages = groupedAssetMessageList.collectAsLazyPagingItems() + + if (lazyPagingMessages.itemCount > 0) { + AssetMessagesListContent( + groupedAssetMessageList = lazyPagingMessages, + audioMessagesState = audioMessagesState, + onAudioItemClicked = onAudioItemClicked, + onAssetItemClicked = onAssetItemClicked + ) + } else { + EmptyMediaContentScreen( + text = stringResource(R.string.label_conversation_files_empty) + ) + } +} + +@Composable +private fun AssetMessagesListContent( + groupedAssetMessageList: LazyPagingItems, + audioMessagesState: Map, + onAudioItemClicked: (String) -> Unit, + onAssetItemClicked: (String) -> Unit, +) { + LazyColumn { + items( + count = groupedAssetMessageList.itemCount, + key = groupedAssetMessageList.itemKey { + when (it) { + is UIPagingItem.Label -> it.date + is UIPagingItem.Message -> it.uiMessage.header.messageId + } + }, + contentType = groupedAssetMessageList.itemContentType { it } + ) { index -> + val uiPagingItem: UIPagingItem = groupedAssetMessageList[index] ?: return@items + + when (uiPagingItem) { + is UIPagingItem.Label -> Box( + modifier = Modifier + .fillMaxWidth() + .padding( + bottom = dimensions().spacing6x, + // first label should not have top padding + top = if (index == 0) dimensions().spacing0x else dimensions().spacing6x, + ) + ) { + FolderHeader( + name = uiPagingItem.date.uppercase(), + modifier = Modifier + .background(MaterialTheme.wireColorScheme.background) + .fillMaxWidth() + ) + } + is UIPagingItem.Message -> { + when (val message = uiPagingItem.uiMessage) { + is UIMessage.Regular -> { + MessageItem( + message = message, + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = audioMessagesState, + onAudioClick = onAudioItemClicked, + onChangeAudioPosition = { _, _ -> }, + onLongClicked = { }, + onAssetMessageClicked = onAssetItemClicked, + onImageMessageClicked = { _, _ -> }, + onOpenProfile = { _ -> }, + onReactionClicked = { _, _ -> }, + onResetSessionClicked = { _, _ -> }, + onSelfDeletingMessageRead = { }, + defaultBackgroundColor = colorsScheme().backgroundVariant, + shouldDisplayMessageStatus = false, + shouldDisplayFooter = false, + onLinkClick = { } + ) + } + + is UIMessage.System -> {} + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ImageAssetsContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ImageAssetsContent.kt index b52dd465f04..526a4685b8b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ImageAssetsContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ImageAssetsContent.kt @@ -36,9 +36,12 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.toImageAssetGroupedByMonthAndYear import com.wire.android.ui.home.conversations.model.MediaAssetImage import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage import com.wire.android.ui.home.conversationslist.common.FolderHeader @@ -51,21 +54,38 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.Message import com.wire.kalium.util.map.forEachIndexed import kotlinx.datetime.Instant -import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import java.time.format.TextStyle -import java.util.Locale @Composable -fun AssetGrid( +fun ImageAssetsContent( + groupedImageMessageList: List, + onImageFullScreenMode: (conversationId: ConversationId, messageId: String, isSelfAsset: Boolean) -> Unit, + continueAssetLoading: (shouldContinue: Boolean) -> Unit +) { + if (groupedImageMessageList.isEmpty()) { + EmptyMediaContentScreen( + text = stringResource(R.string.label_conversation_pictures_empty) + ) + } else { + ImageAssetGrid( + uiAssetMessageList = groupedImageMessageList, + onImageFullScreenMode = onImageFullScreenMode, + continueAssetLoading = continueAssetLoading + ) + } +} + +@Composable +private fun ImageAssetGrid( uiAssetMessageList: List, modifier: Modifier = Modifier, onImageFullScreenMode: (conversationId: ConversationId, messageId: String, isSelfAsset: Boolean) -> Unit, continueAssetLoading: (shouldContinue: Boolean) -> Unit ) { val timeZone = remember { TimeZone.currentSystemDefault() } - val groupedAssets = remember(uiAssetMessageList) { groupAssetsByMonthYear(uiAssetMessageList, timeZone) } + val groupedAssets: Map> = remember(uiAssetMessageList) { + uiAssetMessageList.toImageAssetGroupedByMonthAndYear(timeZone = timeZone) + } val scrollState = rememberLazyGridState() val shouldContinue by remember { @@ -150,27 +170,6 @@ fun AssetGrid( } } -fun monthYearHeader(month: Int, year: Int): String { - val currentYear = Instant.fromEpochMilliseconds(System.currentTimeMillis()).toLocalDateTime(TimeZone.currentSystemDefault()).year - val monthYearInstant = LocalDateTime(year = year, monthNumber = month, 1, 0, 0, 0) - - val monthName = monthYearInstant.month.getDisplayName(TextStyle.FULL_STANDALONE, Locale.getDefault()) - return if (year == currentYear) { - // If it's the current year, display only the month name - monthName - } else { - // If it's not the current year, display both the month name and the year - "$monthName $year" - } -} - -fun groupAssetsByMonthYear(uiAssetMessageList: List, timeZone: TimeZone): Map> { - return uiAssetMessageList.groupBy { asset -> - val localDateTime = asset.time.toLocalDateTime(timeZone) - monthYearHeader(year = localDateTime.year, month = localDateTime.monthNumber) - } -} - private const val COLUMN_COUNT = 4 @PreviewMultipleThemes @@ -197,14 +196,14 @@ fun previewAssetGrid() { downloadStatus = Message.DownloadStatus.DOWNLOAD_IN_PROGRESS, ) WireTheme { - AssetGrid( + ImageAssetGrid( uiAssetMessageList = listOf( message1, message2, message3 ), - onImageFullScreenMode = {_, _, _ -> }, - continueAssetLoading = {} + onImageFullScreenMode = { _, _, _ -> }, + continueAssetLoading = { } ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt new file mode 100644 index 00000000000..0ea4b8e29b4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt @@ -0,0 +1,115 @@ +/* + * Wire + * Copyright (C) 2023 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.flatMap +import androidx.paging.insertSeparators +import com.wire.android.mapper.MessageMapper +import com.wire.android.ui.common.monthYearHeader +import com.wire.android.ui.home.conversations.model.UIMessage +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.asset.GetPaginatedFlowOfAssetMessageByConversationIdUseCase +import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import javax.inject.Inject +import kotlin.math.max + +class GetAssetMessagesFromConversationUseCase @Inject constructor( + private val getAssetMessages: GetPaginatedFlowOfAssetMessageByConversationIdUseCase, + private val observeMemberDetailsByIds: ObserveUserListByIdUseCase, + private val messageMapper: MessageMapper, + private val dispatchers: DispatcherProvider +) { + + /** + * This operation combines asset messages from a conversation and its respective user to UI + * @param conversationId The conversation ID that it will look for asset messages in. + * + * @return A [PagingData>] indicating the success of the operation. + */ + suspend operator fun invoke( + conversationId: ConversationId, + initialOffset: Int + ): Flow> { + val pagingConfig = PagingConfig( + pageSize = PAGE_SIZE, + prefetchDistance = PREFETCH_DISTANCE, + initialLoadSize = INITIAL_LOAD_SIZE + ) + + return getAssetMessages( + conversationId = conversationId, + startingOffset = max(0, initialOffset - PREFETCH_DISTANCE).toLong(), + pagingConfig = pagingConfig + ).map { pagingData -> + val currentTime = TimeZone.currentSystemDefault() + val uiMessagePagingData: PagingData = pagingData.flatMap { messageItem -> + observeMemberDetailsByIds(messageMapper.memberIdList(listOf(messageItem))) + .mapLatest { usersList -> + messageMapper.toUIMessage(usersList, messageItem) + ?.let { listOf(UIPagingItem.Message(it, Instant.parse(messageItem.date))) } + ?: emptyList() + }.first() + }.insertSeparators { before: UIPagingItem.Message?, after: UIPagingItem.Message? -> + if (before == null && after != null) { + val localDateTime = after.date.toLocalDateTime(currentTime) + UIPagingItem.Label(monthYearHeader(year = localDateTime.year, month = localDateTime.monthNumber)) + } else if (before != null && after != null) { + val beforeDateTime = before.date.toLocalDateTime(currentTime) + val afterDateTime = after.date.toLocalDateTime(currentTime) + + if (beforeDateTime.year != afterDateTime.year + || beforeDateTime.month != afterDateTime.month + ) { + UIPagingItem.Label(monthYearHeader(year = afterDateTime.year, month = afterDateTime.monthNumber)) + } else { + null + } + } else { + // no separator - either end of list, or first + // letters of items are the same + null + } + } + uiMessagePagingData + }.flowOn(dispatchers.io()) + } + + private companion object { + const val PAGE_SIZE = 20 + const val INITIAL_LOAD_SIZE = 20 + const val PREFETCH_DISTANCE = 30 + } +} + +sealed class UIPagingItem { + + data class Message(val uiMessage: UIMessage, val date: Instant) : UIPagingItem() + + data class Label(val date: String) : UIPagingItem() +} diff --git a/kalium b/kalium index b7d2a81dd1c..54912c9c7f6 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b7d2a81dd1c3f8053d2f44507b2ae55553b81b0e +Subproject commit 54912c9c7f68f979113e4c56b85f680989816f2e