Fix glitch when canceling a search and reopening search

This commit is contained in:
Andrew 2024-05-09 00:55:10 +09:30
parent a054fae758
commit 9935b641e1
6 changed files with 46 additions and 57 deletions

View File

@ -708,12 +708,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
viewContainer.setTypists(recipients) viewContainer.setTypists(recipients)
} }
if (textSecurePreferences.isTypingIndicatorsEnabled()) { if (textSecurePreferences.isTypingIndicatorsEnabled()) {
binding!!.inputBar.addTextChangedListener(object : SimpleTextWatcher() { binding!!.inputBar.addTextChangedListener {
ApplicationContext.getInstance(this).typingStatusSender.onTypingStarted(viewModel.threadId)
override fun onTextChanged(text: String?) { }
ApplicationContext.getInstance(this@ConversationActivityV2).typingStatusSender.onTypingStarted(viewModel.threadId)
}
})
} }
} }

View File

@ -11,6 +11,7 @@ import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -26,6 +27,8 @@ import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SimpleTextWatcher
import org.thoughtcrime.securesms.util.addTextChangedListener
import org.thoughtcrime.securesms.util.contains import org.thoughtcrime.securesms.util.contains
import org.thoughtcrime.securesms.util.toDp import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
@ -219,8 +222,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
setOf(attachmentsButton, microphoneButton).forEach { it.snIsEnabled = showMediaControls } setOf(attachmentsButton, microphoneButton).forEach { it.snIsEnabled = showMediaControls }
} }
fun addTextChangedListener(textWatcher: TextWatcher) { fun addTextChangedListener(listener: (String) -> Unit) {
binding.inputBarEditText.addTextChangedListener(textWatcher) binding.inputBarEditText.addTextChangedListener(listener)
} }
fun setSelection(index: Int) { fun setSelection(index: Int) {

View File

@ -37,6 +37,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -67,7 +69,6 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.ProfilePictureModifiedEvent import org.session.libsession.utilities.ProfilePictureModifiedEvent
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.associateByNotNull
import org.session.libsession.utilities.groupByNotNull import org.session.libsession.utilities.groupByNotNull
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -278,8 +279,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// monitor the global search VM query // monitor the global search VM query
launch { launch {
binding.globalSearchInputLayout.query binding.globalSearchInputLayout.query
.onEach(globalSearchViewModel::postQuery) .collect(globalSearchViewModel::setQuery)
.collect()
} }
// Get group results and display them // Get group results and display them
launch { launch {
@ -434,7 +434,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
if (hasFocus) { if (hasFocus) {
setSearchShown(true) setSearchShown(true)
} else { } else {
setSearchShown(!binding.globalSearchInputLayout.query.value.isNullOrEmpty()) setSearchShown(binding.globalSearchInputLayout.query.value.isNotEmpty())
} }
} }
@ -444,7 +444,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.recyclerView.isVisible = !isShown binding.recyclerView.isVisible = !isShown
binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as HomeAdapter).itemCount == 0 && binding.recyclerView.isVisible binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as HomeAdapter).itemCount == 0 && binding.recyclerView.isVisible
binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown
binding.globalSearchRecycler.isVisible = isShown binding.globalSearchRecycler.isInvisible = !isShown
binding.newConversationButton.isVisible = !isShown binding.newConversationButton.isVisible = !isShown
} }
@ -572,11 +572,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// region Interaction // region Interaction
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun onBackPressed() { override fun onBackPressed() {
if (binding.globalSearchRecycler.isVisible) { if (binding.globalSearchRecycler.isVisible) binding.globalSearchInputLayout.clearSearch(true)
binding.globalSearchInputLayout.clearSearch(true) else super.onBackPressed()
return
}
super.onBackPressed()
} }
override fun onConversationClick(thread: ThreadRecord) { override fun onConversationClick(thread: ThreadRecord) {

View File

@ -16,42 +16,37 @@ import android.widget.TextView
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import network.loki.messenger.databinding.ViewGlobalSearchInputBinding import network.loki.messenger.databinding.ViewGlobalSearchInputBinding
import org.thoughtcrime.securesms.util.SimpleTextWatcher
import org.thoughtcrime.securesms.util.addTextChangedListener
class GlobalSearchInputLayout @JvmOverloads constructor( class GlobalSearchInputLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs), ) : LinearLayout(context, attrs),
View.OnFocusChangeListener, View.OnFocusChangeListener,
View.OnClickListener, TextView.OnEditorActionListener {
TextWatcher, TextView.OnEditorActionListener {
var binding: ViewGlobalSearchInputBinding = ViewGlobalSearchInputBinding.inflate(LayoutInflater.from(context), this, true) var binding: ViewGlobalSearchInputBinding = ViewGlobalSearchInputBinding.inflate(LayoutInflater.from(context), this, true)
var listener: GlobalSearchInputLayoutListener? = null var listener: GlobalSearchInputLayoutListener? = null
private val _query = MutableStateFlow<CharSequence?>(null) private val _query = MutableStateFlow<CharSequence>("")
val query: StateFlow<CharSequence?> = _query val query: StateFlow<CharSequence> = _query
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
binding.searchInput.onFocusChangeListener = this binding.searchInput.onFocusChangeListener = this
binding.searchInput.addTextChangedListener(this) binding.searchInput.addTextChangedListener(::setQuery)
binding.searchInput.setOnEditorActionListener(this) binding.searchInput.setOnEditorActionListener(this)
binding.searchInput.setFilters( arrayOf<InputFilter>(LengthFilter(100)) ) // 100 char search limit binding.searchInput.filters = arrayOf<InputFilter>(LengthFilter(100)) // 100 char search limit
binding.searchCancel.setOnClickListener(this) binding.searchCancel.setOnClickListener { clearSearch(true) }
binding.searchClear.setOnClickListener(this) binding.searchClear.setOnClickListener { clearSearch(false) }
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
} }
override fun onFocusChange(v: View?, hasFocus: Boolean) { override fun onFocusChange(v: View?, hasFocus: Boolean) {
if (v === binding.searchInput) { if (v === binding.searchInput) {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).apply {
if (!hasFocus) { if (hasFocus) showSoftInput(v, 0)
imm.hideSoftInputFromWindow(windowToken, 0) else hideSoftInputFromWindow(windowToken, 0)
} else {
imm.showSoftInput(v, 0)
} }
listener?.onInputFocusChanged(hasFocus) listener?.onInputFocusChanged(hasFocus)
} }
@ -65,27 +60,16 @@ class GlobalSearchInputLayout @JvmOverloads constructor(
return false return false
} }
override fun onClick(v: View?) {
if (v === binding.searchCancel) {
clearSearch(true)
} else if (v === binding.searchClear) {
clearSearch(false)
}
}
fun clearSearch(clearFocus: Boolean) { fun clearSearch(clearFocus: Boolean) {
binding.searchInput.text = null binding.searchInput.text = null
setQuery("")
if (clearFocus) { if (clearFocus) {
binding.searchInput.clearFocus() binding.searchInput.clearFocus()
} }
} }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} private fun setQuery(query: String) {
_query.value = query
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
_query.value = s?.toString()
} }
interface GlobalSearchInputLayoutListener { interface GlobalSearchInputLayoutListener {

View File

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.search.model.SearchResult
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
class GlobalSearchViewModel @Inject constructor( class GlobalSearchViewModel @Inject constructor(
private val searchRepository: SearchRepository, private val searchRepository: SearchRepository,
@ -40,12 +41,11 @@ class GlobalSearchViewModel @Inject constructor(
val result: StateFlow<GlobalSearchResult> = _result val result: StateFlow<GlobalSearchResult> = _result
val refreshes = Channel<Unit>() private val refreshes = Channel<Unit>()
private val _queryText: MutableStateFlow<CharSequence> = MutableStateFlow("") private val _queryText: MutableStateFlow<CharSequence> = MutableStateFlow("")
fun postQuery(charSequence: CharSequence?) { fun setQuery(charSequence: CharSequence) {
charSequence ?: return
_queryText.value = charSequence _queryText.value = charSequence
} }
@ -58,15 +58,14 @@ class GlobalSearchViewModel @Inject constructor(
.reEmit(refreshes) .reEmit(refreshes)
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) .buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
.mapLatest { query -> .mapLatest { query ->
// User input delay in case we get a new query within a few hundred ms this
// coroutine will be cancelled and the expensive query will not be run.
delay(300)
if (query.trim().isEmpty()) { if (query.trim().isEmpty()) {
// searching for 05 as contactDb#getAllContacts was not returning contacts // searching for 05 as contactDb#getAllContacts was not returning contacts
// without a nickname/name who haven't approved us. // without a nickname/name who haven't approved us.
GlobalSearchResult(query.toString(), searchRepository.queryContacts("05").first.toList()) GlobalSearchResult(query.toString(), searchRepository.queryContacts("05").first.toList())
} else { } else {
// User input delay in case we get a new query within a few hundred ms this
// coroutine will be cancelled and the expensive query will not be run.
delay(300)
val settableFuture = SettableFuture<SearchResult>() val settableFuture = SettableFuture<SearchResult>()
searchRepository.query(query.toString(), settableFuture::set) searchRepository.query(query.toString(), settableFuture::set)
try { try {
@ -84,7 +83,7 @@ class GlobalSearchViewModel @Inject constructor(
} }
/** /**
* Re-emit whenevr refreshes emits. * Re-emit whenever refreshes emits.
* */ * */
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun <T> Flow<T>.reEmit(refreshes: Channel<Unit>) = flatMapLatest { query -> merge(flowOf(query), refreshes.consumeAsFlow().map { query }) } private fun <T> Flow<T>.reEmit(refreshes: Channel<Unit>) = flatMapLatest { query -> merge(flowOf(query), refreshes.consumeAsFlow().map { query }) }

View File

@ -16,6 +16,7 @@ import androidx.annotation.DimenRes
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.graphics.applyCanvas import androidx.core.graphics.applyCanvas
@ -111,3 +112,11 @@ fun Size.coerceAtMost(longestWidth: Int): Size =
height.coerceAtMost(longestWidth).let { Size((it * aspect).roundToInt(), it) } height.coerceAtMost(longestWidth).let { Size((it * aspect).roundToInt(), it) }
} }
} }
fun EditText.addTextChangedListener(listener: (String) -> Unit) {
addTextChangedListener(object: SimpleTextWatcher() {
override fun onTextChanged(text: String) {
listener(text)
}
})
}