Add split pane UI for add group members screen.

This commit is contained in:
jeffrey-signal
2025-11-18 13:11:17 -05:00
committed by Cody Henthorne
parent 16d5db3639
commit c851387f57
12 changed files with 544 additions and 26 deletions

View File

@@ -116,7 +116,6 @@ private interface UiCallbacks :
RecipientPickerCallbacks.NewCall {
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = true
override fun onPendingRecipientSelectionsConsumed() = Unit
fun onUserMessageDismissed(userMessage: UserMessage)
fun onBackPressed()

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.settings.conversation
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.SelectionLimits
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -13,9 +14,7 @@ sealed class ConversationSettingsEvent {
class AddMembersToGroup(
val groupId: GroupId,
val selectionWarning: Int,
val selectionLimit: Int,
val isAnnouncementGroup: Boolean,
val selectionLimits: SelectionLimits,
val groupMembersWithoutSelf: List<RecipientId>
) : ConversationSettingsEvent()

View File

@@ -955,9 +955,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
requireContext(),
addMembersToGroup.groupId,
ContactSelectionDisplayMode.FLAG_PUSH,
addMembersToGroup.selectionWarning,
addMembersToGroup.selectionLimit,
addMembersToGroup.isAnnouncementGroup,
addMembersToGroup.selectionLimits.recommendedLimit,
addMembersToGroup.selectionLimits.hardLimit,
addMembersToGroup.groupMembersWithoutSelf
),
REQUEST_CODE_ADD_MEMBERS_TO_GROUP

View File

@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.LiveGroup
import org.thoughtcrime.securesms.groups.SelectionLimits
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.groups.v2.GroupAddMembersResult
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -435,9 +436,7 @@ sealed class ConversationSettingsViewModel(
internalEvents.onNext(
ConversationSettingsEvent.AddMembersToGroup(
groupId,
capacityResult.getSelectionWarning(),
capacityResult.getSelectionLimit(),
capacityResult.isAnnouncementGroup,
SelectionLimits(capacityResult.getSelectionWarning(), capacityResult.getSelectionLimit()),
capacityResult.getMembersWithoutSelf()
)
)

View File

@@ -275,7 +275,6 @@ private interface UiCallbacks :
fun onRemoveConfirmed(recipient: Recipient)
fun onBlockConfirmed(recipient: Recipient)
fun onUserMessageDismissed(userMessage: UserMessage)
override fun onPendingRecipientSelectionsConsumed() = Unit
fun onBackPressed()
object Empty : UiCallbacks {
@@ -285,7 +284,6 @@ private interface UiCallbacks :
override fun onFindByPhoneNumber() = Unit
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = true
override fun onRecipientSelected(selection: RecipientSelection) = Unit
override fun onPendingRecipientSelectionsConsumed() = Unit
override fun onMessage(id: RecipientId) = Unit
override fun onVoiceCall(recipient: Recipient) = Unit
override fun onVideoCall(recipient: Recipient) = Unit

View File

@@ -39,7 +39,6 @@ import java.util.function.Consumer;
public class AddMembersActivity extends PushContactSelectionActivity implements ContactSelectionListFragment.FindByCallback {
public static final String GROUP_ID = "group_id";
public static final String ANNOUNCEMENT_GROUP = "announcement_group";
private View done;
private AddMembersViewModel viewModel;
@@ -50,12 +49,9 @@ public class AddMembersActivity extends PushContactSelectionActivity implements
int displayModeFlags,
int selectionWarning,
int selectionLimit,
boolean isAnnouncementGroup,
@NonNull List<RecipientId> membersWithoutSelf) {
Intent intent = new Intent(context, AddMembersActivity.class);
intent.putExtra(GROUP_ID, groupId.toString());
intent.putExtra(ANNOUNCEMENT_GROUP, isAnnouncementGroup);
intent.putExtra(ContactSelectionArguments.DISPLAY_MODE, displayModeFlags);
intent.putExtra(ContactSelectionArguments.SELECTION_LIMITS, new SelectionLimits(selectionWarning, selectionLimit));
intent.putParcelableArrayListExtra(ContactSelectionArguments.CURRENT_SELECTION, new ArrayList<>(membersWithoutSelf));
@@ -184,10 +180,6 @@ public class AddMembersActivity extends PushContactSelectionActivity implements
return GroupId.parseOrThrow(getIntent().getStringExtra(GROUP_ID));
}
private boolean isAnnouncementGroup() {
return getIntent().getBooleanExtra(ANNOUNCEMENT_GROUP, false);
}
private void displayAlertMessage(@NonNull AddMembersViewModel.AddMemberDialogMessageState state) {
Recipient recipient = Util.firstNonNull(state.getRecipient(), Recipient.UNKNOWN);

View File

@@ -0,0 +1,339 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.groups.ui.addmembers
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.util.getParcelableArrayListExtraCompat
import org.signal.core.util.getParcelableExtraCompat
import org.signal.core.util.nullIfBlank
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.PushContactSelectionActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsEvent
import org.thoughtcrime.securesms.compose.SignalTheme
import org.thoughtcrime.securesms.contacts.SelectedContact
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.SelectionLimits
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersUiState.UserMessage
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.RecipientLookupFailureMessage
import org.thoughtcrime.securesms.recipients.ui.RecipientPicker
import org.thoughtcrime.securesms.recipients.ui.RecipientPickerCallbacks
import org.thoughtcrime.securesms.recipients.ui.RecipientPickerScaffold
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
import java.text.NumberFormat
/**
* Allows members to be added to an existing Signal group by selecting from a list of recipients.
*/
class AddMembersActivityV2 : PassphraseRequiredActivity() {
companion object {
private const val EXTRA_GROUP_ID = "group_id"
private const val EXTRA_SELECTION_LIMITS = "selection_limits"
private const val EXTRA_PRESELECTED_RECIPIENTS = "preselected_recipients"
fun createIntent(
context: Context,
event: ConversationSettingsEvent.AddMembersToGroup
): Intent {
return Intent(context, AddMembersActivityV2::class.java).apply {
putExtra(EXTRA_GROUP_ID, event.groupId)
putExtra(EXTRA_SELECTION_LIMITS, event.selectionLimits)
putParcelableArrayListExtra(EXTRA_PRESELECTED_RECIPIENTS, ArrayList(event.groupMembersWithoutSelf))
}
}
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
enableEdgeToEdge()
super.onCreate(savedInstanceState, ready)
setContent {
SignalTheme {
AddMembersScreen(
viewModel = viewModel {
AddMembersViewModelV2(
groupId = intent.getParcelableExtraCompat(EXTRA_GROUP_ID, GroupId::class.java)!!,
existingMembersMinusSelf = intent.getParcelableArrayListExtraCompat(EXTRA_PRESELECTED_RECIPIENTS, RecipientId::class.java)!!.toSet(),
selectionLimits = intent.getParcelableExtraCompat(EXTRA_SELECTION_LIMITS, SelectionLimits::class.java)!!
)
},
activityIntent = intent,
closeScreen = { result ->
setResult(result.resultCode, result.data)
onBackPressedDispatcher.onBackPressed()
}
)
}
}
}
}
@Composable
private fun AddMembersScreen(
viewModel: AddMembersViewModelV2,
activityIntent: Intent,
closeScreen: (result: ActivityResult) -> Unit
) {
val findByLauncher: ActivityResultLauncher<FindByMode> = rememberLauncherForActivityResult(
contract = FindByActivity.Contract(),
onResult = { id -> id?.let(viewModel::selectRecipient) }
)
val callbacks = remember {
object : UiCallbacks {
override fun onSearchQueryChanged(query: String) = viewModel.onSearchQueryChanged(query)
override fun onFindByUsername() = findByLauncher.launch(FindByMode.USERNAME)
override fun onFindByPhoneNumber() = findByLauncher.launch(FindByMode.PHONE_NUMBER)
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = viewModel.shouldAllowSelection(selection)
override fun onSelectionChanged(newSelections: List<SelectedContact>, totalMembersCount: Int) = viewModel.onSelectionChanged(newSelections)
override fun onPendingRecipientSelectionsConsumed() = viewModel.clearPendingRecipientSelections()
override fun onDoneClicked() = viewModel.addSelectedMembers()
override fun onAddConfirmed(recipientIds: Set<RecipientId>) {
val resultIntent = activityIntent.apply {
putParcelableArrayListExtra(
PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS,
ArrayList(recipientIds.toList())
)
}
closeScreen(ActivityResult(RESULT_OK, resultIntent))
}
override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.clearUserMessage()
override fun onBackPressed() = closeScreen(ActivityResult(RESULT_CANCELED, null))
}
}
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
AddMembersScreenUi(
uiState = uiState,
callbacks = callbacks
)
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun AddMembersScreenUi(
uiState: AddMembersUiState,
callbacks: UiCallbacks
) {
val title = if (uiState.totalMembersCount > 0) {
pluralStringResource(
id = R.plurals.CreateGroupActivity__s_members,
count = uiState.totalMembersCount,
NumberFormat.getInstance().format(uiState.totalMembersCount)
)
} else {
stringResource(R.string.AddMembersActivity__add_members)
}
RecipientPickerScaffold(
title = title,
forceSplitPane = uiState.forceSplitPane,
onNavigateUpClick = callbacks::onBackPressed,
topAppBarActions = {},
snackbarHostState = remember { SnackbarHostState() },
primaryContent = {
AddMembersRecipientPicker(
uiState = uiState,
callbacks = callbacks
)
UserMessagesHost(
userMessage = uiState.userMessage,
onAddConfirmed = callbacks::onAddConfirmed,
onDismiss = callbacks::onUserMessageDismissed
)
if (uiState.isLookingUpRecipient) {
Dialogs.IndeterminateProgressDialog()
}
}
)
}
@Composable
private fun AddMembersRecipientPicker(
uiState: AddMembersUiState,
callbacks: UiCallbacks,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
RecipientPicker(
searchQuery = uiState.searchQuery,
displayModes = setOf(RecipientPicker.DisplayMode.PUSH),
selectionLimits = uiState.selectionLimits,
preselectedRecipients = uiState.existingMembersMinusSelf,
pendingRecipientSelections = uiState.pendingRecipientSelections,
isRefreshing = false,
listBottomPadding = 64.dp,
clipListToPadding = false,
callbacks = RecipientPickerCallbacks(
listActions = callbacks,
findByUsername = callbacks,
findByPhoneNumber = callbacks
),
modifier = modifier.fillMaxSize()
)
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
) {
Buttons.MediumTonal(
enabled = uiState.newSelections.isNotEmpty(),
onClick = callbacks::onDoneClicked
) {
Text(text = stringResource(R.string.AddMembersActivity__done))
}
}
}
}
private interface UiCallbacks :
RecipientPickerCallbacks.ListActions,
RecipientPickerCallbacks.FindByUsername,
RecipientPickerCallbacks.FindByPhoneNumber {
override fun onRecipientSelected(selection: RecipientSelection) = Unit
fun onDoneClicked()
fun onAddConfirmed(recipientIds: Set<RecipientId>)
fun onUserMessageDismissed(userMessage: UserMessage)
fun onBackPressed()
object Empty : UiCallbacks {
override fun onSearchQueryChanged(query: String) = Unit
override fun onFindByUsername() = Unit
override fun onFindByPhoneNumber() = Unit
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = true
override fun onDoneClicked() = Unit
override fun onAddConfirmed(recipientIds: Set<RecipientId>) = Unit
override fun onUserMessageDismissed(userMessage: UserMessage) = Unit
override fun onBackPressed() = Unit
}
}
@Composable
private fun UserMessagesHost(
userMessage: UserMessage?,
onAddConfirmed: (Set<RecipientId>) -> Unit,
onDismiss: (UserMessage) -> Unit
) {
when (userMessage) {
null -> {}
is UserMessage.RecipientLookupFailed -> {
RecipientLookupFailureMessage(
failure = userMessage.failure,
onDismissed = { onDismiss(userMessage) }
)
}
is UserMessage.CantAddRecipientToLegacyGroup -> {
Toast.makeText(LocalContext.current, R.string.AddMembersActivity__this_person_cant_be_added_to_legacy_groups, Toast.LENGTH_SHORT).show()
onDismiss(userMessage)
}
is UserMessage.GroupAddConfirmation -> {
GroupAddConfirmationDialog(
message = userMessage,
onAddConfirmed = onAddConfirmed,
onDismiss = onDismiss
)
}
}
}
@Composable
private fun GroupAddConfirmationDialog(
message: UserMessage.GroupAddConfirmation,
onAddConfirmed: (Set<RecipientId>) -> Unit,
onDismiss: (UserMessage) -> Unit
) {
val context: Context = LocalContext.current
val bodyText: String = when (message) {
is UserMessage.ConfirmAddMember -> {
stringResource(
id = R.string.AddMembersActivityV2__add_member_to_s,
message.recipient.getDisplayName(context),
message.group.getDisplayTitle(context)
)
}
is UserMessage.ConfirmAddMembers -> {
pluralStringResource(
id = R.plurals.AddMembersActivityV2__add_d_members_to_s,
message.recipientIds.size,
message.recipientIds.size,
message.group.getDisplayTitle(context)
)
}
}
Dialogs.SimpleAlertDialog(
title = "",
body = bodyText,
confirm = stringResource(R.string.AddMembersActivity__add),
dismiss = stringResource(android.R.string.cancel),
onConfirm = { onAddConfirmed(message.recipientIds) },
onDismiss = { onDismiss(message) }
)
}
private fun GroupRecord.getDisplayTitle(context: Context): String {
return this.title.nullIfBlank() ?: context.getString(R.string.Recipient_unknown)
}
@AllDevicePreviews
@Composable
private fun AddMembersScreenPreview() {
Previews.Preview {
AddMembersScreenUi(
uiState = AddMembersUiState(
forceSplitPane = false,
selectionLimits = SelectionLimits.NO_LIMITS
),
callbacks = UiCallbacks.Empty
)
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.groups.ui.addmembers
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.thoughtcrime.securesms.contacts.SelectedContact
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.SelectionLimits
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersUiState.UserMessage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import kotlin.collections.plus
class AddMembersViewModelV2(
private val groupId: GroupId,
existingMembersMinusSelf: Set<RecipientId>,
selectionLimits: SelectionLimits
) : ViewModel() {
private val group: GroupRecord = SignalDatabase.groups.requireGroup(groupId)
private val internalUiState = MutableStateFlow(
AddMembersUiState(
existingMembersMinusSelf = existingMembersMinusSelf,
selectionLimits = selectionLimits
)
)
val uiState: StateFlow<AddMembersUiState> = internalUiState.asStateFlow()
fun onSearchQueryChanged(query: String) {
internalUiState.update { it.copy(searchQuery = query) }
}
suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean {
val recipientHasE164 = selection is RecipientSelection.HasId &&
withContext(Dispatchers.IO) { Recipient.resolved(selection.id) }.hasE164
return when {
groupId.isV1 && !recipientHasE164 -> {
internalUiState.update {
it.copy(userMessage = UserMessage.CantAddRecipientToLegacyGroup)
}
false
}
selection is RecipientSelection.HasId -> true
selection is RecipientSelection.HasPhone -> recipientExists(selection.phone)
else -> false
}
}
private suspend fun recipientExists(phone: PhoneNumber): Boolean {
internalUiState.update { it.copy(isLookingUpRecipient = true) }
return when (val lookupResult = RecipientRepository.lookup(phone)) {
is RecipientRepository.LookupResult.Success -> {
internalUiState.update { it.copy(isLookingUpRecipient = false) }
true
}
is RecipientRepository.LookupResult.Failure -> {
internalUiState.update {
it.copy(
isLookingUpRecipient = false,
userMessage = UserMessage.RecipientLookupFailed(failure = lookupResult)
)
}
false
}
}
}
fun onSelectionChanged(newSelections: List<SelectedContact>) {
internalUiState.update {
it.copy(
searchQuery = "",
newSelections = newSelections
)
}
}
fun addSelectedMembers() {
viewModelScope.launch {
val confirmAddMessage = if (uiState.value.newSelections.size == 1) {
UserMessage.ConfirmAddMember(
group = group,
recipient = withContext(Dispatchers.IO) {
Recipient.resolved(uiState.value.newSelections.single().orCreateRecipientId)
}
)
} else {
UserMessage.ConfirmAddMembers(
group = group,
recipientIds = uiState.value.newSelections.map { it.orCreateRecipientId }.toSet()
)
}
internalUiState.update { it.copy(userMessage = confirmAddMessage) }
}
}
fun selectRecipient(id: RecipientId) {
internalUiState.update {
it.copy(pendingRecipientSelections = it.pendingRecipientSelections + id)
}
}
fun clearPendingRecipientSelections() {
internalUiState.update {
it.copy(pendingRecipientSelections = emptySet())
}
}
fun clearUserMessage() {
internalUiState.update { it.copy(userMessage = null) }
}
}
data class AddMembersUiState(
val forceSplitPane: Boolean = SignalStore.internal.forceSplitPane,
val searchQuery: String = "",
val existingMembersMinusSelf: Set<RecipientId> = emptySet(),
val selectionLimits: SelectionLimits,
val newSelections: List<SelectedContact> = emptyList(),
val isLookingUpRecipient: Boolean = false,
val pendingRecipientSelections: Set<RecipientId> = emptySet(),
val userMessage: UserMessage? = null
) {
val totalMembersCount: Int
get() = existingMembersMinusSelf.size + newSelections.size + 1
sealed interface UserMessage {
data class RecipientLookupFailed(val failure: RecipientRepository.LookupResult.Failure) : UserMessage
data object CantAddRecipientToLegacyGroup : UserMessage
sealed interface GroupAddConfirmation : UserMessage {
val group: GroupRecord
val recipientIds: Set<RecipientId>
}
data class ConfirmAddMember(
override val group: GroupRecord,
val recipient: Recipient
) : GroupAddConfirmation {
override val recipientIds: Set<RecipientId> = setOf(recipient.id)
}
data class ConfirmAddMembers(
override val group: GroupRecord,
override val recipientIds: Set<RecipientId>
) : GroupAddConfirmation
}
}

View File

@@ -37,8 +37,8 @@ class CreateGroupViewModel : ViewModel() {
}
suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = when (selection) {
is RecipientSelection.WithId, is RecipientSelection.WithIdAndPhone -> true
is RecipientSelection.WithPhone -> recipientExists(selection.phone)
is RecipientSelection.HasId -> true
is RecipientSelection.HasPhone -> recipientExists(selection.phone)
}
private suspend fun recipientExists(phone: PhoneNumber): Boolean {

View File

@@ -70,6 +70,7 @@ fun RecipientPicker(
selectionLimits: SelectionLimits? = ContactSelectionArguments.Defaults.SELECTION_LIMITS,
isRefreshing: Boolean,
focusAndShowKeyboard: Boolean = LocalConfiguration.current.screenHeightDp.dp > 600.dp,
preselectedRecipients: Set<RecipientId> = emptySet(),
pendingRecipientSelections: Set<RecipientId> = emptySet(),
shouldResetContactsList: Boolean = false,
listBottomPadding: Dp? = null,
@@ -109,6 +110,7 @@ fun RecipientPicker(
selectionLimits = selectionLimits,
searchQuery = searchQuery,
isRefreshing = isRefreshing,
preselectedRecipients = preselectedRecipients,
pendingRecipientSelections = pendingRecipientSelections,
shouldResetContactsList = shouldResetContactsList,
bottomPadding = listBottomPadding,
@@ -127,6 +129,7 @@ private fun RecipientSearchResultsList(
displayModes: Set<RecipientPicker.DisplayMode>,
searchQuery: String,
isRefreshing: Boolean,
preselectedRecipients: Set<RecipientId>,
pendingRecipientSelections: Set<RecipientId>,
shouldResetContactsList: Boolean,
selectionLimits: SelectionLimits? = ContactSelectionArguments.Defaults.SELECTION_LIMITS,
@@ -142,6 +145,7 @@ private fun RecipientSearchResultsList(
enableFindByUsername = callbacks.findByUsername != null,
enableFindByPhoneNumber = callbacks.findByPhoneNumber != null,
showCallButtons = callbacks.newCall != null,
currentSelection = preselectedRecipients,
selectionLimits = selectionLimits,
recyclerPadBottom = with(LocalDensity.current) { bottomPadding?.toPx()?.toInt() ?: ContactSelectionArguments.Defaults.RECYCLER_PADDING_BOTTOM },
recyclerChildClipping = clipListToPadding
@@ -396,7 +400,7 @@ class RecipientPickerCallbacks(
suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean
fun onRecipientSelected(selection: RecipientSelection)
fun onSelectionChanged(newSelections: List<SelectedContact>, totalMembersCount: Int) = Unit
fun onPendingRecipientSelectionsConsumed()
fun onPendingRecipientSelectionsConsumed() = Unit
fun onContactsListReset() = Unit
object Empty : ListActions {

View File

@@ -9,7 +9,15 @@ import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.RecipientId
sealed interface RecipientSelection {
data class WithId(val id: RecipientId) : RecipientSelection
data class WithPhone(val phone: PhoneNumber) : RecipientSelection
data class WithIdAndPhone(val id: RecipientId, val phone: PhoneNumber) : RecipientSelection
sealed interface HasId : RecipientSelection {
val id: RecipientId
}
sealed interface HasPhone : RecipientSelection {
val phone: PhoneNumber
}
data class WithId(override val id: RecipientId) : HasId
data class WithPhone(override val phone: PhoneNumber) : HasPhone
data class WithIdAndPhone(override val id: RecipientId, override val phone: PhoneNumber) : HasId, HasPhone
}

View File

@@ -1396,11 +1396,22 @@
<!-- AddMembersActivity -->
<string name="AddMembersActivity__done">Done</string>
<string name="AddMembersActivity__this_person_cant_be_added_to_legacy_groups">This person can\'t be added to legacy groups.</string>
<!-- Confirmation text when adding a member to a group. If one person is added, %1$s is their name. If multiple people are added, the total number is %3$d. %2$s is the name of the group -->
<plurals name="AddMembersActivity__add_d_members_to_s">
<item quantity="one">Add \"%1$s\" to \"%2$s\"?</item>
<item quantity="other">Add %3$d members to \"%2$s\"?</item>
</plurals>
<!-- Confirmation text when adding one member to a group. %1$s is the member's name, %2$s is the group name. -->
<string name="AddMembersActivityV2__add_member_to_s">Add \"%1$s\" to \"%2$s\"?</string>
<!-- Confirmation text when adding members to a group. %1$d is the number of members added. %2$s is the name of the group. -->
<plurals name="AddMembersActivityV2__add_d_members_to_s">
<item quantity="one">Add %1$d member to \"%2$s\"?</item>
<item quantity="other">Add %1$d members to \"%2$s\"?</item>
</plurals>
<string name="AddMembersActivity__add">Add</string>
<string name="AddMembersActivity__add_members">Add members</string>