Add group search membership.

This commit is contained in:
Michelle Tang
2026-06-30 12:19:30 -04:00
parent dd671a8489
commit ec18fb4317
16 changed files with 359 additions and 26 deletions
@@ -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() }
}
}
}
@@ -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
)
}
}
@@ -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)
@@ -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)
}
)
}
@@ -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()
)
}
}
@@ -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
)
}
}
}
+13 -3
View File
@@ -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>
+3
View File
@@ -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>