diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt index 9e54cc639e..08626d85ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt @@ -274,8 +274,17 @@ class DividerPreferenceViewHolder(itemView: View) : MappingViewHolder(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() } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index bf4d4f9f38..8b8f4e4af8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt index 94995ce5a7..a70c0b69a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt @@ -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() -class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel() +class SectionHeaderPreference( + override val title: DSLSettingsText, + override val iconEnd: DSLSettingsIcon? = null, + val onClick: (() -> Unit)? = null +) : PreferenceModel() diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java index 21fcf4dc4b..73d708fd52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearch.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearch.kt index 936877c159..027ee8e5cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearch.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearch.kt @@ -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 ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt index cfc294e8ab..1c5d3a270b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt @@ -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) /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchModels.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchModels.kt index 53c06ae8c1..009e350691 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchModels.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchModels.kt @@ -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( + 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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt index a4726264d0..720a54d70f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt @@ -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 { - return CursorSearchIterator(contactSearchPagedDataSourceRepository.queryGroupMemberContacts(query)) + private fun getGroupMembersSearchIterator(section: ContactSearchConfiguration.Section.GroupMembers, query: String?): ContactSearchIterator { + return CursorSearchIterator(contactSearchPagedDataSourceRepository.queryGroupMemberContacts(section, query)) } private fun readContactData( @@ -445,7 +446,7 @@ class ContactSearchPagedDataSource( @WorkerThread private fun getGroupMembersContactData(section: ContactSearchConfiguration.Section.GroupMembers, query: String?, startIndex: Int, endIndex: Int): List { - 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) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt index e47a190872..d81bfd7940 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt @@ -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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchView.kt index 3bac888f31..ac4919ffe9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchView.kt @@ -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() ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardScreen.kt index a8ba90b77d..2f8b0e9e06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardScreen.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index e9b3b0aa44..4bb34e9edf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -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 = diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/MemberSearchFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/MemberSearchFragment.kt new file mode 100644 index 0000000000..9ce5534b41 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/MemberSearchFragment.kt @@ -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()?.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): Set { + val recipientId = contactSearchKeys.filterIsInstance().firstOrNull()?.recipientId + if (recipientId != null) { + RecipientBottomSheetDialogFragment.show(fragmentManager, recipientId, groupId) + } + return emptySet() + } + } + + private fun getConfiguration(contactSearchState: ContactSearchState): ContactSearchConfiguration { + return findListener()?.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 + ) + } + } +} diff --git a/app/src/main/res/layout/dsl_section_header.xml b/app/src/main/res/layout/dsl_section_header.xml index bb5a38d2cb..a7298a62c4 100644 --- a/app/src/main/res/layout/dsl_section_header.xml +++ b/app/src/main/res/layout/dsl_section_header.xml @@ -1,10 +1,11 @@ - - + + + + diff --git a/app/src/main/res/navigation/conversation_settings.xml b/app/src/main/res/navigation/conversation_settings.xml index f13d5846f5..80fb688322 100644 --- a/app/src/main/res/navigation/conversation_settings.xml +++ b/app/src/main/res/navigation/conversation_settings.xml @@ -130,6 +130,20 @@ app:nullable="false" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index beee50599a..64f04713b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9870,6 +9870,9 @@ %1$d groups with the same members + + Search members + Can\'t restore backup