From 561d6c7b2a61dc112d4c79a7f1e338027d045da5 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Sat, 14 Feb 2026 09:29:22 +0100 Subject: [PATCH] chore: Migrate geo location search to compose Signed-off-by: Andy Scherzinger --- app/build.gradle | 1 + .../talk/adapters/GeocodingAdapter.kt | 59 ------ .../talk/location/GeocodingActivity.kt | 192 +++--------------- .../talk/location/LocationPickerActivity.kt | 4 +- .../components/GeocodingResultItem.kt | 75 +++++++ .../location/components/GeocodingScreen.kt | 168 +++++++++++++++ .../GeocodingScreenListenerInputs.kt | 16 ++ .../talk/location/components/SearchField.kt | 84 ++++++++ .../talk/viewmodels/GeoCodingViewModel.kt | 14 +- .../main/res/layout/activity_geocoding.xml | 38 ---- app/src/main/res/layout/geocoding_item.xml | 43 ---- app/src/main/res/menu/menu_geocoding.xml | 19 -- app/src/main/res/values/dimens.xml | 2 - gradle/verification-metadata.xml | 48 +++++ 14 files changed, 429 insertions(+), 334 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/location/components/GeocodingResultItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/location/components/GeocodingScreen.kt create mode 100644 app/src/main/java/com/nextcloud/talk/location/components/GeocodingScreenListenerInputs.kt create mode 100644 app/src/main/java/com/nextcloud/talk/location/components/SearchField.kt delete mode 100644 app/src/main/res/layout/activity_geocoding.xml delete mode 100644 app/src/main/res/layout/geocoding_item.xml delete mode 100644 app/src/main/res/menu/menu_geocoding.xml diff --git a/app/build.gradle b/app/build.gradle index 6c570c2446f..2a7da3c455e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -329,6 +329,7 @@ dependencies { implementation 'androidx.compose.material:material-icons-core:1.7.8' implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.runtime:runtime-livedata") implementation 'androidx.activity:activity-compose:1.12.4' debugImplementation("androidx.compose.ui:ui-tooling") diff --git a/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt deleted file mode 100644 index 70d714fd1e7..00000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2021 Marcel Hibbe - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.adapters - -import android.annotation.SuppressLint -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.nextcloud.talk.R -import fr.dudie.nominatim.model.Address - -class GeocodingAdapter(private val context: Context, private var dataSource: List
) : - RecyclerView.Adapter() { - - interface OnItemClickListener { - fun onItemClick(position: Int) - } - - @SuppressLint("NotifyDataSetChanged") - fun updateData(data: List
) { - this.dataSource = data - notifyDataSetChanged() - } - - private var listener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.listener = listener - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(context) - val view = inflater.inflate(R.layout.geocoding_item, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val address = dataSource[position] - holder.nameView.text = address.displayName - - holder.itemView.setOnClickListener { - listener?.onItemClick(position) - } - } - - override fun getItemCount(): Int = dataSource.size - - fun getItem(position: Int): Any = dataSource[position] - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val nameView: TextView = itemView.findViewById(R.id.name) - } -} diff --git a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt index 2e774eaa7c7..f9d89848d83 100644 --- a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt @@ -1,82 +1,54 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-3.0-or-later */ package com.nextcloud.talk.location -import android.app.SearchManager -import android.content.Context import android.content.Intent import android.os.Bundle -import android.text.InputType -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import android.view.inputmethod.EditorInfo -import androidx.appcompat.widget.SearchView -import androidx.core.graphics.drawable.toDrawable +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import autodagger.AutoInjector import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity -import com.nextcloud.talk.adapters.GeocodingAdapter import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.databinding.ActivityGeocodingBinding +import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.location.components.GeocodingScreen import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.viewmodels.GeoCodingViewModel import fr.dudie.nominatim.client.TalkJsonNominatimClient import fr.dudie.nominatim.model.Address import okhttp3.OkHttpClient -import org.osmdroid.config.Configuration import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class GeocodingActivity : BaseActivity() { - private lateinit var binding: ActivityGeocodingBinding - @Inject lateinit var ncApi: NcApi @Inject lateinit var okHttpClient: OkHttpClient - lateinit var roomToken: String + private lateinit var roomToken: String private var chatApiVersion: Int = 1 - private var nominatimClient: TalkJsonNominatimClient? = null - - private var searchItem: MenuItem? = null - var searchView: SearchView? = null - - lateinit var adapter: GeocodingAdapter - private var geocodingResults: List
= ArrayList() - private lateinit var recyclerView: RecyclerView private lateinit var viewModel: GeoCodingViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) - binding = ActivityGeocodingBinding.inflate(layoutInflater) - setupActionBar() - setContentView(binding.root) - initSystemBars() - - Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context)) + org.osmdroid.config.Configuration.getInstance() + .load(context, PreferenceManager.getDefaultSharedPreferences(context)) roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!! chatApiVersion = intent.getIntExtra(BundleKeys.KEY_CHAT_API_VERSION, 1) - recyclerView = findViewById(R.id.geocoding_results) - recyclerView.layoutManager = LinearLayoutManager(this) - adapter = GeocodingAdapter(this, geocodingResults) - recyclerView.adapter = adapter viewModel = ViewModelProvider(this)[GeoCodingViewModel::class.java] var query = viewModel.getQuery() @@ -84,140 +56,36 @@ class GeocodingActivity : BaseActivity() { query = intent.getStringExtra(BundleKeys.KEY_GEOCODING_QUERY).orEmpty() viewModel.setQuery(query) } - val savedResults = viewModel.getGeocodingResults() - initAdapter(savedResults) - viewModel.getGeocodingResultsLiveData().observe(this) { results -> - geocodingResults = results - adapter.updateData(results) - } + val baseUrl = getString(R.string.osm_geocoder_url) val email = context.getString(R.string.osm_geocoder_contact) - nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email) - } - - override fun onStart() { - super.onStart() - initAdapter(geocodingResults) - initGeocoder() - } - - override fun onResume() { - super.onResume() - - if (viewModel.getQuery().isNotEmpty() && adapter.itemCount == 0) { - viewModel.searchLocation() - } else { - Log.e(TAG, "search string that was passed to GeocodingActivity was null or empty") - } - adapter.setOnItemClickListener(object : GeocodingAdapter.OnItemClickListener { - override fun onItemClick(position: Int) { - val address: Address = adapter.getItem(position) as Address - val geocodingResult = GeocodingResult(address.latitude, address.longitude, address.displayName) - val intent = Intent(this@GeocodingActivity, LocationPickerActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken) - intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) - intent.putExtra(BundleKeys.KEY_GEOCODING_RESULT, geocodingResult) - startActivity(intent) + TalkJsonNominatimClient(baseUrl, okHttpClient, email) + + setContent { + val colorScheme = viewThemeUtils.getColorScheme(this) + MaterialTheme(colorScheme = colorScheme) { + ColoredStatusBar() + GeocodingScreen( + viewModel = viewModel, + onBack = { onBackPressedDispatcher.onBackPressed() }, + onAddressSelected = { address -> navigateToLocationPicker(address) } + ) } - }) - searchView?.setQuery(viewModel.getQuery(), false) - } - - private fun setupActionBar() { - setSupportActionBar(binding.geocodingToolbar) - binding.geocodingToolbar.setNavigationOnClickListener { - onBackPressedDispatcher.onBackPressed() } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setDisplayShowHomeEnabled(true) - supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable()) - supportActionBar?.title = "" - viewThemeUtils.material.themeToolbar(binding.geocodingToolbar) } - private fun initAdapter(addresses: List
) { - adapter = GeocodingAdapter(binding.geocodingResults.context!!, addresses) - adapter.setOnItemClickListener(object : GeocodingAdapter.OnItemClickListener { - override fun onItemClick(position: Int) { - val address: Address = adapter.getItem(position) as Address - val geocodingResult = GeocodingResult(address.latitude, address.longitude, address.displayName) - val intent = Intent(this@GeocodingActivity, LocationPickerActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken) - intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) - intent.putExtra(BundleKeys.KEY_GEOCODING_RESULT, geocodingResult) - startActivity(intent) - } - }) - binding.geocodingResults.adapter = adapter - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - super.onCreateOptionsMenu(menu) - menuInflater.inflate(R.menu.menu_geocoding, menu) - searchItem = menu.findItem(R.id.geocoding_action_search) - initSearchView() - searchItem?.expandActionView() - searchView?.setQuery(viewModel.getQuery(), false) - searchView?.clearFocus() - return true - } - - private fun initSearchView() { - val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager - if (searchItem != null) { - searchView = searchItem!!.actionView as SearchView? - - searchView?.maxWidth = Int.MAX_VALUE - searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER - var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN - if (appPreferences.isKeyboardIncognito) { - imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING - } - searchView?.imeOptions = imeOptions - searchView?.queryHint = resources!!.getString(R.string.nc_search) - searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName)) - searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - viewModel.setQuery(query) - viewModel.searchLocation() - searchView?.clearFocus() - return true - } - - override fun onQueryTextChange(query: String): Boolean { - // This is a workaround to not set viewModel data when onQueryTextChange is triggered on startup - // Otherwise it would be set to an empty string. - if (searchView?.width!! > 0) { - viewModel.setQuery(query) - } - return true - } - }) - - searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean = true - - override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean { - val intent = Intent(context, LocationPickerActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken) - intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) - startActivity(intent) - return true - } - }) - } - } - - private fun initGeocoder() { - val baseUrl = getString(R.string.osm_geocoder_url) - val email = context.getString(R.string.osm_geocoder_contact) - nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email) + private fun navigateToLocationPicker(address: Address) { + val geocodingResult = GeocodingResult(address.latitude, address.longitude, address.displayName) + val intent = Intent(this, LocationPickerActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken) + intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) + intent.putExtra(BundleKeys.KEY_GEOCODING_RESULT, geocodingResult) + startActivity(intent) + finish() } companion object { - val TAG = GeocodingActivity::class.java.simpleName + val TAG: String = GeocodingActivity::class.java.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt index 8db14692594..3733c2ee312 100644 --- a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt @@ -1,8 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham - * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-3.0-or-later */ package com.nextcloud.talk.location @@ -229,6 +228,7 @@ class LocationPickerActivity : override fun onQueryTextSubmit(query: String?): Boolean { if (!query.isNullOrEmpty()) { val intent = Intent(this, GeocodingActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.putExtra(BundleKeys.KEY_GEOCODING_QUERY, query) intent.putExtra(KEY_ROOM_TOKEN, roomToken) intent.putExtra(KEY_CHAT_API_VERSION, chatApiVersion) diff --git a/app/src/main/java/com/nextcloud/talk/location/components/GeocodingResultItem.kt b/app/src/main/java/com/nextcloud/talk/location/components/GeocodingResultItem.kt new file mode 100644 index 00000000000..9297b18b5c2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/location/components/GeocodingResultItem.kt @@ -0,0 +1,75 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.location.components + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nextcloud.talk.R + +@Composable +fun GeocodingResultItem(displayName: String, onClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(16.dp), + verticalAlignment = Alignment.Top + ) { + Image( + painter = painterResource(R.drawable.ic_circular_location), + contentDescription = null, + modifier = Modifier + .size(40.dp) + ) + Text( + text = displayName, + fontSize = 18.sp, + color = colorResource(R.color.high_emphasis_text), + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) + } +} + +@Preview(name = "Light", showBackground = true) +@Preview(name = "Dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewGeocodingResultItem() { + MaterialTheme { + GeocodingResultItem( + displayName = "Sonnenallee 50, Rixdorf, Neukölln, Berlin, 12055, Deutschland", + onClick = {} + ) + } +} + +@Preview(name = "RTL - Arabic", showBackground = true, locale = "ar") +@Composable +private fun PreviewGeocodingResultItemRtl() { + MaterialTheme { + GeocodingResultItem( + displayName = "شارع الملك فهد، الرياض، المملكة العربية السعودية", + onClick = {} + ) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/location/components/GeocodingScreen.kt b/app/src/main/java/com/nextcloud/talk/location/components/GeocodingScreen.kt new file mode 100644 index 00000000000..8b2a738726c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/location/components/GeocodingScreen.kt @@ -0,0 +1,168 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.location.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nextcloud.talk.R +import com.nextcloud.talk.viewmodels.GeoCodingViewModel +import fr.dudie.nominatim.model.Address + +@Composable +fun GeocodingScreen(viewModel: GeoCodingViewModel, onBack: () -> Unit, onAddressSelected: (Address) -> Unit) { + val results by viewModel.geocodingResults.collectAsStateWithLifecycle() + val initialQuery = viewModel.getQuery() + var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(initialQuery, TextRange(initialQuery.length))) + } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + if (viewModel.getQuery().isNotEmpty() && results.isEmpty()) { + viewModel.searchLocation() + } + } + + GeocodingScreenContent( + query = textFieldValue, + results = results.map { it.displayName }, + inputs = GeocodingScreenListenerInputs( + onQueryChange = { newValue -> + textFieldValue = newValue + viewModel.setQuery(textFieldValue.text) + }, + onSearch = { + viewModel.setQuery(textFieldValue.text) + viewModel.searchLocation() + keyboardController?.hide() + }, + onBack = onBack, + onItemClick = { index -> onAddressSelected(results[index]) } + ) + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun GeocodingScreenContent( + query: TextFieldValue, + results: List, + inputs: GeocodingScreenListenerInputs +) { + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Scaffold( + topBar = { + TopAppBar( + title = { + SearchField( + value = query, + onValueChange = inputs.onQueryChange, + onSearch = inputs.onSearch, + modifier = Modifier.focusRequester(focusRequester) + ) + }, + navigationIcon = { + IconButton(onClick = inputs.onBack) { + Icon( + Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = stringResource(R.string.back_button) + ) + } + } + ) + }, + contentWindowInsets = WindowInsets.safeDrawing + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + items(results.size) { index -> + GeocodingResultItem( + displayName = results[index], + onClick = { inputs.onItemClick(index) } + ) + } + } + } +} + +@Preview(name = "Light", showBackground = true) +@Preview(name = "Dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewGeocodingScreen() { + MaterialTheme { + GeocodingScreenContent( + query = TextFieldValue("Berlin", TextRange("Berlin".length)), + results = listOf( + "Sonnenallee 50, Rixdorf, Neukölln, Berlin, 12055, Deutschland", + "Alexanderplatz, Mitte, Berlin, 10178, Deutschland", + "Brandenburger Tor, Pariser Platz, Mitte, Berlin, 10117, Deutschland" + ), + inputs = GeocodingScreenListenerInputs( + onQueryChange = {}, + onSearch = {}, + onBack = {}, + onItemClick = {} + ) + ) + } +} + +@Preview(name = "RTL - Arabic", showBackground = true, locale = "ar") +@Composable +private fun PreviewGeocodingScreenRtl() { + MaterialTheme { + GeocodingScreenContent( + query = TextFieldValue("الرياض", TextRange("الرياض".length)), + results = listOf( + "شارع الملك فهد، الرياض، المملكة العربية السعودية", + "حي العليا، الرياض، المملكة العربية السعودية" + ), + inputs = GeocodingScreenListenerInputs( + onQueryChange = {}, + onSearch = {}, + onBack = {}, + onItemClick = {} + ) + ) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/location/components/GeocodingScreenListenerInputs.kt b/app/src/main/java/com/nextcloud/talk/location/components/GeocodingScreenListenerInputs.kt new file mode 100644 index 00000000000..05360377f87 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/location/components/GeocodingScreenListenerInputs.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.location.components + +import androidx.compose.ui.text.input.TextFieldValue + +data class GeocodingScreenListenerInputs( + val onQueryChange: (TextFieldValue) -> Unit, + val onSearch: () -> Unit, + val onBack: () -> Unit, + val onItemClick: (Int) -> Unit +) diff --git a/app/src/main/java/com/nextcloud/talk/location/components/SearchField.kt b/app/src/main/java/com/nextcloud/talk/location/components/SearchField.kt new file mode 100644 index 00000000000..060b5d7bc01 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/location/components/SearchField.kt @@ -0,0 +1,84 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.location.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import com.nextcloud.talk.R + +@Composable +fun SearchField( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + onSearch: () -> Unit, + modifier: Modifier = Modifier +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier.fillMaxWidth(), + placeholder = { Text(stringResource(R.string.nc_search)) }, + trailingIcon = { + if (value.text.isNotEmpty()) { + IconButton(onClick = { onValueChange(TextFieldValue()) }) { + Icon(Icons.Outlined.Clear, contentDescription = null) + } + } + }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { onSearch() }), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent + ) + ) +} + +@Preview(name = "Search - Light", showBackground = true) +@Preview(name = "Search - Dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewSearchField() { + MaterialTheme { + SearchField( + value = TextFieldValue("Berlin"), + onValueChange = {}, + onSearch = {} + ) + } +} + +@Preview(name = "Search - RTL", showBackground = true, locale = "ar") +@Composable +private fun PreviewSearchFieldRtl() { + MaterialTheme { + SearchField( + value = TextFieldValue("الرياض"), + onValueChange = {}, + onSearch = {} + ) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt b/app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt index cd641e8f570..a93f55445ac 100644 --- a/app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt @@ -7,24 +7,23 @@ package com.nextcloud.talk.viewmodels import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import fr.dudie.nominatim.client.TalkJsonNominatimClient import fr.dudie.nominatim.model.Address import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import okhttp3.OkHttpClient import java.io.IOException class GeoCodingViewModel : ViewModel() { - private val geocodingResultsLiveData = MutableLiveData>() + private val _geocodingResults = MutableStateFlow>(emptyList()) + val geocodingResults: StateFlow> = _geocodingResults private val nominatimClient: TalkJsonNominatimClient private val okHttpClient: OkHttpClient = OkHttpClient.Builder().build() - private var geocodingResults: List
= ArrayList() private var query: String = "" - fun getGeocodingResultsLiveData(): LiveData> = geocodingResultsLiveData fun getQuery(): String = query @@ -32,8 +31,6 @@ class GeoCodingViewModel : ViewModel() { this.query = query } - fun getGeocodingResults(): List
= geocodingResults - init { nominatimClient = TalkJsonNominatimClient( "https://nominatim.openstreetmap.org/", @@ -47,8 +44,7 @@ class GeoCodingViewModel : ViewModel() { CoroutineScope(Dispatchers.IO).launch { try { val results = nominatimClient.search(query) as ArrayList
- geocodingResults = results - geocodingResultsLiveData.postValue(results) + _geocodingResults.value = results } catch (e: IOException) { Log.e(TAG, "Failed to get geocoded addresses", e) } diff --git a/app/src/main/res/layout/activity_geocoding.xml b/app/src/main/res/layout/activity_geocoding.xml deleted file mode 100644 index 0e921e5e05d..00000000000 --- a/app/src/main/res/layout/activity_geocoding.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/geocoding_item.xml b/app/src/main/res/layout/geocoding_item.xml deleted file mode 100644 index 9b3add59362..00000000000 --- a/app/src/main/res/layout/geocoding_item.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/menu/menu_geocoding.xml b/app/src/main/res/menu/menu_geocoding.xml deleted file mode 100644 index fb8426c02b0..00000000000 --- a/app/src/main/res/menu/menu_geocoding.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 2d74e3a8b30..0a6e5e603a6 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -37,8 +37,6 @@ 120dp 220dp - 18sp - 192dp 16dp diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 885a1323694..ad2df26d837 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2652,6 +2652,14 @@ + + + + + + + + @@ -27974,6 +27982,14 @@ + + + + + + + + @@ -28063,6 +28079,14 @@ + + + + + + + + @@ -28151,6 +28175,14 @@ + + + + + + + + @@ -28175,6 +28207,14 @@ + + + + + + + + @@ -28263,6 +28303,14 @@ + + + + + + + +