Fix contact search list flickering on query change.

This commit is contained in:
Alex Hart
2026-06-10 15:20:00 -04:00
committed by Cody Henthorne
parent 9e3ee16e65
commit 4cdd1f70ac
6 changed files with 162 additions and 30 deletions
@@ -35,6 +35,7 @@ public class ContactRepository {
public static final String ID_COLUMN = "id";
public static final String NAME_COLUMN = "name";
public static final String SORT_NAME_COLUMN = "sort_name";
static final String NUMBER_COLUMN = "number";
static final String NUMBER_TYPE_COLUMN = "number_type";
static final String LABEL_COLUMN = "label";
@@ -55,6 +56,11 @@ public class ContactRepository {
return Util.getFirstNonEmpty(system, profile);
}));
// The key the results are actually ordered by (nickname/system/profile/username, lowercased). Letter
// headers must derive from this rather than NAME_COLUMN, which omits nickname/username and can begin
// with a different letter than the row's sort position.
add(new Pair<>(SORT_NAME_COLUMN, cursor -> CursorUtil.requireString(cursor, RecipientTable.SORT_NAME)));
add(new Pair<>(NUMBER_COLUMN, cursor -> {
String phone = CursorUtil.requireString(cursor, RecipientTable.E164);
String email = CursorUtil.requireString(cursor, RecipientTable.EMAIL);
@@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.database.model.ThreadWithRecipient
import org.thoughtcrime.securesms.keyvalue.StorySend
import org.thoughtcrime.securesms.phonenumbers.NumberUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.search.MessageResult
import org.thoughtcrime.securesms.search.MessageSearchResult
import org.thoughtcrime.securesms.search.SearchRepository
@@ -240,13 +239,33 @@ class ContactSearchPagedDataSource(
return 0
}
private fun getNonGroupHeaderLetterMap(section: ContactSearchConfiguration.Section.Individuals, query: String?): Map<RecipientId, String> {
return contactSearchPagedDataSourceRepository.querySignalContactLetterHeaders(
query = query,
includeSelfMode = section.includeSelfMode,
includePush = true,
includeSms = false
)
/**
* Returns the letter header to display above the recipient at the cursor's current row, or null if
* none should be shown. A header is shown only when this row begins a new letter group, determined by
* comparing its letter to the immediately preceding row in display order. Peeking that single adjacent
* row means a letter group split across pages still yields exactly one header, anchored to the first
* row of the group, without re-scanning the whole contact set.
*
* The cursor is restored to its original position before returning so iteration is unaffected.
*/
private fun getHeaderLetterForCurrentRow(cursor: Cursor): String? {
val position = cursor.position
val currentLetter = letterForCurrentRow(cursor) ?: return null
if (position <= 0) {
return currentLetter
}
cursor.moveToPosition(position - 1)
val previousLetter = letterForCurrentRow(cursor)
cursor.moveToPosition(position)
return if (previousLetter != currentLetter) currentLetter else null
}
private fun letterForCurrentRow(cursor: Cursor): String? {
val sortName = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.SORT_NAME_COLUMN))
return sortName?.takeIf { it.isNotEmpty() }?.first()?.uppercaseChar()?.toString()
}
private fun getStoriesSearchIterator(query: String?): ContactSearchIterator<Cursor> {
@@ -379,12 +398,6 @@ class ContactSearchPagedDataSource(
}
private fun getNonGroupContactsData(section: ContactSearchConfiguration.Section.Individuals, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
val headerMap: Map<RecipientId, String> = if (section.includeLetterHeaders) {
getNonGroupHeaderLetterMap(section, query)
} else {
emptyMap()
}
return getNonGroupSearchIterator(section, query).use { records ->
readContactData(
records = records,
@@ -394,7 +407,8 @@ class ContactSearchPagedDataSource(
endIndex = endIndex,
recordMapper = {
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromSearchCursor(it)
ContactSearchData.KnownRecipient(section.sectionKey, recipient, headerLetter = headerMap[recipient.id])
val headerLetter = if (section.includeLetterHeaders) getHeaderLetterForCurrentRow(it) else null
ContactSearchData.KnownRecipient(section.sectionKey, recipient, headerLetter = headerLetter)
}
)
}
@@ -43,10 +43,6 @@ open class ContactSearchPagedDataSourceRepository(
return contactRepository.querySignalContacts(contactsSearchQuery)
}
open fun querySignalContactLetterHeaders(query: String?, includeSelfMode: RecipientTable.IncludeSelfMode, includePush: Boolean, includeSms: Boolean): Map<RecipientId, String> {
return SignalDatabase.recipients.querySignalContactLetterHeaders(query ?: "", includeSelfMode, includePush, includeSms)
}
open fun queryGroupMemberContacts(query: String?): Cursor? {
return contactRepository.queryGroupMemberContacts(query ?: "")
}
@@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -98,7 +97,7 @@ class ContactSearchViewModel(
init {
viewModelScope.launch {
rawQuery.drop(1).debounce(300).collect { query ->
rawQuery.drop(1).collect { query ->
savedStateHandle[QUERY] = query
internalConfigurationState.update { it.copy(query = query) }
}
@@ -148,15 +147,17 @@ class ContactSearchViewModel(
}
suspend fun setConfiguration(contactSearchConfiguration: ContactSearchConfiguration) {
val pagedDataSource = ContactSearchPagedDataSource(
contactSearchConfiguration,
arbitraryRepository = arbitraryRepository,
searchRepository = searchRepository,
contactSearchPagedDataSourceRepository = contactSearchPagedDataSourceRepository
)
val size = withContext(Dispatchers.IO) { pagedDataSource.size() }
val (pagedDataSource, size) = withContext(Dispatchers.IO) {
val source = ContactSearchPagedDataSource(
contactSearchConfiguration,
arbitraryRepository = arbitraryRepository,
searchRepository = searchRepository,
contactSearchPagedDataSourceRepository = contactSearchPagedDataSourceRepository
)
source to source.size()
}
internalTotalCount.value = size
pagedData.value = PagedData.createForStateFlow(pagedDataSource, pagingConfig)
pagedData.value = PagedData.createForStateFlow(pagedDataSource, pagingConfig, data.value)
}
fun setQuery(query: String?) {