mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 05:14:50 +01:00
Update group membership search.
This commit is contained in:
@@ -183,7 +183,8 @@ public class ContactRepository {
|
||||
|
||||
@Override
|
||||
public String getString(int columnIndex) {
|
||||
return String.valueOf(mappers.get(columnIndex).getSecond().get(wrapped));
|
||||
Object value = mappers.get(columnIndex).getSecond().get(wrapped);
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -35,7 +35,8 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
|
||||
val headerLetter: String? = null,
|
||||
val groupsInCommon: GroupsInCommonSummary = GroupsInCommonSummary(listOf()),
|
||||
val showSelfAsYou: Boolean = false,
|
||||
val showAdminLabel: Boolean = false
|
||||
val showAdminLabel: Boolean = false,
|
||||
val query: String? = null
|
||||
) : ContactSearchData(ContactSearchKey.RecipientSearchKey(recipient.id, false))
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
@@ -54,6 +55,7 @@ import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.SearchUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -63,6 +65,7 @@ import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.compose.MappingEntryProvider
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.compose.MappingEntryProviderBuilder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Locale
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
@@ -230,7 +233,7 @@ object ContactSearchModels {
|
||||
key = { "EmptyModel" }
|
||||
) { model ->
|
||||
Text(
|
||||
text = stringResource(R.string.SearchFragment_no_results, model.empty.query ?: ""),
|
||||
text = if (model.empty.query.isNullOrEmpty()) stringResource(R.string.SearchFragment_no_results_empty) else stringResource(R.string.SearchFragment_no_results, model.empty.query),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -635,6 +638,10 @@ object ContactSearchModels {
|
||||
override fun bindLongPress(model: RecipientModel) {
|
||||
itemView.setOnLongClickListener { onLongClick.onLongClicked(itemView, model.knownRecipient) }
|
||||
}
|
||||
|
||||
override fun getHighlightQuery(model: RecipientModel): String? {
|
||||
return model.knownRecipient.query
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -656,6 +663,7 @@ object ContactSearchModels {
|
||||
protected val adminLabel: TextView = itemView.findViewById(R.id.admin_label)
|
||||
private val startAudio: View = itemView.findViewById(R.id.start_audio)
|
||||
private val startVideo: View = itemView.findViewById(R.id.start_video)
|
||||
private val searchStyleFactory = SearchUtil.StyleFactory { arrayOf(ForegroundColorSpan(ContextCompat.getColor(context, CoreUiR.color.signal_colorOnSurface)), SpanUtil.getBoldSpan()) }
|
||||
|
||||
override fun bind(model: T) {
|
||||
if (isEnabled(model)) {
|
||||
@@ -682,7 +690,14 @@ object ContactSearchModels {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
name.setText(recipient, recipient.getDisplayName(context), suffix, true, showSelfAsYou(model))
|
||||
val query = getHighlightQuery(model)
|
||||
val displayName: CharSequence = if (!query.isNullOrBlank()) {
|
||||
SearchUtil.getHighlightedSpan(Locale.getDefault(), searchStyleFactory, recipient.getDisplayName(context), query, SearchUtil.MATCH_ALL)
|
||||
} else {
|
||||
recipient.getDisplayName(context)
|
||||
}
|
||||
|
||||
name.setText(recipient, displayName, suffix, true, showSelfAsYou(model))
|
||||
|
||||
badge.setBadgeFromRecipient(getRecipient(model))
|
||||
|
||||
@@ -699,6 +714,7 @@ object ContactSearchModels {
|
||||
|
||||
protected open fun isEnabled(model: T): Boolean = true
|
||||
protected open fun showSelfAsYou(model: T): Boolean = false
|
||||
protected open fun getHighlightQuery(model: T): String? = null
|
||||
|
||||
protected open fun bindAvatar(model: T) {
|
||||
avatar.setAvatar(getRecipient(model))
|
||||
|
||||
+13
-9
@@ -243,22 +243,25 @@ class ContactSearchPagedDataSource(
|
||||
/**
|
||||
* 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
|
||||
* comparing its letter to the first visible preceding row in display order (filtered by recordPredicate). Peeking that single
|
||||
* 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? {
|
||||
private fun getHeaderLetterForCurrentRow(cursor: Cursor, recordPredicate: ((Cursor) -> Boolean)? = null): String? {
|
||||
val position = cursor.position
|
||||
val currentLetter = letterForCurrentRow(cursor) ?: return null
|
||||
|
||||
if (position <= 0) {
|
||||
return currentLetter
|
||||
var foundPrevious = false
|
||||
while (cursor.moveToPrevious()) {
|
||||
if (recordPredicate == null || recordPredicate.invoke(cursor)) {
|
||||
foundPrevious = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cursor.moveToPosition(position - 1)
|
||||
val previousLetter = letterForCurrentRow(cursor)
|
||||
val previousLetter = if (foundPrevious) letterForCurrentRow(cursor) else null
|
||||
cursor.moveToPosition(position)
|
||||
|
||||
return if (previousLetter != currentLetter) currentLetter else null
|
||||
@@ -463,18 +466,19 @@ class ContactSearchPagedDataSource(
|
||||
@WorkerThread
|
||||
private fun getGroupMembersContactData(section: ContactSearchConfiguration.Section.GroupMembers, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
|
||||
val groupRecord = section.groupId?.let { contactSearchPagedDataSourceRepository.getGroupRecord(it) }
|
||||
val recordPredicate = filterByRole(section)
|
||||
return getGroupMembersSearchIterator(section, query).use { records ->
|
||||
readContactData(
|
||||
records = records,
|
||||
recordsPredicate = filterByRole(section),
|
||||
recordsPredicate = recordPredicate,
|
||||
section = section,
|
||||
startIndex = startIndex,
|
||||
endIndex = endIndex,
|
||||
recordMapper = {
|
||||
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromSearchCursor(it)
|
||||
val groupsInCommon = if (section.showGroupsInCommon) contactSearchPagedDataSourceRepository.getGroupsInCommon(recipient) else GroupsInCommonSummary(listOf())
|
||||
val headerLetter = if (section.includeLetterHeaders) getHeaderLetterForCurrentRow(it) else null
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, recipient, groupsInCommon = groupsInCommon, headerLetter = headerLetter, showSelfAsYou = section.showSelfAsYou, showAdminLabel = groupRecord?.isAdmin(recipient) == true)
|
||||
val headerLetter = if (section.includeLetterHeaders) getHeaderLetterForCurrentRow(it, recordPredicate) else null
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, recipient, groupsInCommon = groupsInCommon, headerLetter = headerLetter, showSelfAsYou = section.showSelfAsYou, showAdminLabel = groupRecord?.isAdmin(recipient) == true, query = query)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4622,12 +4622,12 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
|
||||
/**
|
||||
* By default, SQLite will prefer numbers over letters when sorting. e.g. (b, a, 1) is sorted as (1, a, b).
|
||||
* This order by will using a GLOB pattern to instead sort it as (a, b, 1).
|
||||
* This order by will using a GLOB pattern to instead sort it as (a, b, 1). We also put null names (eg deleted accounts) at the end
|
||||
*
|
||||
* @param column The name of the column to sort by
|
||||
*/
|
||||
private fun orderByPreferringAlphaOverNumeric(column: String): String {
|
||||
return "CASE WHEN $column GLOB '[0-9]*' THEN 1 ELSE 0 END, $column"
|
||||
return "CASE WHEN $column IS NULL THEN 2 WHEN $column GLOB '[0-9]*' THEN 1 ELSE 0 END, $column"
|
||||
}
|
||||
|
||||
private fun <T> Optional<T>.isAbsent(): Boolean {
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
@@ -29,6 +28,7 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -45,7 +45,6 @@ import org.signal.core.ui.compose.Dividers
|
||||
import org.signal.core.ui.compose.LocalFragmentManager
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -292,9 +291,13 @@ fun MemberSearchScreen(
|
||||
val filterActive = memberFilter != MemberSearchViewModel.MemberFilter.ALL
|
||||
IconButton(onClick = { showFilterDialog = true }) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_filter_24),
|
||||
tint = if (filterActive) MaterialTheme.colorScheme.primary else LocalContentColor.current,
|
||||
contentDescription = stringResource(R.string.MemberSearchFragment__filter)
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_filter_20),
|
||||
tint = if (filterActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface,
|
||||
contentDescription = stringResource(R.string.MemberSearchFragment__filter),
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.background(color = if (filterActive) MaterialTheme.colorScheme.primary else Color.Transparent, shape = CircleShape)
|
||||
.padding(3.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -375,13 +378,17 @@ private fun MemberSearchContent(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
val query by contactViewModel.query.collectAsStateWithLifecycle()
|
||||
val hint = when (memberFilter) {
|
||||
MemberSearchViewModel.MemberFilter.ALL -> stringResource(R.string.MemberSearchFragment__search_members)
|
||||
MemberSearchViewModel.MemberFilter.ADMINS -> stringResource(R.string.MemberSearchFragment__search_admins)
|
||||
MemberSearchViewModel.MemberFilter.CONTACTS -> stringResource(R.string.MemberSearchFragment__search_contacts)
|
||||
}
|
||||
RecipientSearchBar(
|
||||
hint = stringResource(R.string.MemberSearchFragment__search_members),
|
||||
hint = hint,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
.focusRequester(focusRequester)
|
||||
.horizontalGutters(),
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp)
|
||||
.focusRequester(focusRequester),
|
||||
query = query ?: "",
|
||||
onQueryChange = { contactViewModel.setQuery(it) },
|
||||
onSearch = { contactViewModel.setQuery(it) }
|
||||
|
||||
Reference in New Issue
Block a user