mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-04 20:34:14 +01:00
Add invite and add member to group search.
This commit is contained in:
+8
-2
@@ -84,6 +84,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.MemberSearchFragment
|
||||
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity
|
||||
@@ -221,6 +222,10 @@ class ConversationSettingsFragment :
|
||||
navController.safeNavigate(ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToMemberLabelFragment(groupId))
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(MemberSearchFragment.RESULT_ADD_MEMBERS, viewLifecycleOwner) { _, _ ->
|
||||
viewModel.onAddToGroup()
|
||||
}
|
||||
|
||||
recyclerView?.addOnScrollListener(ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatarContainer, toolbarTitle, toolbarBackground))
|
||||
}
|
||||
|
||||
@@ -835,6 +840,7 @@ class ConversationSettingsFragment :
|
||||
|
||||
state.withGroupSettingsState { groupState ->
|
||||
val memberCount = groupState.allMembers.size
|
||||
val canAdd = groupState.canAddToGroup && !groupState.isTerminated && !state.isDeprecatedOrUnregistered
|
||||
|
||||
if (groupState.canAddToGroup || memberCount > 0) {
|
||||
dividerPref()
|
||||
@@ -850,7 +856,7 @@ class ConversationSettingsFragment :
|
||||
title = DSLSettingsText.from(memberHeaderText),
|
||||
iconEnd = DSLSettingsIcon.from(CoreUiR.drawable.symbol_search_24),
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToMemberSearchFragment(groupState.groupId)
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToMemberSearchFragment(groupState.groupId, canAdd, groupState.groupLinkEnabled)
|
||||
navController.safeNavigate(action)
|
||||
}
|
||||
)
|
||||
@@ -859,7 +865,7 @@ class ConversationSettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
if (groupState.canAddToGroup && !groupState.isTerminated && !state.isDeprecatedOrUnregistered) {
|
||||
if (canAdd) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_members),
|
||||
|
||||
@@ -377,4 +377,8 @@ class SectionHeaderPreference(
|
||||
override val title: DSLSettingsText,
|
||||
override val iconEnd: DSLSettingsIcon? = null,
|
||||
val onClick: (() -> Unit)? = null
|
||||
) : PreferenceModel<SectionHeaderPreference>()
|
||||
) : PreferenceModel<SectionHeaderPreference>() {
|
||||
override fun areContentsTheSame(newItem: SectionHeaderPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && onClick == null && newItem.onClick == null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package org.thoughtcrime.securesms.groups.ui
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -32,20 +35,26 @@ 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.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.ui.compose.ComposeFragment
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Dividers
|
||||
import org.signal.core.ui.compose.LocalFragmentManager
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository
|
||||
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.ContactSearchData
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchPagedDataSourceRepository
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchRepository
|
||||
@@ -55,8 +64,15 @@ 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.groups.ui.MemberSearchFragment.AddMembersModel
|
||||
import org.thoughtcrime.securesms.groups.ui.MemberSearchFragment.DividerModel
|
||||
import org.thoughtcrime.securesms.groups.ui.MemberSearchFragment.InviteViaModel
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.GroupLinkBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.search.SearchRepository
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.compose.MappingEntryProvider
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.compose.rememberMappingEntryProvider
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
||||
/**
|
||||
@@ -68,11 +84,17 @@ class MemberSearchFragment : ComposeFragment(), RecipientBottomSheetDialogFragme
|
||||
|
||||
private val TAG = Log.tag(MemberSearchFragment::class.java)
|
||||
private const val ARG_GROUP_ID = "group_id"
|
||||
private const val ARG_CAN_ADD = "can_add"
|
||||
private const val ARG_HAS_GROUP_LINK = "has_group_link"
|
||||
|
||||
fun newInstance(groupId: GroupId.V2): MemberSearchFragment {
|
||||
const val RESULT_ADD_MEMBERS = "result_add"
|
||||
|
||||
fun newInstance(groupId: GroupId.V2, canAdd: Boolean, hasGroupLink: Boolean): MemberSearchFragment {
|
||||
return MemberSearchFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_GROUP_ID, groupId)
|
||||
putBoolean(ARG_CAN_ADD, canAdd)
|
||||
putBoolean(ARG_HAS_GROUP_LINK, hasGroupLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +110,7 @@ class MemberSearchFragment : ComposeFragment(), RecipientBottomSheetDialogFragme
|
||||
isMultiSelect = false,
|
||||
repository = ContactSearchRepository(),
|
||||
performSafetyNumberChecks = false,
|
||||
arbitraryRepository = findListener<SearchConfigurationProvider>()?.getArbitraryRepository(),
|
||||
arbitraryRepository = findListener<SearchConfigurationProvider>()?.getArbitraryRepository() ?: MemberArbitraryRepository(),
|
||||
searchRepository = SearchRepository(requireContext().getString(R.string.Recipient_you)),
|
||||
contactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(requireContext(), requireContext().getString(R.string.Recipient_you))
|
||||
)
|
||||
@@ -105,16 +127,22 @@ class MemberSearchFragment : ComposeFragment(), RecipientBottomSheetDialogFragme
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val memberFilter by memberSearchViewModel.memberFilter.collectAsStateWithLifecycle()
|
||||
val canAdd = requireArguments().getBoolean(ARG_CAN_ADD)
|
||||
val hasGroupLink = requireArguments().getBoolean(ARG_HAS_GROUP_LINK)
|
||||
CompositionLocalProvider(LocalFragmentManager provides childFragmentManager) {
|
||||
MemberSearchScreen(
|
||||
contactViewModel = contactViewModel,
|
||||
memberFilter = memberFilter,
|
||||
onFilterSelected = { memberSearchViewModel.setFilter(it) },
|
||||
mapStateToConfiguration = { state -> getConfiguration(state, memberFilter) },
|
||||
mapStateToConfiguration = { state -> getConfiguration(state, memberFilter, canAdd, hasGroupLink) },
|
||||
contactSearchCallbacks = remember {
|
||||
SearchCallbacks(
|
||||
fragmentManager = childFragmentManager,
|
||||
groupId = groupId
|
||||
groupId = groupId,
|
||||
onAddMembers = {
|
||||
setFragmentResult(RESULT_ADD_MEMBERS, Bundle.EMPTY)
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -123,7 +151,8 @@ class MemberSearchFragment : ComposeFragment(), RecipientBottomSheetDialogFragme
|
||||
|
||||
class SearchCallbacks(
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val groupId: GroupId.V2
|
||||
private val groupId: GroupId.V2,
|
||||
private val onAddMembers: () -> Unit
|
||||
) : ContactSearchCallbacks.Simple() {
|
||||
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
|
||||
val recipientId = contactSearchKeys.filterIsInstance<ContactSearchKey.RecipientSearchKey>().firstOrNull()?.recipientId
|
||||
@@ -132,9 +161,17 @@ class MemberSearchFragment : ComposeFragment(), RecipientBottomSheetDialogFragme
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
fun onAdd() {
|
||||
onAddMembers()
|
||||
}
|
||||
|
||||
fun onInvite() {
|
||||
GroupLinkBottomSheetDialogFragment.show(fragmentManager, groupId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(contactSearchState: ContactSearchState, memberFilter: MemberSearchViewModel.MemberFilter): ContactSearchConfiguration {
|
||||
private fun getConfiguration(contactSearchState: ContactSearchState, memberFilter: MemberSearchViewModel.MemberFilter, canAdd: Boolean, hasGroupLink: Boolean): ContactSearchConfiguration {
|
||||
return findListener<SearchConfigurationProvider>()?.getSearchConfiguration(childFragmentManager, contactSearchState) ?: ContactSearchConfiguration.build {
|
||||
query = contactSearchState.query
|
||||
|
||||
@@ -153,11 +190,65 @@ class MemberSearchFragment : ComposeFragment(), RecipientBottomSheetDialogFragme
|
||||
)
|
||||
)
|
||||
|
||||
if (canAdd || hasGroupLink) {
|
||||
val set = if (canAdd && hasGroupLink) {
|
||||
setOf(MemberArbitraryRepository.TYPE_DIVIDER, MemberArbitraryRepository.TYPE_ADD_MEMBERS, MemberArbitraryRepository.TYPE_INVITE_VIA)
|
||||
} else if (canAdd) {
|
||||
setOf(MemberArbitraryRepository.TYPE_DIVIDER, MemberArbitraryRepository.TYPE_ADD_MEMBERS)
|
||||
} else {
|
||||
setOf(MemberArbitraryRepository.TYPE_DIVIDER, MemberArbitraryRepository.TYPE_INVITE_VIA)
|
||||
}
|
||||
addSection(ContactSearchConfiguration.Section.Arbitrary(set))
|
||||
}
|
||||
|
||||
withEmptyState {
|
||||
addSection(ContactSearchConfiguration.Section.Empty)
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Arbitrary(
|
||||
setOf(MemberArbitraryRepository.TYPE_DIVIDER, MemberArbitraryRepository.TYPE_ADD_MEMBERS, MemberArbitraryRepository.TYPE_INVITE_VIA)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DividerModel : MappingModel<DividerModel> {
|
||||
override fun areItemsTheSame(newItem: DividerModel) = true
|
||||
override fun areContentsTheSame(newItem: DividerModel) = true
|
||||
}
|
||||
|
||||
class AddMembersModel : MappingModel<AddMembersModel> {
|
||||
override fun areItemsTheSame(newItem: AddMembersModel) = true
|
||||
override fun areContentsTheSame(newItem: AddMembersModel) = true
|
||||
}
|
||||
|
||||
class InviteViaModel : MappingModel<InviteViaModel> {
|
||||
override fun areItemsTheSame(newItem: InviteViaModel) = true
|
||||
override fun areContentsTheSame(newItem: InviteViaModel) = true
|
||||
}
|
||||
|
||||
class MemberArbitraryRepository : ArbitraryRepository {
|
||||
override fun getSize(section: ContactSearchConfiguration.Section.Arbitrary, query: String?) = section.types.size
|
||||
|
||||
override fun getData(section: ContactSearchConfiguration.Section.Arbitrary, query: String?, startIndex: Int, endIndex: Int, totalSearchSize: Int): List<ContactSearchData.Arbitrary> {
|
||||
return section.types.toList().subList(startIndex, endIndex).map { ContactSearchData.Arbitrary(it) }
|
||||
}
|
||||
|
||||
override fun getMappingModel(arbitrary: ContactSearchData.Arbitrary): MappingModel<*> {
|
||||
return when (arbitrary.type) {
|
||||
TYPE_DIVIDER -> DividerModel()
|
||||
TYPE_ADD_MEMBERS -> AddMembersModel()
|
||||
TYPE_INVITE_VIA -> InviteViaModel()
|
||||
else -> throw IllegalArgumentException("Unknown type: ${arbitrary.type}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_DIVIDER = "divider"
|
||||
const val TYPE_ADD_MEMBERS = "add-members"
|
||||
const val TYPE_INVITE_VIA = "invite-via"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -229,6 +320,45 @@ private fun MemberSearchContent(
|
||||
contactSearchCallbacks: MemberSearchFragment.SearchCallbacks
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val additionalEntries: MappingEntryProvider<Any> = rememberMappingEntryProvider {
|
||||
entry<DividerModel> { Dividers.Default() }
|
||||
|
||||
entry<AddMembersModel> {
|
||||
Rows.TextRow(
|
||||
text = { Text(text = stringResource(R.string.AddMembersActivity__add_members)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(org.signal.core.ui.R.drawable.symbol_plus_24),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = CircleShape)
|
||||
.padding(8.dp)
|
||||
)
|
||||
},
|
||||
onClick = contactSearchCallbacks::onAdd
|
||||
)
|
||||
}
|
||||
|
||||
entry<InviteViaModel> {
|
||||
Rows.TextRow(
|
||||
text = { Text(text = stringResource(R.string.MemberSearchFragment__invite_via)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(org.signal.core.ui.R.drawable.symbol_link_24),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = CircleShape)
|
||||
.padding(8.dp)
|
||||
)
|
||||
},
|
||||
onClick = contactSearchCallbacks::onInvite
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
@@ -265,6 +395,7 @@ private fun MemberSearchContent(
|
||||
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
|
||||
)
|
||||
},
|
||||
additionalEntries = additionalEntries,
|
||||
callbacks = contactSearchCallbacks,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
@@ -142,6 +142,14 @@
|
||||
android:name="group_id"
|
||||
app:argType="org.thoughtcrime.securesms.groups.GroupId"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="can_add"
|
||||
app:argType="boolean" />
|
||||
|
||||
<argument
|
||||
android:name="has_group_link"
|
||||
app:argType="boolean" />
|
||||
</action>
|
||||
|
||||
</fragment>
|
||||
|
||||
@@ -9898,6 +9898,8 @@
|
||||
<string name="MemberSearchFragment__filter_admins">Admins</string>
|
||||
<!-- Dialog option for your contacts -->
|
||||
<string name="MemberSearchFragment__filter_contacts">Contacts</string>
|
||||
<!-- Button to invite members via a group link -->
|
||||
<string name="MemberSearchFragment__invite_via">Invite via group link</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