mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-13 17:23:56 +01:00
Add search to internal preferences.
This commit is contained in:
committed by
Alex Hart
parent
348387f2d0
commit
48f55bba0a
+63
-3
@@ -6,9 +6,12 @@ import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -74,8 +77,10 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.registration.data.QuickstartCredentialExporter
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.setIncognitoKeyboardEnabled
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
@@ -84,13 +89,14 @@ import kotlin.math.max
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
|
||||
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences, R.menu.internal_settings) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(InternalSettingsFragment::class.java)
|
||||
}
|
||||
|
||||
private lateinit var viewModel: InternalSettingsViewModel
|
||||
private var searchMenuItem: MenuItem? = null
|
||||
|
||||
private var scrollToPosition: Int = 0
|
||||
private val layoutManager: LinearLayoutManager?
|
||||
@@ -107,6 +113,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
scrollToPosition = SignalStore.internal.lastScrollPosition
|
||||
initializeSearch(view)
|
||||
|
||||
setFragmentResultListener(CallQualityBottomSheetFragment.REQUEST_KEY) { _, bundle ->
|
||||
if (bundle.getBoolean(CallQualityBottomSheetFragment.REQUEST_KEY, false)) {
|
||||
@@ -125,8 +132,11 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
viewModel = ViewModelProvider(this, factory)[InternalSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList()) {
|
||||
if (scrollToPosition != 0) {
|
||||
val mappingModelList = getConfiguration(it).toMappingModelList()
|
||||
val filteredList = viewModel.filterPreferences(requireContext(), mappingModelList, it.searchQuery)
|
||||
|
||||
adapter.submitList(filteredList) {
|
||||
if (scrollToPosition != 0 && it.searchQuery.isBlank()) {
|
||||
layoutManager?.scrollToPositionWithOffset(scrollToPosition, 0)
|
||||
scrollToPosition = 0
|
||||
}
|
||||
@@ -134,6 +144,56 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
}
|
||||
|
||||
override fun onToolbarNavigationClicked() {
|
||||
if (searchMenuItem?.isActionViewExpanded == true) {
|
||||
searchMenuItem?.collapseActionView()
|
||||
} else {
|
||||
super.onToolbarNavigationClicked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeSearch(view: View) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
searchMenuItem = toolbar.menu.findItem(R.id.menu_search)
|
||||
|
||||
val searchView: SearchView = searchMenuItem?.actionView as? SearchView ?: return
|
||||
val queryListener = object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
searchView.clearFocus()
|
||||
viewModel.setSearchQuery(query.orEmpty())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
viewModel.setSearchQuery(newText.orEmpty())
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
searchView.maxWidth = Integer.MAX_VALUE
|
||||
searchView.queryHint = getString(R.string.CameraContacts__menu_search)
|
||||
|
||||
searchMenuItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
searchView.setIncognitoKeyboardEnabled(TextSecurePreferences.isIncognitoKeyboardEnabled(requireContext()))
|
||||
searchView.setOnQueryTextListener(queryListener)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
searchView.setOnQueryTextListener(null)
|
||||
searchView.setQuery("", false)
|
||||
viewModel.setSearchQuery("")
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
val currentQuery = viewModel.state.value?.searchQuery.orEmpty()
|
||||
if (currentQuery.isNotBlank() && searchMenuItem?.expandActionView() == true) {
|
||||
searchView.setQuery(currentQuery, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: InternalSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(DSLSettingsText.from("Account"))
|
||||
|
||||
+2
-1
@@ -32,5 +32,6 @@ data class InternalSettingsState(
|
||||
val forceSplitPane: Boolean,
|
||||
val forceSinglePane: Boolean,
|
||||
val useNewMediaActivity: Boolean,
|
||||
val disableInternalUser: Boolean
|
||||
val disableInternalUser: Boolean,
|
||||
val searchQuery: String = ""
|
||||
)
|
||||
|
||||
+99
-1
@@ -1,10 +1,14 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.components.settings.DividerPreference
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.components.settings.SectionHeaderPreference
|
||||
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
|
||||
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob
|
||||
import org.thoughtcrime.securesms.keyvalue.InternalValues
|
||||
@@ -12,7 +16,10 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.util.Locale
|
||||
|
||||
class InternalSettingsViewModel(private val repository: InternalSettingsRepository) : ViewModel() {
|
||||
private val preferenceDataStore = SignalStore.getPreferenceDataStore()
|
||||
@@ -167,7 +174,47 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
store.update { getState().copy(emojiVersion = it.emojiVersion) }
|
||||
store.update { getState().copy(emojiVersion = it.emojiVersion, searchQuery = it.searchQuery) }
|
||||
}
|
||||
|
||||
fun setSearchQuery(query: String) {
|
||||
store.update {
|
||||
if (it.searchQuery == query) {
|
||||
it
|
||||
} else {
|
||||
it.copy(searchQuery = query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filterPreferences(context: Context, items: MappingModelList, query: String): MappingModelList {
|
||||
val normalizedQuery = query.trim().lowercase(Locale.getDefault())
|
||||
if (normalizedQuery.isBlank()) {
|
||||
return items
|
||||
}
|
||||
|
||||
val groups = buildSearchGroups(items)
|
||||
val filtered = MappingModelList()
|
||||
|
||||
groups.forEach { group ->
|
||||
val headerMatches = group.header?.searchableText(context)?.contains(normalizedQuery) == true
|
||||
val matchingItems = if (headerMatches) {
|
||||
group.items
|
||||
} else {
|
||||
group.items.filter { it.searchableText(context)?.contains(normalizedQuery) == true }
|
||||
}
|
||||
|
||||
if (headerMatches || matchingItems.isNotEmpty()) {
|
||||
if (filtered.isNotEmpty() && group.divider != null) {
|
||||
filtered.add(group.divider)
|
||||
}
|
||||
|
||||
group.header?.let { filtered.add(it) }
|
||||
filtered.addAll(matchingItems)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
private fun getState() = InternalSettingsState(
|
||||
@@ -225,6 +272,57 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun buildSearchGroups(items: MappingModelList): List<SearchGroup> {
|
||||
val groups = mutableListOf<SearchGroup>()
|
||||
var divider: DividerPreference? = null
|
||||
var header: SectionHeaderPreference? = null
|
||||
var groupItems = mutableListOf<MappingModel<*>>()
|
||||
|
||||
fun flush() {
|
||||
if (header != null || groupItems.isNotEmpty()) {
|
||||
groups.add(SearchGroup(divider, header, groupItems))
|
||||
}
|
||||
|
||||
divider = null
|
||||
header = null
|
||||
groupItems = mutableListOf()
|
||||
}
|
||||
|
||||
items.forEach { item ->
|
||||
when (item) {
|
||||
is DividerPreference -> {
|
||||
flush()
|
||||
divider = item
|
||||
}
|
||||
is SectionHeaderPreference -> {
|
||||
flush()
|
||||
header = item
|
||||
}
|
||||
else -> groupItems.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
flush()
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
private fun MappingModel<*>.searchableText(context: Context): String? {
|
||||
return if (this is PreferenceModel<*>) {
|
||||
listOfNotNull(title, summary)
|
||||
.joinToString(separator = " ") { it.resolve(context).toString() }
|
||||
.lowercase(Locale.getDefault())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private data class SearchGroup(
|
||||
val divider: DividerPreference?,
|
||||
val header: SectionHeaderPreference?,
|
||||
val items: List<MappingModel<*>>
|
||||
)
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_search"
|
||||
android:icon="@drawable/symbol_search_24"
|
||||
android:title="@string/CameraContacts__menu_search"
|
||||
app:actionViewClass="org.thoughtcrime.securesms.components.SearchView"
|
||||
app:iconTint="@color/signal_colorOnSurface"
|
||||
app:showAsAction="collapseActionView|always" />
|
||||
|
||||
</menu>
|
||||
Reference in New Issue
Block a user