diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ab96f79f4..6b644d755 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -492,6 +492,9 @@
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
index 24b6e4592..948236a4b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
@@ -51,6 +51,8 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.transition.AutoTransition;
import androidx.transition.TransitionManager;
+import com.annimon.stream.Collectors;
+import com.annimon.stream.Stream;
import com.google.android.material.chip.ChipGroup;
import com.pnikosis.materialishprogress.ProgressWheel;
@@ -83,6 +85,7 @@ import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
/**
* Fragment for selecting a one or more contacts from a list.
@@ -101,11 +104,12 @@ public final class ContactSelectionListFragment extends Fragment
public static final int NO_LIMIT = Integer.MAX_VALUE;
- public static final String DISPLAY_MODE = "display_mode";
- public static final String MULTI_SELECT = "multi_select";
- public static final String REFRESHABLE = "refreshable";
- public static final String RECENTS = "recents";
- public static final String SELECTION_LIMIT = "selection_limit";
+ public static final String DISPLAY_MODE = "display_mode";
+ public static final String MULTI_SELECT = "multi_select";
+ public static final String REFRESHABLE = "refreshable";
+ public static final String RECENTS = "recents";
+ public static final String TOTAL_CAPACITY = "total_capacity";
+ public static final String CURRENT_SELECTION = "current_selection";
private ConstraintLayout constraintLayout;
private TextView emptyText;
@@ -129,6 +133,7 @@ public final class ContactSelectionListFragment extends Fragment
@Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests;
private int selectionLimit;
+ private Set currentSelection;
@Override
public void onAttach(@NonNull Context context) {
@@ -205,16 +210,17 @@ public final class ContactSelectionListFragment extends Fragment
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
- selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
+ selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
+ currentSelection = getCurrentSelection();
updateGroupLimit(getChipCount());
return view;
}
- private void updateGroupLimit(int childCount) {
+ private void updateGroupLimit(int chipCount) {
if (selectionLimit != NO_LIMIT) {
- groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", childCount, selectionLimit));
+ groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
groupLimit.setVisibility(View.VISIBLE);
} else {
groupLimit.setVisibility(View.GONE);
@@ -242,6 +248,13 @@ public final class ContactSelectionListFragment extends Fragment
return cursorRecyclerViewAdapter.getSelectedContactsCount();
}
+ private Set getCurrentSelection() {
+ List currentSelection = requireActivity().getIntent().getParcelableArrayListExtra(CURRENT_SELECTION);
+
+ return currentSelection == null ? Collections.emptySet()
+ : Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet()));
+ }
+
private boolean isMulti() {
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
}
@@ -253,7 +266,8 @@ public final class ContactSelectionListFragment extends Fragment
glideRequests,
null,
new ListClickListener(),
- isMulti());
+ isMulti(),
+ currentSelection);
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
@@ -546,7 +560,13 @@ public final class ContactSelectionListFragment extends Fragment
chip.setText(recipient.getShortDisplayName(requireContext()));
chip.setContact(selectedContact);
chip.setCloseIconVisible(true);
- chip.setOnCloseIconClickListener(view -> markContactUnselected(selectedContact));
+ chip.setOnCloseIconClickListener(view -> {
+ markContactUnselected(selectedContact);
+
+ if (onContactSelectedListener != null) {
+ onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
+ }
+ });
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/PushContactSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PushContactSelectionActivity.java
index bda0009ee..2f3ce6102 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/PushContactSelectionActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/PushContactSelectionActivity.java
@@ -45,16 +45,24 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
super.onCreate(icicle, ready);
+ initializeToolbar();
+ }
+
+ protected void initializeToolbar() {
getToolbar().setNavigationIcon(R.drawable.ic_check_24);
getToolbar().setNavigationOnClickListener(v -> {
- Intent resultIntent = getIntent();
- List selectedContacts = contactsFragment.getSelectedContacts();
- List recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
-
- resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
-
- setResult(RESULT_OK, resultIntent);
- finish();
+ onFinishedSelection();
});
}
+
+ protected final void onFinishedSelection() {
+ Intent resultIntent = getIntent();
+ List selectedContacts = contactsFragment.getSelectedContacts();
+ List recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
+
+ resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
+
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
index 4e4a3361a..78dcdcd29 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.util.Util;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
/**
* List adapter to display all contacts and their related information
@@ -67,10 +68,11 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter currentContacts;
private final SelectedContactSet selectedContacts = new SelectedContactSet();
@@ -102,6 +104,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter currentContacts)
{
super(context, cursor);
- this.li = LayoutInflater.from(context);
- this.glideRequests = glideRequests;
- this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
- this.multiSelect = multiSelect;
- this.clickListener = clickListener;
+ this.layoutInflater = LayoutInflater.from(context);
+ this.glideRequests = glideRequests;
+ this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
+ this.multiSelect = multiSelect;
+ this.clickListener = clickListener;
+ this.currentContacts = currentContacts;
}
@Override
@@ -188,9 +201,9 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter members = groupRecord.getMembers();
- byte[] avatar = Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId()));
+ byte[] avatar = groupRecord.hasAvatar() ? Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId())) : null;
Set addresses = new HashSet<>(members);
addresses.addAll(newMembers);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java
index df354e8c9..69fa098f9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java
@@ -17,6 +17,7 @@ import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.util.UUIDUtil;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
@@ -88,6 +89,18 @@ public final class GroupProtoUtil {
return Recipient.externalPush(context, uuid, null);
}
+ @WorkerThread
+ public static @NonNull RecipientId uuidByteStringToRecipientId(@NonNull ByteString uuidByteString) {
+ UUID uuid = UUIDUtil.deserialize(uuidByteString.toByteArray());
+
+ if (uuid.equals(GroupsV2Operations.UNKNOWN_UUID)) {
+ return RecipientId.UNKNOWN;
+ }
+
+ return RecipientId.from(uuid, null);
+ }
+
+
public static boolean isMember(@NonNull UUID uuid, @NonNull List membersList) {
ByteString uuidBytes = UuidUtil.toByteString(uuid);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/AddMembersResultCallback.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/AddMembersResultCallback.java
new file mode 100644
index 000000000..d5bc85133
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/AddMembersResultCallback.java
@@ -0,0 +1,5 @@
+package org.thoughtcrime.securesms.groups.ui;
+
+public interface AddMembersResultCallback {
+ void onMembersAdded(int numberOfMembersAdded);
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java
new file mode 100644
index 000000000..c7fd39df1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java
@@ -0,0 +1,115 @@
+package org.thoughtcrime.securesms.groups.ui.addmembers;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.lifecycle.ViewModelProviders;
+
+import org.thoughtcrime.securesms.ContactSelectionActivity;
+import org.thoughtcrime.securesms.PushContactSelectionActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.groups.GroupId;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.Util;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+public class AddMembersActivity extends PushContactSelectionActivity {
+
+ public static final String GROUP_ID = "group_id";
+
+ private View done;
+ private AlertDialog alert;
+ private AddMembersViewModel viewModel;
+
+ @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);
+ alert = buildConfirmationAlertDialog();
+ viewModel = ViewModelProviders.of(this, factory)
+ .get(AddMembersViewModel.class);
+
+ viewModel.getAddMemberDialogState().observe(this, state -> AddMembersActivity.updateAlertMessage(alert, state));
+
+ //noinspection CodeBlock2Expr
+ done.setOnClickListener(v -> {
+ viewModel.setDialogStateForSelectedContacts(contactsFragment.getSelectedContacts());
+ alert.show();
+ });
+
+ disableDone();
+ }
+
+ @Override
+ protected void initializeToolbar() {
+ getToolbar().setNavigationIcon(R.drawable.ic_arrow_left_24);
+ getToolbar().setNavigationOnClickListener(v -> {
+ setResult(RESULT_CANCELED);
+ finish();
+ });
+ }
+
+ @Override
+ public void onContactSelected(Optional recipientId, String number) {
+ if (contactsFragment.hasQueryFilter()) {
+ getToolbar().clear();
+ }
+
+ if (contactsFragment.getSelectedContactsCount() >= 1) {
+ enableDone();
+ }
+ }
+
+ @Override
+ public void onContactDeselected(Optional recipientId, String number) {
+ if (contactsFragment.hasQueryFilter()) {
+ getToolbar().clear();
+ }
+
+ if (contactsFragment.getSelectedContactsCount() < 1) {
+ disableDone();
+ }
+ }
+
+ 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 AlertDialog buildConfirmationAlertDialog() {
+ return new AlertDialog.Builder(this)
+ .setMessage(" ")
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ dialog.dismiss();
+ onFinishedSelection();
+ })
+ .setCancelable(true)
+ .create();
+ }
+
+ private static void updateAlertMessage(@NonNull AlertDialog alertDialog, @NonNull AddMembersViewModel.AddMemberDialogMessageState state) {
+ Context context = alertDialog.getContext();
+ Recipient recipient = Util.firstNonNull(state.getRecipient(), Recipient.UNKNOWN);
+
+ alertDialog.setMessage(context.getResources().getQuantityString(R.plurals.AddMembersActivity__add_d_members_to_s, state.getSelectionCount(),
+ recipient.getDisplayName(context), state.getGroupTitle(), state.getSelectionCount()));
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersRepository.java
new file mode 100644
index 000000000..dc46171fd
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersRepository.java
@@ -0,0 +1,24 @@
+package org.thoughtcrime.securesms.groups.ui.addmembers;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+import org.thoughtcrime.securesms.contacts.SelectedContact;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+
+class AddMembersRepository {
+
+ private final Context context;
+
+ AddMembersRepository() {
+ this.context = ApplicationDependencies.getApplication();
+ }
+
+ void getOrCreateRecipientId(@NonNull SelectedContact selectedContact, @NonNull Consumer consumer) {
+ SignalExecutors.BOUNDED.execute(() -> consumer.accept(selectedContact.getOrCreateRecipientId(context)));
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersViewModel.java
new file mode 100644
index 000000000..33031577e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersViewModel.java
@@ -0,0 +1,142 @@
+package org.thoughtcrime.securesms.groups.ui.addmembers;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.contacts.SelectedContact;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.groups.GroupId;
+import org.thoughtcrime.securesms.groups.LiveGroup;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.DefaultValueLiveData;
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
+import org.whispersystems.libsignal.util.guava.Preconditions;
+
+import java.util.List;
+import java.util.Objects;
+
+public final class AddMembersViewModel extends ViewModel {
+
+ private final AddMembersRepository repository;
+ private final LiveData addMemberDialogState;
+ private final MutableLiveData partialState;
+
+ private AddMembersViewModel(@NonNull GroupId groupId) {
+ repository = new AddMembersRepository();
+ partialState = new MutableLiveData<>();
+ addMemberDialogState = LiveDataUtil.combineLatest(Transformations.map(new LiveGroup(groupId).getTitle(), AddMembersViewModel::titleOrDefault),
+ Transformations.switchMap(partialState, AddMembersViewModel::getStateWithoutGroupTitle),
+ AddMembersViewModel::getStateWithGroupTitle);
+ }
+
+ LiveData getAddMemberDialogState() {
+ return addMemberDialogState;
+ }
+
+ void setDialogStateForSelectedContacts(@NonNull List selectedContacts) {
+ if (selectedContacts.size() == 1) {
+ setDialogStateForSingleRecipient(selectedContacts.get(0));
+ } else {
+ setDialogStateForMultipleRecipients(selectedContacts.size());
+ }
+ }
+
+ private void setDialogStateForSingleRecipient(@NonNull SelectedContact selectedContact) {
+ //noinspection CodeBlock2Expr
+ repository.getOrCreateRecipientId(selectedContact, recipientId -> {
+ partialState.postValue(new AddMemberDialogMessageStatePartial(recipientId));
+ });
+ }
+
+ private void setDialogStateForMultipleRecipients(int recipientCount) {
+ partialState.setValue(new AddMemberDialogMessageStatePartial(recipientCount));
+ }
+
+ private static LiveData getStateWithoutGroupTitle(@NonNull AddMemberDialogMessageStatePartial partialState) {
+ if (partialState.recipientId != null) {
+ return Transformations.map(Recipient.live(partialState.recipientId).getLiveData(), r -> new AddMemberDialogMessageState(r, ""));
+ } else {
+ return new DefaultValueLiveData<>(new AddMemberDialogMessageState(partialState.memberCount, ""));
+ }
+ }
+
+ private static AddMemberDialogMessageState getStateWithGroupTitle(@NonNull String title, @NonNull AddMemberDialogMessageState stateWithoutTitle) {
+ return new AddMemberDialogMessageState(stateWithoutTitle.recipient, stateWithoutTitle.selectionCount, title);
+ }
+
+ private static @NonNull String titleOrDefault(@Nullable String title) {
+ return TextUtils.isEmpty(title) ? ApplicationDependencies.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(@NonNull Recipient recipient, @NonNull String groupTitle) {
+ this(recipient, 1, groupTitle);
+ }
+
+ private AddMemberDialogMessageState(int selectionCount, @NonNull String groupTitle) {
+ this(null, selectionCount, groupTitle);
+ }
+
+ 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 create(@NonNull Class modelClass) {
+ return Objects.requireNonNull(modelClass.cast(new AddMembersViewModel(groupId)));
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
index 9a87e818b..14d0f8c80 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
@@ -44,7 +44,7 @@ public class CreateGroupActivity extends ContactSelectionActivity {
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
- intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, FeatureFlags.gv2GroupCapacity() - 1);
+ intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, FeatureFlags.gv2GroupCapacity() - 1);
return intent;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
index 4800a7c6b..6a5b8cfa7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
@@ -19,8 +19,11 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.ViewModelProviders;
+import com.google.android.material.snackbar.Snackbar;
+
import org.thoughtcrime.securesms.AvatarPreviewActivity;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MuteDialog;
@@ -44,6 +47,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.DateUtils;
+import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.List;
@@ -322,6 +326,8 @@ public class ManageGroupFragment extends Fragment {
} else {
customNotificationsRow.setVisibility(View.GONE);
}
+
+ viewModel.getSnackbarEvents().observe(getViewLifecycleOwner(), this::handleSnackbarEvent);
}
public boolean onMenuItemSelected(@NonNull MenuItem item) {
@@ -349,6 +355,8 @@ public class ManageGroupFragment extends Fragment {
if (context == null) return;
if (this.cursorFactory != null) {
Cursor cursor = this.cursorFactory.create();
+ getViewLifecycleOwner().getLifecycle().addObserver(new LifecycleCursorWrapper(cursor));
+
threadPhotoRailView.setCursor(GlideApp.with(context), cursor);
groupMediaCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
} else {
@@ -357,6 +365,16 @@ public class ManageGroupFragment extends Fragment {
}
}
+ private void handleSnackbarEvent(@NonNull ManageGroupViewModel.SnackbarEvent snackbarEvent) {
+ Snackbar.make(requireView(), buildSnackbarString(snackbarEvent), Snackbar.LENGTH_SHORT).show();
+ }
+
+ private @NonNull String buildSnackbarString(@NonNull ManageGroupViewModel.SnackbarEvent snackbarEvent) {
+ return getResources().getQuantityString(R.plurals.ManageGroupActivity_added,
+ snackbarEvent.getNumberOfMembersAdded(),
+ snackbarEvent.getNumberOfMembersAdded());
+ }
+
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java
index f1ad51ac5..28a64190c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java
@@ -6,7 +6,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
+import com.annimon.stream.Stream;
+import com.google.protobuf.ByteString;
+
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
+import org.signal.zkgroup.util.UUIDUtil;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
@@ -18,7 +22,9 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
+import org.thoughtcrime.securesms.groups.GroupProtoUtil;
import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
+import org.thoughtcrime.securesms.groups.ui.AddMembersResultCallback;
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.logging.Log;
@@ -27,9 +33,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import java.io.IOException;
+import java.util.LinkedList;
import java.util.List;
+import java.util.UUID;
final class ManageGroupRepository {
@@ -55,10 +64,17 @@ final class ManageGroupRepository {
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
if (groupRecord.isV2Group()) {
- DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
- return new GroupCapacityResult(decryptedGroup.getMembersCount(), decryptedGroup.getPendingMembersCount(), FeatureFlags.gv2GroupCapacity());
+ DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
+ List pendingMembers = Stream.of(decryptedGroup.getPendingMembersList())
+ .map(member -> GroupProtoUtil.uuidByteStringToRecipientId(member.getUuid()))
+ .toList();
+ List members = new LinkedList<>(groupRecord.getMembers());
+
+ members.addAll(pendingMembers);
+
+ return new GroupCapacityResult(members, FeatureFlags.gv2GroupCapacity());
} else {
- return new GroupCapacityResult(groupRecord.getMembers().size(), 0, ContactSelectionListFragment.NO_LIMIT);
+ return new GroupCapacityResult(groupRecord.getMembers(), ContactSelectionListFragment.NO_LIMIT);
}
}, onGroupCapacityLoaded::accept);
}
@@ -130,10 +146,11 @@ final class ManageGroupRepository {
});
}
- void addMembers(@NonNull List selected, @NonNull GroupChangeErrorCallback error) {
+ void addMembers(@NonNull List selected, @NonNull AddMembersResultCallback addMembersResultCallback, @NonNull GroupChangeErrorCallback error) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.addMembers(context, groupId, selected);
+ addMembersResultCallback.onMembersAdded(selected.size());
} catch (GroupInsufficientRightsException | GroupNotAMemberException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.NO_RIGHTS);
@@ -169,18 +186,20 @@ final class ManageGroupRepository {
}
static final class GroupCapacityResult {
- private final int fullMembers;
- private final int pendingMembers;
- private final int totalCapacity;
+ private final List members;
+ private final int totalCapacity;
- GroupCapacityResult(int fullMembers, int pendingMembers, int totalCapacity) {
- this.fullMembers = fullMembers;
- this.pendingMembers = pendingMembers;
+ GroupCapacityResult(@NonNull List members, int totalCapacity) {
+ this.members = members;
this.totalCapacity = totalCapacity;
}
- public int getRemainingCapacity() {
- return totalCapacity - fullMembers - pendingMembers;
+ public @NonNull List getMembers() {
+ return members;
+ }
+
+ public int getTotalCapacity() {
+ return totalCapacity;
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
index 89969ce51..df96cc486 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups.ui.managegroup;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
+import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -18,7 +19,6 @@ import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.ExpirationDialog;
-import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.database.MediaDatabase;
@@ -30,14 +30,17 @@ import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
+import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.ExpirationUtil;
+import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
+import java.util.ArrayList;
import java.util.List;
public class ManageGroupViewModel extends ViewModel {
@@ -46,6 +49,7 @@ public class ManageGroupViewModel extends ViewModel {
private final Context context;
private final ManageGroupRepository manageGroupRepository;
+ private final SingleLiveEvent snackbarEvents = new SingleLiveEvent<>();
private final LiveData title;
private final LiveData isAdmin;
private final LiveData canEditGroupAttributes;
@@ -72,7 +76,9 @@ public class ManageGroupViewModel extends ViewModel {
LiveGroup liveGroup = new LiveGroup(manageGroupRepository.getGroupId());
- this.title = liveGroup.getTitle();
+ this.title = Transformations.map(liveGroup.getTitle(),
+ title -> TextUtils.isEmpty(title) ? context.getString(R.string.Recipient_unknown)
+ : title);
this.isAdmin = liveGroup.isSelfAdmin();
this.canCollapseMemberList = LiveDataUtil.combineLatest(memberListCollapseState,
Transformations.map(liveGroup.getFullMembers(), m -> m.size() > MAX_COLLAPSED_MEMBERS),
@@ -162,6 +168,10 @@ public class ManageGroupViewModel extends ViewModel {
return hasCustomNotifications;
}
+ SingleLiveEvent getSnackbarEvents() {
+ return snackbarEvents;
+ }
+
public LiveData getCanCollapseMemberList() {
return canCollapseMemberList;
}
@@ -187,7 +197,7 @@ public class ManageGroupViewModel extends ViewModel {
}
void onAddMembers(List selected) {
- manageGroupRepository.addMembers(selected, this::showErrorToast);
+ manageGroupRepository.addMembers(selected, this::showSuccessSnackbar, this::showErrorToast);
}
void setMuteUntil(long muteUntil) {
@@ -212,6 +222,11 @@ public class ManageGroupViewModel extends ViewModel {
}
}
+ @WorkerThread
+ private void showSuccessSnackbar(int numberOfMembersAdded) {
+ snackbarEvents.postValue(new SnackbarEvent(numberOfMembersAdded));
+ }
+
@WorkerThread
private void showErrorToast(@NonNull GroupChangeFailureReason e) {
Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show());
@@ -219,13 +234,15 @@ public class ManageGroupViewModel extends ViewModel {
public void onAddMembersClick(@NonNull Fragment fragment, int resultCode) {
manageGroupRepository.getGroupCapacity(capacity -> {
- int remainingCapacity = capacity.getRemainingCapacity();
+ int remainingCapacity = capacity.getTotalCapacity();
if (remainingCapacity <= 0) {
Toast.makeText(fragment.requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
} else {
- Intent intent = new Intent(fragment.requireActivity(), PushContactSelectionActivity.class);
+ Intent intent = new Intent(fragment.requireActivity(), AddMembersActivity.class);
+ intent.putExtra(AddMembersActivity.GROUP_ID, manageGroupRepository.getGroupId().toString());
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
- intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, remainingCapacity);
+ intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, remainingCapacity);
+ intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(capacity.getMembers()));
fragment.startActivityForResult(intent, resultCode);
}
});
@@ -276,6 +293,18 @@ public class ManageGroupViewModel extends ViewModel {
}
}
+ static final class SnackbarEvent {
+ private final int numberOfMembersAdded;
+
+ private SnackbarEvent(int numberOfMembersAdded) {
+ this.numberOfMembersAdded = numberOfMembersAdded;
+ }
+
+ public int getNumberOfMembersAdded() {
+ return numberOfMembersAdded;
+ }
+ }
+
private enum CollapseState {
OPEN,
COLLAPSED
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleCursorWrapper.java b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleCursorWrapper.java
new file mode 100644
index 000000000..3562476f8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleCursorWrapper.java
@@ -0,0 +1,27 @@
+package org.thoughtcrime.securesms.util;
+
+import android.database.Cursor;
+import android.database.CursorWrapper;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+
+/**
+ * Wraps a {@link Cursor} that will be closed automatically when the {@link Lifecycle.Event}.ON_DESTROY
+ * is fired from the lifecycle this object is observing.
+ */
+public class LifecycleCursorWrapper extends CursorWrapper implements DefaultLifecycleObserver {
+
+ public LifecycleCursorWrapper(Cursor cursor) {
+ super(cursor);
+ }
+
+ @Override
+ public void onDestroy(@NonNull LifecycleOwner owner) {
+ close();
+ }
+}
diff --git a/app/src/main/res/drawable/contact_selection_checkbox_dark.xml b/app/src/main/res/drawable/contact_selection_checkbox_dark.xml
new file mode 100644
index 000000000..2f2b7f548
--- /dev/null
+++ b/app/src/main/res/drawable/contact_selection_checkbox_dark.xml
@@ -0,0 +1,28 @@
+
+
+ -
+
+
-
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+
+
+
+
+
+ -
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/contact_selection_checkbox.xml b/app/src/main/res/drawable/contact_selection_checkbox_light.xml
similarity index 57%
rename from app/src/main/res/drawable/contact_selection_checkbox.xml
rename to app/src/main/res/drawable/contact_selection_checkbox_light.xml
index c1228f8cd..9a87f5d8f 100644
--- a/app/src/main/res/drawable/contact_selection_checkbox.xml
+++ b/app/src/main/res/drawable/contact_selection_checkbox_light.xml
@@ -1,5 +1,16 @@
+ -
+
+
-
+
+
+
+
+
+
+
+
-
-
diff --git a/app/src/main/res/drawable/progress_button_state_dark.xml b/app/src/main/res/drawable/progress_button_state_dark.xml
new file mode 100644
index 000000000..317f3b400
--- /dev/null
+++ b/app/src/main/res/drawable/progress_button_state_dark.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/progress_button_state.xml b/app/src/main/res/drawable/progress_button_state_light.xml
similarity index 100%
rename from app/src/main/res/drawable/progress_button_state.xml
rename to app/src/main/res/drawable/progress_button_state_light.xml
diff --git a/app/src/main/res/layout/add_group_details_fragment.xml b/app/src/main/res/layout/add_group_details_fragment.xml
index 0a5a48a49..bbf6ca2c1 100644
--- a/app/src/main/res/layout/add_group_details_fragment.xml
+++ b/app/src/main/res/layout/add_group_details_fragment.xml
@@ -44,7 +44,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
- android:background="@color/core_ultramarine"
+ android:background="?colorAccent"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
@@ -84,9 +84,9 @@
android:layout_marginBottom="16dp"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
- app:cpb_colorProgress="@color/core_ultramarine"
+ app:cpb_colorProgress="?colorAccent"
app:cpb_cornerRadius="28dp"
- app:cpb_selectorIdle="@drawable/progress_button_state"
+ app:cpb_selectorIdle="?attr/circular_progress_button_state"
app:cpb_textIdle="@string/AddGroupDetailsFragment__create"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
diff --git a/app/src/main/res/layout/add_members_activity.xml b/app/src/main/res/layout/add_members_activity.xml
new file mode 100644
index 000000000..0072ab117
--- /dev/null
+++ b/app/src/main/res/layout/add_members_activity.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/contact_selection_list_item.xml b/app/src/main/res/layout/contact_selection_list_item.xml
index 0101d5411..37d614740 100644
--- a/app/src/main/res/layout/contact_selection_list_item.xml
+++ b/app/src/main/res/layout/contact_selection_list_item.xml
@@ -27,7 +27,7 @@
diff --git a/app/src/main/res/layout/sticker_preview_activity.xml b/app/src/main/res/layout/sticker_preview_activity.xml
index c9bcbb703..4b9a62a17 100644
--- a/app/src/main/res/layout/sticker_preview_activity.xml
+++ b/app/src/main/res/layout/sticker_preview_activity.xml
@@ -102,7 +102,7 @@
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/StickerPreviewActivity_install_button_color"
app:cpb_cornerRadius="4dp"
- app:cpb_selectorIdle="@drawable/progress_button_state"
+ app:cpb_selectorIdle="@drawable/progress_button_state_light"
app:cpb_textIdle="@string/StickerPackPreviewActivity_install"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/sticker_install_list"
diff --git a/app/src/main/res/layout/submit_debug_log_activity.xml b/app/src/main/res/layout/submit_debug_log_activity.xml
index 9d0d4819e..c78c540f0 100644
--- a/app/src/main/res/layout/submit_debug_log_activity.xml
+++ b/app/src/main/res/layout/submit_debug_log_activity.xml
@@ -95,7 +95,7 @@
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/core_ultramarine"
app:cpb_cornerRadius="4dp"
- app:cpb_selectorIdle="@drawable/progress_button_state"
+ app:cpb_selectorIdle="@drawable/progress_button_state_light"
app:cpb_textIdle="@string/SubmitDebugLogActivity_submit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/username_edit_fragment.xml b/app/src/main/res/layout/username_edit_fragment.xml
index 5916c2498..70d5dfaa9 100644
--- a/app/src/main/res/layout/username_edit_fragment.xml
+++ b/app/src/main/res/layout/username_edit_fragment.xml
@@ -55,7 +55,7 @@
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/core_ultramarine"
app:cpb_cornerRadius="4dp"
- app:cpb_selectorIdle="@drawable/progress_button_state"
+ app:cpb_selectorIdle="@drawable/progress_button_state_light"
app:cpb_textIdle="@string/UsernameEditFragment_submit"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 353264472..fd3122dd9 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -158,6 +158,9 @@
+
+
+
diff --git a/app/src/main/res/values/registration_styles.xml b/app/src/main/res/values/registration_styles.xml
index 9ab06f8c9..9a780bd83 100644
--- a/app/src/main/res/values/registration_styles.xml
+++ b/app/src/main/res/values/registration_styles.xml
@@ -8,7 +8,7 @@
- 4dp
- @color/white
- @color/core_ultramarine
- - @drawable/progress_button_state
+ - @drawable/progress_button_state_light