mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-21 04:54:41 +01:00
Add group search membership.
This commit is contained in:
@@ -274,8 +274,17 @@ class DividerPreferenceViewHolder(itemView: View) : MappingViewHolder<DividerPre
|
||||
class SectionHeaderPreferenceViewHolder(itemView: View) : MappingViewHolder<SectionHeaderPreference>(itemView) {
|
||||
|
||||
private val sectionHeader: TextView = itemView.findViewById(R.id.section_header)
|
||||
private val iconEndView: ImageView = itemView.findViewById(R.id.icon_end)
|
||||
|
||||
override fun bind(model: SectionHeaderPreference) {
|
||||
sectionHeader.text = model.title.resolve(context)
|
||||
|
||||
val iconEnd = model.iconEnd?.resolve(context)
|
||||
iconEndView.setImageDrawable(iconEnd)
|
||||
iconEndView.visible = iconEnd != null
|
||||
|
||||
if (model.onClick != null) {
|
||||
iconEndView.setOnClickListener { model.onClick() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -830,7 +830,19 @@ class ConversationSettingsFragment :
|
||||
} else {
|
||||
resources.getQuantityString(R.plurals.ContactSelectionListFragment_d_members, memberCount, memberCount)
|
||||
}
|
||||
sectionHeaderPref(DSLSettingsText.from(memberHeaderText))
|
||||
|
||||
if (RemoteConfig.internalUser) {
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(memberHeaderText),
|
||||
iconEnd = DSLSettingsIcon.from(CoreUiR.drawable.symbol_search_24),
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToMemberSearchFragment(groupState.groupId)
|
||||
navController.safeNavigate(action)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
sectionHeaderPref(DSLSettingsText.from(memberHeaderText))
|
||||
}
|
||||
}
|
||||
|
||||
if (groupState.canAddToGroup && !groupState.isTerminated && !state.isDeprecatedOrUnregistered) {
|
||||
|
||||
@@ -137,13 +137,21 @@ class DSLConfiguration {
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun sectionHeaderPref(title: DSLSettingsText) {
|
||||
val preference = SectionHeaderPreference(title)
|
||||
fun sectionHeaderPref(
|
||||
title: DSLSettingsText,
|
||||
iconEnd: DSLSettingsIcon? = null,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
val preference = SectionHeaderPreference(title, iconEnd, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun sectionHeaderPref(title: Int) {
|
||||
val preference = SectionHeaderPreference(DSLSettingsText.from(title))
|
||||
fun sectionHeaderPref(
|
||||
title: Int,
|
||||
iconEnd: DSLSettingsIcon? = null,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
val preference = SectionHeaderPreference(DSLSettingsText.from(title), iconEnd, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
@@ -365,4 +373,8 @@ class ExternalLinkPreference(
|
||||
@StringRes val linkId: Int
|
||||
) : PreferenceModel<ExternalLinkPreference>()
|
||||
|
||||
class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel<SectionHeaderPreference>()
|
||||
class SectionHeaderPreference(
|
||||
override val title: DSLSettingsText,
|
||||
override val iconEnd: DSLSettingsIcon? = null,
|
||||
val onClick: (() -> Unit)? = null
|
||||
) : PreferenceModel<SectionHeaderPreference>()
|
||||
|
||||
@@ -5,12 +5,14 @@ import android.database.CursorWrapper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.util.SignalE164Util;
|
||||
import org.signal.core.util.Util;
|
||||
|
||||
@@ -118,9 +120,15 @@ public class ContactRepository {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull Cursor queryGroupMemberContacts(@NonNull String query) {
|
||||
Cursor cursor = TextUtils.isEmpty(query) ? recipientTable.getGroupMemberContacts()
|
||||
: recipientTable.queryGroupMemberContacts(query);
|
||||
public @NonNull Cursor queryGroupMemberContacts(@NonNull String query, @Nullable GroupId groupId) {
|
||||
Cursor cursor;
|
||||
if (groupId != null) {
|
||||
cursor = recipientTable.queryGroupMemberContactsForGroup(groupId, query);
|
||||
} else if (TextUtils.isEmpty(query)) {
|
||||
cursor = recipientTable.getGroupMemberContacts();
|
||||
} else {
|
||||
cursor = recipientTable.queryGroupMemberContacts(query);
|
||||
}
|
||||
|
||||
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS);
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ fun ContactSearch(
|
||||
userScrollEnabled = !isDisplayingContextMenu,
|
||||
fastScrollerState = fastScrollerState,
|
||||
lazyListState = lazyListState,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
modifier = modifier,
|
||||
letterContent = {
|
||||
Emojifier(text = it.toString()) { annotatedText, inlineContent ->
|
||||
Text(
|
||||
@@ -154,7 +154,7 @@ fun ContactSearch(
|
||||
userScrollEnabled = !isDisplayingContextMenu,
|
||||
controller = mappingCtrl,
|
||||
lazyListState = it,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.search.SearchFilter
|
||||
|
||||
/**
|
||||
@@ -131,7 +132,10 @@ class ContactSearchConfiguration private constructor(
|
||||
*/
|
||||
data class GroupMembers(
|
||||
override val includeHeader: Boolean = true,
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
override val expandConfig: ExpandConfig? = null,
|
||||
val includeLetterHeaders: Boolean = false,
|
||||
val showGroupsInCommon: Boolean = true,
|
||||
val groupId: GroupId? = null
|
||||
) : Section(SectionKey.GROUP_MEMBERS)
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,8 +26,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
@@ -225,6 +227,17 @@ object ContactSearchModels {
|
||||
R.layout.contact_search_chat_type_item
|
||||
).createViewHolder(FrameLayout(ctx))
|
||||
}
|
||||
entry<EmptyModel>(
|
||||
key = { "EmptyModel" }
|
||||
) { model ->
|
||||
Text(
|
||||
text = stringResource(R.string.SearchFragment_no_results, model.empty.query ?: ""),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@@ -566,9 +579,11 @@ object ContactSearchModels {
|
||||
override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient
|
||||
override fun bindNumberField(model: RecipientModel) {
|
||||
val recipient = getRecipient(model)
|
||||
if (model.knownRecipient.sectionKey == ContactSearchConfiguration.SectionKey.GROUP_MEMBERS) {
|
||||
if (model.knownRecipient.sectionKey == ContactSearchConfiguration.SectionKey.GROUP_MEMBERS && displayOptions.displaySecondaryInformation != ContactSearchAdapter.DisplaySecondaryInformation.NEVER) {
|
||||
number.text = model.knownRecipient.groupsInCommon.toDisplayText(context, displayGroupsLimit = 2)
|
||||
number.visible = true
|
||||
} else if (model.knownRecipient.sectionKey == ContactSearchConfiguration.SectionKey.GROUP_MEMBERS) {
|
||||
number.visible = false
|
||||
} else if (model.shortSummary && recipient.isGroup) {
|
||||
val count = recipient.participantIds.size
|
||||
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
|
||||
|
||||
+8
-6
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.database.model.ThreadWithRecipient
|
||||
import org.thoughtcrime.securesms.groups.GroupsInCommonSummary
|
||||
import org.thoughtcrime.securesms.keyvalue.StorySend
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -135,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(query).getCollectionSizeAndClose(section, query, null)
|
||||
is ContactSearchConfiguration.Section.GroupMembers -> getGroupMembersSearchIterator(section, query).getCollectionSizeAndClose(section, query, null)
|
||||
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)
|
||||
@@ -296,8 +297,8 @@ class ContactSearchPagedDataSource(
|
||||
return CursorSearchIterator(contactSearchPagedDataSourceRepository.getRecents(section))
|
||||
}
|
||||
|
||||
private fun getGroupMembersSearchIterator(query: String?): ContactSearchIterator<Cursor> {
|
||||
return CursorSearchIterator(contactSearchPagedDataSourceRepository.queryGroupMemberContacts(query))
|
||||
private fun getGroupMembersSearchIterator(section: ContactSearchConfiguration.Section.GroupMembers, query: String?): ContactSearchIterator<Cursor> {
|
||||
return CursorSearchIterator(contactSearchPagedDataSourceRepository.queryGroupMemberContacts(section, query))
|
||||
}
|
||||
|
||||
private fun <R> readContactData(
|
||||
@@ -445,7 +446,7 @@ class ContactSearchPagedDataSource(
|
||||
|
||||
@WorkerThread
|
||||
private fun getGroupMembersContactData(section: ContactSearchConfiguration.Section.GroupMembers, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
|
||||
return getGroupMembersSearchIterator(query).use { records ->
|
||||
return getGroupMembersSearchIterator(section, query).use { records ->
|
||||
readContactData(
|
||||
records = records,
|
||||
recordsPredicate = null,
|
||||
@@ -454,8 +455,9 @@ class ContactSearchPagedDataSource(
|
||||
endIndex = endIndex,
|
||||
recordMapper = {
|
||||
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromSearchCursor(it)
|
||||
val groupsInCommon = contactSearchPagedDataSourceRepository.getGroupsInCommon(recipient)
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, recipient, groupsInCommon = groupsInCommon)
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ open class ContactSearchPagedDataSourceRepository(
|
||||
return contactRepository.querySignalContacts(contactsSearchQuery)
|
||||
}
|
||||
|
||||
open fun queryGroupMemberContacts(query: String?): Cursor? {
|
||||
return contactRepository.queryGroupMemberContacts(query ?: "")
|
||||
open fun queryGroupMemberContacts(section: ContactSearchConfiguration.Section.GroupMembers, query: String?): Cursor? {
|
||||
return contactRepository.queryGroupMemberContacts(query ?: "", section.groupId)
|
||||
}
|
||||
|
||||
open fun getGroupSearchIterator(
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -158,7 +159,7 @@ class ContactSearchView : AbstractComposeView {
|
||||
longClickCallbacks = currentLongClickCallbacks ?: rememberDefaultContactSearchItemLongClickCallbacks(),
|
||||
storyContextMenuCallbacks = currentStoryContextMenuCallbacks ?: rememberDefaultContactSearchItemStoryContextMenuCallbacks(vm),
|
||||
callButtonClickCallbacks = currentCallButtonClickCallbacks ?: rememberDefaultContactSearchItemCallButtonClickCallbacks(),
|
||||
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())
|
||||
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()).fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.conversation.mutiselect.forward
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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
|
||||
@@ -161,7 +162,7 @@ private fun MultiselectForwardContent(
|
||||
viewModel = contactSearchViewModel,
|
||||
mapStateToConfiguration = mapStateToConfiguration,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize()
|
||||
.weight(1f),
|
||||
additionalEntries = additionalEntries,
|
||||
displayOptions = remember {
|
||||
|
||||
@@ -3712,6 +3712,26 @@ 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? {
|
||||
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())
|
||||
} else {
|
||||
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
|
||||
mutableListOf(Recipient.self().id.serialize(), groupId.toString(), query, query)
|
||||
}
|
||||
|
||||
return readableDatabase.query(TABLE_NAME, searchProjection(IncludeSelfMode.Exclude), selection, args.toTypedArray(), null, null, orderBy)
|
||||
}
|
||||
|
||||
fun queryAllContacts(inputQuery: String, includeSelfMode: IncludeSelfMode): Cursor? {
|
||||
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
|
||||
val selection =
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
package org.thoughtcrime.securesms.groups.ui
|
||||
|
||||
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.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.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.LocalFragmentManager
|
||||
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
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearch
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchCallbacks
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchPagedDataSourceRepository
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchRepository
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchViewModel
|
||||
import org.thoughtcrime.securesms.conversation.RecipientSearchBar
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.SearchConfigurationProvider
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.search.SearchRepository
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
||||
/**
|
||||
* Fragment that shows all members in a group (excluding self)
|
||||
*/
|
||||
class MemberSearchFragment : ComposeFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = Log.tag(MemberSearchFragment::class.java)
|
||||
private const val ARG_GROUP_ID = "group_id"
|
||||
|
||||
fun newInstance(groupId: GroupId.V2): MemberSearchFragment {
|
||||
return MemberSearchFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_GROUP_ID, groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val groupId: GroupId.V2 by lazy {
|
||||
requireArguments().requireParcelableCompat(ARG_GROUP_ID, GroupId.V2::class.java)
|
||||
}
|
||||
|
||||
private val contactViewModel: ContactSearchViewModel by viewModels {
|
||||
ContactSearchViewModel.Factory(
|
||||
selectionLimits = SelectionLimits(0, 0),
|
||||
isMultiSelect = false,
|
||||
repository = ContactSearchRepository(),
|
||||
performSafetyNumberChecks = false,
|
||||
arbitraryRepository = findListener<SearchConfigurationProvider>()?.getArbitraryRepository(),
|
||||
searchRepository = SearchRepository(requireContext().getString(R.string.Recipient_you)),
|
||||
contactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(requireContext())
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
CompositionLocalProvider(LocalFragmentManager provides childFragmentManager) {
|
||||
MemberSearchScreen(
|
||||
contactViewModel = contactViewModel,
|
||||
mapStateToConfiguration = this::getConfiguration,
|
||||
contactSearchCallbacks = remember {
|
||||
SearchCallbacks(
|
||||
fragmentManager = childFragmentManager,
|
||||
groupId = groupId
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SearchCallbacks(
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val groupId: GroupId.V2
|
||||
) : ContactSearchCallbacks.Simple() {
|
||||
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
|
||||
val recipientId = contactSearchKeys.filterIsInstance<ContactSearchKey.RecipientSearchKey>().firstOrNull()?.recipientId
|
||||
if (recipientId != null) {
|
||||
RecipientBottomSheetDialogFragment.show(fragmentManager, recipientId, groupId)
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(contactSearchState: ContactSearchState): ContactSearchConfiguration {
|
||||
return findListener<SearchConfigurationProvider>()?.getSearchConfiguration(childFragmentManager, contactSearchState) ?: ContactSearchConfiguration.build {
|
||||
query = contactSearchState.query
|
||||
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.GroupMembers(
|
||||
includeHeader = false,
|
||||
includeLetterHeaders = true,
|
||||
groupId = groupId,
|
||||
showGroupsInCommon = false
|
||||
)
|
||||
)
|
||||
|
||||
withEmptyState {
|
||||
addSection(ContactSearchConfiguration.Section.Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MemberSearchScreen(
|
||||
contactViewModel: ContactSearchViewModel,
|
||||
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
|
||||
contactSearchCallbacks: MemberSearchFragment.SearchCallbacks
|
||||
) {
|
||||
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(R.string.MemberSearchFragment__search_members)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = SignalIcons.ArrowStart.imageVector,
|
||||
contentDescription = stringResource(R.string.DSLSettingsToolbar__navigate_up)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
MemberSearchContent(
|
||||
contactViewModel = contactViewModel,
|
||||
mapStateToConfiguration = mapStateToConfiguration,
|
||||
contactSearchCallbacks = contactSearchCallbacks,
|
||||
modifier = Modifier.padding(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberSearchContent(
|
||||
contactViewModel: ContactSearchViewModel,
|
||||
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
|
||||
modifier: Modifier = Modifier,
|
||||
contactSearchCallbacks: MemberSearchFragment.SearchCallbacks
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
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) }
|
||||
)
|
||||
|
||||
ContactSearch(
|
||||
viewModel = contactViewModel,
|
||||
mapStateToConfiguration = mapStateToConfiguration,
|
||||
displayOptions = remember {
|
||||
ContactSearchAdapter.DisplayOptions(
|
||||
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
|
||||
)
|
||||
},
|
||||
callbacks = contactSearchCallbacks
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
||||
@@ -12,8 +13,17 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/section_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="@style/Signal.Text.TitleSmall"
|
||||
tools:text="Section Header" />
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -130,6 +130,20 @@
|
||||
app:nullable="false" />
|
||||
</action>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_conversationSettingsFragment_to_memberSearchFragment"
|
||||
app:destination="@id/memberSearchFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
|
||||
<argument
|
||||
android:name="group_id"
|
||||
app:argType="org.thoughtcrime.securesms.groups.GroupId"
|
||||
app:nullable="false" />
|
||||
</action>
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
@@ -220,6 +234,16 @@
|
||||
app:nullable="false" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/memberSearchFragment"
|
||||
android:name="org.thoughtcrime.securesms.groups.ui.MemberSearchFragment">
|
||||
|
||||
<argument
|
||||
android:name="group_id"
|
||||
app:argType="org.thoughtcrime.securesms.groups.GroupId"
|
||||
app:nullable="false" />
|
||||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/app_settings_expire_timer" />
|
||||
|
||||
</navigation>
|
||||
@@ -9870,6 +9870,9 @@
|
||||
<item quantity="other">%1$d groups with the same members</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Header for a section showing all group member -->
|
||||
<string name="MemberSearchFragment__search_members">Search members</string>
|
||||
|
||||
<!-- Title of the sheet shown when a local backup restore could not be completed -->
|
||||
<string name="CouldNotCompleteBackupRestoreSheet__title">Can\'t restore backup</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user