Enable split pane UI for add group members screen.

This commit is contained in:
jeffrey-signal
2025-11-18 13:45:31 -05:00
committed by Cody Henthorne
parent c851387f57
commit 94241f7068
9 changed files with 14 additions and 431 deletions

View File

@@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.L
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.groups.GroupId
@@ -953,11 +952,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
startActivityForResult(
AddMembersActivity.createIntent(
requireContext(),
addMembersToGroup.groupId,
ContactSelectionDisplayMode.FLAG_PUSH,
addMembersToGroup.selectionLimits.recommendedLimit,
addMembersToGroup.selectionLimits.hardLimit,
addMembersToGroup.groupMembersWithoutSelf
addMembersToGroup
),
REQUEST_CODE_ADD_MEMBERS_TO_GROUP
)

View File

@@ -1,199 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.addmembers;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.ContactSelectionActivity;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.paged.ChatType;
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientRepository;
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity;
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
public class AddMembersActivity extends PushContactSelectionActivity implements ContactSelectionListFragment.FindByCallback {
public static final String GROUP_ID = "group_id";
private View done;
private AddMembersViewModel viewModel;
private ActivityResultLauncher<FindByMode> findByActivityLauncher;
public static @NonNull Intent createIntent(@NonNull Context context,
@NonNull GroupId groupId,
int displayModeFlags,
int selectionWarning,
int selectionLimit,
@NonNull List<RecipientId> membersWithoutSelf) {
Intent intent = new Intent(context, AddMembersActivity.class);
intent.putExtra(GROUP_ID, groupId.toString());
intent.putExtra(ContactSelectionArguments.DISPLAY_MODE, displayModeFlags);
intent.putExtra(ContactSelectionArguments.SELECTION_LIMITS, new SelectionLimits(selectionWarning, selectionLimit));
intent.putParcelableArrayListExtra(ContactSelectionArguments.CURRENT_SELECTION, new ArrayList<>(membersWithoutSelf));
intent.putExtra(ContactSelectionArguments.RV_PADDING_BOTTOM, (int) DimensionUnit.DP.toPixels(64f));
intent.putExtra(ContactSelectionArguments.RV_CLIP, false);
return intent;
}
@Override
protected void onCreate(Bundle icicle, boolean ready) {
getIntent().putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.add_members_activity);
super.onCreate(icicle, ready);
AddMembersViewModel.Factory factory = new AddMembersViewModel.Factory(getGroupId());
done = findViewById(R.id.done);
viewModel = new ViewModelProvider(this, factory).get(AddMembersViewModel.class);
done.setOnClickListener(v ->
viewModel.getDialogStateForSelectedContacts(contactsFragment.getSelectedContacts(), this::displayAlertMessage)
);
disableDone();
findByActivityLauncher = registerForActivityResult(new FindByActivity.Contract(), result -> {
if (result != null) {
contactsFragment.addRecipientToSelectionIfAble(result);
}
});
}
@Override
protected void initializeToolbar() {
getToolbar().setNavigationIcon(R.drawable.symbol_arrow_start_24);
getToolbar().setNavigationOnClickListener(v -> {
setResult(RESULT_CANCELED);
finish();
});
}
@Override
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Optional<ChatType> chatType, @NonNull Consumer<Boolean> callback) {
if (getGroupId().isV1() && recipientId.isPresent() && !Recipient.resolved(recipientId.get()).getHasE164()) {
Toast.makeText(this, R.string.AddMembersActivity__this_person_cant_be_added_to_legacy_groups, Toast.LENGTH_SHORT).show();
callback.accept(false);
return;
}
if (contactsFragment.hasQueryFilter()) {
getContactFilterView().clear();
}
if (recipientId.isPresent()) {
callback.accept(true);
enableDone();
return;
}
AlertDialog progress = SimpleProgressDialog.show(this);
SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(number), result -> {
progress.dismiss();
if (result instanceof RecipientRepository.LookupResult.Success) {
enableDone();
callback.accept(true);
} else if (result instanceof RecipientRepository.PhoneLookupResult.NotFound || result instanceof RecipientRepository.PhoneLookupResult.InvalidPhone) {
new MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.RecipientLookup_error__s_is_not_a_signal_user, number))
.setPositiveButton(android.R.string.ok, null)
.show();
callback.accept(false);
} else {
new MaterialAlertDialogBuilder(this)
.setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
.setPositiveButton(android.R.string.ok, null)
.show();
callback.accept(false);
}
});
}
@Override
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Optional<ChatType> chatType) {
if (contactsFragment.hasQueryFilter()) {
getContactFilterView().clear();
}
if (contactsFragment.getSelectedContactsCount() < 1) {
disableDone();
}
}
@Override
public void onSelectionChanged() {
int selectedContactsCount = contactsFragment.getTotalMemberCount() + 1;
if (selectedContactsCount == 0) {
getToolbar().setTitle(getString(R.string.AddMembersActivity__add_members));
} else {
getToolbar().setTitle(getResources().getQuantityString(R.plurals.CreateGroupActivity__d_members, selectedContactsCount, selectedContactsCount));
}
}
@Override
public void onFindByPhoneNumber() {
findByActivityLauncher.launch(FindByMode.PHONE_NUMBER);
}
@Override
public void onFindByUsername() {
findByActivityLauncher.launch(FindByMode.USERNAME);
}
private void enableDone() {
done.setEnabled(true);
done.animate().alpha(1f);
}
private void disableDone() {
done.setEnabled(false);
done.animate().alpha(0.5f);
}
private GroupId getGroupId() {
return GroupId.parseOrThrow(getIntent().getStringExtra(GROUP_ID));
}
private void displayAlertMessage(@NonNull AddMembersViewModel.AddMemberDialogMessageState state) {
Recipient recipient = Util.firstNonNull(state.getRecipient(), Recipient.UNKNOWN);
String message = getResources().getQuantityString(R.plurals.AddMembersActivity__add_d_members_to_s, state.getSelectionCount(),
recipient.getDisplayName(this), state.getGroupTitle(), state.getSelectionCount());
new MaterialAlertDialogBuilder(this)
.setMessage(message)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
.setPositiveButton(R.string.AddMembersActivity__add, (dialog, which) -> {
dialog.dismiss();
onFinishedSelection();
})
.setCancelable(true)
.show();
}
}

View File

@@ -64,7 +64,7 @@ import java.text.NumberFormat
/**
* Allows members to be added to an existing Signal group by selecting from a list of recipients.
*/
class AddMembersActivityV2 : PassphraseRequiredActivity() {
class AddMembersActivity : PassphraseRequiredActivity() {
companion object {
private const val EXTRA_GROUP_ID = "group_id"
private const val EXTRA_SELECTION_LIMITS = "selection_limits"
@@ -74,7 +74,7 @@ class AddMembersActivityV2 : PassphraseRequiredActivity() {
context: Context,
event: ConversationSettingsEvent.AddMembersToGroup
): Intent {
return Intent(context, AddMembersActivityV2::class.java).apply {
return Intent(context, AddMembersActivity::class.java).apply {
putExtra(EXTRA_GROUP_ID, event.groupId)
putExtra(EXTRA_SELECTION_LIMITS, event.selectionLimits)
putParcelableArrayListExtra(EXTRA_PRESELECTED_RECIPIENTS, ArrayList(event.groupMembersWithoutSelf))
@@ -90,7 +90,7 @@ class AddMembersActivityV2 : PassphraseRequiredActivity() {
SignalTheme {
AddMembersScreen(
viewModel = viewModel {
AddMembersViewModelV2(
AddMembersViewModel(
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)!!
@@ -109,7 +109,7 @@ class AddMembersActivityV2 : PassphraseRequiredActivity() {
@Composable
private fun AddMembersScreen(
viewModel: AddMembersViewModelV2,
viewModel: AddMembersViewModel,
activityIntent: Intent,
closeScreen: (result: ActivityResult) -> Unit
) {
@@ -294,7 +294,7 @@ private fun GroupAddConfirmationDialog(
val bodyText: String = when (message) {
is UserMessage.ConfirmAddMember -> {
stringResource(
id = R.string.AddMembersActivityV2__add_member_to_s,
id = R.string.AddMembersActivity__add_member_to_s,
message.recipient.getDisplayName(context),
message.group.getDisplayTitle(context)
)
@@ -302,7 +302,7 @@ private fun GroupAddConfirmationDialog(
is UserMessage.ConfirmAddMembers -> {
pluralStringResource(
id = R.plurals.AddMembersActivityV2__add_d_members_to_s,
id = R.plurals.AddMembersActivity__add_d_members_to_s,
message.recipientIds.size,
message.recipientIds.size,
message.group.getDisplayTitle(context)

View File

@@ -1,33 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.addmembers;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.RecipientId;
final class AddMembersRepository {
private final Context context;
private final GroupId groupId;
AddMembersRepository(@NonNull GroupId groupId) {
this.groupId = groupId;
this.context = AppDependencies.getApplication();
}
@WorkerThread
RecipientId getOrCreateRecipientId(@NonNull SelectedContact selectedContact) {
return selectedContact.getOrCreateRecipientId();
}
@WorkerThread
String getGroupTitle() {
return SignalDatabase.groups().requireGroup(groupId).getTitle();
}
}

View File

@@ -1,114 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.addmembers;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.List;
import java.util.Objects;
public final class AddMembersViewModel extends ViewModel {
private final AddMembersRepository repository;
private AddMembersViewModel(@NonNull GroupId groupId) {
this.repository = new AddMembersRepository(groupId);
}
void getDialogStateForSelectedContacts(@NonNull List<SelectedContact> selectedContacts,
@NonNull Consumer<AddMemberDialogMessageState> callback)
{
SimpleTask.run(
() -> {
AddMemberDialogMessageStatePartial partialState = selectedContacts.size() == 1 ? getDialogStateForSingleRecipient(selectedContacts.get(0))
: getDialogStateForMultipleRecipients(selectedContacts.size());
return new AddMemberDialogMessageState(partialState.recipientId == null ? Recipient.UNKNOWN : Recipient.resolved(partialState.recipientId),
partialState.memberCount, titleOrDefault(repository.getGroupTitle()));
},
callback::accept
);
}
@WorkerThread
private AddMemberDialogMessageStatePartial getDialogStateForSingleRecipient(@NonNull SelectedContact selectedContact) {
return new AddMemberDialogMessageStatePartial(repository.getOrCreateRecipientId(selectedContact));
}
private AddMemberDialogMessageStatePartial getDialogStateForMultipleRecipients(int recipientCount) {
return new AddMemberDialogMessageStatePartial(recipientCount);
}
private static @NonNull String titleOrDefault(@Nullable String title) {
return TextUtils.isEmpty(title) ? AppDependencies.getApplication().getString(R.string.Recipient_unknown)
: Objects.requireNonNull(title);
}
private static final class AddMemberDialogMessageStatePartial {
private final RecipientId recipientId;
private final int memberCount;
private AddMemberDialogMessageStatePartial(@NonNull RecipientId recipientId) {
this.recipientId = recipientId;
this.memberCount = 1;
}
private AddMemberDialogMessageStatePartial(int memberCount) {
Preconditions.checkArgument(memberCount > 1);
this.memberCount = memberCount;
this.recipientId = null;
}
}
public static final class AddMemberDialogMessageState {
private final Recipient recipient;
private final String groupTitle;
private final int selectionCount;
private AddMemberDialogMessageState(@Nullable Recipient recipient, int selectionCount, @NonNull String groupTitle) {
this.recipient = recipient;
this.groupTitle = groupTitle;
this.selectionCount = selectionCount;
}
public Recipient getRecipient() {
return recipient;
}
public int getSelectionCount() {
return selectionCount;
}
public @NonNull String getGroupTitle() {
return groupTitle;
}
}
public static class Factory implements ViewModelProvider.Factory {
private final GroupId groupId;
public Factory(@NonNull GroupId groupId) {
this.groupId = groupId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new AddMembersViewModel(groupId)));
}
}
}

View File

@@ -28,7 +28,7 @@ import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import kotlin.collections.plus
class AddMembersViewModelV2(
class AddMembersViewModel(
private val groupId: GroupId,
existingMembersMinusSelf: Set<RecipientId>,
selectionLimits: SelectionLimits