From fc6e59e1d60001250cc30c460a42a5e17030cbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Mon, 4 Dec 2023 12:32:26 +0100 Subject: [PATCH] feat: media gallery [WPB-4989] (#2490) --- .../android/di/accountScoped/MessageModule.kt | 12 +- .../mapper/RegularMessageContentMapper.kt | 2 +- .../com/wire/android/mapper/UIAssetMapper.kt | 41 +++++ .../details/GroupConversationDetailsScreen.kt | 23 ++- ...roupConversationDetailsTopBarCollapsing.kt | 9 +- .../details/SearchAndMediaRow.kt | 45 +++++ .../ConversationAssetMessagesViewModel.kt | 134 ++++++++++++++ .../ConversationAssetMessagesViewState.kt | 31 ++++ .../media/ConversationMediaButton.kt | 52 ++++++ .../media/ConversationMediaNavArgs.kt | 24 +++ .../media/ConversationMediaScreen.kt | 99 ++++++++++ .../conversations/media/ImageAssetsContent.kt | 169 ++++++++++++++++++ .../home/conversations/model/MessageTypes.kt | 90 +++++++++- .../messagetypes/asset/UIAssetMessage.kt | 37 ++++ .../messagetypes/image/ImageMessageTypes.kt | 101 +++++++---- .../SearchConversationMessagesButton.kt | 59 +++--- .../ui/userprofile/common/UserProfileInfo.kt | 10 +- .../other/OtherUserProfileScreen.kt | 24 ++- app/src/main/res/values/strings.xml | 5 + kalium | 2 +- 20 files changed, 871 insertions(+), 98 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/mapper/UIAssetMapper.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/SearchAndMediaRow.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewState.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaButton.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaNavArgs.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ImageAssetsContent.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/UIAssetMessage.kt 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 5a2a9f28a30..f60d1179c00 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 @@ -19,11 +19,9 @@ package com.wire.android.di.accountScoped import com.wire.android.di.CurrentAccount import com.wire.android.di.KaliumCoreLogic -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn 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.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase import com.wire.kalium.logic.feature.asset.UpdateAssetMessageDownloadStatusUseCase @@ -47,6 +45,9 @@ import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletio 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.Module +import dagger.Provides +import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.scopes.ViewModelScoped @@ -151,6 +152,11 @@ class MessageModule { fun provideGetPaginatedMessagesUseCase(messageScope: MessageScope): GetPaginatedFlowOfMessagesByConversationUseCase = messageScope.getPaginatedFlowOfMessagesByConversation + @ViewModelScoped + @Provides + fun provideGetAssetMessagesUseCase(messageScope: MessageScope): GetAssetMessagesForConversationUseCase = + messageScope.getAssetMessagesByConversation + @ViewModelScoped @Provides fun provideGetPaginatedFlowOfMessagesBySearchQueryAndConversation( diff --git a/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt index ef6043ccb7e..39ab4ca1334 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt @@ -59,7 +59,7 @@ class RegularMessageMapper @Inject constructor( message: Message.Regular, sender: User?, userList: List - ) = when (val content = message.content) { + ): UIMessageContent = when (val content = message.content) { is Asset -> { when (val metadata = content.value.metadata) { is AssetContent.AssetMetadata.Audio -> { diff --git a/app/src/main/kotlin/com/wire/android/mapper/UIAssetMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/UIAssetMapper.kt new file mode 100644 index 00000000000..c5d761e5a6c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/mapper/UIAssetMapper.kt @@ -0,0 +1,41 @@ +/* + * 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.mapper + +import com.wire.android.R +import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage +import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.data.asset.AssetMessage +import javax.inject.Inject + +class UIAssetMapper @Inject constructor() { + + fun toUIAsset(assetMessage: AssetMessage): UIAssetMessage { + return UIAssetMessage( + assetId = assetMessage.assetId, + time = assetMessage.time, + username = assetMessage.username?.let { UIText.DynamicString(it) } + ?: UIText.StringResource(R.string.username_unavailable_label), + conversationId = assetMessage.conversationId, + messageId = assetMessage.messageId, + assetPath = assetMessage.assetPath, + downloadStatus = assetMessage.downloadStatus, + isSelfAsset = assetMessage.isSelfAsset + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt index 06d801293f1..daba07b43f7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt @@ -86,6 +86,7 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.WireTopAppBarTitle import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.AddMembersSearchScreenDestination +import com.wire.android.ui.destinations.ConversationMediaScreenDestination import com.wire.android.ui.destinations.EditConversationNameScreenDestination import com.wire.android.ui.destinations.EditGuestAccessScreenDestination import com.wire.android.ui.destinations.EditSelfDeletingMessagesScreenDestination @@ -142,6 +143,16 @@ fun GroupConversationDetailsScreen( ) } + val onConversationMediaClick: () -> Unit = { + navigator.navigate( + NavigationCommand( + ConversationMediaScreenDestination( + conversationId = viewModel.conversationId + ) + ) + ) + } + GroupConversationDetailsContent( conversationSheetContent = viewModel.conversationSheetContent, bottomSheetEventsHandler = viewModel, @@ -222,7 +233,8 @@ fun GroupConversationDetailsScreen( navigator.navigate(NavigationCommand(EditConversationNameScreenDestination(viewModel.conversationId))) }, isLoading = viewModel.requestInProgress, - onSearchConversationMessagesClick = onSearchConversationMessagesClick + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + onConversationMediaClick = onConversationMediaClick ) val tryAgainSnackBarMessage = stringResource(id = R.string.error_unknown_message) @@ -263,7 +275,8 @@ private fun GroupConversationDetailsContent( onDeleteGroup: (GroupDialogState) -> Unit, groupParticipantsState: GroupConversationParticipantsState, isLoading: Boolean, - onSearchConversationMessagesClick: () -> Unit + onSearchConversationMessagesClick: () -> Unit, + onConversationMediaClick: () -> Unit ) { val scope = rememberCoroutineScope() val resources = LocalContext.current.resources @@ -332,7 +345,8 @@ private fun GroupConversationDetailsContent( conversationId = it.conversationId, totalParticipants = groupParticipantsState.data.allCount, isLoading = isLoading, - onSearchConversationMessagesClick = onSearchConversationMessagesClick + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + onConversationMediaClick = onConversationMediaClick ) } WireTabRow( @@ -539,7 +553,8 @@ fun PreviewGroupConversationDetails() { onEditGroupName = {}, onEditSelfDeletingMessages = {}, onEditGuestAccess = {}, - onSearchConversationMessagesClick = {} + onSearchConversationMessagesClick = {}, + onConversationMediaClick = {} ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt index d2cf6f777da..f85ffebcfe3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt @@ -38,7 +38,7 @@ import com.wire.android.R import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.conversationColor import com.wire.android.ui.common.dimensions -import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton +import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.home.conversationslist.common.GroupConversationAvatar import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography @@ -52,6 +52,7 @@ fun GroupConversationDetailsTopBarCollapsing( totalParticipants: Int, isLoading: Boolean, onSearchConversationMessagesClick: () -> Unit, + onConversationMediaClick: () -> Unit, modifier: Modifier = Modifier ) { Column( @@ -123,8 +124,10 @@ fun GroupConversationDetailsTopBarCollapsing( } } - SearchConversationMessagesButton( - onSearchConversationMessagesClick = onSearchConversationMessagesClick + VerticalSpace.x24() + SearchAndMediaRow( + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + onConversationMediaClick = onConversationMediaClick ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/SearchAndMediaRow.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/SearchAndMediaRow.kt new file mode 100644 index 00000000000..eae894282b1 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/SearchAndMediaRow.kt @@ -0,0 +1,45 @@ +/* + * 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.details + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.spacers.HorizontalSpace +import com.wire.android.ui.home.conversations.media.ConversationMediaButton +import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton + +@Composable +fun SearchAndMediaRow( + onSearchConversationMessagesClick: () -> Unit, + onConversationMediaClick: () -> Unit +) { + Row(modifier = Modifier.padding(horizontal = dimensions().spacing16x)) { + SearchConversationMessagesButton( + modifier = Modifier.weight(1F), + onClick = onSearchConversationMessagesClick + ) + HorizontalSpace.x8() + ConversationMediaButton( + modifier = Modifier.weight(1F), + onClick = onConversationMediaClick + ) + } +} 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 new file mode 100644 index 00000000000..a74f09767ea --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt @@ -0,0 +1,134 @@ +/* + * 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.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +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.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.GetMessageAssetUseCase +import com.wire.kalium.logic.feature.asset.MessageAssetResult +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +@Suppress("LongParameterList", "TooManyFunctions") +class ConversationAssetMessagesViewModel @Inject constructor( + override val savedStateHandle: SavedStateHandle, + private val dispatchers: DispatcherProvider, + private val getAssets: GetAssetMessagesForConversationUseCase, + private val getPrivateAsset: GetMessageAssetUseCase, + private val assetMapper: UIAssetMapper, +) : SavedStateViewModel(savedStateHandle) { + + private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() + val conversationId: QualifiedID = conversationNavArgs.conversationId + + var viewState by mutableStateOf(ConversationAssetMessagesViewState()) + private set + + private var continueLoading = true + private var isLoading = false + private var currentOffset: Int = 0 + + init { + loadAssets() + } + + fun continueLoading(shouldContinue: Boolean) { + if (shouldContinue) { + if (!continueLoading) { + continueLoading = true + loadAssets() + } + } else { + continueLoading = false + } + } + + private fun loadAssets() = viewModelScope.launch { + if (isLoading) { + return@launch + } + isLoading = true + try { + while (continueLoading) { + val uiAssetList = withContext(dispatchers.io()) { + getAssets.invoke( + conversationId = conversationId, + limit = BATCH_SIZE, + offset = currentOffset + ).map(assetMapper::toUIAsset) + } + + // imitate loading new asset batch + viewState = viewState.copy(messages = viewState.messages.plus(uiAssetList.map { + it.copy( + downloadStatus = if (it.assetPath == null && it.downloadStatus != Message.DownloadStatus.FAILED_DOWNLOAD) { + Message.DownloadStatus.DOWNLOAD_IN_PROGRESS + } else { + it.downloadStatus + } + ) + }).toImmutableList()) + + if (uiAssetList.size >= BATCH_SIZE) { + val uiMessages = uiAssetList.map { uiAsset -> + if (uiAsset.assetPath == null) { + val assetPath = withContext(dispatchers.io()) { + when (val asset = getPrivateAsset.invoke(uiAsset.conversationId, uiAsset.messageId).await()) { + is MessageAssetResult.Failure -> null + is MessageAssetResult.Success -> asset.decodedAssetPath + } + } + uiAsset.copy(assetPath = assetPath) + } else { + uiAsset + } + } + currentOffset += BATCH_SIZE + + viewState = viewState.copy( + messages = viewState.messages.dropLast(uiMessages.size).plus(uiMessages).toImmutableList(), + ) + } else { + continueLoading = false + } + } + } finally { + isLoading = false + } + } + + companion object { + const val BATCH_SIZE = 5 + } +} 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 new file mode 100644 index 00000000000..6dc6832f9c4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewState.kt @@ -0,0 +1,31 @@ +/* + * 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.runtime.Stable +import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Stable +data class ConversationAssetMessagesViewState( + val messages: ImmutableList = persistentListOf() +) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaButton.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaButton.kt new file mode 100644 index 00000000000..5763e3ce6b0 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaButton.kt @@ -0,0 +1,52 @@ +/* + * 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.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.wireDimensions + +@Composable +fun ConversationMediaButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + WireSecondaryButton( + modifier = modifier, + text = stringResource(R.string.label_conversation_media), + onClick = onClick, + minSize = MaterialTheme.wireDimensions.buttonMinSize, + fillMaxWidth = true, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_gallery), + contentDescription = stringResource(R.string.label_conversation_media), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(end = dimensions().spacing8x) + ) + } + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaNavArgs.kt new file mode 100644 index 00000000000..5e2a4405ea1 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaNavArgs.kt @@ -0,0 +1,24 @@ +/* + * 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 com.wire.kalium.logic.data.id.ConversationId + +data class ConversationMediaNavArgs( + val conversationId: ConversationId +) 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 new file mode 100644 index 00000000000..eab4f1e8a67 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt @@ -0,0 +1,99 @@ +/* + * 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.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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.navigation.NavigationCommand +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.scaffold.WireScaffold +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.kalium.logic.data.id.ConversationId + +@RootNavGraph +@Destination( + navArgsDelegate = ConversationMediaNavArgs::class, + style = PopUpNavigationAnimation::class +) +@Composable +fun ConversationMediaScreen(navigator: Navigator) { + val viewModel: ConversationAssetMessagesViewModel = hiltViewModel() + val state: ConversationAssetMessagesViewState = viewModel.viewState + + Content( + state = state, + onNavigationPressed = { navigator.navigateBack() }, + onImageFullScreenMode = { conversationId, messageId, isSelfAsset -> + navigator.navigate( + NavigationCommand( + MediaGalleryScreenDestination( + conversationId = conversationId, + messageId = messageId, + isSelfAsset = isSelfAsset, + isEphemeral = false + ) + ) + ) + }, + continueAssetLoading = { shouldContinue -> + viewModel.continueLoading(shouldContinue) + } + ) +} + +@Composable +private fun Content( + state: ConversationAssetMessagesViewState, + onNavigationPressed: () -> Unit = {}, + onImageFullScreenMode: (conversationId: ConversationId, messageId: String, isSelfAsset: Boolean) -> Unit, + continueAssetLoading: (shouldContinue: Boolean) -> Unit +) { + WireScaffold( + modifier = Modifier + .background(color = colorsScheme().backgroundVariant), + topBar = { + WireCenterAlignedTopAppBar( + elevation = dimensions().spacing0x, + title = stringResource(id = R.string.label_conversation_media), + navigationIconType = NavigationIconType.Back, + onNavigationPressed = onNavigationPressed + ) + }, + ) { padding -> + // TODO implement tab here for https://wearezeta.atlassian.net/browse/WPB-5378 + AssetGrid( + uiAssetMessageList = state.messages, + modifier = Modifier.padding(padding), + onImageFullScreenMode = onImageFullScreenMode, + continueAssetLoading = continueAssetLoading + ) + } +} 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 new file mode 100644 index 00000000000..ed8fe782bbe --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ImageAssetsContent.kt @@ -0,0 +1,169 @@ +/* + * 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.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +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.home.conversations.model.MediaAssetImage +import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage +import com.wire.android.ui.home.conversationslist.common.FolderHeader +import com.wire.android.ui.theme.wireColorScheme +import com.wire.kalium.logic.data.id.ConversationId +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( + 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 scrollState = rememberLazyGridState() + val shouldContinue by remember { + derivedStateOf { + !scrollState.canScrollForward + } + } + + // act when end of list reached + LaunchedEffect(shouldContinue) { + continueAssetLoading(shouldContinue) + } + + BoxWithConstraints( + modifier + .fillMaxSize() + .background(color = colorsScheme().backgroundVariant) + ) { + val screenWidth = maxWidth + val horizontalPadding = dimensions().spacing12x + val itemSpacing = dimensions().spacing2x * 2 + val totalItemSpacing = itemSpacing * COLUMN_COUNT + val availableWidth = screenWidth - horizontalPadding - totalItemSpacing + val itemSize = availableWidth / COLUMN_COUNT + + LazyVerticalGrid( + columns = GridCells.Fixed(COLUMN_COUNT), + state = scrollState, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + groupedAssets.forEachIndexed { index, entry -> + val label = entry.key + item( + key = entry.key, + span = { GridItemSpan(COLUMN_COUNT) }) { + 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 = label.uppercase(), + modifier = Modifier + .background(MaterialTheme.wireColorScheme.background) + .fillMaxWidth() + ) + } + } + + items( + count = entry.value.size, + key = { entry.value[it].assetId } + ) { + val uiAsset = entry.value[it] + val currentOnImageClick = remember(uiAsset) { + Clickable(enabled = true, onClick = { + onImageFullScreenMode( + uiAsset.conversationId, uiAsset.messageId, uiAsset.isSelfAsset + ) + }) + } + Box( + modifier = Modifier + .padding(all = dimensions().spacing2x) + ) { + MediaAssetImage( + asset = null, + width = itemSize, + height = itemSize, + downloadStatus = uiAsset.downloadStatus, + onImageClick = currentOnImageClick, + assetPath = uiAsset.assetPath + ) + } + } + } + } + } +} + +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 diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt index b1be7d16978..f883037f43a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt @@ -38,16 +38,18 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Dp import com.wire.android.di.hiltViewModelScoped import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.clickable import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversations.CompositeMessageViewModel import com.wire.android.ui.home.conversations.CompositeMessageViewModelImpl import com.wire.android.ui.home.conversations.model.messagetypes.asset.MessageAsset +import com.wire.android.ui.home.conversations.model.messagetypes.image.AsyncImageMessage import com.wire.android.ui.home.conversations.model.messagetypes.image.DisplayableImageMessage import com.wire.android.ui.home.conversations.model.messagetypes.image.ImageMessageFailed import com.wire.android.ui.home.conversations.model.messagetypes.image.ImageMessageInProgress @@ -64,8 +66,10 @@ import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.Message.DownloadStatus.DOWNLOAD_IN_PROGRESS import com.wire.kalium.logic.data.message.Message.DownloadStatus.FAILED_DOWNLOAD +import com.wire.kalium.logic.data.message.Message.DownloadStatus.NOT_FOUND import com.wire.kalium.logic.data.message.Message.UploadStatus.FAILED_UPLOAD import com.wire.kalium.logic.data.message.Message.UploadStatus.UPLOAD_IN_PROGRESS +import okio.Path import org.commonmark.Extension import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension import org.commonmark.ext.gfm.tables.TablesExtension @@ -183,7 +187,7 @@ fun MessageImage( color = MaterialTheme.wireColorScheme.onPrimary, shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) ) .border( - width = 1.dp, + width = dimensions().spacing1x, color = MaterialTheme.wireColorScheme.secondaryButtonDisabledOutline, shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) ) @@ -197,17 +201,93 @@ fun MessageImage( when { // Trying to upload the asset uploadStatus == UPLOAD_IN_PROGRESS || downloadStatus == DOWNLOAD_IN_PROGRESS -> { - ImageMessageInProgress(imgParams, downloadStatus == DOWNLOAD_IN_PROGRESS) + ImageMessageInProgress( + imgParams.normalizedWidth, imgParams.normalizedHeight, + downloadStatus == DOWNLOAD_IN_PROGRESS + ) + } + + downloadStatus == NOT_FOUND -> { + ImageMessageFailed( + imgParams.normalizedWidth, imgParams.normalizedHeight, + true + ) } asset != null -> { if (isImportedMediaAsset) ImportedImageMessage(asset, shouldFillMaxWidth) - else DisplayableImageMessage(asset, imgParams) + else DisplayableImageMessage(asset, imgParams.normalizedWidth, imgParams.normalizedHeight) } // Show error placeholder uploadStatus == FAILED_UPLOAD || downloadStatus == FAILED_DOWNLOAD -> { - ImageMessageFailed(imgParams, downloadStatus == FAILED_DOWNLOAD) + ImageMessageFailed( + imgParams.normalizedWidth, imgParams.normalizedHeight, + downloadStatus == FAILED_DOWNLOAD + ) + } + } + } +} + +@Composable +fun MediaAssetImage( + asset: ImageAsset?, + width: Dp, + height: Dp, + downloadStatus: Message.DownloadStatus, + assetPath: Path? = null, + onImageClick: Clickable +) { + Box( + Modifier + .padding(top = MaterialTheme.wireDimensions.spacing2x) + .clip(shape = RoundedCornerShape(dimensions().messageAssetBorderRadius)) + .background( + color = MaterialTheme.wireColorScheme.onPrimary, shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) + ) + .border( + width = dimensions().spacing1x, + color = MaterialTheme.wireColorScheme.secondaryButtonDisabledOutline, + shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) + ) + .wrapContentSize() + .clickable(onImageClick) + ) { + when { + // Trying to upload the asset + downloadStatus == DOWNLOAD_IN_PROGRESS -> { + ImageMessageInProgress( + width = width, + height = height, + isDownloading = true, + showText = false + ) + } + + assetPath != null -> { + AsyncImageMessage(assetPath, width, height) + } + + asset != null -> { + DisplayableImageMessage(asset, width, height) + } + + // Show error placeholder + downloadStatus == FAILED_DOWNLOAD -> { + ImageMessageFailed( + width = width, + height = height, + isDownloadFailure = true + ) + } + + downloadStatus == NOT_FOUND -> { + ImageMessageFailed( + width = width, + height = height, + isDownloadFailure = true + ) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/UIAssetMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/UIAssetMessage.kt new file mode 100644 index 00000000000..d4e02c90e8f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/UIAssetMessage.kt @@ -0,0 +1,37 @@ +/* + * 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.model.messagetypes.asset + +import androidx.compose.runtime.Stable +import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.message.Message +import kotlinx.datetime.Instant +import okio.Path + +@Stable +data class UIAssetMessage( + val assetId: String, + val time: Instant, + val username: UIText, + val messageId: String, + val conversationId: QualifiedID, + val assetPath: Path?, + val downloadStatus: Message.DownloadStatus, + val isSelfAsset: Boolean +) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/image/ImageMessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/image/ImageMessageTypes.kt index 1b46d5a5f88..a3597fe59f8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/image/ImageMessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/image/ImageMessageTypes.kt @@ -29,17 +29,19 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import coil.compose.SubcomposeAsyncImage import com.wire.android.R import com.wire.android.model.ImageAsset import com.wire.android.ui.common.dimensions @@ -47,23 +49,52 @@ import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import okio.Path @Composable fun DisplayableImageMessage( imageData: ImageAsset, - imgParams: ImageMessageParams, + width: Dp, + height: Dp, modifier: Modifier = Modifier ) { Image( painter = imageData.paint(), contentDescription = stringResource(R.string.content_description_image_message), modifier = modifier - .width(imgParams.normalizedWidth) - .height(imgParams.normalizedHeight), + .width(width) + .height(height), alignment = Alignment.Center, contentScale = ContentScale.Crop ) } + +@Composable +fun AsyncImageMessage( + assetPath: Path, + width: Dp, + height: Dp, + modifier: Modifier = Modifier +) { + SubcomposeAsyncImage( + assetPath.toFile(), + contentDescription = stringResource(R.string.content_description_image_message), + modifier = modifier + .width(width) + .height(height), + loading = { _ -> + WireCircularProgressIndicator( + progressColor = MaterialTheme.wireColorScheme.primary, + modifier = Modifier.align( + Alignment.Center + ).padding(dimensions().spacing24x) + ) + }, + alignment = Alignment.Center, + contentScale = ContentScale.Crop + ) +} + @Composable fun ImportedImageMessage( imageData: ImageAsset, @@ -85,66 +116,64 @@ fun ImportedImageMessage( } @Composable -fun ImageMessageInProgress(imgParams: ImageMessageParams, isDownloading: Boolean) { +fun ImageMessageInProgress(width: Dp, height: Dp, isDownloading: Boolean, showText: Boolean = true) { Box { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .align(Alignment.Center) - .width(imgParams.normalizedWidth) - .height(imgParams.normalizedHeight) + .width(width) + .height(height) .padding(MaterialTheme.wireDimensions.spacing8x) ) { WireCircularProgressIndicator( progressColor = MaterialTheme.wireColorScheme.primary, size = MaterialTheme.wireDimensions.spacing24x ) - Spacer(modifier = Modifier.height(MaterialTheme.wireDimensions.spacing8x)) - Text( - text = stringResource( - id = if (isDownloading) R.string.asset_message_download_in_progress_text - else R.string.asset_message_upload_in_progress_text - ), - style = MaterialTheme.wireTypography.subline01.copy(color = MaterialTheme.wireColorScheme.secondaryText), - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) + if (showText) { + Spacer(modifier = Modifier.height(MaterialTheme.wireDimensions.spacing8x)) + Text( + text = stringResource( + id = if (isDownloading) R.string.asset_message_download_in_progress_text + else R.string.asset_message_upload_in_progress_text + ), + style = MaterialTheme.wireTypography.subline01.copy(color = MaterialTheme.wireColorScheme.secondaryText), + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } } } } @Composable -fun ImageMessageFailed(imgParams: ImageMessageParams, isDownloadFailure: Boolean) { +fun ImageMessageFailed(width: Dp, height: Dp, isDownloadFailure: Boolean) { Box { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .align(Alignment.Center) - .width(imgParams.normalizedWidth) - .height(imgParams.normalizedHeight) + .width(width) + .height(height) .padding(MaterialTheme.wireDimensions.spacing8x) ) { - Image( + Icon( painter = painterResource(id = R.drawable.ic_gallery), contentDescription = null, + tint = MaterialTheme.colorScheme.error, modifier = Modifier - .width(dimensions().spacing24x) - .height(dimensions().spacing24x), - alignment = Alignment.CenterStart, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), - contentScale = ContentScale.Crop ) - Spacer(modifier = Modifier.height(MaterialTheme.wireDimensions.spacing8x)) - Text( - text = stringResource( - id = if (isDownloadFailure) R.string.error_downloading_image_message - else R.string.error_uploading_image_message - ), - textAlign = TextAlign.Center, - style = MaterialTheme.wireTypography.subline01.copy(color = MaterialTheme.wireColorScheme.error) - ) - } + Spacer(modifier = Modifier.height(MaterialTheme.wireDimensions.spacing8x)) + Text( + text = stringResource( + id = if (isDownloadFailure) R.string.error_downloading_image_message + else R.string.error_uploading_image_message + ), + textAlign = TextAlign.Center, + style = MaterialTheme.wireTypography.subline01.copy(color = MaterialTheme.wireColorScheme.error) + ) + } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt index b58750a2512..18cf16f3b9e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt @@ -17,70 +17,51 @@ */ package com.wire.android.ui.home.conversations.search.messages -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.DpSize import com.wire.android.R import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.debug.LocalFeatureVisibilityFlags @Composable fun SearchConversationMessagesButton( - onSearchConversationMessagesClick: () -> Unit + modifier: Modifier = Modifier, + onClick: () -> Unit ) { val localFeatureVisibilityFlags = LocalFeatureVisibilityFlags.current if (localFeatureVisibilityFlags.SearchConversationMessages) { SearchConversationMessagesButtonContent( - onSearchConversationMessagesClick = onSearchConversationMessagesClick + onClick = onClick, + modifier = modifier ) } } @Composable private fun SearchConversationMessagesButtonContent( - onSearchConversationMessagesClick: () -> Unit + modifier: Modifier = Modifier, + onClick: () -> Unit ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .wrapContentSize() - .fillMaxWidth() - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding( - top = dimensions().spacing24x, - start = dimensions().spacing16x, - end = dimensions().spacing16x, - ) - .fillMaxWidth() - ) { - WireSecondaryButton( - text = stringResource(R.string.label_search_button), - onClick = onSearchConversationMessagesClick, - minSize = DpSize(dimensions().spacing0x, dimensions().spacing48x), - fillMaxWidth = true, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_search), - contentDescription = stringResource(R.string.label_search_messages), - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(end = dimensions().spacing8x) - ) - } + WireSecondaryButton( + modifier = modifier, + text = stringResource(R.string.label_search_button), + onClick = onClick, + minSize = MaterialTheme.wireDimensions.buttonMinSize, + fillMaxWidth = true, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = stringResource(R.string.label_search_messages), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(end = dimensions().spacing8x) ) } - } + ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index c45204efa01..7ebf3334a79 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -62,7 +62,8 @@ import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.banner.SecurityClassificationBannerForUser import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator -import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton +import com.wire.android.ui.common.spacers.VerticalSpace +import com.wire.android.ui.home.conversations.details.SearchAndMediaRow import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography @@ -91,6 +92,7 @@ fun UserProfileInfo( delayToShowPlaceholderIfNoAsset: Duration = 200.milliseconds, isProteusVerified: Boolean = false, onSearchConversationMessagesClick: () -> Unit = {}, + onConversationMediaClick: () -> Unit = {}, shouldShowSearchButton: Boolean = false ) { Column( @@ -232,8 +234,10 @@ fun UserProfileInfo( } if (shouldShowSearchButton) { - SearchConversationMessagesButton( - onSearchConversationMessagesClick = onSearchConversationMessagesClick + VerticalSpace.x24() + SearchAndMediaRow( + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + onConversationMediaClick = onConversationMediaClick ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index 9f308cd6c9e..4d99c1d8c32 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -87,6 +87,7 @@ import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.connection.ConnectionActionButton +import com.wire.android.ui.destinations.ConversationMediaScreenDestination import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.DeviceDetailsScreenDestination import com.wire.android.ui.destinations.SearchConversationMessagesScreenDestination @@ -143,6 +144,18 @@ fun OtherUserProfileScreen( } } + val onConversationMediaClick: () -> Unit = { + conversationId?.let { + navigator.navigate( + NavigationCommand( + ConversationMediaScreenDestination( + conversationId = it + ) + ) + ) + } + } + OtherProfileScreenContent( scope = scope, state = viewModel.state, @@ -161,6 +174,7 @@ fun OtherUserProfileScreen( onSearchConversationMessagesClick = onSearchConversationMessagesClick, navigateBack = navigator::navigateBack, navigationIconType = NavigationIconType.Close, + onConversationMediaClick = onConversationMediaClick ) LaunchedEffect(Unit) { @@ -192,6 +206,7 @@ fun OtherProfileScreenContent( onOpenConversation: (ConversationId) -> Unit = {}, onOpenDeviceDetails: (Device) -> Unit = {}, onSearchConversationMessagesClick: () -> Unit, + onConversationMediaClick: () -> Unit = {}, navigateBack: () -> Unit = {} ) { val otherUserProfileScreenState = rememberOtherUserProfileScreenState() @@ -269,7 +284,8 @@ fun OtherProfileScreenContent( topBarCollapsing = { TopBarCollapsing( state = state, - onSearchConversationMessagesClick = onSearchConversationMessagesClick + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + onConversationMediaClick = onConversationMediaClick ) }, topBarFooter = { TopBarFooter(state, pagerState, tabBarElevationState, tabItems, currentTabState, scope) }, @@ -369,7 +385,8 @@ private fun TopBarHeader( @Composable private fun TopBarCollapsing( state: OtherUserProfileState, - onSearchConversationMessagesClick: () -> Unit + onSearchConversationMessagesClick: () -> Unit, + onConversationMediaClick: () -> Unit = {} ) { Crossfade( targetState = state, @@ -388,7 +405,8 @@ private fun TopBarCollapsing( connection = targetState.connectionState, isProteusVerified = targetState.isProteusVerified, onSearchConversationMessagesClick = onSearchConversationMessagesClick, - shouldShowSearchButton = state.shouldShowSearchButton() + shouldShowSearchButton = state.shouldShowSearchButton(), + onConversationMediaClick = onConversationMediaClick ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c82699a498f..ef04e8ef998 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -708,6 +708,11 @@ Search all messages in this conversation. No results could be found.\nPlease refine your search and try again. Search + + Media + Pictures + Files + Links CONTACTS New Group diff --git a/kalium b/kalium index 2ddf5b82add..c2e46cdf3c5 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 2ddf5b82add97ed9bf32bd642847f2e095f5dbf2 +Subproject commit c2e46cdf3c55beb60b35596f961a729b7dede6c3