mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add split pane UI for add group members screen.
This commit is contained in:
committed by
Cody Henthorne
parent
16d5db3639
commit
c851387f57
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user