Add more features to group member search.

This commit is contained in:
Michelle Tang
2026-07-02 15:16:18 -04:00
committed by Alex Hart
parent 3d0a743fe3
commit a849af6880
15 changed files with 232 additions and 62 deletions
@@ -280,7 +280,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
false,
new ContactSelectionListAdapter.ArbitraryRepository(),
new SearchRepository(requireContext().getString(R.string.note_to_self)),
new ContactSearchPagedDataSourceRepository(requireContext()),
new ContactSearchPagedDataSourceRepository(requireContext(), requireContext().getString(R.string.note_to_self)),
fixedContacts,
false
)
@@ -33,7 +33,7 @@ import kotlin.Pair;
public class ContactRepository {
private final RecipientTable recipientTable;
private final String noteToSelfTitle;
private final String selfTitle;
public static final String ID_COLUMN = "id";
public static final String NAME_COLUMN = "name";
@@ -101,14 +101,14 @@ public class ContactRepository {
}));
}};
public ContactRepository(@NonNull String noteToSelfTitle) {
this.noteToSelfTitle = noteToSelfTitle;
this.recipientTable = SignalDatabase.recipients();
public ContactRepository(@NonNull String selfTitle) {
this.selfTitle = selfTitle;
this.recipientTable = SignalDatabase.recipients();
}
@WorkerThread
public @NonNull Cursor querySignalContacts(@NonNull String query) {
return querySignalContacts(new RecipientTable.ContactSearchQuery(query, new RecipientTable.IncludeSelfMode.IncludeWithRemap(noteToSelfTitle), ContactSearchSortOrder.NATURAL));
return querySignalContacts(new RecipientTable.ContactSearchQuery(query, new RecipientTable.IncludeSelfMode.IncludeWithRemap(selfTitle), ContactSearchSortOrder.NATURAL));
}
@WorkerThread
@@ -123,7 +123,7 @@ public class ContactRepository {
public @NonNull Cursor queryGroupMemberContacts(@NonNull String query, @Nullable GroupId groupId) {
Cursor cursor;
if (groupId != null) {
cursor = recipientTable.queryGroupMemberContactsForGroup(groupId, query);
cursor = recipientTable.queryGroupMemberContactsForGroup(groupId, query, selfTitle);
} else if (TextUtils.isEmpty(query)) {
cursor = recipientTable.getGroupMemberContacts();
} else {
@@ -135,7 +135,9 @@ class ContactSearchConfiguration private constructor(
override val expandConfig: ExpandConfig? = null,
val includeLetterHeaders: Boolean = false,
val showGroupsInCommon: Boolean = true,
val groupId: GroupId? = null
val groupId: GroupId? = null,
val showSelfAsYou: Boolean = false,
val roleFilter: MemberRole = MemberRole.ALL
) : Section(SectionKey.GROUP_MEMBERS)
/**
@@ -299,6 +301,15 @@ class ContactSearchConfiguration private constructor(
val maxCountWhenNotExpanded: (ActiveContactCount) -> Int = { 2 }
)
/**
* Role-based filter for [Section.GroupMembers].
*/
enum class MemberRole {
ALL,
ADMINS,
CONTACTS
}
/**
* Network transport type for individual recipients.
*/
@@ -33,7 +33,9 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
val recipient: Recipient,
val shortSummary: Boolean = false,
val headerLetter: String? = null,
val groupsInCommon: GroupsInCommonSummary = GroupsInCommonSummary(listOf())
val groupsInCommon: GroupsInCommonSummary = GroupsInCommonSummary(listOf()),
val showSelfAsYou: Boolean = false,
val showAdminLabel: Boolean = false
) : ContactSearchData(ContactSearchKey.RecipientSearchKey(recipient.id, false))
/**
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -467,7 +466,9 @@ object ContactSearchModels {
) : MappingModel<RecipientModel>, FastScrollCharacterProvider {
override fun getFastScrollCharacter(context: Context): CharSequence {
val name = if (knownRecipient.recipient.isSelf) {
val name = if (knownRecipient.recipient.isSelf && knownRecipient.showSelfAsYou) {
context.getString(R.string.Recipient_you)
} else if (knownRecipient.recipient.isSelf) {
context.getString(R.string.note_to_self)
} else {
knownRecipient.recipient.getDisplayName(context)
@@ -577,6 +578,16 @@ object ContactSearchModels {
override fun isSelected(model: RecipientModel): Boolean = model.isSelected
override fun getData(model: RecipientModel): ContactSearchData.KnownRecipient = model.knownRecipient
override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient
override fun showSelfAsYou(model: RecipientModel): Boolean = model.knownRecipient.showSelfAsYou
override fun bindAvatar(model: RecipientModel) {
if (model.knownRecipient.showSelfAsYou) {
avatar.setAvatarUsingProfile(getRecipient(model))
} else {
super.bindAvatar(model)
}
}
override fun bindNumberField(model: RecipientModel) {
val recipient = getRecipient(model)
if (model.knownRecipient.sectionKey == ContactSearchConfiguration.SectionKey.GROUP_MEMBERS && displayOptions.displaySecondaryInformation != ContactSearchAdapter.DisplaySecondaryInformation.NEVER) {
@@ -609,6 +620,10 @@ object ContactSearchModels {
checkbox.isEnabled = !fixedContacts.contains(model.knownRecipient.contactSearchKey)
}
override fun bindLabelField(model: RecipientModel) {
adminLabel.visible = model.knownRecipient.showAdminLabel
}
override fun isEnabled(model: RecipientModel): Boolean {
return !fixedContacts.contains(model.knownRecipient.contactSearchKey)
}
@@ -638,6 +653,7 @@ object ContactSearchModels {
protected val name: FromTextView = itemView.findViewById(R.id.name)
protected val number: TextView = itemView.findViewById(R.id.number)
protected val label: TextView = itemView.findViewById(R.id.label)
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)
@@ -666,7 +682,7 @@ object ContactSearchModels {
} else {
null
}
name.setText(recipient, suffix)
name.setText(recipient, recipient.getDisplayName(context), suffix, true, showSelfAsYou(model))
badge.setBadgeFromRecipient(getRecipient(model))
@@ -682,6 +698,7 @@ object ContactSearchModels {
}
protected open fun isEnabled(model: T): Boolean = true
protected open fun showSelfAsYou(model: T): Boolean = false
protected open fun bindAvatar(model: T) {
avatar.setAvatar(getRecipient(model))
@@ -136,7 +136,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.Recents -> getRecentsSearchIterator(section, query).getCollectionSizeAndClose(section, query, null)
is ContactSearchConfiguration.Section.Stories -> getStoriesSearchIterator(query).getCollectionSizeAndClose(section, query, null)
is ContactSearchConfiguration.Section.Arbitrary -> arbitraryRepository?.getSize(section, query) ?: error("Invalid arbitrary section.")
is ContactSearchConfiguration.Section.GroupMembers -> getGroupMembersSearchIterator(section, query).getCollectionSizeAndClose(section, query, null)
is ContactSearchConfiguration.Section.GroupMembers -> getGroupMembersSearchIterator(section, query).getCollectionSizeAndClose(section, query, filterByRole(section))
is ContactSearchConfiguration.Section.Chats -> getThreadData(query, section.isUnreadOnly).getCollectionSizeAndClose(section, query, null)
is ContactSearchConfiguration.Section.Messages -> getMessageData(query).getCollectionSizeAndClose(section, query, null)
is ContactSearchConfiguration.Section.GroupsWithMembers -> getGroupsWithMembersIterator(query).getCollectionSizeAndClose(section, query, null)
@@ -444,12 +444,29 @@ class ContactSearchPagedDataSource(
}
}
private fun filterByRole(section: ContactSearchConfiguration.Section.GroupMembers): ((Cursor) -> Boolean)? {
if (section.roleFilter == ContactSearchConfiguration.MemberRole.ALL || section.groupId == null) {
return null
}
val groupRecord = contactSearchPagedDataSourceRepository.getGroupRecord(section.groupId) ?: return null
return { cursor ->
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromSearchCursor(cursor)
when (section.roleFilter) {
ContactSearchConfiguration.MemberRole.ALL -> true
ContactSearchConfiguration.MemberRole.ADMINS -> groupRecord.isAdmin(recipient)
ContactSearchConfiguration.MemberRole.CONTACTS -> recipient.isSystemContact
}
}
}
@WorkerThread
private fun getGroupMembersContactData(section: ContactSearchConfiguration.Section.GroupMembers, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
val groupRecord = section.groupId?.let { contactSearchPagedDataSourceRepository.getGroupRecord(it) }
return getGroupMembersSearchIterator(section, query).use { records ->
readContactData(
records = records,
recordsPredicate = null,
recordsPredicate = filterByRole(section),
section = section,
startIndex = startIndex,
endIndex = endIndex,
@@ -457,7 +474,7 @@ class ContactSearchPagedDataSource(
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)
ContactSearchData.KnownRecipient(section.sectionKey, recipient, groupsInCommon = groupsInCommon, headerLetter = headerLetter, showSelfAsYou = section.showSelfAsYou, showAdminLabel = groupRecord?.isAdmin(recipient) == true)
}
)
}
@@ -6,6 +6,7 @@ import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.signal.core.util.CursorUtil
import org.signal.core.util.LRUCache
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.ContactRepository
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
@@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupsInCommonRepository
import org.thoughtcrime.securesms.groups.GroupsInCommonSummary
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -28,11 +30,13 @@ import org.thoughtcrime.securesms.recipients.RecipientId
* having to deal with database access.
*/
open class ContactSearchPagedDataSourceRepository(
context: Context
context: Context,
selfTitle: String = context.getString(R.string.note_to_self)
) {
private val contactRepository = ContactRepository(context.getString(R.string.note_to_self))
private val contactRepository = ContactRepository(selfTitle)
private val context = context.applicationContext
private val groupRecordCache = LRUCache<GroupId, GroupRecord?>(100)
open fun getLatestStorySends(activeStoryCutoffDuration: Long): List<StorySend> {
return SignalStore.story
@@ -142,4 +146,15 @@ open class ContactSearchPagedDataSourceRepository(
val myStory = context.getString(R.string.Recipient_my_story)
return myStory.contains(query, ignoreCase = true)
}
open fun getGroupRecord(groupId: GroupId): GroupRecord? {
if (!groupRecordCache.containsKey(groupId)) {
groupRecordCache[groupId] = SignalDatabase.groups.getGroup(groupId).orElse(null)
}
return groupRecordCache[groupId]
}
open fun clearGroupRecordCache() {
groupRecordCache.clear()
}
}
@@ -269,6 +269,11 @@ class ContactSearchViewModel(
controller.value?.onDataInvalidated()
}
fun refreshGroupData() {
contactSearchPagedDataSourceRepository.clearGroupRecordCache()
refresh()
}
data class ScrollRequest(val position: Int)
class Factory(
@@ -340,7 +340,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
false,
new ConversationListSearchAdapter.ChatFilterRepository(),
new SearchRepository(requireContext().getString(R.string.note_to_self)),
new ContactSearchPagedDataSourceRepository(requireContext()),
new ContactSearchPagedDataSourceRepository(requireContext(), requireContext().getString(R.string.note_to_self)),
Collections.emptySet(),
true
)).get(ContactSearchViewModel.class);
@@ -3546,7 +3546,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
val selfId = Recipient.self().id.toLong()
arrayOf(
ID,
"""CASE WHEN ${TABLE_NAME}.$ID = $selfId THEN '${includeSelfMode.noteToSelfTitle}' ELSE $SYSTEM_JOINED_NAME END AS $SYSTEM_JOINED_NAME""",
"""CASE WHEN ${TABLE_NAME}.$ID = $selfId THEN '${includeSelfMode.title}' ELSE $SYSTEM_JOINED_NAME END AS $SYSTEM_JOINED_NAME""",
E164,
EMAIL,
SYSTEM_PHONE_LABEL,
@@ -3556,9 +3556,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
ABOUT_EMOJI,
EXTRAS,
GROUPS_IN_COMMON,
"""CASE WHEN ${TABLE_NAME}.$ID = $selfId THEN '${includeSelfMode.noteToSelfTitle}' ELSE COALESCE(NULLIF($PROFILE_JOINED_NAME, ''), NULLIF($PROFILE_GIVEN_NAME, '')) END AS $SEARCH_PROFILE_NAME""",
"""CASE WHEN ${TABLE_NAME}.$ID = $selfId THEN '${includeSelfMode.title}' ELSE COALESCE(NULLIF($PROFILE_JOINED_NAME, ''), NULLIF($PROFILE_GIVEN_NAME, '')) END AS $SEARCH_PROFILE_NAME""",
"""
CASE WHEN ${TABLE_NAME}.$ID = $selfId THEN '${includeSelfMode.noteToSelfTitle.lowercase()}' ELSE
CASE WHEN ${TABLE_NAME}.$ID = $selfId THEN '${includeSelfMode.title.lowercase()}' ELSE
LOWER(
COALESCE(
NULLIF($NICKNAME_JOINED_NAME, ''),
@@ -3712,24 +3712,23 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return readableDatabase.query(TABLE_NAME, searchProjection(IncludeSelfMode.Exclude), selection, args, null, null, orderBy)
}
fun queryGroupMemberContactsForGroup(groupId: GroupId, inputQuery: String): Cursor? {
fun queryGroupMemberContactsForGroup(groupId: GroupId, inputQuery: String, selfTitle: String): Cursor? {
val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + E164
val queryFilter = if (inputQuery.isNotEmpty()) "AND ($SORT_NAME GLOB ? OR $USERNAME GLOB ?)" else ""
val selection = """
$ID != ? AND
$ID IN (SELECT ${GroupTable.MembershipTable.RECIPIENT_ID} FROM ${GroupTable.MembershipTable.TABLE_NAME} WHERE ${GroupTable.MembershipTable.GROUP_ID} = ?)
$queryFilter
"""
val args = if (queryFilter.isBlank()) {
mutableListOf(Recipient.self().id.serialize(), groupId.toString())
mutableListOf(groupId.toString())
} else {
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
mutableListOf(Recipient.self().id.serialize(), groupId.toString(), query, query)
mutableListOf(groupId.toString(), query, query)
}
return readableDatabase.query(TABLE_NAME, searchProjection(IncludeSelfMode.Exclude), selection, args.toTypedArray(), null, null, orderBy)
return readableDatabase.query(TABLE_NAME, searchProjection(IncludeSelfMode.IncludeWithRemap(selfTitle)), selection, args.toTypedArray(), null, null, orderBy)
}
fun queryAllContacts(inputQuery: String, includeSelfMode: IncludeSelfMode): Cursor? {
@@ -4785,7 +4784,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
data object Exclude : IncludeSelfMode
data object IncludeWithoutRemap : IncludeSelfMode
data class IncludeWithRemap(val noteToSelfTitle: String) : IncludeSelfMode
data class IncludeWithRemap(val title: String) : IncludeSelfMode
}
@VisibleForTesting
@@ -4,12 +4,14 @@ import android.os.Bundle
import android.view.View
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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
import androidx.compose.material3.TopAppBar
@@ -17,16 +19,23 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
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.vector.ImageVector
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.LocalFragmentManager
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.horizontalGutters
@@ -51,9 +60,9 @@ import org.thoughtcrime.securesms.search.SearchRepository
import org.thoughtcrime.securesms.util.fragments.findListener
/**
* Fragment that shows all members in a group (excluding self)
* Fragment that shows all members in a group (including self)
*/
class MemberSearchFragment : ComposeFragment() {
class MemberSearchFragment : ComposeFragment(), RecipientBottomSheetDialogFragment.Callback {
companion object {
@@ -81,16 +90,27 @@ class MemberSearchFragment : ComposeFragment() {
performSafetyNumberChecks = false,
arbitraryRepository = findListener<SearchConfigurationProvider>()?.getArbitraryRepository(),
searchRepository = SearchRepository(requireContext().getString(R.string.Recipient_you)),
contactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(requireContext())
contactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(requireContext(), requireContext().getString(R.string.Recipient_you))
)
}
private val memberSearchViewModel: MemberSearchViewModel by viewModels()
override fun onRecipientBottomSheetDismissed() {
contactViewModel.refreshGroupData()
}
override fun onMessageClicked() = Unit
@Composable
override fun FragmentContent() {
val memberFilter by memberSearchViewModel.memberFilter.collectAsStateWithLifecycle()
CompositionLocalProvider(LocalFragmentManager provides childFragmentManager) {
MemberSearchScreen(
contactViewModel = contactViewModel,
mapStateToConfiguration = this::getConfiguration,
memberFilter = memberFilter,
onFilterSelected = { memberSearchViewModel.setFilter(it) },
mapStateToConfiguration = { state -> getConfiguration(state, memberFilter) },
contactSearchCallbacks = remember {
SearchCallbacks(
fragmentManager = childFragmentManager,
@@ -114,7 +134,7 @@ class MemberSearchFragment : ComposeFragment() {
}
}
private fun getConfiguration(contactSearchState: ContactSearchState): ContactSearchConfiguration {
private fun getConfiguration(contactSearchState: ContactSearchState, memberFilter: MemberSearchViewModel.MemberFilter): ContactSearchConfiguration {
return findListener<SearchConfigurationProvider>()?.getSearchConfiguration(childFragmentManager, contactSearchState) ?: ContactSearchConfiguration.build {
query = contactSearchState.query
@@ -123,7 +143,13 @@ class MemberSearchFragment : ComposeFragment() {
includeHeader = false,
includeLetterHeaders = true,
groupId = groupId,
showGroupsInCommon = false
showGroupsInCommon = false,
showSelfAsYou = true,
roleFilter = when (memberFilter) {
MemberSearchViewModel.MemberFilter.ALL -> ContactSearchConfiguration.MemberRole.ALL
MemberSearchViewModel.MemberFilter.ADMINS -> ContactSearchConfiguration.MemberRole.ADMINS
MemberSearchViewModel.MemberFilter.CONTACTS -> ContactSearchConfiguration.MemberRole.CONTACTS
}
)
)
@@ -138,10 +164,27 @@ class MemberSearchFragment : ComposeFragment() {
@Composable
fun MemberSearchScreen(
contactViewModel: ContactSearchViewModel,
memberFilter: MemberSearchViewModel.MemberFilter,
onFilterSelected: (MemberSearchViewModel.MemberFilter) -> Unit,
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
contactSearchCallbacks: MemberSearchFragment.SearchCallbacks
) {
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
var showFilterDialog by remember { mutableStateOf(false) }
if (showFilterDialog) {
val filters = MemberSearchViewModel.MemberFilter.entries
Dialogs.RadioListDialog(
onDismissRequest = { showFilterDialog = false },
title = stringResource(R.string.MemberSearchFragment__filter),
labels = stringArrayResource(R.array.filter_search_entries),
values = filters.map { it.name }.toTypedArray(),
selectedIndex = filters.indexOf(memberFilter),
onSelected = { index -> onFilterSelected(filters[index]) }
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -153,12 +196,23 @@ fun MemberSearchScreen(
contentDescription = stringResource(R.string.DSLSettingsToolbar__navigate_up)
)
}
},
actions = {
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)
)
}
}
)
}
) {
MemberSearchContent(
contactViewModel = contactViewModel,
memberFilter = memberFilter,
mapStateToConfiguration = mapStateToConfiguration,
contactSearchCallbacks = contactSearchCallbacks,
modifier = Modifier.padding(it)
@@ -169,6 +223,7 @@ fun MemberSearchScreen(
@Composable
private fun MemberSearchContent(
contactViewModel: ContactSearchViewModel,
memberFilter: MemberSearchViewModel.MemberFilter,
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
modifier: Modifier = Modifier,
contactSearchCallbacks: MemberSearchFragment.SearchCallbacks
@@ -179,34 +234,39 @@ private fun MemberSearchContent(
focusRequester.requestFocus()
}
Row(modifier = modifier) {
Column(
modifier = Modifier
.weight(1f)
) {
val query by contactViewModel.query.collectAsStateWithLifecycle()
RecipientSearchBar(
hint = stringResource(R.string.MemberSearchFragment__search_members),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp)
.focusRequester(focusRequester)
.horizontalGutters(),
query = query ?: "",
onQueryChange = { contactViewModel.setQuery(it) },
onSearch = { contactViewModel.setQuery(it) }
)
val currentMapStateToConfiguration by rememberUpdatedState(mapStateToConfiguration)
LaunchedEffect(memberFilter) {
contactViewModel.setConfiguration(
currentMapStateToConfiguration(contactViewModel.configurationState.value)
)
}
ContactSearch(
viewModel = contactViewModel,
mapStateToConfiguration = mapStateToConfiguration,
displayOptions = remember {
ContactSearchAdapter.DisplayOptions(
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
)
},
callbacks = contactSearchCallbacks
)
}
Column(
modifier = modifier.fillMaxSize()
) {
val query by contactViewModel.query.collectAsStateWithLifecycle()
RecipientSearchBar(
hint = stringResource(R.string.MemberSearchFragment__search_members),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp)
.focusRequester(focusRequester)
.horizontalGutters(),
query = query ?: "",
onQueryChange = { contactViewModel.setQuery(it) },
onSearch = { contactViewModel.setQuery(it) }
)
ContactSearch(
viewModel = contactViewModel,
mapStateToConfiguration = mapStateToConfiguration,
displayOptions = remember {
ContactSearchAdapter.DisplayOptions(
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
)
},
callbacks = contactSearchCallbacks,
modifier = Modifier.weight(1f)
)
}
}
@@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.groups.ui
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class MemberSearchViewModel : ViewModel() {
private val _memberFilter = MutableStateFlow(MemberFilter.ALL)
val memberFilter: StateFlow<MemberFilter> = _memberFilter
fun setFilter(filter: MemberFilter) {
_memberFilter.value = filter
}
enum class MemberFilter {
ALL,
ADMINS,
CONTACTS
}
}