From 1832263ade9a1d0521c6d835ce2f741197a88ca1 Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 16 Jan 2026 11:45:33 +0100 Subject: [PATCH 01/22] fix: typo --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31712746ed1..8add30dd3d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -634,7 +634,7 @@ SECURITY LEVEL: UNCLASSIFIED Self-deleting messages When this is on, all messages in this conversation will disappear after a certain time. - The feature is not available for conversations with a shared Drive. + The feature is not available for conversations with a Shared Drive. Guests When this is ON, people from outside your team can join this conversation Turn this option ON to open this conversation to people outside your team, even if they don\'t have Wire. @@ -1883,7 +1883,7 @@ In group conversations, the group admin can overwrite this setting. Search files Drive Shared Drive - Enable participants to manage their documents and media files in a shared Drive. This can\’t be undone. + Enable participants to manage their documents and media files in a Shared Drive. This can’t be undone. Retry upload Remove Apply Filter From 4cc619f3510254e2896682ce53d0c11441d81335 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 5 Jan 2026 13:34:55 +0100 Subject: [PATCH 02/22] chore: update release note --- app/src/main/play/release-notes/de-DE/4.18.0.txt | 6 ++++++ app/src/main/play/release-notes/en-US/4.18.0.txt | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 app/src/main/play/release-notes/de-DE/4.18.0.txt create mode 100644 app/src/main/play/release-notes/en-US/4.18.0.txt diff --git a/app/src/main/play/release-notes/de-DE/4.18.0.txt b/app/src/main/play/release-notes/de-DE/4.18.0.txt new file mode 100644 index 00000000000..f8291cdd83c --- /dev/null +++ b/app/src/main/play/release-notes/de-DE/4.18.0.txt @@ -0,0 +1,6 @@ +Verbessert +- Audionachrichten +- Leistung, insbesondere in Unterhaltungen + +Behoben +- Fehlende Anrufbenachrichtigungen diff --git a/app/src/main/play/release-notes/en-US/4.18.0.txt b/app/src/main/play/release-notes/en-US/4.18.0.txt new file mode 100644 index 00000000000..dc743a3b0c9 --- /dev/null +++ b/app/src/main/play/release-notes/en-US/4.18.0.txt @@ -0,0 +1,6 @@ +Improvements +- Audio messages +- Performance, especially in conversations + +Fixes +- Missing missed-call messages From 3b626e58b230a058549acc864db9ac09f0f24665 Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 23 Jan 2026 15:23:34 +0100 Subject: [PATCH 03/22] fix: missing DE translation --- app/src/main/res/values-de/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e2e71589bce..7fbe93a32e5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1743,6 +1743,7 @@ registriert. Bitte versuchen Sie es mit einer anderen. Optionen Dateien suchen Geteiltes Drive + Ermöglichen Sie den Teilnehmern, ihre Dokumente und Mediendateien in Geteiltes Drive zu verwalten. Dieser Vorgang kann nicht rückgängig gemacht werden. Upload wiederholen Entfernen Filter anwenden From 6dc904f1e4eace4a6bfbe65e1eefa4c34decd813 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 22 Jan 2026 12:38:49 +0100 Subject: [PATCH 04/22] fix: incorrect translation for files tab (WPB-22523) (#4541) --- .../ui/home/conversations/media/ConversationMediaScreen.kt | 6 +++--- .../conversations/media/ConversationSharedDriveButton.kt | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-si/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 +- 10 files changed, 12 insertions(+), 7 deletions(-) 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 dbacb3467f7..5048866d0b7 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 @@ -224,7 +224,7 @@ private fun Content( onImageLongClicked = onOpenAssetOptions ) - ConversationMediaScreenTabItem.SHARED_DRIVE -> FileAssetsContent( + ConversationMediaScreenTabItem.FILES -> FileAssetsContent( groupedAssetMessageList = state.assetMessages, assetStatuses = state.assetStatuses, onAssetItemClicked = onAssetItemClicked, @@ -267,7 +267,7 @@ private fun AssetOptionsModalSheetLayout( enum class ConversationMediaScreenTabItem(@StringRes val titleResId: Int) : TabItem { PICTURES(R.string.label_conversation_pictures), - SHARED_DRIVE(R.string.label_conversation_shared_drive); + FILES(R.string.label_conversation_files); override val title: UIText = UIText.StringResource(titleResId) } @@ -305,7 +305,7 @@ fun PreviewConversationMediaScreenFilesContent() = WireTheme { assetMessages = flowOfAssets, assetStatuses = assetStatuses, ), - initialPage = ConversationMediaScreenTabItem.SHARED_DRIVE, + initialPage = ConversationMediaScreenTabItem.FILES, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationSharedDriveButton.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationSharedDriveButton.kt index 6f46fcb8d26..b4d53a94f80 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationSharedDriveButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationSharedDriveButton.kt @@ -38,7 +38,7 @@ fun ConversationSharedDriveButton( ) { WireSecondaryButton( modifier = modifier, - text = stringResource(R.string.label_conversation_shared_drive), + text = stringResource(R.string.label_conversation_files), onClick = onClick, minSize = MaterialTheme.wireDimensions.buttonMinSize, fillMaxWidth = true, diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7fbe93a32e5..b6e3ad60c88 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -892,7 +892,7 @@ Medien Bilder - Geteiltes Drive + Dateien Geteiltes Drive öffnen Bislang hat niemand Bilder in dieser Unterhaltung geteilt 🥲 Bislang hat niemand Dateien in dieser Unterhaltung geteilt 🙀 diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index d2e7008655f..678e42ad43d 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -831,6 +831,7 @@ Média Képek Ebben a beszélgetésben még senki nem osztott meg képet 🥲 + Fájlok Ebben a beszégetésben még senki nem osztott meg fájlt 🙀 Kiválasztva diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ed5c72c0030..e9f38360df0 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -618,6 +618,7 @@ Un messaggio eliminato non può essere ripristinato. Multimedia Immagini Nessuno ha ancora condiviso immagini in questa conversazione 🥲 + File Nessuno ha ancora condiviso dei file in questa conversazione 🙀 Nuovo Gruppo diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b7fe72d58ce..73c2f4241aa 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -790,6 +790,7 @@ Uma mensagem excluída não pode ser restaurada. Mídia Imagens Ninguém compartilhou fotos nesta conversa ainda 🥲 + Arquivos Ninguém compartilhou arquivos nesta conversa ainda 🙀 Selecionado diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 23c98c51de3..9afbf9e2f22 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -971,7 +971,7 @@ Медиа Изображения - Общий диск + Файлы Открыть общий диск Никто не делился фотографиями в этой беседе 🥲 Никто не делился файлами в этой беседе 🙀 diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index a727912248e..183c9bb8687 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -847,6 +847,7 @@ මාධ්‍ය ඡායාරූප කිසිවෙක් මෙම සංවාදයෙහි ඡායාරූප බෙදාගෙන නැත 🥲 + ගොනු කිසිවෙක් මෙම සංවාදයෙහි ගොනු බෙදාගෙන නැත 🙀 තෝරා ගන්නා ලදී diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 4c8419fb6bc..b2c1394d08a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -642,4 +642,5 @@ + Filer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8add30dd3d0..9b250ea7262 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -974,7 +974,7 @@ Media Pictures - Shared Drive + Files Open Shared Drive Nobody shared pictures in this conversation yet 🥲 Nobody shared files in this conversation yet 🙀 From 4e119fb69e46c606ab340387c2228d68321b878e Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 30 Jan 2026 15:56:49 +0100 Subject: [PATCH 05/22] fix: crash when opening Drive screen --- .../java/com/wire/android/feature/cells/ui/CellFilesNavArgs.kt | 2 +- .../java/com/wire/android/feature/cells/ui/CellViewModel.kt | 2 +- .../wire/android/feature/cells/ui/ConversationFilesScreen.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesNavArgs.kt index d72ab8dede9..c7855966c72 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesNavArgs.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesNavArgs.kt @@ -23,7 +23,7 @@ data class CellFilesNavArgs( val isRecycleBin: Boolean? = false, val breadcrumbs: Array? = null, val parentFolderUuid: String? = null, - val isSearchByDefaultActive: Boolean = false, + val isSearchByDefaultActive: Boolean? = false, ) { override fun hashCode(): Int { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index 2edfaa404c9..e7e4fc30641 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -99,7 +99,7 @@ class CellViewModel @Inject constructor( private val navArgs: CellFilesNavArgs = savedStateHandle.navArgs() - val isSearchByDefaultActive: Boolean = navArgs.isSearchByDefaultActive + val isSearchByDefaultActive: Boolean? = navArgs.isSearchByDefaultActive // Show menu with actions for the selected file. private val _menu: MutableSharedFlow = MutableSharedFlow() diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index a53e940303b..b93ac075dd9 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -105,7 +105,7 @@ fun ConversationFilesScreen( navigator: WireNavigator, viewModel: CellViewModel = hiltViewModel(), ) { - val conversationSearchBarState = rememberSearchbarState(viewModel.isSearchByDefaultActive) + val conversationSearchBarState = rememberSearchbarState(viewModel.isSearchByDefaultActive == true) LaunchedEffect(conversationSearchBarState.searchQueryTextState.text) { viewModel.onSearchQueryUpdated(conversationSearchBarState.searchQueryTextState.text.toString()) From f6dac18ff96f2b8f1f123d7b4ad0ee10f1a3d1ad Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 30 Jan 2026 16:21:32 +0100 Subject: [PATCH 06/22] fix: cleanup --- build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt index 23da4ecca31..58985842519 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt @@ -26,7 +26,7 @@ object AndroidSdk { object AndroidApp { const val id = "com.wire.android" - const val versionName = "4.21.0" + const val versionName = "4.20.0" val versionCode by lazy { Versionizer(_rootDir).versionCode } @@ -37,7 +37,7 @@ object AndroidApp { } /** - * The last 5 digits of the VersionCode. From 0 to 99_999.Œ + * The last 5 digits of the VersionCode. From 0 to 99_999. * It's an [Int], so it can be less than 5 digits when doing [toString], of course. * Considering versionCode bumps every 5min, these are * 288 per day From b98285f5aa0d64a0ac92f9c5d6ea1bf9c30228f6 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 3 Feb 2026 15:24:51 +0100 Subject: [PATCH 07/22] chore: avoid nullable value --- .../java/com/wire/android/feature/cells/ui/CellViewModel.kt | 2 +- .../wire/android/feature/cells/ui/ConversationFilesScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index e7e4fc30641..bec2d19e22c 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -99,7 +99,7 @@ class CellViewModel @Inject constructor( private val navArgs: CellFilesNavArgs = savedStateHandle.navArgs() - val isSearchByDefaultActive: Boolean? = navArgs.isSearchByDefaultActive + val isSearchByDefaultActive: Boolean = navArgs.isSearchByDefaultActive ?: false // Show menu with actions for the selected file. private val _menu: MutableSharedFlow = MutableSharedFlow() diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index b93ac075dd9..a53e940303b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -105,7 +105,7 @@ fun ConversationFilesScreen( navigator: WireNavigator, viewModel: CellViewModel = hiltViewModel(), ) { - val conversationSearchBarState = rememberSearchbarState(viewModel.isSearchByDefaultActive == true) + val conversationSearchBarState = rememberSearchbarState(viewModel.isSearchByDefaultActive) LaunchedEffect(conversationSearchBarState.searchQueryTextState.text) { viewModel.onSearchQueryUpdated(conversationSearchBarState.searchQueryTextState.text.toString()) From 7ef8539d5a5b8ab1e2bc047bcaab30168f2fc66b Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 18 Feb 2026 18:32:00 +0100 Subject: [PATCH 08/22] feat: filter files in Shared drive --- .../wire/android/navigation/MainNavHost.kt | 140 ++++---- .../android/navigation/WireMainNavGraph.kt | 2 + .../com/wire/android/ui/home/HomeScreen.kt | 2 + .../channels/BrowseChannelsScreen.kt | 6 +- .../search/SearchUsersAndAppsScreen.kt | 3 + .../SearchConversationMessagesScreen.kt | 5 +- .../android/ui/sharing/ImportMediaScreen.kt | 3 + .../com/wire/android/ui/common/SearchBar.kt | 6 +- .../common/bottomsheet/WireModalSheetState.kt | 7 +- .../android/ui/common/chip/WireFilterChip.kt | 89 +++-- .../common/textfield/WirePasswordTextField.kt | 2 +- .../ui/common/textfield/WireTextField.kt | 3 +- .../common/textfield/WireTextFieldLayout.kt | 23 +- .../common/topappbar/search/SearchTopBar.kt | 75 +++-- .../wire/android/ui/theme/WireDimensions.kt | 4 + .../src/main/res/drawable/ic_search.xml | 15 +- .../feature/cells/ui/CellFilesScreen.kt | 4 +- .../android/feature/cells/ui/CellListItem.kt | 2 + .../feature/cells/ui/CellScreenContent.kt | 11 +- .../android/feature/cells/ui/CellViewModel.kt | 1 + .../cells/ui/ConversationFilesScreen.kt | 226 +++++++------ ...rsationFilesWithSlideInTransitionScreen.kt | 8 +- .../cells/ui/dialog/NodeActionsBottomSheet.kt | 2 + .../ui/download/DownloadFileBottomSheet.kt | 4 + .../cells/ui/filter/FilterBottomSheet.kt | 3 +- .../feature/cells/ui/model/CellNodeUi.kt | 10 + .../movetofolder/MoveToFolderScreenContent.kt | 2 + .../cells/ui/recyclebin/RecycleBinScreen.kt | 1 - .../feature/cells/ui/search/SearchNavArgs.kt | 22 ++ .../feature/cells/ui/search/SearchScreen.kt | 304 ++++++++++++++++++ .../cells/ui/search/SearchScreenViewModel.kt | 272 ++++++++++++++++ .../feature/cells/ui/search/SearchUiState.kt | 44 +++ .../cells/ui/search/filter/FilterChipsRow.kt | 127 ++++++++ .../bottomsheet/FilterByTypeBottomSheet.kt | 226 +++++++++++++ .../owner/FilterByOwnerBottomSheet.kt | 269 ++++++++++++++++ .../owner/OwnersFilterSheetState.kt | 71 ++++ .../tags/FilterByTagsBottomSheet.kt | 214 ++++++++++++ .../bottomsheet/tags/TagsFilterSheetState.kt | 77 +++++ .../ui/search/filter/data/FilterOwnerUi.kt | 28 ++ .../ui/search/filter/data/FilterTagUi.kt | 24 ++ .../ui/search/filter/data/FilterTypeUi.kt | 28 ++ .../cells/ui/search/filter/data/TypeFilter.kt | 86 +++++ .../cells/ui/tags/AddRemoveTagsScreen.kt | 4 +- .../main/res/drawable/ic_dropdown_chevron.xml | 10 + .../cells/src/main/res/values-de/strings.xml | 1 - .../cells/src/main/res/values-ru/strings.xml | 1 - .../cells/src/main/res/values/strings.xml | 22 +- gradle/libs.versions.toml | 4 +- kalium | 2 +- 49 files changed, 2223 insertions(+), 272 deletions(-) create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchNavArgs.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/FilterChipsRow.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/OwnersFilterSheetState.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/TagsFilterSheetState.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterOwnerUi.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterTagUi.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterTypeUi.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt create mode 100644 features/cells/src/main/res/drawable/ic_dropdown_chevron.xml diff --git a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt index 4134fc55965..37bc909bc6a 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt @@ -19,7 +19,10 @@ package com.wire.android.navigation import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -30,6 +33,7 @@ import com.ramcosta.composedestinations.navigation.dependency import com.ramcosta.composedestinations.scope.resultBackNavigator import com.ramcosta.composedestinations.scope.resultRecipient import com.ramcosta.composedestinations.spec.Route +import com.wire.android.feature.cells.ui.LocalSharedTransitionScope import com.wire.android.feature.sketch.destinations.DrawingCanvasScreenDestination import com.wire.android.feature.sketch.model.DrawingCanvasNavBackArgs import com.wire.android.navigation.style.DefaultNestedNavGraphAnimations @@ -44,7 +48,7 @@ import com.wire.android.ui.home.conversations.ConversationScreen import com.wire.android.ui.home.newconversation.NewConversationViewModel import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel -@OptIn(ExperimentalAnimationApi::class) +@OptIn(ExperimentalAnimationApi::class, ExperimentalSharedTransitionApi::class) @Composable fun MainNavHost( navigator: Navigator, @@ -62,77 +66,81 @@ fun MainNavHost( ) AdjustDestinationStylesForTablets() - DestinationsNavHost( - modifier = modifier, - navGraph = WireMainNavGraph, - engine = navHostEngine, - startRoute = startDestination, - navController = navigator.navController, - dependenciesContainerBuilder = { - // 👇 To make Navigator available to all destinations as a non-navigation parameter - dependency(navigator) + SharedTransitionLayout(modifier = modifier) { + CompositionLocalProvider(LocalSharedTransitionScope provides this) { + DestinationsNavHost( + modifier = Modifier, + navGraph = WireMainNavGraph, + engine = navHostEngine, + startRoute = startDestination, + navController = navigator.navController, + dependenciesContainerBuilder = { + // 👇 To make Navigator available to all destinations as a non-navigation parameter + dependency(navigator) - // 👇 To make LoginTypeSelector available to all destinations as a non-navigation parameter if provided - if (loginTypeSelector != null) dependency(loginTypeSelector) + // 👇 To make LoginTypeSelector available to all destinations as a non-navigation parameter if provided + if (loginTypeSelector != null) dependency(loginTypeSelector) - // 👇 To tie NewConversationViewModel to nested NewConversationNavGraph, making it shared between all screens that belong to it - dependency(NavGraphs.newConversation) { - val parentEntry = remember(navBackStackEntry) { - navController.getBackStackEntry(NavGraphs.newConversation.route) - } - hiltViewModel(parentEntry) - } + // 👇 To tie NewConversationViewModel to nested NewConversationNavGraph, making it shared between all screens that belong to it + dependency(NavGraphs.newConversation) { + val parentEntry = remember(navBackStackEntry) { + navController.getBackStackEntry(NavGraphs.newConversation.route) + } + hiltViewModel(parentEntry) + } - // 👇 To reuse LoginEmailViewModel from NewLoginPasswordScreen on NewLoginVerificationCodeScreen - dependency(NewLoginVerificationCodeScreenDestination) { - val loginPasswordEntry = remember(navBackStackEntry) { - navController.getBackStackEntry(NewLoginPasswordScreenDestination.route) - } - hiltViewModel(loginPasswordEntry) - } + // 👇 To reuse LoginEmailViewModel from NewLoginPasswordScreen on NewLoginVerificationCodeScreen + dependency(NewLoginVerificationCodeScreenDestination) { + val loginPasswordEntry = remember(navBackStackEntry) { + navController.getBackStackEntry(NewLoginPasswordScreenDestination.route) + } + hiltViewModel(loginPasswordEntry) + } - // 👇 To tie SSOUrlConfigHolder to nested LoginNavGraph, making it shared between all screens that belong to it - dependency(NavGraphs.login) { - val parentEntry = remember(navBackStackEntry) { - navController.getBackStackEntry(NavGraphs.login.route) - } - SSOUrlConfigHolderImpl(parentEntry.savedStateHandle) - } + // 👇 To tie SSOUrlConfigHolder to nested LoginNavGraph, making it shared between all screens that belong to it + dependency(NavGraphs.login) { + val parentEntry = remember(navBackStackEntry) { + navController.getBackStackEntry(NavGraphs.login.route) + } + SSOUrlConfigHolderImpl(parentEntry.savedStateHandle) + } - // 👇 To tie SSOUrlConfigHolder to nested NewLoginNavGraph, making it shared between all screens that belong to it - dependency(NavGraphs.newLogin) { - val parentEntry = remember(navBackStackEntry) { - navController.getBackStackEntry(NavGraphs.newLogin.route) - } - SSOUrlConfigHolderImpl(parentEntry.savedStateHandle) - } + // 👇 To tie SSOUrlConfigHolder to nested NewLoginNavGraph, making it shared between all screens that belong to it + dependency(NavGraphs.newLogin) { + val parentEntry = remember(navBackStackEntry) { + navController.getBackStackEntry(NavGraphs.newLogin.route) + } + SSOUrlConfigHolderImpl(parentEntry.savedStateHandle) + } - // 👇 To tie TeamMigrationViewModel to PersonalToTeamMigrationNavGraph, making it shared between all screens that belong to it - dependency(NavGraphs.personalToTeamMigration) { - val parentEntry = remember(navBackStackEntry) { - navController.getBackStackEntry(NavGraphs.personalToTeamMigration.route) + // 👇 To tie TeamMigrationViewModel to PersonalToTeamMigrationNavGraph, making it shared between all screens that belong to it + dependency(NavGraphs.personalToTeamMigration) { + val parentEntry = remember(navBackStackEntry) { + navController.getBackStackEntry(NavGraphs.personalToTeamMigration.route) + } + hiltViewModel(parentEntry) + } + }, + manualComposableCallsBuilder = { + /** + * In compose-destinations v1 it's not possible for code generation to use destination generated in another module, + * that's why it's necessary to use "open" approach and manually call the composable function for the destination. + * In v2 this will be possible, so that we will be able to use regular `ResultRecipient` instead of `OpenResultRecipient` + * and provide it directly inside the destination's composable without the need to passing it manually. + * https://github.com/raamcosta/compose-destinations/issues/508#issuecomment-1883166574 + */ + composable(ConversationScreenDestination) { + ConversationScreen( + navigator = navigator, + groupDetailsScreenResultRecipient = resultRecipient(), + mediaGalleryScreenResultRecipient = resultRecipient(), + imagePreviewScreenResultRecipient = resultRecipient(), + drawingCanvasScreenResultRecipient = resultRecipient(), + resultNavigator = resultBackNavigator(), + ) + } } - hiltViewModel(parentEntry) - } - }, - manualComposableCallsBuilder = { - /** - * In compose-destinations v1 it's not possible for code generation to use destination generated in another module, - * that's why it's necessary to use "open" approach and manually call the composable function for the destination. - * In v2 this will be possible, so that we will be able to use regular `ResultRecipient` instead of `OpenResultRecipient` - * and provide it directly inside the destination's composable without the need to passing it manually. - * https://github.com/raamcosta/compose-destinations/issues/508#issuecomment-1883166574 - */ - composable(ConversationScreenDestination) { - ConversationScreen( - navigator = navigator, - groupDetailsScreenResultRecipient = resultRecipient(), - mediaGalleryScreenResultRecipient = resultRecipient(), - imagePreviewScreenResultRecipient = resultRecipient(), - drawingCanvasScreenResultRecipient = resultRecipient(), - resultNavigator = resultBackNavigator(), - ) - } + ) } - ) + } } diff --git a/app/src/main/kotlin/com/wire/android/navigation/WireMainNavGraph.kt b/app/src/main/kotlin/com/wire/android/navigation/WireMainNavGraph.kt index 3b63b8c18e8..bde923b1293 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/WireMainNavGraph.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/WireMainNavGraph.kt @@ -31,6 +31,7 @@ import com.wire.android.feature.cells.ui.destinations.PublicLinkPasswordScreenDe import com.wire.android.feature.cells.ui.destinations.PublicLinkScreenDestination import com.wire.android.feature.cells.ui.destinations.RecycleBinScreenDestination import com.wire.android.feature.cells.ui.destinations.RenameNodeScreenDestination +import com.wire.android.feature.cells.ui.destinations.SearchScreenDestination import com.wire.android.feature.cells.ui.destinations.VersionHistoryScreenDestination import com.wire.android.feature.sketch.destinations.DrawingCanvasScreenDestination import com.wire.android.ui.NavGraphs @@ -42,6 +43,7 @@ object WireMainNavGraph : NavGraphSpec { .plus(DrawingCanvasScreenDestination) .plus(PublicLinkScreenDestination) .plus(ConversationFilesScreenDestination) + .plus(SearchScreenDestination) .plus(ConversationFilesWithSlideInTransitionScreenDestination) .plus(CreateFolderScreenDestination) .plus(CreateFileScreenDestination) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 6bb98bbdb73..70cf98fc9d5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale @@ -346,6 +347,7 @@ fun HomeContent( searchBarHint = stringResource(searchBar.hint), searchQueryTextState = searchBarState.searchQueryTextState, onActiveChanged = searchBarState::searchActiveChanged, + focusRequester = remember { FocusRequester() } ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/channels/BrowseChannelsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/channels/BrowseChannelsScreen.kt index 9c9559870c1..40e403e3f72 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/channels/BrowseChannelsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/channels/BrowseChannelsScreen.kt @@ -22,7 +22,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.res.stringResource import com.wire.android.R import com.wire.android.navigation.Navigator @@ -56,6 +58,7 @@ private fun Content( modifier: Modifier = Modifier ) { val lazyListState = rememberLazyListState() + val focusRequester = remember { FocusRequester() } WireScaffold( modifier = modifier, topBar = { @@ -69,7 +72,8 @@ private fun Content( isSearchActive = true, searchBarHint = stringResource(id = R.string.label_search_public_channels), searchQueryTextState = searchQueryTextState, - isLoading = false + isLoading = false, + focusRequester = focusRequester, ) } }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt index 6e46835fba7..4c93f992f76 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R @@ -140,6 +141,7 @@ fun SearchUsersAndAppsScreen( } }, topBarCollapsing = { + val focusRequester = remember { FocusRequester() } SearchTopBar( isSearchActive = searchBarState.isSearchActive, searchBarHint = searchBarTitle, @@ -147,6 +149,7 @@ fun SearchUsersAndAppsScreen( searchBarDescription = stringResource(R.string.content_description_add_participants_search_field), searchQueryTextState = searchBarState.searchQueryTextState, onActiveChanged = searchBarState::searchActiveChanged, + focusRequester = focusRequester, ) }, topBarFooter = { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt index fe6b4745947..0d1e031e29f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt @@ -25,7 +25,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel @@ -117,7 +119,8 @@ fun SearchConversationMessagesResultContent( searchBarHint = stringResource(id = R.string.label_search_messages), searchQueryTextState = searchQueryTextState, onCloseSearchClicked = onCloseSearchClicked, - isLoading = state.isLoading + isLoading = state.isLoading, + focusRequester = remember { FocusRequester() }, ) } if (isCellsConversation) { diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index 80d88c839f1..59926fd44c4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -526,6 +527,7 @@ fun ImportMediaTopBarContent( thickness = 1.dp, modifier = Modifier.padding(top = dimensions().spacing12x) ) + val focusRequester = remember { FocusRequester() } SearchTopBar( isSearchActive = searchBarState.isSearchActive, searchBarHint = stringResource( @@ -534,6 +536,7 @@ fun ImportMediaTopBarContent( ), searchQueryTextState = searchQueryTextState, onActiveChanged = searchBarState::searchActiveChanged, + focusRequester = focusRequester, ) } } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt index 61c08bf7adc..c4b3fdbd577 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt @@ -59,7 +59,8 @@ fun SearchBarInput( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, textStyle: TextStyle = LocalTextStyle.current, isLoading: Boolean = false, - semanticDescription: String? = null + semanticDescription: String? = null, + onTap: (() -> Unit)? = null ) { WireTextField( @@ -108,7 +109,8 @@ fun SearchBarInput( placeholderAlignment = placeholderAlignment, placeholderText = placeholderText, lineLimits = TextFieldLineLimits.SingleLine, - semanticDescription = semanticDescription + semanticDescription = semanticDescription, + onTap = onTap, ) } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt index 78e17df6710..41a0578cc51 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filter @@ -43,15 +44,17 @@ open class WireModalSheetState( private val scope: CoroutineScope, private val keyboardController: SoftwareKeyboardController? = null, private val onDismissAction: () -> Unit = {}, + positionalThreshold: () -> Float = { with(density) { 56.dp.toPx() } }, + velocityThreshold: () -> Float = { with(density) { 125.dp.toPx() } }, initialValue: WireSheetValue = WireSheetValue.Hidden, skipPartiallyExpanded: Boolean = true, ) { val sheetState: SheetState = SheetState( - density = density, skipPartiallyExpanded = skipPartiallyExpanded, initialValue = initialValue.originalValue, + positionalThreshold = positionalThreshold, + velocityThreshold = velocityThreshold, confirmValueChange = { true }, - skipHiddenState = false ) var currentValue: WireSheetValue by mutableStateOf(initialValue) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/WireFilterChip.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/WireFilterChip.kt index 07875e39f7d..703190696e2 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/WireFilterChip.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/WireFilterChip.kt @@ -17,20 +17,25 @@ */ package com.wire.android.ui.common.chip -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource -import com.wire.android.ui.common.R import com.wire.android.ui.common.button.wireChipColors +import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.theme.WireTheme @@ -41,39 +46,75 @@ fun WireFilterChip( label: String, isSelected: Boolean, modifier: Modifier = Modifier, + count: Int? = null, isEnabled: Boolean = true, - onSelectChip: (String) -> Unit = {} + onClick: (String) -> Unit = {}, + trailingIconResource: Int? = null ) { - val rotationAngle by animateFloatAsState( - targetValue = if (isSelected) 0f else 45f, - ) - FilterChip( modifier = modifier.wrapContentSize(), - onClick = { onSelectChip(label) }, + onClick = { onClick(label) }, label = { - Text( - text = label, - style = MaterialTheme.wireTypography.button02, - maxLines = 1 - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x) + ) { + Text( + text = label, + style = MaterialTheme.wireTypography.button02, + maxLines = 1 + ) + + if (count != null && count > 0) { + CountBadge(count = count) + } + } }, enabled = isEnabled, selected = isSelected, colors = wireChipColors(), trailingIcon = { - Icon( - modifier = Modifier - .size(dimensions().spacing12x) - .rotate(rotationAngle), - painter = painterResource(id = R.drawable.ic_close), - contentDescription = null, - ) + trailingIconResource ?.let { + Icon( + modifier = Modifier.width(dimensions().spacing14x), + painter = painterResource(id = it), + contentDescription = null, + ) + } }, + border = FilterChipDefaults.filterChipBorder( + enabled = isEnabled, + selected = isSelected, + borderColor = colorsScheme().outline, + selectedBorderColor = colorsScheme().primary, + ) ) } +@Composable +fun CountBadge( + count: Int, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .background( + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(36) + ) + .padding(horizontal = dimensions().spacing4x, vertical = dimensions().spacing1x), + contentAlignment = Alignment.Center + ) { + Text( + text = count.toString(), + style = MaterialTheme.wireTypography.label02, + color = colorsScheme().onPrimary + ) + } +} + + @MultipleThemePreviews @Composable fun PreviewFilterChip() { @@ -86,7 +127,7 @@ fun PreviewFilterChip() { @Composable fun PreviewSelectedFilterChip() { WireTheme { - WireFilterChip(label = "Selected", isSelected = true) + WireFilterChip(label = "Selected", count = 4, isSelected = true) } } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt index ccf1a450c4a..17f5882fa83 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt @@ -86,7 +86,7 @@ fun WirePasswordTextField( inputMinHeight: Dp = MaterialTheme.wireDimensions.textFieldMinHeight, shape: Shape = RoundedCornerShape(MaterialTheme.wireDimensions.textFieldCornerSize), colors: WireTextFieldColors = wireTextFieldColors(), - onTap: ((Offset) -> Unit)? = null, + onTap: (() -> Unit)? = null, testTag: String = String.EMPTY ) { val autoFillType = if (autoFill) WireAutoFillType.Password else WireAutoFillType.None diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt index e5eded949cc..97256d7b64b 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt @@ -42,7 +42,6 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource @@ -90,7 +89,7 @@ fun WireTextField( colors: WireTextFieldColors = wireTextFieldColors(), onSelectedLineIndexChanged: (Int) -> Unit = { }, onLineBottomYCoordinateChanged: (Float) -> Unit = { }, - onTap: ((Offset) -> Unit)? = null, + onTap: (() -> Unit)? = null, testTag: String = String.EMPTY, validateKeyboardOptions: Boolean = true, ) { diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldLayout.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldLayout.kt index ea36102b2c9..af4f7485a4b 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldLayout.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldLayout.kt @@ -21,7 +21,7 @@ package com.wire.android.ui.common.textfield import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -38,9 +38,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.clearAndSetSemantics @@ -83,7 +81,7 @@ internal fun WireTextFieldLayout( inputMinHeight: Dp = MaterialTheme.wireDimensions.textFieldMinHeight, shape: Shape = RoundedCornerShape(MaterialTheme.wireDimensions.textFieldCornerSize), colors: WireTextFieldColors = wireTextFieldColors(), - onTap: ((Offset) -> Unit)? = null, + onTap: (() -> Unit)? = null, testTag: String = String.EMPTY ) { Column(modifier = modifier) { @@ -158,20 +156,17 @@ private fun InnerTextLayout( placeholderAlignment: Alignment.Horizontal = Alignment.Start, inputMinHeight: Dp = dimensions().spacing48x, colors: WireTextFieldColors = wireTextFieldColors(), - onTap: ((Offset) -> Unit)? = null + onTap: (() -> Unit)? = null ) { - val modifier: Modifier = Modifier.apply { - if (onTap != null) { - pointerInput(Unit) { - detectTapGestures(onTap = onTap) - } - } - } - Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = Modifier .heightIn(min = inputMinHeight) + .then( + onTap?.let { + Modifier.clickable { onTap() } + } ?: Modifier + ) ) { val trailingOrStateIcon: @Composable (() -> Unit)? = when { trailingIcon != null -> trailingIcon diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt index 7b01b123c91..6758592b6f3 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt @@ -46,11 +46,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import com.wire.android.ui.common.R @@ -65,52 +65,60 @@ fun SearchTopBar( isSearchActive: Boolean, searchBarHint: String, searchQueryTextState: TextFieldState, + focusRequester: FocusRequester, modifier: Modifier = Modifier, isLoading: Boolean = false, backIconContentDescription: String? = null, searchBarDescription: String? = null, onCloseSearchClicked: (() -> Unit)? = null, onActiveChanged: (isActive: Boolean) -> Unit = {}, - bottomContent: @Composable ColumnScope.() -> Unit = {} + bottomContent: @Composable ColumnScope.() -> Unit = {}, + onTap: (() -> Unit)? = null, + focusManager: FocusManager = LocalFocusManager.current, ) { + val interactionSource = remember { MutableInteractionSource() } + +// LaunchedEffect(isSearchActive) { +// if (isSearchActive) { +// focusRequester.requestFocus() +// } else { +// focusManager.clearFocus(force = true) +// // Optional: if you want to clear when leaving active mode +// // searchQueryTextState.clearText() +// } +// } + + fun setActive(isActive: Boolean) { + if (isActive) { + focusRequester.requestFocus() + } else { + focusManager.clearFocus() + searchQueryTextState.clearText() + } + } + + LaunchedEffect(isSearchActive) { + setActive(isSearchActive) + } + + val placeholderAlignment by animateHorizontalAlignmentAsState( + targetAlignment = if (isSearchActive) Alignment.CenterStart else Alignment.Center + ) + Column( modifier = modifier .wrapContentHeight() .fillMaxWidth() .background(MaterialTheme.wireColorScheme.background) ) { - val interactionSource = remember { MutableInteractionSource() } - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - val focusManager = LocalFocusManager.current - - fun setActive(isActive: Boolean) { - if (isActive) { - focusRequester.requestFocus() - keyboardController?.show() - } else { - focusManager.clearFocus() - keyboardController?.hide() - searchQueryTextState.clearText() - } - } - - LaunchedEffect(isSearchActive) { - setActive(isSearchActive) - } - - val placeholderAlignment by animateHorizontalAlignmentAsState( - targetAlignment = if (isSearchActive) Alignment.CenterStart else Alignment.Center - ) - SearchBarInput( placeholderText = searchBarHint, semanticDescription = searchBarDescription, textState = searchQueryTextState, isLoading = isLoading, leadingIcon = { - AnimatedContent(!isSearchActive, label = "") { isVisible -> - if (isVisible) { + AnimatedContent(!isSearchActive, label = "") { showSearchIcon -> + if (showSearchIcon) { Box( contentAlignment = Alignment.Center, modifier = Modifier.size(dimensions().buttonCircleMinSize) @@ -123,7 +131,7 @@ fun SearchTopBar( } } else { IconButton( - onClick = { onCloseSearchClicked?.invoke() ?: setActive(false) }, + onClick = { onCloseSearchClicked?.invoke() }, modifier = Modifier.size(dimensions().buttonCircleMinSize) ) { Icon( @@ -135,13 +143,16 @@ fun SearchTopBar( } } }, - placeholderTextStyle = LocalTextStyle.current.copy(textAlign = if (!isSearchActive) TextAlign.Center else TextAlign.Start), + placeholderTextStyle = LocalTextStyle.current.copy( + textAlign = if (!isSearchActive) TextAlign.Center else TextAlign.Start + ), placeholderAlignment = placeholderAlignment, textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Start), interactionSource = interactionSource, + onTap = onTap, modifier = Modifier .padding(dimensions().spacing8x) - .focusable(true) + .focusable(enabled = isSearchActive) .focusRequester(focusRequester) .onFocusEvent { onActiveChanged(it.isFocused) } ) @@ -167,6 +178,7 @@ fun PreviewSearchTopBarActive() { searchBarHint = "Search", searchQueryTextState = rememberTextFieldState(), onActiveChanged = {}, + focusRequester = remember { FocusRequester() } ) } } @@ -180,6 +192,7 @@ fun PreviewSearchTopBarInactive() { searchBarHint = "Search", searchQueryTextState = rememberTextFieldState(), onActiveChanged = {}, + focusRequester = remember { FocusRequester() } ) } } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt index 5dd75639ea1..5a8487b47ea 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt @@ -171,6 +171,7 @@ data class WireDimensions( val spacing200x: Dp, val spacing270x: Dp, val spacing300x: Dp, + val spacing700x: Dp, // Corners val corner2x: Dp, val corner3x: Dp, @@ -182,6 +183,7 @@ data class WireDimensions( val corner12x: Dp, val corner14x: Dp, val corner16x: Dp, + val corner36x: Dp, val corner100x: Dp, // Notifications val notificationBadgeHeight: Dp, @@ -354,6 +356,7 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( spacing200x = 200.dp, spacing270x = 270.dp, spacing300x = 300.dp, + spacing700x = 700.dp, corner2x = 2.dp, corner3x = 3.dp, corner4x = 4.dp, @@ -364,6 +367,7 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( corner12x = 12.dp, corner14x = 14.dp, corner16x = 16.dp, + corner36x = 36.dp, corner100x = 100.dp, notificationBadgeHeight = 18.dp, notificationBadgeRadius = 6.dp, diff --git a/core/ui-common/src/main/res/drawable/ic_search.xml b/core/ui-common/src/main/res/drawable/ic_search.xml index f2ac86bd23a..03cc4aa65bc 100644 --- a/core/ui-common/src/main/res/drawable/ic_search.xml +++ b/core/ui-common/src/main/res/drawable/ic_search.xml @@ -1,6 +1,6 @@ - - - - + + diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt index 76ad2f8c268..a0375735c22 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt @@ -61,9 +61,11 @@ internal fun CellFilesScreen( isRefreshing: State, onRefresh: () -> Unit, onItemClick: (CellNodeUi) -> Unit, - onItemMenuClick: (CellNodeUi) -> Unit, + modifier: Modifier = Modifier, + onItemMenuClick: (CellNodeUi) -> Unit ) { PullToRefreshBox( + modifier = modifier, isRefreshing = isRefreshing.value, onRefresh = onRefresh, ) { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellListItem.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellListItem.kt index 7e55e3e2ad9..748a59578db 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellListItem.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellListItem.kt @@ -285,6 +285,8 @@ private fun PreviewCellListItem() { mimeType = "image/jpg", publicLinkId = "", userName = "Test User", + userHandle = "userId", + ownerUserId = "userId", conversationName = "Test Conversation", modifiedTime = null, remotePath = null, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt index 98d3f37b23c..0b085b1d089 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt @@ -88,8 +88,9 @@ internal fun CellScreenContent( onRefresh: () -> Unit, isRestoreInProgress: Boolean, isDeleteInProgress: Boolean, - isAllFiles: Boolean, - isRecycleBin: Boolean, + modifier: Modifier = Modifier, + isRecycleBin: Boolean = false, + isAllFiles: Boolean = false, isSearchResult: Boolean = false, isFiltering: Boolean = false, retryEditNodeError: (String) -> Unit = {}, @@ -113,12 +114,14 @@ internal fun CellScreenContent( pagingListItems.isError() -> { val error = (pagingListItems.loadState.refresh as? LoadState.Error)?.error ErrorScreen( + modifier = modifier, isConnectionError = (error as? FileListLoadError)?.isConnectionError ?: false, onRetry = { pagingListItems.retry() } ) } pagingListItems.itemCount == 0 -> EmptyScreen( + modifier = modifier, isSearchResult = isSearchResult, isAllFiles = isAllFiles, isRecycleBin = isRecycleBin, @@ -127,6 +130,7 @@ internal fun CellScreenContent( else -> CellFilesScreen( + modifier = modifier, cellNodes = pagingListItems, onItemClick = { sendIntent(CellViewIntent.OnItemClick(it)) }, onItemMenuClick = { sendIntent(CellViewIntent.OnItemMenuClick(it)) }, @@ -257,13 +261,14 @@ internal fun CellScreenContent( @Composable private fun EmptyScreen( + modifier: Modifier = Modifier, isSearchResult: Boolean = false, isAllFiles: Boolean = true, isRecycleBin: Boolean = false, isFiltering: Boolean = false, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .padding(dimensions().spacing16x), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index bec2d19e22c..f72644ecdf8 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -79,6 +79,7 @@ import okio.Path.Companion.toPath import javax.inject.Inject import kotlin.time.Duration.Companion.seconds +// TODO: to cleanup this viewModel as search has been moved to a separate screen in upcoming PRs @Suppress("TooManyFunctions", "LongParameterList") @HiltViewModel class CellViewModel @Inject constructor( diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index a53e940303b..0438d29bfc9 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -19,18 +19,15 @@ package com.wire.android.feature.cells.ui import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme @@ -40,7 +37,9 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -51,7 +50,6 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.wire.android.feature.cells.R import com.wire.android.feature.cells.domain.model.AttachmentFileType -import com.wire.android.feature.cells.ui.common.Breadcrumbs import com.wire.android.feature.cells.ui.create.FileTypeBottomSheetDialog import com.wire.android.feature.cells.ui.create.file.CreateFileScreenNavArgs import com.wire.android.feature.cells.ui.destinations.AddRemoveTagsScreenDestination @@ -62,23 +60,25 @@ import com.wire.android.feature.cells.ui.destinations.MoveToFolderScreenDestinat import com.wire.android.feature.cells.ui.destinations.PublicLinkScreenDestination import com.wire.android.feature.cells.ui.destinations.RecycleBinScreenDestination import com.wire.android.feature.cells.ui.destinations.RenameNodeScreenDestination +import com.wire.android.feature.cells.ui.destinations.SearchScreenDestination import com.wire.android.feature.cells.ui.destinations.VersionHistoryScreenDestination import com.wire.android.feature.cells.ui.dialog.CellsNewActionBottomSheet import com.wire.android.feature.cells.ui.dialog.CellsOptionsBottomSheet import com.wire.android.feature.cells.ui.model.CellNodeUi +import com.wire.android.feature.cells.ui.search.sharedElementSearchInputKey import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.PreviewNavigator import com.wire.android.navigation.WireNavigator import com.wire.android.navigation.annotation.features.cells.WireDestination import com.wire.android.navigation.style.PopUpNavigationAnimation -import com.wire.android.ui.common.CollapsingTopBarScaffold import com.wire.android.ui.common.MoreOptionIcon import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.bottomsheet.show import com.wire.android.ui.common.button.FloatingActionButton import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.search.SearchBarState import com.wire.android.ui.common.search.rememberSearchbarState import com.wire.android.ui.common.topappbar.NavigationIconType @@ -92,6 +92,12 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf +@OptIn(ExperimentalSharedTransitionApi::class) +val LocalSharedTransitionScope = + staticCompositionLocalOf { + error("SharedTransitionScope not provided. Wrap NavHost in SharedTransitionLayout.") + } + /** * Show files in one conversation. * Conversation id is passed to view model via navigation parameters [CellFilesNavArgs]. @@ -103,6 +109,7 @@ import kotlinx.coroutines.flow.flowOf @Composable fun ConversationFilesScreen( navigator: WireNavigator, + animatedVisibilityScope: AnimatedVisibilityScope, viewModel: CellViewModel = hiltViewModel(), ) { val conversationSearchBarState = rememberSearchbarState(viewModel.isSearchByDefaultActive) @@ -116,6 +123,7 @@ fun ConversationFilesScreen( } ConversationFilesScreenContent( + animatedVisibilityScope = animatedVisibilityScope, navigator = navigator, currentNodeUuid = viewModel.currentNodeUuid(), conversationSearchBarState = conversationSearchBarState, @@ -139,8 +147,10 @@ fun ConversationFilesScreen( } } +@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun ConversationFilesScreenContent( + animatedVisibilityScope: AnimatedVisibilityScope, navigator: WireNavigator, currentNodeUuid: String?, conversationSearchBarState: SearchBarState, @@ -154,7 +164,6 @@ fun ConversationFilesScreenContent( onRefresh: () -> Unit, retryEditNodeError: (String) -> Unit, modifier: Modifier = Modifier, - onBreadcrumbsFolderClick: (index: Int) -> Unit = {}, isDeleteInProgress: Boolean = false, screenTitle: String? = null, isRecycleBin: Boolean = false, @@ -219,57 +228,53 @@ fun ConversationFilesScreenContent( }, ) - CollapsingTopBarScaffold( + WireScaffold( modifier = modifier, - topBarHeader = { - AnimatedVisibility( - modifier = Modifier.background(MaterialTheme.colorScheme.background), - visible = !conversationSearchBarState.isSearchActive, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut(), - ) { - Column { - WireCenterAlignedTopAppBar( - onNavigationPressed = { navigator.navigateBack() }, - title = screenTitle ?: stringResource(R.string.conversation_files_title), - navigationIconType = NavigationIconType.Back(), - elevation = dimensions().spacing0x, - actions = { - if (!isRecycleBin) { - MoreOptionIcon( - contentDescription = R.string.content_description_conversation_files_more_button, - onButtonClicked = { optionsBottomSheetState.show() } + topBar = { + Column { + WireCenterAlignedTopAppBar( + onNavigationPressed = { navigator.navigateBack() }, + title = screenTitle ?: stringResource(R.string.conversation_files_title), + navigationIconType = NavigationIconType.Back(), + elevation = dimensions().spacing0x, + actions = { + if (!isRecycleBin) { + MoreOptionIcon( + contentDescription = R.string.content_description_conversation_files_more_button, + onButtonClicked = { optionsBottomSheetState.show() } + ) + } + } + ) + + val sharedScope = LocalSharedTransitionScope.current + val focusRequester = remember { FocusRequester() } + + with(sharedScope) { + SearchTopBar( + modifier = Modifier + .sharedElement( + sharedContentState = rememberSharedContentState(key = sharedElementSearchInputKey), + animatedVisibilityScope = animatedVisibilityScope + ), + isSearchActive = conversationSearchBarState.isSearchActive, + searchBarHint = stringResource(R.string.search_shared_drive_text_input_hint), + searchQueryTextState = conversationSearchBarState.searchQueryTextState, + onActiveChanged = conversationSearchBarState::searchActiveChanged, + onTap = { + currentNodeUuid?.let { + navigator.navigate( + NavigationCommand( + SearchScreenDestination(conversationId = it) + ) ) } - } + }, + focusRequester = focusRequester, ) - breadcrumbs?.let { - Breadcrumbs( - modifier = Modifier - .height(dimensions().spacing32x) - .fillMaxWidth(), - isRecycleBin = isRecycleBin, - pathSegments = it, - onBreadcrumbsFolderClick = onBreadcrumbsFolderClick - ) - } } } }, - topBarCollapsing = { - AnimatedVisibility( - visible = conversationSearchBarState.isSearchVisible, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically() - ) { - SearchTopBar( - isSearchActive = conversationSearchBarState.isSearchActive, - searchBarHint = stringResource(R.string.search_text_input_hint_for_files_folders_in_conversation), - searchQueryTextState = conversationSearchBarState.searchQueryTextState, - onActiveChanged = conversationSearchBarState::searchActiveChanged, - ) - } - }, floatingActionButton = { if (isFabVisible) { AnimatedVisibility( @@ -298,8 +303,8 @@ fun ConversationFilesScreenContent( } } }, - ) { - Box { + ) { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { CellScreenContent( actionsFlow = actions, pagingListItems = pagingListItems, @@ -307,7 +312,6 @@ fun ConversationFilesScreenContent( downloadFileState = downloadFileSheet, menuState = menu, isSearchResult = isSearchResult, - isAllFiles = false, isRestoreInProgress = isRestoreInProgress, isDeleteInProgress = isDeleteInProgress, isRecycleBin = isRecycleBin, @@ -379,58 +383,72 @@ fun ConversationFilesScreenContent( } } + +@OptIn(ExperimentalSharedTransitionApi::class) @Composable @MultipleThemePreviews fun PreviewConversationFilesScreen() { WireTheme { - ConversationFilesScreenContent( - navigator = PreviewNavigator, - currentNodeUuid = "conversationId", - conversationSearchBarState = rememberSearchbarState(), - isSearchResult = false, - actions = flowOf(), - pagingListItems = MutableStateFlow( - PagingData.from( - listOf( - CellNodeUi.File( - uuid = "file1", - name = "File 1", - downloadProgress = 0.5f, - assetType = AttachmentFileType.IMAGE, - size = 123456, - localPath = null, - mimeType = "image/png", - publicLinkId = "link1", - userName = "User A", - conversationName = "Conversation A", - modifiedTime = "2023-10-01T12:00:00Z", - remotePath = "/path/to/file1.png", - contentHash = null, - contentUrl = null, - previewUrl = null - ), - CellNodeUi.Folder( - uuid = "folder1", - name = "Folder 1", - remotePath = "/path/to/folder1", - userName = "User B", - conversationName = "Conversation B", - modifiedTime = "2023-10-01T12:00:00Z", - size = 123456, + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + ConversationFilesScreenContent( + animatedVisibilityScope = this, + navigator = PreviewNavigator, + currentNodeUuid = "conversationId", + conversationSearchBarState = rememberSearchbarState(), + isSearchResult = false, + actions = flowOf(), + pagingListItems = MutableStateFlow( + PagingData.from( + listOf( + CellNodeUi.File( + uuid = "file1", + name = "File 1", + downloadProgress = 0.5f, + assetType = AttachmentFileType.IMAGE, + size = 123456, + localPath = null, + mimeType = "image/png", + publicLinkId = "link1", + userName = "User A", + userHandle = "userHandle", + ownerUserId = "userA", + conversationName = "Conversation A", + modifiedTime = "2023-10-01T12:00:00Z", + remotePath = "/path/to/file1.png", + contentHash = null, + contentUrl = null, + previewUrl = null + ), + CellNodeUi.Folder( + uuid = "folder1", + name = "Folder 1", + remotePath = "/path/to/folder1", + userName = "User B", + userHandle = "userHandle", + ownerUserId = "userB", + conversationName = "Conversation B", + modifiedTime = "2023-10-01T12:00:00Z", + size = 123456, + ) + ) ) - ) + ).collectAsLazyPagingItems(), + downloadFileSheet = MutableStateFlow(null), + menu = MutableSharedFlow(replay = 0), + sendIntent = {}, + screenTitle = "Android", + isRecycleBin = false, + breadcrumbs = arrayOf("Engineering", "Android"), + isRefreshing = remember { mutableStateOf(false) }, + onRefresh = {}, + retryEditNodeError = {}, ) - ).collectAsLazyPagingItems(), - downloadFileSheet = MutableStateFlow(null), - menu = MutableSharedFlow(replay = 0), - sendIntent = {}, - onBreadcrumbsFolderClick = {}, - screenTitle = "Android", - isRecycleBin = false, - breadcrumbs = arrayOf("Engineering", "Android"), - isRefreshing = remember { mutableStateOf(false) }, - onRefresh = {}, - retryEditNodeError = {}, - ) + + } + + + } + } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt index 1b50601fce5..4f6d0204cda 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt @@ -18,6 +18,7 @@ package com.wire.android.feature.cells.ui import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -25,7 +26,6 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.wire.android.feature.cells.R -import com.wire.android.feature.cells.ui.destinations.ConversationFilesWithSlideInTransitionScreenDestination import com.wire.android.feature.cells.ui.destinations.RecycleBinScreenDestination import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -42,6 +42,7 @@ import com.wire.android.ui.common.search.rememberSearchbarState fun ConversationFilesWithSlideInTransitionScreen( navigator: WireNavigator, cellFilesNavArgs: CellFilesNavArgs, + animatedVisibilityScope: AnimatedVisibilityScope, viewModel: CellViewModel = hiltViewModel(), ) { val conversationSearchBarState = rememberSearchbarState() @@ -69,6 +70,7 @@ fun ConversationFilesWithSlideInTransitionScreen( } ConversationFilesScreenContent( + animatedVisibilityScope = animatedVisibilityScope, navigator = navigator, currentNodeUuid = viewModel.currentNodeUuid(), conversationSearchBarState = conversationSearchBarState, @@ -83,10 +85,6 @@ fun ConversationFilesWithSlideInTransitionScreen( isDeleteInProgress = viewModel.isDeleteInProgress.collectAsState().value, isRefreshing = viewModel.isPullToRefresh.collectAsState(), breadcrumbs = cellFilesNavArgs.breadcrumbs, - onBreadcrumbsFolderClick = { - val stepsBack = viewModel.breadcrumbs()?.size!! - it - 1 - navigator.navigateBackAndRemoveAllConsecutiveXTimes(ConversationFilesWithSlideInTransitionScreenDestination.route, stepsBack) - }, sendIntent = viewModel::sendIntent, onRefresh = viewModel::onPullToRefresh, retryEditNodeError = viewModel::editNode diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/dialog/NodeActionsBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/dialog/NodeActionsBottomSheet.kt index 20f14924abc..2d6bf464041 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/dialog/NodeActionsBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/dialog/NodeActionsBottomSheet.kt @@ -142,6 +142,8 @@ private fun PreviewFileActionsBottomSheet() { size = 2342342, localPath = "", userName = null, + userHandle = "userHandle", + ownerUserId = "userId", conversationName = null, modifiedTime = null ), diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/download/DownloadFileBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/download/DownloadFileBottomSheet.kt index eab4a077b5a..a0a13fec6f5 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/download/DownloadFileBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/download/DownloadFileBottomSheet.kt @@ -174,6 +174,8 @@ private fun DownloadFileBottomSheetPreview() { size = 23432532532, localPath = null, userName = null, + ownerUserId = "userId", + userHandle = "userHandle", modifiedTime = null, remotePath = null, contentHash = null, @@ -202,6 +204,8 @@ private fun DownloadFileBottomSheetDownloadingPreview() { size = 23432532532, localPath = null, userName = null, + ownerUserId = "userId", + userHandle = "userHandle", modifiedTime = null, remotePath = null, contentHash = null, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/filter/FilterBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/filter/FilterBottomSheet.kt index e7ac685b768..fc43cd19975 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/filter/FilterBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/filter/FilterBottomSheet.kt @@ -60,6 +60,7 @@ import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography +//TODO: To be removed in upcoming PRs when filter feature is fully implemented @Composable fun FilterBottomSheet( selectableTags: List, @@ -177,7 +178,7 @@ private fun SheetContent( label = tag, isSelected = isSelected, modifier = Modifier.padding(end = dimensions().spacing16x), - onSelectChip = { label -> + onClick = { label -> selectedChips = if (isSelected) { selectedChips - label } else { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/CellNodeUi.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/CellNodeUi.kt index cc49d2870f2..e661653b16d 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/CellNodeUi.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/model/CellNodeUi.kt @@ -29,6 +29,8 @@ sealed class CellNodeUi { abstract val name: String? abstract val uuid: String abstract val userName: String? + abstract val userHandle: String? + abstract val ownerUserId: String? abstract val conversationName: String? abstract val modifiedTime: String? abstract val publicLinkId: String? @@ -41,6 +43,8 @@ sealed class CellNodeUi { override val name: String?, override val uuid: String, override val userName: String?, + override val userHandle: String?, + override val ownerUserId: String?, override val conversationName: String?, override val modifiedTime: String?, override val publicLinkId: String? = null, @@ -54,6 +58,8 @@ sealed class CellNodeUi { override val name: String?, override val uuid: String, override val userName: String?, + override val userHandle: String?, + override val ownerUserId: String?, override val conversationName: String?, override val modifiedTime: String?, override val publicLinkId: String? = null, @@ -83,6 +89,8 @@ internal fun Node.File.toUiModel() = CellNodeUi.File( contentUrl = contentUrl, previewUrl = previewUrl, userName = userName, + userHandle = userHandle, + ownerUserId = ownerUserId, conversationName = conversationName, publicLinkId = publicLinkId, modifiedTime = formattedModifiedTime(), @@ -94,6 +102,8 @@ internal fun Node.Folder.toUiModel() = CellNodeUi.Folder( uuid = uuid, name = name, userName = userName, + userHandle = userHandle, + ownerUserId = ownerUserId, conversationName = conversationName, modifiedTime = formattedModifiedTime(), remotePath = remotePath, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderScreenContent.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderScreenContent.kt index b00dc8cf491..5c7cdbb6ec3 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderScreenContent.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderScreenContent.kt @@ -124,6 +124,8 @@ fun PreviewMoveToFolderItem() { uuid = "243567990900989897", name = "some folder.pdf", userName = "User", + ownerUserId = "userId", + userHandle = "userHandle", conversationName = "Conversation", modifiedTime = null, size = 1234, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/recyclebin/RecycleBinScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/recyclebin/RecycleBinScreen.kt index 824f707bd59..ec87c9dc402 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/recyclebin/RecycleBinScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/recyclebin/RecycleBinScreen.kt @@ -97,7 +97,6 @@ fun RecycleBinScreen( sendIntent = { cellViewModel.sendIntent(it) }, downloadFileState = cellViewModel.downloadFileSheet, menuState = cellViewModel.menu, - isAllFiles = false, isRecycleBin = true, isRestoreInProgress = cellViewModel.isRestoreInProgress.collectAsState().value, isDeleteInProgress = cellViewModel.isDeleteInProgress.collectAsState().value, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchNavArgs.kt new file mode 100644 index 00000000000..f63531226e9 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchNavArgs.kt @@ -0,0 +1,22 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search + +data class SearchNavArgs( + val conversationId: String, +) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt new file mode 100644 index 00000000000..697cb7381b1 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -0,0 +1,304 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.compose.collectAsLazyPagingItems +import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.CellScreenContent +import com.wire.android.feature.cells.ui.CellViewModel +import com.wire.android.feature.cells.ui.LocalSharedTransitionScope +import com.wire.android.feature.cells.ui.destinations.AddRemoveTagsScreenDestination +import com.wire.android.feature.cells.ui.destinations.MoveToFolderScreenDestination +import com.wire.android.feature.cells.ui.destinations.PublicLinkScreenDestination +import com.wire.android.feature.cells.ui.destinations.RenameNodeScreenDestination +import com.wire.android.feature.cells.ui.destinations.VersionHistoryScreenDestination +import com.wire.android.feature.cells.ui.model.CellNodeUi +import com.wire.android.feature.cells.ui.search.filter.FilterChipsRow +import com.wire.android.feature.cells.ui.search.filter.bottomsheet.owner.FilterByOwnerBottomSheet +import com.wire.android.feature.cells.ui.search.filter.bottomsheet.tags.FilterByTagsBottomSheet +import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FilterByTypeBottomSheet +import com.wire.android.navigation.NavigationCommand +import com.wire.android.navigation.WireNavigator +import com.wire.android.navigation.annotation.features.cells.WireDestination +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.topappbar.search.SearchTopBar +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +const val sharedElementSearchInputKey = "search_bar" + +@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class) +@WireDestination( + style = PopUpNavigationAnimation::class, + navArgsDelegate = SearchNavArgs::class, +) +@Composable +fun SearchScreen( + navigator: WireNavigator, + animatedVisibilityScope: AnimatedVisibilityScope, + searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), + cellViewModel: CellViewModel = hiltViewModel(), +) { + val scope = rememberCoroutineScope() + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + cellViewModel.isSearchByDefaultActive + + val uiState by searchScreenViewModel.uiState.collectAsStateWithLifecycle() + + val filterTypeSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val filterTagsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val filterOwnerSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + fun closeSheet(sheetState: SheetState, onCloseFlag: () -> Unit) { + scope.launch { + sheetState.hide() + onCloseFlag() + searchScreenViewModel.onSetSearchActive(true) + } + } + + fun openSheet(onOpenFlag: () -> Unit = { }) { + scope.launch { + focusManager.clearFocus() + onOpenFlag() + searchScreenViewModel.onSetSearchActive(false) + } + } + + val sharedScope = LocalSharedTransitionScope.current + var playScrollHint by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + delay(1500) + playScrollHint = true + delay(2000) + playScrollHint = false + } + + val searchState = remember { TextFieldState() } + + LaunchedEffect(searchState) { + snapshotFlow { searchState.text.toString() } + .collect { searchScreenViewModel.onSearchQueryChanged(it) } + } + + + with(sharedScope) { + + WireScaffold( + topBar = { + Column { + SearchTopBar( + modifier = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = sharedElementSearchInputKey), + animatedVisibilityScope = animatedVisibilityScope + ), + isSearchActive = uiState.isSearchActive, + searchBarHint = stringResource(R.string.search_shared_drive_text_input_hint), + searchQueryTextState = searchState, + onCloseSearchClicked = { navigator.navigateBack() }, + onActiveChanged = { }, + focusRequester = focusRequester, + focusManager = focusManager + ) + FilterChipsRow( + shouldPlayHint = playScrollHint, + isSharedByLinkSelected = uiState.isSharedByMe, + tagsCount = uiState.tagsCount, + typeCount = uiState.typeCount, + ownerCount = uiState.ownerCount, + hasAnyFilter = uiState.hasAnyFilter, + onFilterByTagsClicked = { + openSheet { searchScreenViewModel.onFilterByTagsClicked() } + }, + onFilterByTypeClicked = { + openSheet { searchScreenViewModel.onFilterByTypeClicked() } + }, + onFilterByOwnerClicked = { + openSheet { searchScreenViewModel.onFilterByOwnerClicked() } + }, + onFilterBySharedByLinkClicked = { + searchScreenViewModel.onSharedByMeClicked() + }, + onRemoveAllFiltersClicked = { + searchScreenViewModel.onRemoveAllFilters() + } + ) + } + } + ) { innerPadding -> + with(searchScreenViewModel.cellNodesFlow.collectAsLazyPagingItems()) { + CellScreenContent( + modifier = Modifier.padding(innerPadding), + actionsFlow = cellViewModel.actions, + pagingListItems = this, + sendIntent = { cellViewModel.sendIntent(it) }, + downloadFileState = cellViewModel.downloadFileSheet, + menuState = cellViewModel.menu, + isSearchResult = true, + isRestoreInProgress = cellViewModel.isRestoreInProgress.collectAsState().value, + isDeleteInProgress = cellViewModel.isDeleteInProgress.collectAsState().value, + openFolder = { _, _, _ -> }, + showPublicLinkScreen = { publicLinkScreenData -> + navigator.navigate( + NavigationCommand( + PublicLinkScreenDestination( + assetId = publicLinkScreenData.assetId, + fileName = publicLinkScreenData.fileName, + publicLinkId = publicLinkScreenData.linkId, + isFolder = publicLinkScreenData.isFolder + ) + ) + ) + }, + showMoveToFolderScreen = { currentPath, nodePath, uuid -> + navigator.navigate( + NavigationCommand( + MoveToFolderScreenDestination( + currentPath = currentPath, + nodeToMovePath = nodePath, + uuid = uuid + ) + ) + ) + }, + showRenameScreen = { cellNodeUi -> + navigator.navigate( + NavigationCommand( + RenameNodeScreenDestination( + uuid = cellNodeUi.uuid, + currentPath = cellNodeUi.remotePath, + isFolder = cellNodeUi is CellNodeUi.Folder, + nodeName = cellNodeUi.name, + ) + ) + ) + }, + showAddRemoveTagsScreen = { node -> + navigator.navigate( + NavigationCommand( + AddRemoveTagsScreenDestination(node.uuid, node.tags.toCollection(ArrayList())) + ) + ) + }, + showVersionHistoryScreen = { uuid, fileName -> + navigator.navigate(NavigationCommand(VersionHistoryScreenDestination(uuid, fileName))) + }, + retryEditNodeError = { cellViewModel.editNode(it) }, + isRefreshing = remember { mutableStateOf(false) }, + onRefresh = { } + ) + } + + if (uiState.showFilterByTags) { + FilterByTagsBottomSheet( + items = searchScreenViewModel.uiState.collectAsState().value.availableTags, + sheetState = filterTagsSheetState, + onDismiss = { + closeSheet( + sheetState = filterTagsSheetState, + onCloseFlag = { searchScreenViewModel.onCloseTagsSheet() } + ) + }, + onSave = { selectedItems -> + searchScreenViewModel.onSaveTags(selectedItems) + closeSheet( + sheetState = filterTagsSheetState, + onCloseFlag = { searchScreenViewModel.onCloseTagsSheet() } + ) + }, + onRemoveAll = { + searchScreenViewModel.onRemoveAllTags() + } + ) + } + + if (uiState.showFilterByType) { + FilterByTypeBottomSheet( + items = searchScreenViewModel.uiState.collectAsState().value.availableTypes, + sheetState = filterTypeSheetState, + onDismiss = { + closeSheet( + sheetState = filterTypeSheetState, + onCloseFlag = { searchScreenViewModel.onCloseTypeSheet() } + ) + }, + onSave = { selectedItems -> + + searchScreenViewModel.onSaveTypes(selectedItems) + closeSheet( + sheetState = filterTypeSheetState, + onCloseFlag = { searchScreenViewModel.onCloseTypeSheet() } + ) + }, + onRemoveFilter = { + searchScreenViewModel.onRemoveTypeFilter() + } + ) + } + + if (uiState.showFilterByOwner) { + FilterByOwnerBottomSheet( + items = searchScreenViewModel.uiState.collectAsState().value.availableOwners, + sheetState = filterOwnerSheetState, + onDismiss = { + closeSheet( + sheetState = filterOwnerSheetState, + onCloseFlag = { searchScreenViewModel.onCloseOwnerSheet() } + ) + }, + onSave = { selectedItems -> + + searchScreenViewModel.onSaveOwners(selectedItems) + + closeSheet( + sheetState = filterOwnerSheetState, + onCloseFlag = { searchScreenViewModel.onCloseOwnerSheet() } + ) + }, + onRemoveAll = { searchScreenViewModel.onRemoveOwners() } + ) + } + } + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt new file mode 100644 index 00000000000..e77d09fca33 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt @@ -0,0 +1,272 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map +import com.wire.android.feature.cells.ui.model.CellNodeUi +import com.wire.android.feature.cells.ui.model.toUiModel +import com.wire.android.feature.cells.ui.navArgs +import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi +import com.wire.android.feature.cells.ui.search.filter.data.FilterTagUi +import com.wire.android.feature.cells.ui.search.filter.data.FilterTypeUi +import com.wire.android.feature.cells.ui.search.SearchUiState +import com.wire.android.model.ImageAsset +import com.wire.kalium.cells.data.MIMEType +import com.wire.kalium.cells.domain.model.Node +import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase +import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase +import com.wire.kalium.common.functional.onSuccess +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.data.id.toQualifiedID +import com.wire.kalium.logic.data.user.UserAssetId +import com.wire.kalium.logic.feature.user.GetUserInfoResult +import com.wire.kalium.logic.feature.user.GetUserInfoUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +// TODO: to cover it with unit test in upcoming PR +@HiltViewModel +class SearchScreenViewModel @Inject constructor( + val savedStateHandle: SavedStateHandle, + private val qualifiedIdMapper: QualifiedIdMapper, + private val getAllTagsUseCase: GetAllTagsUseCase, + private val getUserInfo: GetUserInfoUseCase, + private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, +) : ViewModel() { + + private data class SearchParams( + val query: String, + val tagIds: List, + val ownerIds: List, + val mimeTypes: List, + val sharedByMe: Boolean, + ) + + private val navArgs: SearchNavArgs = savedStateHandle.navArgs() + + private val _uiState = MutableStateFlow(SearchUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val queryFlow = MutableStateFlow("") + + private val searchParamsFlow: Flow = + combine( + queryFlow, + uiState + ) { query, state -> + SearchParams( + query = query, + tagIds = state.availableTags.filter { it.selected }.map { it.id }, + ownerIds = state.availableOwners.filter { it.selected }.map { it.id }, + mimeTypes = state.availableTypes.filter { it.selected }.map { it.mimeType }, + sharedByMe = state.isSharedByMe + ) + }.distinctUntilChanged() + + + val cellNodesFlow: Flow> = + searchParamsFlow.flatMapLatest { params -> + getCellFilesPaged( + conversationId = navArgs.conversationId, + query = params.query, + tags = params.tagIds, + owners = params.ownerIds, + mimeTypes = params.mimeTypes, + ).map { pagingData -> + pagingData.map { node -> + if (uiState.value.availableOwners.isEmpty()) { + loadOwners(node) + } + when (node) { + is Node.Folder -> node.toUiModel() + is Node.File -> node.toUiModel() + } + } + } + }.cachedIn(viewModelScope) + + init { + loadTags() + } + + internal fun loadTags() = viewModelScope.launch { + getAllTagsUseCase().onSuccess { updated -> + _uiState.update { + it.copy( + availableTags = updated.map { tag -> + FilterTagUi( + id = tag, + name = tag, + ) + } + ) + } + } + } + + fun onSearchQueryChanged(query: String) { + queryFlow.value = query + } + + fun loadOwners(node: Node) = viewModelScope.launch { + val id = node.ownerUserId + val name = node.userName + val handle = node.userHandle + if (id != null && name != null && handle != null) { + val userInfo = getUserInfo(id.toQualifiedID(qualifiedIdMapper)) + + val userAvatarAsset = if (userInfo is GetUserInfoResult.Success) { + userInfo.otherUser.completePicture?.let { + ImageAsset.UserAvatarAsset( + UserAssetId( + it.value, + it.domain, + ) + ) + } + } else null + _uiState.update { state -> + val existingOwners = state.availableOwners.toMutableList() + if (existingOwners.none { it.id == id }) { + existingOwners += FilterOwnerUi( + id = id, + displayName = name, + handle = handle, + userAvatarAsset = userAvatarAsset + ) + } + state.copy(availableOwners = existingOwners.sortedBy { it.displayName.uppercase() }) + } + } + } + + fun onFilterByTypeClicked() { + _uiState.update { it.copy(showFilterByType = true) } + } + + fun onCloseTypeSheet() { + _uiState.update { it.copy(showFilterByType = false) } + } + + fun onFilterByTagsClicked() { + _uiState.update { it.copy(showFilterByTags = true) } + } + + fun onCloseTagsSheet() { + _uiState.update { it.copy(showFilterByTags = false) } + } + + fun onFilterByOwnerClicked() { + _uiState.update { it.copy(showFilterByOwner = true) } + } + + fun onCloseOwnerSheet() { + _uiState.update { it.copy(showFilterByOwner = false) } + } + + fun onSetSearchActive(active: Boolean) { + _uiState.update { it.copy(isSearchActive = active) } + } + + private fun applySelectedTags(selectedIds: Set) { + _uiState.update { state -> + state.copy( + availableTags = state.availableTags.map { tag -> + tag.copy(selected = tag.id in selectedIds) + } + ) + } + } + + fun onSaveTags(selectedTags: List) { + applySelectedTags(selectedTags.filter { it.selected }.map { it.id }.toSet()) + } + + fun onRemoveAllTags() { + _uiState.update { state -> + state.copy(availableTags = state.availableTags.map { it.copy(selected = false) }) + } + } + + private fun applySelectedTypes(selectedIds: Set) { + _uiState.update { state -> + state.copy( + availableTypes = state.availableTypes.map { tag -> + tag.copy(selected = tag.id in selectedIds) + } + ) + } + } + + fun onSaveTypes(selectedOwners: List) { + applySelectedTypes(selectedOwners.filter { it.selected }.map { it.id }.toSet()) + } + + fun onRemoveTypeFilter() { + _uiState.update { state -> + state.copy(availableTypes = state.availableTypes.map { it.copy(selected = false) }) + } + } + + + fun onSharedByMeClicked() { + _uiState.update { it.copy(isSharedByMe = !it.isSharedByMe) } + } + + private fun applySelectedOwners(selectedIds: Set) { + _uiState.update { state -> + state.copy( + availableOwners = state.availableOwners.map { tag -> + tag.copy(selected = tag.id in selectedIds) + } + ) + } + } + + fun onSaveOwners(selectedOwners: List) { + applySelectedOwners(selectedOwners.filter { it.selected }.map { it.id }.toSet()) + } + + fun onRemoveOwners() { + _uiState.update { state -> + state.copy(availableOwners = state.availableOwners.map { it.copy(selected = false) }) + } + } + + fun onRemoveAllFilters() = _uiState.update { + onRemoveAllTags() + onRemoveOwners() + onRemoveTypeFilter() + it.copy(isSharedByMe = false) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt new file mode 100644 index 00000000000..461f8a69e2d --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt @@ -0,0 +1,44 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search + +import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi +import com.wire.android.feature.cells.ui.search.filter.data.FilterTagUi +import com.wire.android.feature.cells.ui.search.filter.data.FilterTypeUi +import com.wire.android.feature.cells.ui.search.filter.data.TypeFilter + +data class SearchUiState( + val availableTags: List = emptyList(), + val availableOwners: List = emptyList(), + val availableTypes: List = TypeFilter.typeItems, + + val showFilterByType: Boolean = false, + val showFilterByTags: Boolean = false, + val showFilterByOwner: Boolean = false, + + val isSharedByMe: Boolean = false, + + val isSearchActive: Boolean = true, +) { + val tagsCount: Int get() = availableTags.count { it.selected } + val typeCount: Int get() = availableTypes.count { it.selected } + val ownerCount: Int get() = availableOwners.count { it.selected } + + val hasAnyFilter: Boolean + get() = tagsCount > 0 || typeCount > 0 || ownerCount > 0 || isSharedByMe +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/FilterChipsRow.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/FilterChipsRow.kt new file mode 100644 index 00000000000..5b662b51de5 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/FilterChipsRow.kt @@ -0,0 +1,127 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.wire.android.feature.cells.R +import com.wire.android.ui.common.chip.WireFilterChip +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.common.typography +import com.wire.android.ui.theme.WireTheme +import kotlinx.coroutines.delay + +private const val DELAY_300 = 300L +private const val DELAY_150 = 150L + +@Composable +fun FilterChipsRow( + isSharedByLinkSelected: Boolean, + tagsCount: Int, + typeCount: Int, + ownerCount: Int, + hasAnyFilter: Boolean, + modifier: Modifier = Modifier, + shouldPlayHint: Boolean = false, + onFilterByTagsClicked: () -> Unit = { }, + onFilterByTypeClicked: () -> Unit = { }, + onFilterByOwnerClicked: () -> Unit = { }, + onRemoveAllFiltersClicked: () -> Unit = { }, + onFilterBySharedByLinkClicked: () -> Unit = { } +) { + val scrollState = rememberScrollState() + + LaunchedEffect(shouldPlayHint) { + if (shouldPlayHint && scrollState.maxValue > 0) { + delay(DELAY_300) + scrollState.animateScrollTo(80) + delay(DELAY_150) + scrollState.animateScrollTo(0) + } + } + + @Composable + fun DropdownChip(labelRes: Int, count: Int, onClick: () -> Unit) { + WireFilterChip( + label = stringResource(labelRes), + count = count.takeIf { it > 0 }, + isSelected = count > 0, + trailingIconResource = R.drawable.ic_dropdown_chevron, + onClick = { onClick() } + ) + } + + Row( + modifier = modifier + .fillMaxWidth() + .horizontalScroll(scrollState) + .background(colorsScheme().background) + .padding(horizontal = dimensions().spacing12x), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x) + ) { + DropdownChip(R.string.filter_chip_tags, tagsCount, onFilterByTagsClicked) + DropdownChip(R.string.filter_chip_type, typeCount, onFilterByTypeClicked) + DropdownChip(R.string.filter_chip_owner, ownerCount, onFilterByOwnerClicked) + + WireFilterChip( + label = stringResource(R.string.filter_chip_link_sharing), + isSelected = isSharedByLinkSelected, + onClick = { + onFilterBySharedByLinkClicked() + } + ) + if (hasAnyFilter) { + Text( + modifier = Modifier + .align(alignment = Alignment.CenterVertically) + .clickable { onRemoveAllFiltersClicked() }, + text = stringResource(R.string.filter_chip_remove_all_filters), + style = typography().button02, + color = colorsScheme().primary, + ) + } + } +} + +@MultipleThemePreviews +@Composable +fun PreviewFilterChipsRow() { + WireTheme { + FilterChipsRow( + isSharedByLinkSelected = true, + tagsCount = 2, + typeCount = 1, + ownerCount = 0, + hasAnyFilter = true, + ) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt new file mode 100644 index 00000000000..d227ba22dea --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt @@ -0,0 +1,226 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.bottomsheet + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.search.filter.data.FilterTypeUi +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.common.typography +import com.wire.android.ui.theme.WireTheme +import com.wire.kalium.cells.data.MIMEType + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterByTypeBottomSheet( + sheetState: SheetState, + items: List, + onDismiss: () -> Unit, + onSave: (List) -> Unit, + onRemoveFilter: () -> Unit, + modifier: Modifier = Modifier +) { + var itemsState by remember { mutableStateOf(items) } + + val hasChanges = itemsState.any { tag -> + val initial = items.first { it.id == tag.id } + tag.selected != initial.selected + } + + ModalBottomSheet( + modifier = modifier, + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(dimensions().spacing700x) + .padding(bottom = dimensions().spacing16x) + ) { + Text( + text = stringResource(R.string.bottom_sheet_title_filter_by_type), + style = typography().title02, + modifier = Modifier.padding( + horizontal = dimensions().spacing16x, + vertical = dimensions().spacing16x + ) + ) + + HorizontalDivider() + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentPadding = PaddingValues(vertical = dimensions().spacing4x) + ) { + items(itemsState, key = { it.id }) { item -> + FilterRow( + label = stringResource(item.label), + iconRes = item.iconRes, + checked = item.selected, + onCheckedChange = { checked -> + itemsState = itemsState.map { + if (it.id == item.id) it.copy(selected = checked) else it + } + } + ) + HorizontalDivider() + } + } + + Spacer(Modifier.height(dimensions().spacing12x)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensions().spacing16x), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x) + ) { + WireSecondaryButton( + text = stringResource(R.string.button_remove_filter), + onClick = { + itemsState = itemsState.map { it.copy(selected = false) } + onRemoveFilter() + }, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(vertical = dimensions().spacing14x) + ) + + WirePrimaryButton( + text = stringResource(R.string.save_label), + onClick = { onSave(itemsState) }, + modifier = Modifier.weight(1f), + state = if (hasChanges) WireButtonState.Default else WireButtonState.Disabled, + contentPadding = PaddingValues(vertical = dimensions().spacing14x) + ) + } + + Spacer(Modifier.height(dimensions().spacing8x)) + } + } +} + +@Composable +private fun FilterRow( + label: String, + iconRes: Int, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxSize() + .clickable { onCheckedChange(!checked) } + .padding( + horizontal = dimensions().spacing16x, + vertical = dimensions().spacing8x + ), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.size(dimensions().spacing20x) + ) + + Spacer(Modifier.width(dimensions().spacing8x)) + + Text( + text = label, + style = typography().body01, + modifier = Modifier.weight(1f) + ) + + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@MultipleThemePreviews +@Composable +fun PreviewFilterByTypeBottomSheet() { + val sampleItems = listOf( + FilterTypeUi(id = "1", label = R.string.filter_images_type, iconRes = android.R.drawable.ic_menu_gallery, selected = true, mimeType = MIMEType.IMAGES), + FilterTypeUi( + id = "2", + label = R.string.filter_videos_type, + iconRes = android.R.drawable.ic_menu_slideshow, + selected = false, + mimeType = MIMEType.VIDEOS + ), + FilterTypeUi( + id = "3", + label = R.string.filter_documents_type, + iconRes = android.R.drawable.ic_menu_edit, + selected = true, + mimeType = MIMEType.DOCUMENT + ), + FilterTypeUi(id = "4", label = R.string.filter_audio_type, iconRes = android.R.drawable.ic_media_play, selected = false, mimeType = MIMEType.AUDIOS), + FilterTypeUi(id = "5", label = R.string.filter_presentations_type, iconRes = android.R.drawable.ic_menu_save, selected = false, mimeType = MIMEType.OTHERS), + FilterTypeUi(id = "6", label = R.string.filter_spreadsheets_type, iconRes = android.R.drawable.ic_menu_edit, selected = false, mimeType = MIMEType.EXCEL), + FilterTypeUi(id = "7", label = R.string.filter_pdf_type, iconRes = android.R.drawable.ic_menu_view, selected = false, mimeType = MIMEType.PDF), + ) + WireTheme { + FilterByTypeBottomSheet( + items = sampleItems, + onDismiss = {}, + onSave = {}, + onRemoveFilter = {}, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt new file mode 100644 index 00000000000..180a8223273 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt @@ -0,0 +1,269 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.bottomsheet.owner + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +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.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi +import com.wire.android.model.UserAvatarData +import com.wire.android.ui.common.SearchBarInput +import com.wire.android.ui.common.avatar.UserProfileAvatar +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.common.typography +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme +import kotlinx.coroutines.launch +import com.wire.android.ui.common.R as CommonR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterByOwnerBottomSheet( + sheetState: SheetState, + items: List, + onDismiss: () -> Unit, + onSave: (List) -> Unit, + onRemoveAll: () -> Unit, + modifier: Modifier = Modifier, +) { + + val scope = rememberCoroutineScope() + val state = rememberOwnersFilterSheetState(items) + LaunchedEffect(sheetState) { sheetState.show() } + + val searchState = remember { TextFieldState() } + LaunchedEffect(searchState) { + snapshotFlow { searchState.text.toString() } + .collect(state::onQueryChange) + } + + LaunchedEffect(sheetState) { sheetState.show() } + + fun dismiss() { + scope.launch { sheetState.hide() } + .invokeOnCompletion { onDismiss() } + } + + ModalBottomSheet( + onDismissRequest = ::dismiss, + sheetState = sheetState, + modifier = modifier + .absoluteOffset(y = 1.dp) + .statusBarsPadding(), + contentWindowInsets = { WindowInsets.navigationBars }, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = dimensions().spacing700x) + .navigationBarsPadding() + .imePadding() + ) { + Text( + text = stringResource(R.string.bottom_sheet_title_filter_by_owner), + style = typography().title02, + modifier = Modifier.padding( + horizontal = dimensions().spacing16x, + vertical = dimensions().spacing12x + ) + ) + + SearchBarInput( + modifier = Modifier.padding(start = dimensions().spacing16x, end = dimensions().spacing16x), + placeholderText = stringResource(R.string.search_owners_text_input_hint), + textState = searchState, + leadingIcon = { + Icon( + modifier = Modifier.padding( + start = dimensions().spacing12x, + end = dimensions().spacing12x + ), + painter = painterResource(CommonR.drawable.ic_search), + contentDescription = null, + tint = MaterialTheme.wireColorScheme.onBackground, + ) + }, + ) + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = true), + contentPadding = PaddingValues(top = dimensions().spacing8x, bottom = dimensions().spacing8x) + ) { + items( + items = state.filteredOwners, + key = { it.id } + ) { owner -> + OwnerRow( + owner = owner, + onToggle = { state.toggleOwner(owner.id) }, + ) + HorizontalDivider() + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = dimensions().spacing16x, + end = dimensions().spacing16x, + bottom = dimensions().spacing12x + ), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x) + ) { + WireSecondaryButton( + text = stringResource(R.string.button_remove_all_label), + onClick = { + state.removeAll() + onRemoveAll() + }, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(vertical = dimensions().spacing14x) + ) + + WirePrimaryButton( + text = stringResource(R.string.save_label), + onClick = { onSave(state.selectedOwners()) }, + modifier = Modifier.weight(1f), + state = if (state.hasChanges) WireButtonState.Default else WireButtonState.Disabled, + contentPadding = PaddingValues(vertical = dimensions().spacing14x) + ) + } + } + } +} + +@Composable +private fun OwnerRow( + owner: FilterOwnerUi, + onToggle: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxSize() + .clickable { onToggle() } + .padding( + start = dimensions().spacing12x, + end = dimensions().spacing12x, + top = dimensions().spacing8x, + bottom = dimensions().spacing8x + ), + verticalAlignment = Alignment.CenterVertically + ) { + UserProfileAvatar(avatarData = UserAvatarData(owner.userAvatarAsset)) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = owner.displayName, + style = typography().body01, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.wireColorScheme.onSurface + ) + Text( + text = owner.handle, + style = typography().label04, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.wireColorScheme.secondaryText + ) + } + Checkbox( + checked = owner.selected, + onCheckedChange = { onToggle() } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@MultipleThemePreviews +@Composable +fun PreviewFilterByOwnerBottomSheet() { + WireTheme { + FilterByOwnerBottomSheet( + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + items = listOf( + FilterOwnerUi( + id = "1", + displayName = "John Doe", + handle = "@johndoe", + selected = true + ), + FilterOwnerUi( + id = "2", + displayName = "Jane Smith with very long name that should be truncated", + handle = "@janesmith", + selected = false + ), + FilterOwnerUi( + id = "3", + displayName = "Alice Johnson", + handle = "@alicejohnson", + selected = false + ) + ), + onDismiss = {}, + onSave = {}, + onRemoveAll = {} + ) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/OwnersFilterSheetState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/OwnersFilterSheetState.kt new file mode 100644 index 00000000000..d6939e4bb15 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/OwnersFilterSheetState.kt @@ -0,0 +1,71 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.bottomsheet.owner + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi + +@Stable +class OwnersFilterSheetState( + initialItems: List +) { + private val initialById = initialItems.associateBy { it.id } + + var owners by mutableStateOf(initialItems) + private set + + var query by mutableStateOf("") + private set + + val hasChanges: Boolean + get() = owners.any { o -> initialById[o.id]?.selected != o.selected } + + val filteredOwners: List + get() { + val q = query.trim() + return if (q.isBlank()) owners + else owners.filter { + it.displayName.contains(q, ignoreCase = true) || + it.handle.contains(q, ignoreCase = true) + } + } + + fun onQueryChange(text: String) { + query = text + } + + fun toggleOwner(id: String) { + owners = owners.map { if (it.id == id) it.copy(selected = !it.selected) else it } + } + + fun removeAll() { + owners = owners.map { it.copy(selected = false) } + } + + fun selectedOwners(): List = owners.filter { it.selected } +} + +@Composable +fun rememberOwnersFilterSheetState(items: List): OwnersFilterSheetState { + return remember(items) { OwnersFilterSheetState(items) } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt new file mode 100644 index 00000000000..fc9910c79f6 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt @@ -0,0 +1,214 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.bottomsheet.tags + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.search.filter.data.FilterTagUi +import com.wire.android.ui.common.SearchBarInput +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.chip.WireFilterChip +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.common.typography +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme +import kotlinx.coroutines.launch +import com.wire.android.ui.common.R as CommonR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterByTagsBottomSheet( + sheetState: SheetState, + items: List, + onDismiss: () -> Unit, + onSave: (List) -> Unit, + onRemoveAll: () -> Unit, + modifier: Modifier = Modifier +) { + + val scope = rememberCoroutineScope() + + val state = rememberTagsFilterSheetState(items) + + LaunchedEffect(Unit) { + sheetState.show() + } + + val searchState = remember { TextFieldState() } + LaunchedEffect(searchState) { + snapshotFlow { searchState.text.toString() } + .collect(state::onQueryChange) + } + + fun dismiss() { + scope.launch { sheetState.hide() } + .invokeOnCompletion { onDismiss() } + } + ModalBottomSheet( + modifier = modifier, + onDismissRequest = ::dismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = dimensions().spacing700x) + .navigationBarsPadding() + .imePadding() + .padding(horizontal = dimensions().spacing16x) + ) { + Text( + text = stringResource(R.string.bottom_sheet_title_filter_by_tags), + style = typography().title02, + modifier = Modifier.padding( + horizontal = dimensions().spacing4x, + vertical = dimensions().spacing12x + ) + ) + + SearchBarInput( + placeholderText = stringResource(R.string.bottom_sheet_title_search_tags), + textState = searchState, + leadingIcon = { + Icon( + modifier = Modifier.padding( + start = dimensions().spacing12x, + end = dimensions().spacing12x + ), + painter = painterResource(CommonR.drawable.ic_search), + contentDescription = null, + tint = MaterialTheme.wireColorScheme.onBackground, + ) + }, + ) + + Spacer(Modifier.height(dimensions().spacing12x)) + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = true) + .verticalScroll(rememberScrollState()) + ) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + ) { + state.filteredTags.forEach { tag -> + WireFilterChip( + label = tag.name, + isSelected = tag.selected, + onClick = { state.toggle(tag.id) } + ) + } + } + + Spacer(Modifier.height(dimensions().spacing12x)) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = dimensions().spacing12x), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x) + ) { + WireSecondaryButton( + text = stringResource(R.string.button_remove_all_label), + onClick = { + state.removeAll() + onRemoveAll() + }, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(vertical = dimensions().spacing14x) + ) + + WirePrimaryButton( + text = stringResource(R.string.save_label), + onClick = { onSave(state.selectedTags()) }, + modifier = Modifier.weight(1f), + state = if (state.hasChanges) WireButtonState.Default else WireButtonState.Disabled, + contentPadding = PaddingValues(vertical = dimensions().spacing14x) + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@MultipleThemePreviews +@Composable +fun PreviewFilterByTagsBottomSheet() { + WireTheme { + FilterByTagsBottomSheet( + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + items = listOf( + FilterTagUi("1", "Work", true), + FilterTagUi("2", "Personal", true), + FilterTagUi("3", "Important", true), + FilterTagUi("4", "Later"), + FilterTagUi("5", "Travel"), + FilterTagUi("6", "Shopping"), + FilterTagUi("7", "Fitness"), + FilterTagUi("8", "Health"), + FilterTagUi("11", "Work"), + FilterTagUi("21", "Personal"), + FilterTagUi("31", "Important"), + FilterTagUi("41", "Later"), + FilterTagUi("51", "Travel"), + FilterTagUi("61", "Shopping"), + FilterTagUi("71", "Fitness"), + FilterTagUi("81", "Health"), + ), + onDismiss = {}, + onSave = {}, + onRemoveAll = {} + ) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/TagsFilterSheetState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/TagsFilterSheetState.kt new file mode 100644 index 00000000000..fa4aedfc795 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/TagsFilterSheetState.kt @@ -0,0 +1,77 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.bottomsheet.tags + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.wire.android.feature.cells.ui.search.filter.data.FilterTagUi + +@Stable +class TagsFilterSheetState( + initialItems: List +) { + private val initialById = initialItems.associateBy { it.id } + + var tags by mutableStateOf(initialItems) + private set + + var query by mutableStateOf("") + private set + + val hasChanges: Boolean + get() = tags.any { t -> initialById[t.id]?.selected != t.selected } + + val filteredTags: List + get() { + val q = query.trim() + val base = if (q.isBlank()) tags else tags.filter { + it.name.contains(q, ignoreCase = true) + } + return base.sortedWith( + compareByDescending { it.selected } + .thenBy { it.name.lowercase() } + ) + } + + fun updateItems(newItems: List) { + tags = newItems + } + + fun onQueryChange(text: String) { + query = text + } + + fun toggle(id: String) { + tags = tags.map { if (it.id == id) it.copy(selected = !it.selected) else it } + } + + fun removeAll() { + tags = tags.map { it.copy(selected = false) } + } + + fun selectedTags(): List = tags.filter { it.selected } +} + +@Composable +fun rememberTagsFilterSheetState(items: List): TagsFilterSheetState { + return remember(items) { TagsFilterSheetState(items) } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterOwnerUi.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterOwnerUi.kt new file mode 100644 index 00000000000..225782be832 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterOwnerUi.kt @@ -0,0 +1,28 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.data + +import com.wire.android.model.ImageAsset + +data class FilterOwnerUi( + val id: String, + val displayName: String, + val handle: String, + val userAvatarAsset: ImageAsset.UserAvatarAsset? = null, + val selected: Boolean = false +) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterTagUi.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterTagUi.kt new file mode 100644 index 00000000000..59635f9b8c3 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterTagUi.kt @@ -0,0 +1,24 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.data + +data class FilterTagUi( + val id: String, + val name: String, + val selected: Boolean = false +) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterTypeUi.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterTypeUi.kt new file mode 100644 index 00000000000..5dafa726a1d --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/FilterTypeUi.kt @@ -0,0 +1,28 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.data + +import com.wire.kalium.cells.data.MIMEType + +data class FilterTypeUi( + val id: String, + val label: Int, + val iconRes: Int, + val selected: Boolean = false, + val mimeType: MIMEType +) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt new file mode 100644 index 00000000000..b290e0af73b --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt @@ -0,0 +1,86 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.data + +import com.wire.android.feature.cells.R +import com.wire.kalium.cells.data.MIMEType + +object TypeFilter { + val typeItems: List by lazy { + MIMEType.entries.map { it.toFilterTypeUi() } + } +} + +fun MIMEType.toFilterTypeUi(): FilterTypeUi = + when (this) { + MIMEType.PDF -> FilterTypeUi( + id = name, + label = R.string.filter_pdf_type, + iconRes = R.drawable.ic_file_type_pdf, + mimeType = this + ) + + MIMEType.DOCUMENT -> FilterTypeUi( + id = name, + label = R.string.filter_documents_type, + iconRes = R.drawable.ic_file_type_doc, + mimeType = this + ) + + MIMEType.IMAGES -> FilterTypeUi( + id = name, + label = R.string.filter_images_type, + iconRes = R.drawable.ic_file_type_image, + mimeType = this + ) + + MIMEType.EXCEL -> FilterTypeUi( + id = name, + label = R.string.filter_spreadsheets_type, + iconRes = R.drawable.ic_file_type_spreadsheet, + mimeType = this + ) + + MIMEType.PRESENTATION -> FilterTypeUi( + id = name, + label = R.string.filter_presentations_type, + iconRes = R.drawable.ic_file_type_presentation, + mimeType = this + ) + + MIMEType.VIDEOS -> FilterTypeUi( + id = name, + label = R.string.filter_videos_type, + iconRes = R.drawable.ic_file_type_video, + mimeType = this + ) + + MIMEType.AUDIOS -> FilterTypeUi( + id = name, + label = R.string.filter_audio_type, + iconRes = R.drawable.ic_file_type_audio, + mimeType = this + ) + + MIMEType.OTHERS -> FilterTypeUi( + id = name, + label = R.string.filter_other_type, + iconRes = R.drawable.ic_file_type_other, + mimeType = this + ) + } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt index 90899e2cb9d..59d5af0739a 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt @@ -116,7 +116,7 @@ fun AddRemoveTagsScreen( ), label = tag, isSelected = false, - onSelectChip = { addRemoveTagsViewModel.addTag(tag) } + onClick = { addRemoveTagsViewModel.addTag(tag) } ) } } @@ -228,7 +228,7 @@ fun AddRemoveTagsScreenContent( modifier = Modifier.align(Alignment.CenterVertically), label = item, isSelected = true, - onSelectChip = onRemoveTag + onClick = onRemoveTag ) } } diff --git a/features/cells/src/main/res/drawable/ic_dropdown_chevron.xml b/features/cells/src/main/res/drawable/ic_dropdown_chevron.xml new file mode 100644 index 00000000000..79e94cc8c3a --- /dev/null +++ b/features/cells/src/main/res/drawable/ic_dropdown_chevron.xml @@ -0,0 +1,10 @@ + + + diff --git a/features/cells/src/main/res/values-de/strings.xml b/features/cells/src/main/res/values-de/strings.xml index 23442a82992..7c52d862b34 100644 --- a/features/cells/src/main/res/values-de/strings.xml +++ b/features/cells/src/main/res/values-de/strings.xml @@ -183,7 +183,6 @@ Beim Laden der Versionsliste ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut. Anzeigen Herunterladen… - Dateien oder Ordner suchen Datei erstellen Datei kann nicht erstellt werden. Bitte versuchen Sie es erneut Dateiname diff --git a/features/cells/src/main/res/values-ru/strings.xml b/features/cells/src/main/res/values-ru/strings.xml index 8b32434cde2..4315f6a6c44 100644 --- a/features/cells/src/main/res/values-ru/strings.xml +++ b/features/cells/src/main/res/values-ru/strings.xml @@ -196,7 +196,6 @@ сохранено в Загрузках Показать Загрузка… - Поиск файлов или папок Создать файл Не удается создать файл. Пожалуйста, попробуйте снова Название файла diff --git a/features/cells/src/main/res/values/strings.xml b/features/cells/src/main/res/values/strings.xml index 2e9bc050e32..b6ed7dcf57e 100644 --- a/features/cells/src/main/res/values/strings.xml +++ b/features/cells/src/main/res/values/strings.xml @@ -199,7 +199,6 @@ saved to Downloads Show Downloading… - Search files or folders Create File Unable to create file. Please try again File Name @@ -210,4 +209,25 @@ Create %1$s file Learn more about Shared Drive Learn more + Search tags + Filter by owner + Filter by type + Filter by tags + Remove all + Remove filter + Search Shared Drive + Search owner + Tags + Type + Owner + Link Sharing + Remove all filters + PDFs + Documents + Images + Spreadsheets + Presentations + Videos + Audio files + Other diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 097019ff6e8..af0fccb2a70 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ hilt-work = "1.2.0" # Android UI accompanist = "0.32.0" # adjusted to work with compose-destinations "1.9.54" material = "1.12.0" -material3 = "1.3.2" +material3 = "1.4.0" coil = "3.3.0" commonmark = "0.25.1" @@ -152,10 +152,10 @@ googleGms-gradlePlugin = { module = "com.google.gms:google-services", version.re googleGms-location = { module = "com.google.android.gms:play-services-location", version.ref = "gms-location" } aboutLibraries-gradlePlugin = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutLibraries" } kover-gradlePlugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } - ktx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "ktx-serialization" } ktx-dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "ktx-dateTime" } ktx-immutableCollections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "ktx-immutableCollections" } +androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" } ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } ksp-symbol-processing-plugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" } diff --git a/kalium b/kalium index ae71a9a75ae..06393f28415 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ae71a9a75ae63027c8970905ef5bdeabc59fee8b +Subproject commit 06393f28415e79a5fa1f97f3646961d51a8e8f91 From 38589ffc796de7cee319d6e83e15a58e170361a9 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 19 Feb 2026 15:38:29 +0100 Subject: [PATCH 09/22] feat: remove Other filter type --- .../search/filter/bottomsheet/FilterByTypeBottomSheet.kt | 1 - .../feature/cells/ui/search/filter/data/TypeFilter.kt | 7 ------- 2 files changed, 8 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt index d227ba22dea..a7f3b631b3b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt @@ -210,7 +210,6 @@ fun PreviewFilterByTypeBottomSheet() { mimeType = MIMEType.DOCUMENT ), FilterTypeUi(id = "4", label = R.string.filter_audio_type, iconRes = android.R.drawable.ic_media_play, selected = false, mimeType = MIMEType.AUDIOS), - FilterTypeUi(id = "5", label = R.string.filter_presentations_type, iconRes = android.R.drawable.ic_menu_save, selected = false, mimeType = MIMEType.OTHERS), FilterTypeUi(id = "6", label = R.string.filter_spreadsheets_type, iconRes = android.R.drawable.ic_menu_edit, selected = false, mimeType = MIMEType.EXCEL), FilterTypeUi(id = "7", label = R.string.filter_pdf_type, iconRes = android.R.drawable.ic_menu_view, selected = false, mimeType = MIMEType.PDF), ) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt index b290e0af73b..9bad6bd1011 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt @@ -76,11 +76,4 @@ fun MIMEType.toFilterTypeUi(): FilterTypeUi = iconRes = R.drawable.ic_file_type_audio, mimeType = this ) - - MIMEType.OTHERS -> FilterTypeUi( - id = name, - label = R.string.filter_other_type, - iconRes = R.drawable.ic_file_type_other, - mimeType = this - ) } From 63b43ac0b0e20b2798057cbcd68d52ff356b293f Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 20 Feb 2026 09:11:35 +0100 Subject: [PATCH 10/22] feat: support public link filter --- .../android/feature/cells/ui/search/SearchScreen.kt | 2 +- .../feature/cells/ui/search/SearchScreenViewModel.kt | 10 +++++----- .../android/feature/cells/ui/search/SearchUiState.kt | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index 697cb7381b1..13f9ed39bd2 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -143,7 +143,7 @@ fun SearchScreen( ) FilterChipsRow( shouldPlayHint = playScrollHint, - isSharedByLinkSelected = uiState.isSharedByMe, + isSharedByLinkSelected = uiState.filesWithPublicLink, tagsCount = uiState.tagsCount, typeCount = uiState.typeCount, ownerCount = uiState.ownerCount, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt index e77d09fca33..f0b12202c12 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt @@ -29,7 +29,6 @@ import com.wire.android.feature.cells.ui.navArgs import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi import com.wire.android.feature.cells.ui.search.filter.data.FilterTagUi import com.wire.android.feature.cells.ui.search.filter.data.FilterTypeUi -import com.wire.android.feature.cells.ui.search.SearchUiState import com.wire.android.model.ImageAsset import com.wire.kalium.cells.data.MIMEType import com.wire.kalium.cells.domain.model.Node @@ -69,7 +68,7 @@ class SearchScreenViewModel @Inject constructor( val tagIds: List, val ownerIds: List, val mimeTypes: List, - val sharedByMe: Boolean, + val filesWithPublicLink: Boolean?, ) private val navArgs: SearchNavArgs = savedStateHandle.navArgs() @@ -89,7 +88,7 @@ class SearchScreenViewModel @Inject constructor( tagIds = state.availableTags.filter { it.selected }.map { it.id }, ownerIds = state.availableOwners.filter { it.selected }.map { it.id }, mimeTypes = state.availableTypes.filter { it.selected }.map { it.mimeType }, - sharedByMe = state.isSharedByMe + filesWithPublicLink = state.filesWithPublicLink ) }.distinctUntilChanged() @@ -102,6 +101,7 @@ class SearchScreenViewModel @Inject constructor( tags = params.tagIds, owners = params.ownerIds, mimeTypes = params.mimeTypes, + hasPublicLink = params.filesWithPublicLink ).map { pagingData -> pagingData.map { node -> if (uiState.value.availableOwners.isEmpty()) { @@ -240,7 +240,7 @@ class SearchScreenViewModel @Inject constructor( fun onSharedByMeClicked() { - _uiState.update { it.copy(isSharedByMe = !it.isSharedByMe) } + _uiState.update { it.copy(filesWithPublicLink = !it.filesWithPublicLink ) } } private fun applySelectedOwners(selectedIds: Set) { @@ -267,6 +267,6 @@ class SearchScreenViewModel @Inject constructor( onRemoveAllTags() onRemoveOwners() onRemoveTypeFilter() - it.copy(isSharedByMe = false) + it.copy(filesWithPublicLink = false) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt index 461f8a69e2d..b0a42437c98 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt @@ -31,7 +31,7 @@ data class SearchUiState( val showFilterByTags: Boolean = false, val showFilterByOwner: Boolean = false, - val isSharedByMe: Boolean = false, + val filesWithPublicLink: Boolean = false, val isSearchActive: Boolean = true, ) { @@ -40,5 +40,5 @@ data class SearchUiState( val ownerCount: Int get() = availableOwners.count { it.selected } val hasAnyFilter: Boolean - get() = tagsCount > 0 || typeCount > 0 || ownerCount > 0 || isSharedByMe + get() = tagsCount > 0 || typeCount > 0 || ownerCount > 0 || filesWithPublicLink } From 31c21b4590fb9bb552e0442aed01d6ab3a9ac98e Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 20 Feb 2026 15:24:55 +0100 Subject: [PATCH 11/22] chore: conflicts --- .../android/feature/cells/ui/CellViewModel.kt | 11 ++++++----- .../cells/ui/ConversationFilesScreen.kt | 9 +++++---- .../feature/cells/ui/search/SearchScreen.kt | 18 +++++++++--------- .../cells/ui/search/SearchScreenViewModel.kt | 19 +++++++++++-------- kalium | 2 +- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index 32709cb2e24..5a516a8c160 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -38,15 +38,16 @@ import com.wire.android.feature.cells.util.FileHelper import com.wire.android.feature.cells.util.FileNameResolver import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.DEFAULT_SEARCH_QUERY_DEBOUNCE +import com.wire.kalium.cells.data.FileFilters import com.wire.kalium.cells.domain.model.Node import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase -import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase import com.wire.kalium.cells.domain.usecase.IsAtLeastOneCellAvailableUseCase import com.wire.kalium.cells.domain.usecase.RestoreNodeFromRecycleBinUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase import com.wire.kalium.common.functional.fold import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess @@ -65,11 +66,9 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -184,8 +183,10 @@ class CellViewModel @Inject constructor( getCellFilesPaged( conversationId = navArgs.conversationId, query = query, - onlyDeleted = navArgs.isRecycleBin ?: false, - tags = currentTags.toList(), + fileFilters = FileFilters( + tags = currentTags.toList(), + onlyDeleted = navArgs.isRecycleBin ?: false, + ), ).cachedIn(viewModelScope), removedItemsFlow, downloadDataFlow diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index 51b58bdfb4f..a913ea9e88d 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -48,10 +48,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import com.wire.android.feature.cells.R -import com.wire.android.feature.cells.domain.model.AttachmentFileType -import com.wire.android.feature.cells.ui.create.FileTypeBottomSheetDialog -import com.wire.android.feature.cells.ui.create.file.CreateFileScreenNavArgs import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.ConversationFilesWithSlideInTransitionScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.CreateFileScreenDestination @@ -60,7 +56,12 @@ import com.ramcosta.composedestinations.generated.cells.destinations.MoveToFolde import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.RecycleBinScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.RenameNodeScreenDestination +import com.ramcosta.composedestinations.generated.cells.destinations.SearchScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.VersionHistoryScreenDestination +import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.domain.model.AttachmentFileType +import com.wire.android.feature.cells.ui.create.FileTypeBottomSheetDialog +import com.wire.android.feature.cells.ui.create.file.CreateFileScreenNavArgs import com.wire.android.feature.cells.ui.dialog.CellsNewActionBottomSheet import com.wire.android.feature.cells.ui.dialog.CellsOptionsBottomSheet import com.wire.android.feature.cells.ui.model.CellNodeUi diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index 13f9ed39bd2..716de51e46b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -41,23 +41,23 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems +import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination +import com.ramcosta.composedestinations.generated.cells.destinations.MoveToFolderScreenDestination +import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination +import com.ramcosta.composedestinations.generated.cells.destinations.RenameNodeScreenDestination +import com.ramcosta.composedestinations.generated.cells.destinations.VersionHistoryScreenDestination import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.CellScreenContent import com.wire.android.feature.cells.ui.CellViewModel import com.wire.android.feature.cells.ui.LocalSharedTransitionScope -import com.wire.android.feature.cells.ui.destinations.AddRemoveTagsScreenDestination -import com.wire.android.feature.cells.ui.destinations.MoveToFolderScreenDestination -import com.wire.android.feature.cells.ui.destinations.PublicLinkScreenDestination -import com.wire.android.feature.cells.ui.destinations.RenameNodeScreenDestination -import com.wire.android.feature.cells.ui.destinations.VersionHistoryScreenDestination import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.search.filter.FilterChipsRow +import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FilterByTypeBottomSheet import com.wire.android.feature.cells.ui.search.filter.bottomsheet.owner.FilterByOwnerBottomSheet import com.wire.android.feature.cells.ui.search.filter.bottomsheet.tags.FilterByTagsBottomSheet -import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FilterByTypeBottomSheet import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.WireNavigator -import com.wire.android.navigation.annotation.features.cells.WireDestination +import com.wire.android.navigation.annotation.features.cells.WireCellsDestination import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.search.SearchTopBar @@ -67,9 +67,9 @@ import kotlinx.coroutines.launch const val sharedElementSearchInputKey = "search_bar" @OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class) -@WireDestination( +@WireCellsDestination( style = PopUpNavigationAnimation::class, - navArgsDelegate = SearchNavArgs::class, + navArgs = SearchNavArgs::class, ) @Composable fun SearchScreen( diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt index f0b12202c12..1bc9f6c08b6 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt @@ -23,13 +23,14 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map +import com.ramcosta.composedestinations.generated.cells.navArgs import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.model.toUiModel -import com.wire.android.feature.cells.ui.navArgs import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi import com.wire.android.feature.cells.ui.search.filter.data.FilterTagUi import com.wire.android.feature.cells.ui.search.filter.data.FilterTypeUi import com.wire.android.model.ImageAsset +import com.wire.kalium.cells.data.FileFilters import com.wire.kalium.cells.data.MIMEType import com.wire.kalium.cells.domain.model.Node import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase @@ -48,7 +49,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -73,6 +73,8 @@ class SearchScreenViewModel @Inject constructor( private val navArgs: SearchNavArgs = savedStateHandle.navArgs() +// private val navArgs: SearchNavArgs = SearchScreenDestination.argsFrom(savedStateHandle) + private val _uiState = MutableStateFlow(SearchUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -92,16 +94,17 @@ class SearchScreenViewModel @Inject constructor( ) }.distinctUntilChanged() - val cellNodesFlow: Flow> = searchParamsFlow.flatMapLatest { params -> getCellFilesPaged( conversationId = navArgs.conversationId, query = params.query, - tags = params.tagIds, - owners = params.ownerIds, - mimeTypes = params.mimeTypes, - hasPublicLink = params.filesWithPublicLink + fileFilters = FileFilters( + tags = params.tagIds, + owners = params.ownerIds, + mimeTypes = params.mimeTypes, + hasPublicLink = params.filesWithPublicLink + ), ).map { pagingData -> pagingData.map { node -> if (uiState.value.availableOwners.isEmpty()) { @@ -240,7 +243,7 @@ class SearchScreenViewModel @Inject constructor( fun onSharedByMeClicked() { - _uiState.update { it.copy(filesWithPublicLink = !it.filesWithPublicLink ) } + _uiState.update { it.copy(filesWithPublicLink = !it.filesWithPublicLink) } } private fun applySelectedOwners(selectedIds: Set) { diff --git a/kalium b/kalium index 3e3c20c582c..e05d1871202 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 3e3c20c582c9eb1e0a28cce022a6f9dedbb13655 +Subproject commit e05d1871202dbc724a973a6be8cc701d3e877047 From 547a630f4b5f1b82390905c7afd6730e665927e7 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 23 Feb 2026 10:53:11 +0100 Subject: [PATCH 12/22] chore: merge conflicts --- .../android/feature/cells/ui/CellViewModel.kt | 2 + .../cells/ui/search/SearchScreenViewModel.kt | 51 +++++++++---------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index 5a516a8c160..708926b6913 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -66,9 +66,11 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt index 1bc9f6c08b6..d643ddd649b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map -import com.ramcosta.composedestinations.generated.cells.navArgs +import com.ramcosta.composedestinations.generated.cells.destinations.SearchScreenDestination import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.model.toUiModel import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi @@ -49,6 +49,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -71,9 +72,7 @@ class SearchScreenViewModel @Inject constructor( val filesWithPublicLink: Boolean?, ) - private val navArgs: SearchNavArgs = savedStateHandle.navArgs() - -// private val navArgs: SearchNavArgs = SearchScreenDestination.argsFrom(savedStateHandle) + private val navArgs: SearchNavArgs = SearchScreenDestination.argsFrom(savedStateHandle) private val _uiState = MutableStateFlow(SearchUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -95,28 +94,28 @@ class SearchScreenViewModel @Inject constructor( }.distinctUntilChanged() val cellNodesFlow: Flow> = - searchParamsFlow.flatMapLatest { params -> - getCellFilesPaged( - conversationId = navArgs.conversationId, - query = params.query, - fileFilters = FileFilters( - tags = params.tagIds, - owners = params.ownerIds, - mimeTypes = params.mimeTypes, - hasPublicLink = params.filesWithPublicLink - ), - ).map { pagingData -> - pagingData.map { node -> - if (uiState.value.availableOwners.isEmpty()) { - loadOwners(node) - } - when (node) { - is Node.Folder -> node.toUiModel() - is Node.File -> node.toUiModel() - } - } - } - }.cachedIn(viewModelScope) + searchParamsFlow.flatMapLatest> { params: SearchParams -> + getCellFilesPaged( + conversationId = navArgs.conversationId, + query = params.query, + fileFilters = FileFilters( + tags = params.tagIds, + owners = params.ownerIds, + mimeTypes = params.mimeTypes, + hasPublicLink = params.filesWithPublicLink + ), + ).map { pagingData: PagingData -> + pagingData.map { node: Node -> + if (uiState.value.availableOwners.isEmpty()) { + loadOwners(node) + } + when (node) { + is Node.Folder -> node.toUiModel() + is Node.File -> node.toUiModel() + } + } + } + }.cachedIn(viewModelScope) init { loadTags() From 2f58b770f39e3b25396e30635b1f6f404d060666 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 23 Feb 2026 14:03:41 +0100 Subject: [PATCH 13/22] chore: cleanup --- .../ui/common/topappbar/search/SearchTopBar.kt | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt index 6758592b6f3..fc25af14eb2 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt @@ -78,16 +78,6 @@ fun SearchTopBar( ) { val interactionSource = remember { MutableInteractionSource() } -// LaunchedEffect(isSearchActive) { -// if (isSearchActive) { -// focusRequester.requestFocus() -// } else { -// focusManager.clearFocus(force = true) -// // Optional: if you want to clear when leaving active mode -// // searchQueryTextState.clearText() -// } -// } - fun setActive(isActive: Boolean) { if (isActive) { focusRequester.requestFocus() @@ -131,7 +121,7 @@ fun SearchTopBar( } } else { IconButton( - onClick = { onCloseSearchClicked?.invoke() }, + onClick = { onCloseSearchClicked?.invoke() ?: setActive(false) }, modifier = Modifier.size(dimensions().buttonCircleMinSize) ) { Icon( From 9a3c5c63f979d9ce99fcb88439c0cece064f413e Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 25 Feb 2026 10:47:32 +0100 Subject: [PATCH 14/22] chore: cleanup --- .../wire/android/navigation/MainNavHost.kt | 7 ++-- .../transition/LocalSharedTransitionScope.kt | 30 +++++++++++++++++ .../android/ui/common/chip/WireFilterChip.kt | 1 - .../common/textfield/WirePasswordTextField.kt | 1 - .../cells/ui/ConversationFilesScreen.kt | 18 ++--------- ...rsationFilesWithSlideInTransitionScreen.kt | 1 - .../cells/ui/filter/FilterBottomSheet.kt | 2 +- .../feature/cells/ui/search/SearchScreen.kt | 21 +++--------- .../cells/ui/search/SearchScreenViewModel.kt | 6 ++-- .../cells/ui/search/filter/FilterChipsRow.kt | 15 --------- .../bottomsheet/FilterByTypeBottomSheet.kt | 32 ++++++++++++++++--- .../owner/OwnersFilterSheetState.kt | 11 ++++--- .../bottomsheet/tags/TagsFilterSheetState.kt | 8 +++-- .../cells/src/main/res/values/strings.xml | 6 ++-- kalium | 2 +- 15 files changed, 91 insertions(+), 70 deletions(-) create mode 100644 core/navigation/src/main/kotlin/com/wire/android/navigation/transition/LocalSharedTransitionScope.kt diff --git a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt index 27fe7981b54..94ffaf6d45b 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt @@ -19,12 +19,10 @@ package com.wire.android.navigation import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -52,8 +50,8 @@ import com.ramcosta.composedestinations.navigation.navGraph import com.ramcosta.composedestinations.scope.resultBackNavigator import com.ramcosta.composedestinations.scope.resultRecipient import com.ramcosta.composedestinations.spec.Direction -import com.wire.android.feature.cells.ui.LocalSharedTransitionScope import com.wire.android.feature.sketch.model.DrawingCanvasNavBackArgs +import com.wire.android.navigation.transition.LocalSharedTransitionScope import com.wire.android.ui.authentication.login.email.LoginEmailViewModel import com.wire.android.ui.authentication.login.sso.SSOUrlConfigHolder import com.wire.android.ui.authentication.login.sso.SSOUrlConfigHolderImpl @@ -139,7 +137,8 @@ fun MainNavHost( dependency(holder) } - // 👇 To tie TeamMigrationViewModel to PersonalToTeamMigrationNavGraph, making it shared between all screens that belong to it + // 👇 To tie TeamMigrationViewModel to PersonalToTeamMigrationNavGraph, + // making it shared between all screens that belong to it navGraph(PersonalToTeamMigrationGraph) { val parentEntry = remember(navBackStackEntry) { navController.getBackStackEntry(PersonalToTeamMigrationGraph.route) diff --git a/core/navigation/src/main/kotlin/com/wire/android/navigation/transition/LocalSharedTransitionScope.kt b/core/navigation/src/main/kotlin/com/wire/android/navigation/transition/LocalSharedTransitionScope.kt new file mode 100644 index 00000000000..463fb31fe6b --- /dev/null +++ b/core/navigation/src/main/kotlin/com/wire/android/navigation/transition/LocalSharedTransitionScope.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2026 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.navigation.transition + +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.runtime.staticCompositionLocalOf + +@OptIn(ExperimentalSharedTransitionApi::class) +val LocalSharedTransitionScope = + staticCompositionLocalOf { + error("SharedTransitionScope not provided. Wrap NavHost in SharedTransitionLayout.") + } + +const val SHARED_ELEMENT_SEARCH_INPUT_KEY = "search_bar" diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/WireFilterChip.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/WireFilterChip.kt index 703190696e2..967bb0df709 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/WireFilterChip.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/WireFilterChip.kt @@ -114,7 +114,6 @@ fun CountBadge( } } - @MultipleThemePreviews @Composable fun PreviewFilterChip() { diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt index 17f5882fa83..0753949f15d 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt @@ -42,7 +42,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.testTag diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index a913ea9e88d..38b9e59dff5 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image @@ -37,7 +36,6 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.ColorFilter @@ -65,13 +63,14 @@ import com.wire.android.feature.cells.ui.create.file.CreateFileScreenNavArgs import com.wire.android.feature.cells.ui.dialog.CellsNewActionBottomSheet import com.wire.android.feature.cells.ui.dialog.CellsOptionsBottomSheet import com.wire.android.feature.cells.ui.model.CellNodeUi -import com.wire.android.feature.cells.ui.search.sharedElementSearchInputKey import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.PreviewNavigator import com.wire.android.navigation.WireNavigator import com.wire.android.navigation.annotation.features.cells.WireCellsDestination import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.navigation.transition.LocalSharedTransitionScope +import com.wire.android.navigation.transition.SHARED_ELEMENT_SEARCH_INPUT_KEY import com.wire.android.ui.common.MoreOptionIcon import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.bottomsheet.show @@ -92,12 +91,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf -@OptIn(ExperimentalSharedTransitionApi::class) -val LocalSharedTransitionScope = - staticCompositionLocalOf { - error("SharedTransitionScope not provided. Wrap NavHost in SharedTransitionLayout.") - } - /** * Show files in one conversation. * Conversation id is passed to view model via navigation parameters [CellFilesNavArgs]. @@ -255,7 +248,7 @@ fun ConversationFilesScreenContent( SearchTopBar( modifier = Modifier .sharedElement( - sharedContentState = rememberSharedContentState(key = sharedElementSearchInputKey), + sharedContentState = rememberSharedContentState(key = SHARED_ELEMENT_SEARCH_INPUT_KEY), animatedVisibilityScope = animatedVisibilityScope ), isSearchActive = conversationSearchBarState.isSearchActive, @@ -384,7 +377,6 @@ fun ConversationFilesScreenContent( } } - @OptIn(ExperimentalSharedTransitionApi::class) @Composable @MultipleThemePreviews @@ -445,11 +437,7 @@ fun PreviewConversationFilesScreen() { onRefresh = {}, retryEditNodeError = {}, ) - } - - } - } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt index 6e3bc6ad107..23a16533934 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.wire.android.feature.cells.R -import com.ramcosta.composedestinations.generated.cells.destinations.ConversationFilesWithSlideInTransitionScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.RecycleBinScreenDestination import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/filter/FilterBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/filter/FilterBottomSheet.kt index fc43cd19975..bdb718a32a3 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/filter/FilterBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/filter/FilterBottomSheet.kt @@ -60,7 +60,7 @@ import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography -//TODO: To be removed in upcoming PRs when filter feature is fully implemented +// TODO: To be removed in upcoming PRs when filter feature is fully implemented @Composable fun FilterBottomSheet( selectableTags: List, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index 716de51e46b..f8c923eff91 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -49,7 +48,6 @@ import com.ramcosta.composedestinations.generated.cells.destinations.VersionHist import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.CellScreenContent import com.wire.android.feature.cells.ui.CellViewModel -import com.wire.android.feature.cells.ui.LocalSharedTransitionScope import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.search.filter.FilterChipsRow import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FilterByTypeBottomSheet @@ -59,13 +57,12 @@ import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.WireNavigator import com.wire.android.navigation.annotation.features.cells.WireCellsDestination import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.navigation.transition.LocalSharedTransitionScope +import com.wire.android.navigation.transition.SHARED_ELEMENT_SEARCH_INPUT_KEY import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.search.SearchTopBar -import kotlinx.coroutines.delay import kotlinx.coroutines.launch -const val sharedElementSearchInputKey = "search_bar" - @OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class) @WireCellsDestination( style = PopUpNavigationAnimation::class, @@ -75,6 +72,7 @@ const val sharedElementSearchInputKey = "search_bar" fun SearchScreen( navigator: WireNavigator, animatedVisibilityScope: AnimatedVisibilityScope, + modifier: Modifier = Modifier, searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), cellViewModel: CellViewModel = hiltViewModel(), ) { @@ -106,14 +104,6 @@ fun SearchScreen( } val sharedScope = LocalSharedTransitionScope.current - var playScrollHint by remember { mutableStateOf(true) } - - LaunchedEffect(Unit) { - delay(1500) - playScrollHint = true - delay(2000) - playScrollHint = false - } val searchState = remember { TextFieldState() } @@ -122,15 +112,15 @@ fun SearchScreen( .collect { searchScreenViewModel.onSearchQueryChanged(it) } } - with(sharedScope) { WireScaffold( + modifier = modifier, topBar = { Column { SearchTopBar( modifier = Modifier.sharedElement( - sharedContentState = rememberSharedContentState(key = sharedElementSearchInputKey), + sharedContentState = rememberSharedContentState(key = SHARED_ELEMENT_SEARCH_INPUT_KEY), animatedVisibilityScope = animatedVisibilityScope ), isSearchActive = uiState.isSearchActive, @@ -142,7 +132,6 @@ fun SearchScreen( focusManager = focusManager ) FilterChipsRow( - shouldPlayHint = playScrollHint, isSharedByLinkSelected = uiState.filesWithPublicLink, tagsCount = uiState.tagsCount, typeCount = uiState.typeCount, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt index d643ddd649b..81bea23cb8d 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt @@ -55,6 +55,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject // TODO: to cover it with unit test in upcoming PR +@Suppress("TooManyFunctions") @HiltViewModel class SearchScreenViewModel @Inject constructor( val savedStateHandle: SavedStateHandle, @@ -156,7 +157,9 @@ class SearchScreenViewModel @Inject constructor( ) ) } - } else null + } else { + null + } _uiState.update { state -> val existingOwners = state.availableOwners.toMutableList() if (existingOwners.none { it.id == id }) { @@ -240,7 +243,6 @@ class SearchScreenViewModel @Inject constructor( } } - fun onSharedByMeClicked() { _uiState.update { it.copy(filesWithPublicLink = !it.filesWithPublicLink) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/FilterChipsRow.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/FilterChipsRow.kt index 5b662b51de5..35604840592 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/FilterChipsRow.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/FilterChipsRow.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -38,10 +37,6 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.common.typography import com.wire.android.ui.theme.WireTheme -import kotlinx.coroutines.delay - -private const val DELAY_300 = 300L -private const val DELAY_150 = 150L @Composable fun FilterChipsRow( @@ -51,7 +46,6 @@ fun FilterChipsRow( ownerCount: Int, hasAnyFilter: Boolean, modifier: Modifier = Modifier, - shouldPlayHint: Boolean = false, onFilterByTagsClicked: () -> Unit = { }, onFilterByTypeClicked: () -> Unit = { }, onFilterByOwnerClicked: () -> Unit = { }, @@ -60,15 +54,6 @@ fun FilterChipsRow( ) { val scrollState = rememberScrollState() - LaunchedEffect(shouldPlayHint) { - if (shouldPlayHint && scrollState.maxValue > 0) { - delay(DELAY_300) - scrollState.animateScrollTo(80) - delay(DELAY_150) - scrollState.animateScrollTo(0) - } - } - @Composable fun DropdownChip(labelRes: Int, count: Int, onClick: () -> Unit) { WireFilterChip( diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt index a7f3b631b3b..b677a7a758c 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt @@ -194,7 +194,13 @@ private fun FilterRow( @Composable fun PreviewFilterByTypeBottomSheet() { val sampleItems = listOf( - FilterTypeUi(id = "1", label = R.string.filter_images_type, iconRes = android.R.drawable.ic_menu_gallery, selected = true, mimeType = MIMEType.IMAGES), + FilterTypeUi( + id = "1", + label = R.string.filter_images_type, + iconRes = android.R.drawable.ic_menu_gallery, + selected = true, + mimeType = MIMEType.IMAGES + ), FilterTypeUi( id = "2", label = R.string.filter_videos_type, @@ -209,9 +215,27 @@ fun PreviewFilterByTypeBottomSheet() { selected = true, mimeType = MIMEType.DOCUMENT ), - FilterTypeUi(id = "4", label = R.string.filter_audio_type, iconRes = android.R.drawable.ic_media_play, selected = false, mimeType = MIMEType.AUDIOS), - FilterTypeUi(id = "6", label = R.string.filter_spreadsheets_type, iconRes = android.R.drawable.ic_menu_edit, selected = false, mimeType = MIMEType.EXCEL), - FilterTypeUi(id = "7", label = R.string.filter_pdf_type, iconRes = android.R.drawable.ic_menu_view, selected = false, mimeType = MIMEType.PDF), + FilterTypeUi( + id = "4", + label = R.string.filter_audio_type, + iconRes = android.R.drawable.ic_media_play, + selected = false, + mimeType = MIMEType.AUDIOS + ), + FilterTypeUi( + id = "6", + label = R.string.filter_spreadsheets_type, + iconRes = android.R.drawable.ic_menu_edit, + selected = false, + mimeType = MIMEType.EXCEL + ), + FilterTypeUi( + id = "7", + label = R.string.filter_pdf_type, + iconRes = android.R.drawable.ic_menu_view, + selected = false, + mimeType = MIMEType.PDF + ), ) WireTheme { FilterByTypeBottomSheet( diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/OwnersFilterSheetState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/OwnersFilterSheetState.kt index d6939e4bb15..16f33da248c 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/OwnersFilterSheetState.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/OwnersFilterSheetState.kt @@ -43,10 +43,13 @@ class OwnersFilterSheetState( val filteredOwners: List get() { val q = query.trim() - return if (q.isBlank()) owners - else owners.filter { - it.displayName.contains(q, ignoreCase = true) || - it.handle.contains(q, ignoreCase = true) + return if (q.isBlank()) { + owners + } else { + owners.filter { + it.displayName.contains(q, ignoreCase = true) || + it.handle.contains(q, ignoreCase = true) + } } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/TagsFilterSheetState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/TagsFilterSheetState.kt index fa4aedfc795..25234e37eed 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/TagsFilterSheetState.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/TagsFilterSheetState.kt @@ -43,8 +43,12 @@ class TagsFilterSheetState( val filteredTags: List get() { val q = query.trim() - val base = if (q.isBlank()) tags else tags.filter { - it.name.contains(q, ignoreCase = true) + val base = if (q.isBlank()) { + tags + } else { + tags.filter { + it.name.contains(q, ignoreCase = true) + } } return base.sortedWith( compareByDescending { it.selected } diff --git a/features/cells/src/main/res/values/strings.xml b/features/cells/src/main/res/values/strings.xml index d459e5908f0..a186147ca05 100644 --- a/features/cells/src/main/res/values/strings.xml +++ b/features/cells/src/main/res/values/strings.xml @@ -216,10 +216,10 @@ Remove all Remove filter Search Shared Drive - Search owner + Search Created by Tags - Type - Owner + File type + Created by Link Sharing Remove all filters PDFs diff --git a/kalium b/kalium index e05d1871202..5ccf19c2715 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e05d1871202dbc724a973a6be8cc701d3e877047 +Subproject commit 5ccf19c2715ede23f10b04cd4b58ef40f9ee9752 From d914ed455e27328cd67c451ef9bfc77321226a68 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 26 Feb 2026 09:33:47 +0100 Subject: [PATCH 15/22] fix: pausing/jumping bottomsheetmodal --- .../feature/cells/ui/search/SearchScreen.kt | 18 ++++++++------- .../bottomsheet/FilterByTypeBottomSheet.kt | 8 +++---- .../owner/FilterByOwnerBottomSheet.kt | 22 +++---------------- .../tags/FilterByTagsBottomSheet.kt | 12 ++-------- 4 files changed, 19 insertions(+), 41 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index f8c923eff91..574831ef5a2 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -61,6 +62,7 @@ import com.wire.android.navigation.transition.LocalSharedTransitionScope import com.wire.android.navigation.transition.SHARED_ELEMENT_SEARCH_INPUT_KEY import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.search.SearchTopBar +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class) @@ -79,27 +81,28 @@ fun SearchScreen( val scope = rememberCoroutineScope() val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current - cellViewModel.isSearchByDefaultActive val uiState by searchScreenViewModel.uiState.collectAsStateWithLifecycle() val filterTypeSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val filterTagsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val filterOwnerSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val keyboardController = LocalSoftwareKeyboardController.current + fun closeSheet(sheetState: SheetState, onCloseFlag: () -> Unit) { scope.launch { sheetState.hide() onCloseFlag() - searchScreenViewModel.onSetSearchActive(true) } } fun openSheet(onOpenFlag: () -> Unit = { }) { scope.launch { - focusManager.clearFocus() + focusManager.clearFocus(force = true) + keyboardController?.hide() + delay(300) onOpenFlag() - searchScreenViewModel.onSetSearchActive(false) } } @@ -221,7 +224,7 @@ fun SearchScreen( if (uiState.showFilterByTags) { FilterByTagsBottomSheet( - items = searchScreenViewModel.uiState.collectAsState().value.availableTags, + items = uiState.availableTags, sheetState = filterTagsSheetState, onDismiss = { closeSheet( @@ -244,7 +247,7 @@ fun SearchScreen( if (uiState.showFilterByType) { FilterByTypeBottomSheet( - items = searchScreenViewModel.uiState.collectAsState().value.availableTypes, + items = uiState.availableTypes, sheetState = filterTypeSheetState, onDismiss = { closeSheet( @@ -268,7 +271,7 @@ fun SearchScreen( if (uiState.showFilterByOwner) { FilterByOwnerBottomSheet( - items = searchScreenViewModel.uiState.collectAsState().value.availableOwners, + items = uiState.availableOwners, sheetState = filterOwnerSheetState, onDismiss = { closeSheet( @@ -279,7 +282,6 @@ fun SearchScreen( onSave = { selectedItems -> searchScreenViewModel.onSaveOwners(selectedItems) - closeSheet( sheetState = filterOwnerSheetState, onCloseFlag = { searchScreenViewModel.onCloseOwnerSheet() } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt index b677a7a758c..862293c4fd3 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -69,7 +69,7 @@ fun FilterByTypeBottomSheet( onRemoveFilter: () -> Unit, modifier: Modifier = Modifier ) { - var itemsState by remember { mutableStateOf(items) } + var itemsState by remember(items) { mutableStateOf(items) } val hasChanges = itemsState.any { tag -> val initial = items.first { it.id == tag.id } @@ -84,7 +84,7 @@ fun FilterByTypeBottomSheet( Column( modifier = Modifier .fillMaxWidth() - .height(dimensions().spacing700x) + .fillMaxHeight(0.8f) .padding(bottom = dimensions().spacing16x) ) { Text( @@ -160,7 +160,7 @@ private fun FilterRow( ) { Row( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .clickable { onCheckedChange(!checked) } .padding( horizontal = dimensions().spacing16x, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt index 180a8223273..35f32b31b90 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt @@ -22,16 +22,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.input.TextFieldState @@ -54,7 +47,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi import com.wire.android.model.UserAvatarData @@ -84,7 +76,6 @@ fun FilterByOwnerBottomSheet( val scope = rememberCoroutineScope() val state = rememberOwnersFilterSheetState(items) - LaunchedEffect(sheetState) { sheetState.show() } val searchState = remember { TextFieldState() } LaunchedEffect(searchState) { @@ -92,8 +83,6 @@ fun FilterByOwnerBottomSheet( .collect(state::onQueryChange) } - LaunchedEffect(sheetState) { sheetState.show() } - fun dismiss() { scope.launch { sheetState.hide() } .invokeOnCompletion { onDismiss() } @@ -103,16 +92,11 @@ fun FilterByOwnerBottomSheet( onDismissRequest = ::dismiss, sheetState = sheetState, modifier = modifier - .absoluteOffset(y = 1.dp) - .statusBarsPadding(), - contentWindowInsets = { WindowInsets.navigationBars }, ) { Column( modifier = Modifier .fillMaxWidth() - .heightIn(max = dimensions().spacing700x) - .navigationBarsPadding() - .imePadding() + .fillMaxHeight(0.8f), ) { Text( text = stringResource(R.string.bottom_sheet_title_filter_by_owner), @@ -197,7 +181,7 @@ private fun OwnerRow( ) { Row( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .clickable { onToggle() } .padding( start = dimensions().spacing12x, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt index fc9910c79f6..9dc2e05e64a 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt @@ -23,11 +23,9 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.input.TextFieldState @@ -77,10 +75,6 @@ fun FilterByTagsBottomSheet( val state = rememberTagsFilterSheetState(items) - LaunchedEffect(Unit) { - sheetState.show() - } - val searchState = remember { TextFieldState() } LaunchedEffect(searchState) { snapshotFlow { searchState.text.toString() } @@ -99,9 +93,7 @@ fun FilterByTagsBottomSheet( Column( modifier = Modifier .fillMaxWidth() - .heightIn(max = dimensions().spacing700x) - .navigationBarsPadding() - .imePadding() + .fillMaxHeight(0.8f) .padding(horizontal = dimensions().spacing16x) ) { Text( From 9876eafd9d36318f9a92c9c6b04672b9c4715d07 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 26 Feb 2026 09:58:59 +0100 Subject: [PATCH 16/22] fix: pausing/jumping bottomsheetmodal --- .../android/feature/cells/ui/search/SearchScreen.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index 574831ef5a2..eecc4ff178d 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -20,6 +20,9 @@ package com.wire.android.feature.cells.ui.search import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api @@ -65,7 +68,7 @@ import com.wire.android.ui.common.topappbar.search.SearchTopBar import kotlinx.coroutines.delay import kotlinx.coroutines.launch -@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @WireCellsDestination( style = PopUpNavigationAnimation::class, navArgs = SearchNavArgs::class, @@ -88,6 +91,7 @@ fun SearchScreen( val filterTagsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val filterOwnerSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val keyboardController = LocalSoftwareKeyboardController.current + val isImeVisible = WindowInsets.isImeVisible fun closeSheet(sheetState: SheetState, onCloseFlag: () -> Unit) { @@ -100,8 +104,10 @@ fun SearchScreen( fun openSheet(onOpenFlag: () -> Unit = { }) { scope.launch { focusManager.clearFocus(force = true) - keyboardController?.hide() - delay(300) + if (isImeVisible) { + keyboardController?.hide() + delay(300) + } onOpenFlag() } } From d4c7860233ed9374927f739fd159e0d9552c77b7 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 26 Feb 2026 11:56:30 +0100 Subject: [PATCH 17/22] chore: duplication --- .../bottomsheet/FilterByTypeBottomSheet.kt | 43 +++------- .../filter/bottomsheet/FooterButtons.kt | 80 +++++++++++++++++++ .../owner/FilterByOwnerBottomSheet.kt | 42 +++------- .../tags/FilterByTagsBottomSheet.kt | 38 +++------ 4 files changed, 108 insertions(+), 95 deletions(-) create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FooterButtons.kt diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt index 862293c4fd3..ee3a0019219 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt @@ -19,14 +19,12 @@ package com.wire.android.feature.cells.ui.search.filter.bottomsheet import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -50,9 +48,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.search.filter.data.FilterTypeUi -import com.wire.android.ui.common.button.WireButtonState -import com.wire.android.ui.common.button.WirePrimaryButton -import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.common.typography @@ -118,35 +113,15 @@ fun FilterByTypeBottomSheet( HorizontalDivider() } } - - Spacer(Modifier.height(dimensions().spacing12x)) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = dimensions().spacing16x), - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x) - ) { - WireSecondaryButton( - text = stringResource(R.string.button_remove_filter), - onClick = { - itemsState = itemsState.map { it.copy(selected = false) } - onRemoveFilter() - }, - modifier = Modifier.weight(1f), - contentPadding = PaddingValues(vertical = dimensions().spacing14x) - ) - - WirePrimaryButton( - text = stringResource(R.string.save_label), - onClick = { onSave(itemsState) }, - modifier = Modifier.weight(1f), - state = if (hasChanges) WireButtonState.Default else WireButtonState.Disabled, - contentPadding = PaddingValues(vertical = dimensions().spacing14x) - ) - } - - Spacer(Modifier.height(dimensions().spacing8x)) + FooterButtons( + modifier = Modifier.padding(horizontal = dimensions().spacing16x), + onRemoveAll = { + itemsState = itemsState.map { it.copy(selected = false) } + onRemoveFilter() + }, + onSave = { onSave(itemsState) }, + hasChanges = hasChanges + ) } } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FooterButtons.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FooterButtons.kt new file mode 100644 index 00000000000..744d9eaa2a4 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FooterButtons.kt @@ -0,0 +1,80 @@ +/* + * Wire + * Copyright (C) 2026 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.feature.cells.ui.search.filter.bottomsheet + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.wire.android.feature.cells.R +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews + +@Composable +fun FooterButtons( + onRemoveAll: () -> Unit, + onSave: () -> Unit, + modifier: Modifier = Modifier, + hasChanges: Boolean = false +) { + Column { + Spacer(modifier.height(dimensions().spacing12x)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = dimensions().spacing12x), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x) + ) { + WireSecondaryButton( + text = stringResource(R.string.button_remove_all_label), + onClick = onRemoveAll, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(vertical = dimensions().spacing14x) + ) + + WirePrimaryButton( + text = stringResource(R.string.save_label), + onClick = onSave, + modifier = Modifier.weight(1f), + state = if (hasChanges) WireButtonState.Default else WireButtonState.Disabled, + contentPadding = PaddingValues(vertical = dimensions().spacing14x) + ) + } + Spacer(Modifier.height(dimensions().spacing8x)) + } +} + +@MultipleThemePreviews +@Composable +fun FooterButtonsPreview() { + FooterButtons( + onRemoveAll = {}, + onSave = {}, + hasChanges = true + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt index 35f32b31b90..54cac142578 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt @@ -18,7 +18,6 @@ package com.wire.android.feature.cells.ui.search.filter.bottomsheet.owner import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -48,13 +47,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FooterButtons import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.SearchBarInput import com.wire.android.ui.common.avatar.UserProfileAvatar -import com.wire.android.ui.common.button.WireButtonState -import com.wire.android.ui.common.button.WirePrimaryButton -import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.common.typography @@ -142,34 +139,15 @@ fun FilterByOwnerBottomSheet( } } - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - start = dimensions().spacing16x, - end = dimensions().spacing16x, - bottom = dimensions().spacing12x - ), - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x) - ) { - WireSecondaryButton( - text = stringResource(R.string.button_remove_all_label), - onClick = { - state.removeAll() - onRemoveAll() - }, - modifier = Modifier.weight(1f), - contentPadding = PaddingValues(vertical = dimensions().spacing14x) - ) - - WirePrimaryButton( - text = stringResource(R.string.save_label), - onClick = { onSave(state.selectedOwners()) }, - modifier = Modifier.weight(1f), - state = if (state.hasChanges) WireButtonState.Default else WireButtonState.Disabled, - contentPadding = PaddingValues(vertical = dimensions().spacing14x) - ) - } + FooterButtons( + modifier = Modifier.padding(horizontal = dimensions().spacing16x), + onRemoveAll = { + state.removeAll() + onRemoveAll() + }, + onSave = { onSave(state.selectedOwners()) }, + hasChanges = state.hasChanges + ) } } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt index 9dc2e05e64a..f75300a9e89 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt @@ -20,8 +20,6 @@ package com.wire.android.feature.cells.ui.search.filter.bottomsheet.tags import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -46,11 +44,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FooterButtons import com.wire.android.feature.cells.ui.search.filter.data.FilterTagUi import com.wire.android.ui.common.SearchBarInput -import com.wire.android.ui.common.button.WireButtonState -import com.wire.android.ui.common.button.WirePrimaryButton -import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.chip.WireFilterChip import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews @@ -145,30 +141,14 @@ fun FilterByTagsBottomSheet( Spacer(Modifier.height(dimensions().spacing12x)) } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = dimensions().spacing12x), - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x) - ) { - WireSecondaryButton( - text = stringResource(R.string.button_remove_all_label), - onClick = { - state.removeAll() - onRemoveAll() - }, - modifier = Modifier.weight(1f), - contentPadding = PaddingValues(vertical = dimensions().spacing14x) - ) - - WirePrimaryButton( - text = stringResource(R.string.save_label), - onClick = { onSave(state.selectedTags()) }, - modifier = Modifier.weight(1f), - state = if (state.hasChanges) WireButtonState.Default else WireButtonState.Disabled, - contentPadding = PaddingValues(vertical = dimensions().spacing14x) - ) - } + FooterButtons( + onRemoveAll = { + state.removeAll() + onRemoveAll() + }, + onSave = { onSave(state.selectedTags()) }, + hasChanges = state.hasChanges + ) } } } From 87ba1d8ddb018baa1e80abbbacde8c100bfdc791 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 26 Feb 2026 12:23:06 +0100 Subject: [PATCH 18/22] feat: fetch owners from use case --- .../android/di/accountScoped/CellsModule.kt | 5 + .../feature/cells/ui/search/SearchScreen.kt | 6 +- .../cells/ui/search/SearchScreenViewModel.kt | 130 +++++++++--------- .../feature/cells/ui/search/SearchUiState.kt | 6 +- 4 files changed, 78 insertions(+), 69 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt index f7be4f575d5..d9b623cfe11 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt @@ -30,6 +30,7 @@ import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.cells.domain.usecase.GetFoldersUseCase import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase +import com.wire.kalium.cells.domain.usecase.GetOwnersUseCase import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase import com.wire.kalium.cells.domain.usecase.GetPaginatedNodesUseCase import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase @@ -197,6 +198,10 @@ class CellsModule { @Provides fun provideGetAttachmentUseCase(cellsScope: CellsScope): GetMessageAttachmentUseCase = cellsScope.getMessageAttachmentUseCase + @ViewModelScoped + @Provides + fun provideGetOwnersUseCase(cellsScope: CellsScope): GetOwnersUseCase = cellsScope.getOwnersUseCase + @Provides fun provideFileNameResolver(): FileNameResolver = FileNameResolver() diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index eecc4ff178d..835ed5dd532 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -228,7 +228,7 @@ fun SearchScreen( ) } - if (uiState.showFilterByTags) { + if (uiState.showFilterByTagsBottomSheet) { FilterByTagsBottomSheet( items = uiState.availableTags, sheetState = filterTagsSheetState, @@ -251,7 +251,7 @@ fun SearchScreen( ) } - if (uiState.showFilterByType) { + if (uiState.showFilterByTypeBottomSheet) { FilterByTypeBottomSheet( items = uiState.availableTypes, sheetState = filterTypeSheetState, @@ -275,7 +275,7 @@ fun SearchScreen( ) } - if (uiState.showFilterByOwner) { + if (uiState.showFilterByOwnerBottomSheet) { FilterByOwnerBottomSheet( items = uiState.availableOwners, sheetState = filterOwnerSheetState, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt index 81bea23cb8d..a26de7cc1ac 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt @@ -34,13 +34,11 @@ import com.wire.kalium.cells.data.FileFilters import com.wire.kalium.cells.data.MIMEType import com.wire.kalium.cells.domain.model.Node import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase +import com.wire.kalium.cells.domain.usecase.GetOwnersUseCase +import com.wire.kalium.cells.domain.usecase.GetOwnersUseCaseResult import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase import com.wire.kalium.common.functional.onSuccess -import com.wire.kalium.logic.data.id.QualifiedIdMapper -import com.wire.kalium.logic.data.id.toQualifiedID import com.wire.kalium.logic.data.user.UserAssetId -import com.wire.kalium.logic.feature.user.GetUserInfoResult -import com.wire.kalium.logic.feature.user.GetUserInfoUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -59,10 +57,9 @@ import javax.inject.Inject @HiltViewModel class SearchScreenViewModel @Inject constructor( val savedStateHandle: SavedStateHandle, - private val qualifiedIdMapper: QualifiedIdMapper, private val getAllTagsUseCase: GetAllTagsUseCase, - private val getUserInfo: GetUserInfoUseCase, private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, + private val getOwners: GetOwnersUseCase, ) : ViewModel() { private data class SearchParams( @@ -96,30 +93,28 @@ class SearchScreenViewModel @Inject constructor( val cellNodesFlow: Flow> = searchParamsFlow.flatMapLatest> { params: SearchParams -> - getCellFilesPaged( - conversationId = navArgs.conversationId, - query = params.query, - fileFilters = FileFilters( - tags = params.tagIds, - owners = params.ownerIds, - mimeTypes = params.mimeTypes, - hasPublicLink = params.filesWithPublicLink - ), - ).map { pagingData: PagingData -> - pagingData.map { node: Node -> - if (uiState.value.availableOwners.isEmpty()) { - loadOwners(node) - } - when (node) { - is Node.Folder -> node.toUiModel() - is Node.File -> node.toUiModel() - } - } - } - }.cachedIn(viewModelScope) + getCellFilesPaged( + conversationId = navArgs.conversationId, + query = params.query, + fileFilters = FileFilters( + tags = params.tagIds, + owners = params.ownerIds, + mimeTypes = params.mimeTypes, + hasPublicLink = params.filesWithPublicLink + ), + ).map { pagingData: PagingData -> + pagingData.map { node: Node -> + when (node) { + is Node.Folder -> node.toUiModel() + is Node.File -> node.toUiModel() + } + } + } + }.cachedIn(viewModelScope) init { loadTags() + loadOwners() } internal fun loadTags() = viewModelScope.launch { @@ -137,66 +132,75 @@ class SearchScreenViewModel @Inject constructor( } } - fun onSearchQueryChanged(query: String) { - queryFlow.value = query - } + fun loadOwners(conversationId: String? = navArgs.conversationId) { + viewModelScope.launch { + when (val result = getOwners(conversationId = conversationId)) { + is GetOwnersUseCaseResult.Success -> { + val ownersUi = result.owners.mapNotNull { owner -> + val name = owner.name?.takeIf { it.isNotBlank() } + val handle = owner.handle?.takeIf { it.isNotBlank() } + if (name == null || handle == null) return@mapNotNull null + + val picture = owner.completePicture ?: owner.previewPicture + val avatarAsset = picture?.let { pic -> + ImageAsset.UserAvatarAsset( + UserAssetId( + value = pic.value, + domain = pic.domain + ) + ) + } - fun loadOwners(node: Node) = viewModelScope.launch { - val id = node.ownerUserId - val name = node.userName - val handle = node.userHandle - if (id != null && name != null && handle != null) { - val userInfo = getUserInfo(id.toQualifiedID(qualifiedIdMapper)) - - val userAvatarAsset = if (userInfo is GetUserInfoResult.Success) { - userInfo.otherUser.completePicture?.let { - ImageAsset.UserAvatarAsset( - UserAssetId( - it.value, - it.domain, + FilterOwnerUi( + id = owner.id.value, + displayName = name, + handle = handle, + userAvatarAsset = avatarAsset, + selected = false ) - ) + } + .sortedBy { it.displayName.uppercase() } + + _uiState.update { state -> + state.copy( + availableOwners = ownersUi + ) + } } - } else { - null - } - _uiState.update { state -> - val existingOwners = state.availableOwners.toMutableList() - if (existingOwners.none { it.id == id }) { - existingOwners += FilterOwnerUi( - id = id, - displayName = name, - handle = handle, - userAvatarAsset = userAvatarAsset - ) + + is GetOwnersUseCaseResult.Failure -> { + // no need to show error, just keep the owners list empty } - state.copy(availableOwners = existingOwners.sortedBy { it.displayName.uppercase() }) } } } + fun onSearchQueryChanged(query: String) { + queryFlow.value = query + } + fun onFilterByTypeClicked() { - _uiState.update { it.copy(showFilterByType = true) } + _uiState.update { it.copy(showFilterByTypeBottomSheet = true) } } fun onCloseTypeSheet() { - _uiState.update { it.copy(showFilterByType = false) } + _uiState.update { it.copy(showFilterByTypeBottomSheet = false) } } fun onFilterByTagsClicked() { - _uiState.update { it.copy(showFilterByTags = true) } + _uiState.update { it.copy(showFilterByTagsBottomSheet = true) } } fun onCloseTagsSheet() { - _uiState.update { it.copy(showFilterByTags = false) } + _uiState.update { it.copy(showFilterByTagsBottomSheet = false) } } fun onFilterByOwnerClicked() { - _uiState.update { it.copy(showFilterByOwner = true) } + _uiState.update { it.copy(showFilterByOwnerBottomSheet = true) } } fun onCloseOwnerSheet() { - _uiState.update { it.copy(showFilterByOwner = false) } + _uiState.update { it.copy(showFilterByOwnerBottomSheet = false) } } fun onSetSearchActive(active: Boolean) { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt index b0a42437c98..9266ff73851 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt @@ -27,9 +27,9 @@ data class SearchUiState( val availableOwners: List = emptyList(), val availableTypes: List = TypeFilter.typeItems, - val showFilterByType: Boolean = false, - val showFilterByTags: Boolean = false, - val showFilterByOwner: Boolean = false, + val showFilterByTypeBottomSheet: Boolean = false, + val showFilterByTagsBottomSheet: Boolean = false, + val showFilterByOwnerBottomSheet: Boolean = false, val filesWithPublicLink: Boolean = false, From d64f52581d790219ca31422567e8f594edcb86fd Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 26 Feb 2026 13:26:28 +0100 Subject: [PATCH 19/22] feat: add use case to fetch owners --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 5ccf19c2715..b1a4e30c9a0 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5ccf19c2715ede23f10b04cd4b58ef40f9ee9752 +Subproject commit b1a4e30c9a0ad9063bff4c4369ac842f8da9967a From fd7a0e7216954daea415c05b14a040f418f4a2b2 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 26 Feb 2026 13:26:54 +0100 Subject: [PATCH 20/22] feat: add archive and text file types --- .../bottomsheet/FilterByTypeBottomSheet.kt | 6 +- .../cells/ui/search/filter/data/TypeFilter.kt | 20 +++++- .../main/res/drawable/ic_file_type_text.xml | 64 +++++++++++++++++++ .../cells/src/main/res/values/strings.xml | 3 +- kalium | 2 +- 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 features/cells/src/main/res/drawable/ic_file_type_text.xml diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt index ee3a0019219..c6eff9e1655 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt @@ -174,14 +174,14 @@ fun PreviewFilterByTypeBottomSheet() { label = R.string.filter_images_type, iconRes = android.R.drawable.ic_menu_gallery, selected = true, - mimeType = MIMEType.IMAGES + mimeType = MIMEType.IMAGE ), FilterTypeUi( id = "2", label = R.string.filter_videos_type, iconRes = android.R.drawable.ic_menu_slideshow, selected = false, - mimeType = MIMEType.VIDEOS + mimeType = MIMEType.VIDEO ), FilterTypeUi( id = "3", @@ -195,7 +195,7 @@ fun PreviewFilterByTypeBottomSheet() { label = R.string.filter_audio_type, iconRes = android.R.drawable.ic_media_play, selected = false, - mimeType = MIMEType.AUDIOS + mimeType = MIMEType.AUDIO ), FilterTypeUi( id = "6", diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt index 9bad6bd1011..20950086b73 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/data/TypeFilter.kt @@ -42,7 +42,7 @@ fun MIMEType.toFilterTypeUi(): FilterTypeUi = mimeType = this ) - MIMEType.IMAGES -> FilterTypeUi( + MIMEType.IMAGE -> FilterTypeUi( id = name, label = R.string.filter_images_type, iconRes = R.drawable.ic_file_type_image, @@ -63,17 +63,31 @@ fun MIMEType.toFilterTypeUi(): FilterTypeUi = mimeType = this ) - MIMEType.VIDEOS -> FilterTypeUi( + MIMEType.VIDEO -> FilterTypeUi( id = name, label = R.string.filter_videos_type, iconRes = R.drawable.ic_file_type_video, mimeType = this ) - MIMEType.AUDIOS -> FilterTypeUi( + MIMEType.AUDIO -> FilterTypeUi( id = name, label = R.string.filter_audio_type, iconRes = R.drawable.ic_file_type_audio, mimeType = this ) + + MIMEType.ARCHIVE -> FilterTypeUi( + id = name, + label = R.string.filter_archives_type, + iconRes = R.drawable.ic_file_type_archive, + mimeType = this + ) + + MIMEType.TEXT -> FilterTypeUi( + id = name, + label = R.string.filter_text_files_type, + iconRes = R.drawable.ic_file_type_text, + mimeType = this + ) } diff --git a/features/cells/src/main/res/drawable/ic_file_type_text.xml b/features/cells/src/main/res/drawable/ic_file_type_text.xml new file mode 100644 index 00000000000..f6883ae0529 --- /dev/null +++ b/features/cells/src/main/res/drawable/ic_file_type_text.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/features/cells/src/main/res/values/strings.xml b/features/cells/src/main/res/values/strings.xml index a186147ca05..3d4628d5945 100644 --- a/features/cells/src/main/res/values/strings.xml +++ b/features/cells/src/main/res/values/strings.xml @@ -229,5 +229,6 @@ Presentations Videos Audio files - Other + Archives + Text files diff --git a/kalium b/kalium index b1a4e30c9a0..aca1b206de4 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b1a4e30c9a0ad9063bff4c4369ac842f8da9967a +Subproject commit aca1b206de42d161679e976328b9f42e126d4662 From fbcbf7a0c7b69cacf22306168fa0053c705eb615 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 4 Mar 2026 13:13:20 +0100 Subject: [PATCH 21/22] chore: address comments --- .../com/wire/android/ui/home/HomeScreen.kt | 18 +- .../channels/BrowseChannelsScreen.kt | 6 +- .../search/SearchUsersAndAppsScreen.kt | 2 - .../SearchConversationMessagesScreen.kt | 9 +- .../android/ui/sharing/ImportMediaScreen.kt | 11 +- .../common/topappbar/search/SearchTopBar.kt | 6 +- .../cells/ui/ConversationFilesScreen.kt | 3 - .../feature/cells/ui/search/SearchScreen.kt | 158 ++++++------------ .../cells/ui/search/SearchScreenViewModel.kt | 28 ---- .../feature/cells/ui/search/SearchUiState.kt | 4 - .../bottomsheet/FilterByTypeBottomSheet.kt | 13 +- .../owner/FilterByOwnerBottomSheet.kt | 15 +- .../tags/FilterByTagsBottomSheet.kt | 13 +- 13 files changed, 91 insertions(+), 195 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 74658b48c33..f2ee7d88276 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home -import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi @@ -50,7 +49,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale @@ -64,6 +62,13 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.ramcosta.composedestinations.DestinationsNavHost +import com.ramcosta.composedestinations.generated.app.destinations.ConversationFoldersScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.NewConversationSearchPeopleScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.SelfUserProfileScreenDestination +import com.ramcosta.composedestinations.generated.app.navgraphs.HomeGraph +import com.ramcosta.composedestinations.generated.app.navgraphs.WireRootGraph import com.ramcosta.composedestinations.navigation.dependency import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient @@ -73,10 +78,9 @@ import com.wire.android.navigation.HomeDestination import com.wire.android.navigation.HomeDestination.FabOptions import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator +import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.navigation.handleNavigation import com.wire.android.navigation.rememberWireNavHostEngine -import com.ramcosta.composedestinations.generated.app.navgraphs.HomeGraph -import com.ramcosta.composedestinations.generated.app.navgraphs.WireRootGraph import com.wire.android.ui.analytics.AnalyticsUsageViewModel import com.wire.android.ui.common.CollapsingTopBarScaffold import com.wire.android.ui.common.HandleActions @@ -86,11 +90,6 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.search.SearchTopBar import com.wire.android.ui.common.visbility.rememberVisibilityState -import com.ramcosta.composedestinations.generated.app.destinations.ConversationFoldersScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.NewConversationSearchPeopleScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.SelfUserProfileScreenDestination import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState import com.wire.android.ui.home.conversations.details.GroupConversationActionType import com.wire.android.ui.home.conversations.details.GroupConversationDetailsNavBackArgs @@ -346,7 +345,6 @@ fun HomeContent( searchBarHint = stringResource(searchBar.hint), searchQueryTextState = searchBarState.searchQueryTextState, onActiveChanged = searchBarState::searchActiveChanged, - focusRequester = remember { FocusRequester() } ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/channels/BrowseChannelsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/channels/BrowseChannelsScreen.kt index 37ecb244a88..b7de961494e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/channels/BrowseChannelsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/channels/BrowseChannelsScreen.kt @@ -17,18 +17,16 @@ */ package com.wire.android.ui.home.conversations.channels -import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.res.stringResource import com.wire.android.R import com.wire.android.navigation.Navigator +import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.rememberTopBarElevationState import com.wire.android.ui.common.scaffold.WireScaffold @@ -58,7 +56,6 @@ private fun Content( modifier: Modifier = Modifier ) { val lazyListState = rememberLazyListState() - val focusRequester = remember { FocusRequester() } WireScaffold( modifier = modifier, topBar = { @@ -73,7 +70,6 @@ private fun Content( searchBarHint = stringResource(id = R.string.label_search_public_channels), searchQueryTextState = searchQueryTextState, isLoading = false, - focusRequester = focusRequester, ) } }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt index 4c93f992f76..87016582290 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt @@ -141,7 +141,6 @@ fun SearchUsersAndAppsScreen( } }, topBarCollapsing = { - val focusRequester = remember { FocusRequester() } SearchTopBar( isSearchActive = searchBarState.isSearchActive, searchBarHint = searchBarTitle, @@ -149,7 +148,6 @@ fun SearchUsersAndAppsScreen( searchBarDescription = stringResource(R.string.content_description_add_participants_search_field), searchQueryTextState = searchBarState.searchQueryTextState, onActiveChanged = searchBarState::searchActiveChanged, - focusRequester = focusRequester, ) }, topBarFooter = { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt index b6b4cb350df..da89a6f9fb6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.home.conversations.search.messages -import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState @@ -26,25 +25,24 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems -import com.wire.android.R +import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.ConversationFilesScreenDestination +import com.wire.android.R 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.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.search.SearchTopBar -import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme @@ -120,7 +118,6 @@ fun SearchConversationMessagesResultContent( searchQueryTextState = searchQueryTextState, onCloseSearchClicked = onCloseSearchClicked, isLoading = state.isLoading, - focusRequester = remember { FocusRequester() }, ) } if (isCellsConversation) { diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index c341c170684..d6c3e9511a1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt @@ -20,7 +20,6 @@ package com.wire.android.ui.sharing -import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -51,7 +50,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -59,6 +57,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems +import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset @@ -68,6 +69,7 @@ import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.LoginTypeSelector 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.common.avatar.UserProfileAvatar import com.wire.android.ui.common.bottomsheet.WireMenuModalSheetContent import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout @@ -84,9 +86,6 @@ 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.common.topappbar.search.SearchTopBar -import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination import com.wire.android.ui.home.FeatureFlagState import com.wire.android.ui.home.conversations.AssetTooLargeDialog import com.wire.android.ui.home.conversations.ConversationNavArgs @@ -527,7 +526,6 @@ fun ImportMediaTopBarContent( thickness = 1.dp, modifier = Modifier.padding(top = dimensions().spacing12x) ) - val focusRequester = remember { FocusRequester() } SearchTopBar( isSearchActive = searchBarState.isSearchActive, searchBarHint = stringResource( @@ -536,7 +534,6 @@ fun ImportMediaTopBarContent( ), searchQueryTextState = searchQueryTextState, onActiveChanged = searchBarState::searchActiveChanged, - focusRequester = focusRequester, ) } } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt index fc25af14eb2..019b9e51c29 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt @@ -65,7 +65,6 @@ fun SearchTopBar( isSearchActive: Boolean, searchBarHint: String, searchQueryTextState: TextFieldState, - focusRequester: FocusRequester, modifier: Modifier = Modifier, isLoading: Boolean = false, backIconContentDescription: String? = null, @@ -74,9 +73,10 @@ fun SearchTopBar( onActiveChanged: (isActive: Boolean) -> Unit = {}, bottomContent: @Composable ColumnScope.() -> Unit = {}, onTap: (() -> Unit)? = null, - focusManager: FocusManager = LocalFocusManager.current, ) { val interactionSource = remember { MutableInteractionSource() } + val focusManager: FocusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } fun setActive(isActive: Boolean) { if (isActive) { @@ -168,7 +168,6 @@ fun PreviewSearchTopBarActive() { searchBarHint = "Search", searchQueryTextState = rememberTextFieldState(), onActiveChanged = {}, - focusRequester = remember { FocusRequester() } ) } } @@ -182,7 +181,6 @@ fun PreviewSearchTopBarInactive() { searchBarHint = "Search", searchQueryTextState = rememberTextFieldState(), onActiveChanged = {}, - focusRequester = remember { FocusRequester() } ) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index 38b9e59dff5..00bbc58c7f3 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -242,7 +241,6 @@ fun ConversationFilesScreenContent( ) val sharedScope = LocalSharedTransitionScope.current - val focusRequester = remember { FocusRequester() } with(sharedScope) { SearchTopBar( @@ -264,7 +262,6 @@ fun ConversationFilesScreenContent( ) } }, - focusRequester = focusRequester, ) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index 835ed5dd532..3e999325199 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -26,20 +26,14 @@ import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetState -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -63,10 +57,10 @@ import com.wire.android.navigation.annotation.features.cells.WireCellsDestinatio import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.navigation.transition.LocalSharedTransitionScope import com.wire.android.navigation.transition.SHARED_ELEMENT_SEARCH_INPUT_KEY +import com.wire.android.ui.common.bottomsheet.WireSheetValue +import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.search.SearchTopBar -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @WireCellsDestination( @@ -81,36 +75,14 @@ fun SearchScreen( searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), cellViewModel: CellViewModel = hiltViewModel(), ) { - val scope = rememberCoroutineScope() - val focusRequester = remember { FocusRequester() } - val focusManager = LocalFocusManager.current val uiState by searchScreenViewModel.uiState.collectAsStateWithLifecycle() - val filterTypeSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val filterTagsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val filterOwnerSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val keyboardController = LocalSoftwareKeyboardController.current - val isImeVisible = WindowInsets.isImeVisible - - - fun closeSheet(sheetState: SheetState, onCloseFlag: () -> Unit) { - scope.launch { - sheetState.hide() - onCloseFlag() - } - } + val filterTypeSheetState = rememberWireModalSheetState(WireSheetValue.Hidden) + val filterTagsSheetState = rememberWireModalSheetState(WireSheetValue.Hidden) + val filterOwnerSheetState = rememberWireModalSheetState(WireSheetValue.Hidden) - fun openSheet(onOpenFlag: () -> Unit = { }) { - scope.launch { - focusManager.clearFocus(force = true) - if (isImeVisible) { - keyboardController?.hide() - delay(300) - } - onOpenFlag() - } - } + val isImeVisible = WindowInsets.isImeVisible val sharedScope = LocalSharedTransitionScope.current @@ -137,8 +109,6 @@ fun SearchScreen( searchQueryTextState = searchState, onCloseSearchClicked = { navigator.navigateBack() }, onActiveChanged = { }, - focusRequester = focusRequester, - focusManager = focusManager ) FilterChipsRow( isSharedByLinkSelected = uiState.filesWithPublicLink, @@ -147,13 +117,13 @@ fun SearchScreen( ownerCount = uiState.ownerCount, hasAnyFilter = uiState.hasAnyFilter, onFilterByTagsClicked = { - openSheet { searchScreenViewModel.onFilterByTagsClicked() } + filterTagsSheetState.show(Unit, isImeVisible) }, onFilterByTypeClicked = { - openSheet { searchScreenViewModel.onFilterByTypeClicked() } + filterTypeSheetState.show(Unit, isImeVisible) }, onFilterByOwnerClicked = { - openSheet { searchScreenViewModel.onFilterByOwnerClicked() } + filterOwnerSheetState.show(Unit, isImeVisible) }, onFilterBySharedByLinkClicked = { searchScreenViewModel.onSharedByMeClicked() @@ -228,74 +198,48 @@ fun SearchScreen( ) } - if (uiState.showFilterByTagsBottomSheet) { - FilterByTagsBottomSheet( - items = uiState.availableTags, - sheetState = filterTagsSheetState, - onDismiss = { - closeSheet( - sheetState = filterTagsSheetState, - onCloseFlag = { searchScreenViewModel.onCloseTagsSheet() } - ) - }, - onSave = { selectedItems -> - searchScreenViewModel.onSaveTags(selectedItems) - closeSheet( - sheetState = filterTagsSheetState, - onCloseFlag = { searchScreenViewModel.onCloseTagsSheet() } - ) - }, - onRemoveAll = { - searchScreenViewModel.onRemoveAllTags() - } - ) - } - - if (uiState.showFilterByTypeBottomSheet) { - FilterByTypeBottomSheet( - items = uiState.availableTypes, - sheetState = filterTypeSheetState, - onDismiss = { - closeSheet( - sheetState = filterTypeSheetState, - onCloseFlag = { searchScreenViewModel.onCloseTypeSheet() } - ) - }, - onSave = { selectedItems -> - - searchScreenViewModel.onSaveTypes(selectedItems) - closeSheet( - sheetState = filterTypeSheetState, - onCloseFlag = { searchScreenViewModel.onCloseTypeSheet() } - ) - }, - onRemoveFilter = { - searchScreenViewModel.onRemoveTypeFilter() - } - ) - } - - if (uiState.showFilterByOwnerBottomSheet) { - FilterByOwnerBottomSheet( - items = uiState.availableOwners, - sheetState = filterOwnerSheetState, - onDismiss = { - closeSheet( - sheetState = filterOwnerSheetState, - onCloseFlag = { searchScreenViewModel.onCloseOwnerSheet() } - ) - }, - onSave = { selectedItems -> - - searchScreenViewModel.onSaveOwners(selectedItems) - closeSheet( - sheetState = filterOwnerSheetState, - onCloseFlag = { searchScreenViewModel.onCloseOwnerSheet() } - ) - }, - onRemoveAll = { searchScreenViewModel.onRemoveOwners() } - ) - } + FilterByTagsBottomSheet( + items = uiState.availableTags, + sheetState = filterTagsSheetState, + onDismiss = { + filterTagsSheetState.hide() + }, + onSave = { selectedItems -> + searchScreenViewModel.onSaveTags(selectedItems) + filterTagsSheetState.hide() + }, + onRemoveAll = { + searchScreenViewModel.onRemoveAllTags() + } + ) + + FilterByTypeBottomSheet( + items = uiState.availableTypes, + sheetState = filterTypeSheetState, + onDismiss = { + filterTypeSheetState.hide() + }, + onSave = { selectedItems -> + searchScreenViewModel.onSaveTypes(selectedItems) + filterTypeSheetState.hide() + }, + onRemoveFilter = { + searchScreenViewModel.onRemoveTypeFilter() + } + ) + + FilterByOwnerBottomSheet( + items = uiState.availableOwners, + sheetState = filterOwnerSheetState, + onDismiss = { + filterOwnerSheetState.hide() + }, + onSave = { selectedItems -> + searchScreenViewModel.onSaveOwners(selectedItems) + filterOwnerSheetState.hide() + }, + onRemoveAll = { searchScreenViewModel.onRemoveOwners() } + ) } } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt index a26de7cc1ac..66a7aaaa8d7 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt @@ -179,34 +179,6 @@ class SearchScreenViewModel @Inject constructor( queryFlow.value = query } - fun onFilterByTypeClicked() { - _uiState.update { it.copy(showFilterByTypeBottomSheet = true) } - } - - fun onCloseTypeSheet() { - _uiState.update { it.copy(showFilterByTypeBottomSheet = false) } - } - - fun onFilterByTagsClicked() { - _uiState.update { it.copy(showFilterByTagsBottomSheet = true) } - } - - fun onCloseTagsSheet() { - _uiState.update { it.copy(showFilterByTagsBottomSheet = false) } - } - - fun onFilterByOwnerClicked() { - _uiState.update { it.copy(showFilterByOwnerBottomSheet = true) } - } - - fun onCloseOwnerSheet() { - _uiState.update { it.copy(showFilterByOwnerBottomSheet = false) } - } - - fun onSetSearchActive(active: Boolean) { - _uiState.update { it.copy(isSearchActive = active) } - } - private fun applySelectedTags(selectedIds: Set) { _uiState.update { state -> state.copy( diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt index 9266ff73851..55d1040a98e 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchUiState.kt @@ -27,10 +27,6 @@ data class SearchUiState( val availableOwners: List = emptyList(), val availableTypes: List = TypeFilter.typeItems, - val showFilterByTypeBottomSheet: Boolean = false, - val showFilterByTagsBottomSheet: Boolean = false, - val showFilterByOwnerBottomSheet: Boolean = false, - val filesWithPublicLink: Boolean = false, val isSearchActive: Boolean = true, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt index c6eff9e1655..7049a4c02bd 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/FilterByTypeBottomSheet.kt @@ -33,10 +33,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -48,6 +45,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.search.filter.data.FilterTypeUi +import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout +import com.wire.android.ui.common.bottomsheet.WireModalSheetState +import com.wire.android.ui.common.bottomsheet.WireSheetValue +import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.common.typography @@ -57,7 +58,7 @@ import com.wire.kalium.cells.data.MIMEType @OptIn(ExperimentalMaterial3Api::class) @Composable fun FilterByTypeBottomSheet( - sheetState: SheetState, + sheetState: WireModalSheetState, items: List, onDismiss: () -> Unit, onSave: (List) -> Unit, @@ -71,7 +72,7 @@ fun FilterByTypeBottomSheet( tag.selected != initial.selected } - ModalBottomSheet( + WireModalSheetLayout( modifier = modifier, onDismissRequest = onDismiss, sheetState = sheetState, @@ -214,11 +215,11 @@ fun PreviewFilterByTypeBottomSheet() { ) WireTheme { FilterByTypeBottomSheet( + sheetState = rememberWireModalSheetState(WireSheetValue.Expanded(Unit)), items = sampleItems, onDismiss = {}, onSave = {}, onRemoveFilter = {}, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt index 54cac142578..71aabcd6d31 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/owner/FilterByOwnerBottomSheet.kt @@ -32,10 +32,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -52,6 +49,10 @@ import com.wire.android.feature.cells.ui.search.filter.data.FilterOwnerUi import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.SearchBarInput import com.wire.android.ui.common.avatar.UserProfileAvatar +import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout +import com.wire.android.ui.common.bottomsheet.WireModalSheetState +import com.wire.android.ui.common.bottomsheet.WireSheetValue +import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.common.typography @@ -63,12 +64,12 @@ import com.wire.android.ui.common.R as CommonR @OptIn(ExperimentalMaterial3Api::class) @Composable fun FilterByOwnerBottomSheet( - sheetState: SheetState, + sheetState: WireModalSheetState, items: List, onDismiss: () -> Unit, onSave: (List) -> Unit, onRemoveAll: () -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { val scope = rememberCoroutineScope() @@ -85,7 +86,7 @@ fun FilterByOwnerBottomSheet( .invokeOnCompletion { onDismiss() } } - ModalBottomSheet( + WireModalSheetLayout( onDismissRequest = ::dismiss, sheetState = sheetState, modifier = modifier @@ -202,7 +203,7 @@ private fun OwnerRow( fun PreviewFilterByOwnerBottomSheet() { WireTheme { FilterByOwnerBottomSheet( - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + sheetState = rememberWireModalSheetState(WireSheetValue.Expanded(Unit)), items = listOf( FilterOwnerUi( id = "1", diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt index f75300a9e89..3bd05adff28 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/filter/bottomsheet/tags/FilterByTagsBottomSheet.kt @@ -31,10 +31,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -47,6 +44,10 @@ import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FooterButtons import com.wire.android.feature.cells.ui.search.filter.data.FilterTagUi import com.wire.android.ui.common.SearchBarInput +import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout +import com.wire.android.ui.common.bottomsheet.WireModalSheetState +import com.wire.android.ui.common.bottomsheet.WireSheetValue +import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.chip.WireFilterChip import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews @@ -59,7 +60,7 @@ import com.wire.android.ui.common.R as CommonR @OptIn(ExperimentalMaterial3Api::class) @Composable fun FilterByTagsBottomSheet( - sheetState: SheetState, + sheetState: WireModalSheetState, items: List, onDismiss: () -> Unit, onSave: (List) -> Unit, @@ -81,7 +82,7 @@ fun FilterByTagsBottomSheet( scope.launch { sheetState.hide() } .invokeOnCompletion { onDismiss() } } - ModalBottomSheet( + WireModalSheetLayout( modifier = modifier, onDismissRequest = ::dismiss, sheetState = sheetState, @@ -159,7 +160,7 @@ fun FilterByTagsBottomSheet( fun PreviewFilterByTagsBottomSheet() { WireTheme { FilterByTagsBottomSheet( - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + sheetState = rememberWireModalSheetState(WireSheetValue.Expanded(Unit)), items = listOf( FilterTagUi("1", "Work", true), FilterTagUi("2", "Personal", true), From d646d2d5293a5f3132e28efefecbe5a44a0468aa Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 4 Mar 2026 15:52:21 +0100 Subject: [PATCH 22/22] chore: make search text input ReadOnly --- .../src/main/kotlin/com/wire/android/ui/common/SearchBar.kt | 3 +++ .../wire/android/ui/common/topappbar/search/SearchTopBar.kt | 3 +++ .../wire/android/feature/cells/ui/ConversationFilesScreen.kt | 2 ++ 3 files changed, 8 insertions(+) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt index c4b3fdbd577..218f62bc401 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.sp import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.common.textfield.WireTextField +import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.util.PreviewMultipleThemes @@ -58,6 +59,7 @@ fun SearchBarInput( placeholderAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, textStyle: TextStyle = LocalTextStyle.current, + textFieldState: WireTextFieldState = WireTextFieldState.Default, isLoading: Boolean = false, semanticDescription: String? = null, onTap: (() -> Unit)? = null @@ -66,6 +68,7 @@ fun SearchBarInput( WireTextField( modifier = modifier, textState = textState, + state = textFieldState, leadingIcon = { leadingIcon() }, diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt index 019b9e51c29..c159706edd0 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.text.style.TextAlign import com.wire.android.ui.common.R import com.wire.android.ui.common.SearchBarInput import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.util.PreviewMultipleThemes @@ -72,6 +73,7 @@ fun SearchTopBar( onCloseSearchClicked: (() -> Unit)? = null, onActiveChanged: (isActive: Boolean) -> Unit = {}, bottomContent: @Composable ColumnScope.() -> Unit = {}, + textFieldState: WireTextFieldState = WireTextFieldState.Default, onTap: (() -> Unit)? = null, ) { val interactionSource = remember { MutableInteractionSource() } @@ -106,6 +108,7 @@ fun SearchTopBar( semanticDescription = searchBarDescription, textState = searchQueryTextState, isLoading = isLoading, + textFieldState = textFieldState, leadingIcon = { AnimatedContent(!isSearchActive, label = "") { showSearchIcon -> if (showSearchIcon) { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index 00bbc58c7f3..d7a6870dcc7 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -79,6 +79,7 @@ import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.search.SearchBarState import com.wire.android.ui.common.search.rememberSearchbarState +import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.search.SearchTopBar @@ -253,6 +254,7 @@ fun ConversationFilesScreenContent( searchBarHint = stringResource(R.string.search_shared_drive_text_input_hint), searchQueryTextState = conversationSearchBarState.searchQueryTextState, onActiveChanged = conversationSearchBarState::searchActiveChanged, + textFieldState = WireTextFieldState.ReadOnly, onTap = { currentNodeUuid?.let { navigator.navigate(