tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 76191f008c2a5f5d0c67681cc973274164fbbce8
parent 52018874a1c34ae2f61606c6f94da907b9e973cd
Author: Harrison Oglesby <oglesby.harrison@gmail.com>
Date:   Wed, 22 Oct 2025 17:29:33 +0000

Bug 1989460 - Create SettingsSearchResultItem UI r=android-reviewers,android-l10n-reviewers,delphine,petru

Differential Revision: https://phabricator.services.mozilla.com/D265892

Diffstat:
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchBar.kt | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddleware.kt | 5+++++
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchResultItem.kt | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchScreen.kt | 155++++++++++++++++++++++++++++++-------------------------------------------------
Mmobile/android/fenix/app/src/main/res/values/static_strings.xml | 3+++
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddlewareTest.kt | 2+-
6 files changed, 272 insertions(+), 97 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchBar.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchBar.kt @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.settingssearch + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.TopAppBar +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.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import mozilla.components.compose.base.button.IconButton +import mozilla.components.compose.base.textfield.TextField +import mozilla.components.lib.state.ext.observeAsComposableState +import org.mozilla.fenix.R +import org.mozilla.fenix.theme.FirefoxTheme +import mozilla.components.ui.icons.R as iconsR + +/** + * Composable for the settings search bar. + * + * @param store [SettingsSearchStore] for the screen. + * @param onBackClick Invoked when the app bar's back button is clicked. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsSearchBar( + store: SettingsSearchStore, + onBackClick: () -> Unit, +) { + val state by store.observeAsComposableState { it } + var searchQuery by remember { mutableStateOf(state.searchQuery) } + + TopAppBar( + title = { + TextField( + value = searchQuery, + onValueChange = { value -> + searchQuery = value + store.dispatch(SettingsSearchAction.SearchQueryUpdated(value)) + }, + modifier = Modifier + .fillMaxWidth() + .padding(end = 8.dp), + placeholder = stringResource(R.string.settings_search_title), + singleLine = true, + errorText = stringResource(R.string.settings_search_error_message), + trailingIcons = { + when (state) { + is SettingsSearchState.SearchInProgress, + is SettingsSearchState.NoSearchResults, + -> { + IconButton( + onClick = { + searchQuery = "" + store.dispatch(SettingsSearchAction.SearchQueryUpdated("")) + }, + contentDescription = stringResource( + R.string.content_description_settings_search_clear_search, + ), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_cross_circle_fill_24), + contentDescription = null, + tint = FirefoxTheme.colors.textPrimary, + ) + } + } + else -> Unit + } + }, + ) + }, + navigationIcon = { + IconButton( + onClick = onBackClick, + contentDescription = + stringResource( + R.string.content_description_settings_search_navigate_back, + ), + ) { + Icon( + painter = painterResource( + iconsR.drawable.mozac_ic_back_24, + ), + contentDescription = null, + tint = FirefoxTheme.colors.textPrimary, + ) + } + }, + windowInsets = WindowInsets( + top = 0.dp, + bottom = 0.dp, + ), + ) +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddleware.kt @@ -23,9 +23,14 @@ class SettingsSearchMiddleware( next: (SettingsSearchAction) -> Unit, action: SettingsSearchAction, ) { + val store = context.store as SettingsSearchStore when (action) { is SettingsSearchAction.SearchQueryUpdated -> { next(action) + + if (action.query.isNotBlank()) { + store.dispatch(SettingsSearchAction.NoResultsFound(action.query)) + } } else -> { next(action) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchResultItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchResultItem.kt @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.settingssearch + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * Composable for the settings search result item. + * + * @param item [SettingsSearchItem] to display. + * @param onClick Callback for when the item is clicked. + */ +@Composable +fun SettingsSearchResultItem( + item: SettingsSearchItem, + onClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(16.dp), + ) { + if (item.breadcrumbs.isNotEmpty()) { + Text( + text = item.breadcrumbs.joinToString(" > "), + style = FirefoxTheme.typography.caption, + color = FirefoxTheme.colors.textSecondary, + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + Text( + text = item.title, + style = FirefoxTheme.typography.subtitle1, + color = FirefoxTheme.colors.textPrimary, + ) + if (item.summary.isNotBlank()) { + Text( + text = item.summary, + style = FirefoxTheme.typography.caption, + color = FirefoxTheme.colors.textSecondary, + modifier = Modifier.padding(top = 4.dp), + ) + } else { + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +private class SettingsSearchResultItemParameterProvider : PreviewParameterProvider<SettingsSearchItem> { + override val values: Sequence<SettingsSearchItem> + get() = sequenceOf( + SettingsSearchItem( + title = "Search Engine", + summary = "Set your preferred search engine for browsing.", + preferenceKey = "search_engine_main", + breadcrumbs = listOf("Search", "Default Search Engine"), + ), + SettingsSearchItem( + title = "Advanced Settings", + summary = "", // Empty or blank summary + preferenceKey = "advanced_stuff", + breadcrumbs = listOf("Developer", "Experiments"), + ), + ) +} + +/** + * Preview for the Settings Search Result Item. + */ +@PreviewLightDark +@Composable +private fun SettingsSearchResultItemFullPreview( + @PreviewParameter(SettingsSearchResultItemParameterProvider::class) item: SettingsSearchItem, +) { + FirefoxTheme { + SettingsSearchResultItem( + item = item, + onClick = {}, + ) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchScreen.kt @@ -5,30 +5,22 @@ package org.mozilla.fenix.settings.settingssearch import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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.TextAlign import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp -import mozilla.components.compose.base.button.IconButton -import mozilla.components.compose.base.textfield.TextField import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.R import org.mozilla.fenix.theme.FirefoxTheme -import mozilla.components.ui.icons.R as iconsR /** * Composable for the settings search screen. @@ -45,103 +37,74 @@ fun SettingsSearchScreen( Scaffold( topBar = { SettingsSearchBar( - query = state.searchQuery, - onSearchQueryChanged = { - store.dispatch(SettingsSearchAction.SearchQueryUpdated(it)) - }, - onClearSearchClicked = { - store.dispatch(SettingsSearchAction.SearchQueryUpdated("")) - }, + store = store, onBackClick = onBackClick, ) }, ) { paddingValues -> - Column( - modifier = Modifier.padding(paddingValues), - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(R.string.settings_search_empty_query_placeholder), - style = FirefoxTheme.typography.body2, - color = FirefoxTheme.colors.textSecondary, + when (state) { + is SettingsSearchState.Default -> { + SettingsSearchMessageContent( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), ) } + is SettingsSearchState.NoSearchResults -> { + SettingsSearchMessageContent( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + currentUserQuery = state.searchQuery, + ) + } + is SettingsSearchState.SearchInProgress -> { + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + ) { + items(state.searchResults.size) { index -> + if (index != 0) { + HorizontalDivider() + } + SettingsSearchResultItem( + item = state.searchResults[index], + onClick = { + // dispatch navigation click event + }, + ) + } + } + } } } } -/** - * Composable for the settings search bar. - * - * @param query Current search query [String]. - * @param onSearchQueryChanged Callback for when the search query changes. - * @param onClearSearchClicked Callback for when the clear search button is clicked. - * @param onBackClick Callback for when the back button is clicked. - */ -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun SettingsSearchBar( - query: String, - onSearchQueryChanged: (String) -> Unit, - onClearSearchClicked: () -> Unit, - onBackClick: () -> Unit, +private fun SettingsSearchMessageContent( + modifier: Modifier = Modifier, + currentUserQuery: String = "", ) { - TopAppBar( - title = { - TextField( - value = query, - onValueChange = { value -> - onSearchQueryChanged(value) - }, - modifier = Modifier - .fillMaxWidth() - .padding(end = 8.dp), - placeholder = stringResource(R.string.settings_search_title), - singleLine = true, - errorText = stringResource(R.string.settings_search_error_message), - trailingIcons = { - if (query.isNotBlank()) { - IconButton( - onClick = onClearSearchClicked, - contentDescription = stringResource( - R.string.content_description_settings_search_clear_search, - ), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_cross_24), - contentDescription = null, - tint = FirefoxTheme.colors.textPrimary, - ) - } - } - }, - ) - }, - navigationIcon = { - IconButton( - onClick = onBackClick, - contentDescription = - stringResource( - R.string.content_description_settings_search_navigate_back, - ), - ) { - Icon( - painter = painterResource( - iconsR.drawable.mozac_ic_back_24, - ), - contentDescription = null, - tint = FirefoxTheme.colors.textPrimary, - ) - } - }, - windowInsets = WindowInsets( - top = 0.dp, - bottom = 0.dp, - ), - ) + val displayMessage = if (currentUserQuery.isBlank()) { + stringResource(R.string.settings_search_empty_query_placeholder) + } else { + stringResource( + R.string.setttings_search_no_results_found_message, + currentUserQuery, + ) + } + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Text( + text = displayMessage, + textAlign = TextAlign.Center, + style = FirefoxTheme.typography.body2, + color = FirefoxTheme.colors.textSecondary, + ) + } } /** diff --git a/mobile/android/fenix/app/src/main/res/values/static_strings.xml b/mobile/android/fenix/app/src/main/res/values/static_strings.xml @@ -274,4 +274,7 @@ <string name="settings_search_empty_query_placeholder">Search for settings</string> <!-- Message when error happens with Settings Search --> <string name="settings_search_error_message">Error occurred.</string> + <!-- Message displayed when the search field is not empty but search results is empty. + %s will be replaced by user query in search field. --> + <string name="setttings_search_no_results_found_message">No results found for %s</string> </resources> diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddlewareTest.kt @@ -48,7 +48,7 @@ class SettingsSearchMiddlewareTest { store.dispatch(SettingsSearchAction.SearchQueryUpdated(query)) store.waitUntilIdle() - assert(store.state is SettingsSearchState.SearchInProgress) + assert(store.state is SettingsSearchState.NoSearchResults) assert(store.state.searchQuery == query) } }