From 0b279d1df386eab7ba7d18e0ccc7a56b7aa321c0 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Fri, 24 Apr 2020 16:12:33 -0300 Subject: [PATCH] Group contact chips behind feature flag. --- app/sampledata/contacts.json | 29 +++ .../ContactSelectionListFragment.java | 137 ++++++++++---- .../securesms/InviteActivity.java | 12 +- .../securesms/contacts/ContactChip.java | 117 ++++++++++++ .../contacts/ContactSelectionListAdapter.java | 29 ++- .../contacts/ContactSelectionListItem.java | 11 ++ .../securesms/contacts/SelectedContact.java | 27 +-- .../contacts/SelectedContactSet.java | 58 ++++++ .../securesms/recipients/Recipient.java | 10 + .../contact_selection_list_fragment.xml | 171 ++++++++++++------ .../layout/contact_selection_list_item.xml | 8 +- app/src/main/res/values/themes.xml | 28 ++- .../contacts/SelectedContactSetTest.java | 104 +++++++++++ 13 files changed, 610 insertions(+), 131 deletions(-) create mode 100644 app/sampledata/contacts.json create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChip.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContactSet.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/contacts/SelectedContactSetTest.java diff --git a/app/sampledata/contacts.json b/app/sampledata/contacts.json new file mode 100644 index 000000000..c8d255419 --- /dev/null +++ b/app/sampledata/contacts.json @@ -0,0 +1,29 @@ +{ + "data": [ + { + "name": "Ottttooooooooo Ocataaaaaaaavius", + "number": "+1 (555) 555-5555", + "label": "Mobile" + }, + { + "name": "Victor Von Doom Phd", + "number": "+1 (555) 123-4567", + "label": "Home" + }, + { + "name": "Flash Thompson", + "number": "+1 (555) 435-1261", + "label": "Work" + }, + { + "name": "Dr. Curtis Connors", + "number": "+1 (555) 992-1567", + "label": "Mobile" + }, + { + "name": "Billy Russo", + "number": "+1 (555) 234-1516", + "label": "Mobile" + } + ] +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index c1229c576..2715f9734 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -28,6 +28,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.Button; +import android.widget.HorizontalScrollView; import android.widget.TextView; import android.widget.Toast; @@ -35,26 +36,32 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.google.android.material.chip.ChipGroup; import com.pnikosis.materialishprogress.ProgressWheel; import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; +import org.thoughtcrime.securesms.contacts.ContactChip; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; import org.thoughtcrime.securesms.contacts.SelectedContact; +import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; +import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -67,9 +74,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; -import java.util.LinkedList; import java.util.List; -import java.util.Set; /** * Fragment for selecting a one or more contacts from a list. @@ -88,8 +93,9 @@ public final class ContactSelectionListFragment extends Fragment public static final String REFRESHABLE = "refreshable"; public static final String RECENTS = "recents"; + private final Debouncer scrollDebounce = new Debouncer(100); + private TextView emptyText; - private Set selectedContacts; private OnContactSelectedListener onContactSelectedListener; private SwipeRefreshLayout swipeRefresh; private View showContactsLayout; @@ -100,10 +106,13 @@ public final class ContactSelectionListFragment extends Fragment private RecyclerView recyclerView; private RecyclerViewFastScroller fastScroller; private ContactSelectionListAdapter cursorRecyclerViewAdapter; + private ChipGroup chipGroup; + private HorizontalScrollView chipGroupScrollContainer; @Nullable private FixedViewsAdapter headerAdapter; @Nullable private FixedViewsAdapter footerAdapter; @Nullable private ListCallback listCallback; + private GlideRequests glideRequests; @Override public void onAttach(@NonNull Context context) { @@ -132,14 +141,16 @@ public final class ContactSelectionListFragment extends Fragment if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) { handleContactPermissionGranted(); } else { - this.getLoaderManager().initLoader(0, null, this); + LoaderManager.getInstance(this).initLoader(0, null, this); } }) .onAnyDenied(() -> { - getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + FragmentActivity activity = requireActivity(); - if (getActivity().getIntent().getBooleanExtra(RECENTS, false)) { - getLoaderManager().initLoader(0, null, ContactSelectionListFragment.this); + activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + + if (activity.getIntent().getBooleanExtra(RECENTS, false)) { + LoaderManager.getInstance(this).initLoader(0, null, ContactSelectionListFragment.this); } else { initializeNoContactsPermission(); } @@ -151,17 +162,22 @@ public final class ContactSelectionListFragment extends Fragment public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false); - emptyText = ViewUtil.findById(view, android.R.id.empty); - recyclerView = ViewUtil.findById(view, R.id.recycler_view); - swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh); - fastScroller = ViewUtil.findById(view, R.id.fast_scroller); - showContactsLayout = view.findViewById(R.id.show_contacts_container); - showContactsButton = view.findViewById(R.id.show_contacts_button); - showContactsDescription = view.findViewById(R.id.show_contacts_description); - showContactsProgress = view.findViewById(R.id.progress); + emptyText = ViewUtil.findById(view, android.R.id.empty); + recyclerView = ViewUtil.findById(view, R.id.recycler_view); + swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh); + fastScroller = ViewUtil.findById(view, R.id.fast_scroller); + showContactsLayout = view.findViewById(R.id.show_contacts_container); + showContactsButton = view.findViewById(R.id.show_contacts_button); + showContactsDescription = view.findViewById(R.id.show_contacts_description); + showContactsProgress = view.findViewById(R.id.progress); + chipGroup = view.findViewById(R.id.chipGroup); + chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true)); + swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true)); + + autoScrollOnNewItem(); return view; } @@ -171,26 +187,22 @@ public final class ContactSelectionListFragment extends Fragment Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } - public @NonNull List getSelectedContacts() { - List selected = new LinkedList<>(); - if (selectedContacts != null) { - selected.addAll(selectedContacts); - } - - return selected; + @NonNull List getSelectedContacts() { + return cursorRecyclerViewAdapter.getSelectedContacts(); } private boolean isMulti() { - return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false); + return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false); } private void initializeCursor() { + glideRequests = GlideApp.with(this); + cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(), - GlideApp.with(this), + glideRequests, null, new ListClickListener(), isMulti()); - selectedContacts = cursorRecyclerViewAdapter.getSelectedContacts(); RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader(); @@ -263,7 +275,7 @@ public final class ContactSelectionListFragment extends Fragment } public void reset() { - selectedContacts.clear(); + cursorRecyclerViewAdapter.clearSelectedContacts(); if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) { getLoaderManager().restartLoader(0, null, this); @@ -356,7 +368,7 @@ public final class ContactSelectionListFragment extends Fragment SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber()) : SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber()); - if (!isMulti() || !selectedContacts.contains(selectedContact)) { + if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) { if (contact.isUsernameType()) { AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext()); @@ -366,8 +378,8 @@ public final class ContactSelectionListFragment extends Fragment loadingDialog.dismiss(); if (uuid.isPresent()) { Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber()); - selectedContacts.add(SelectedContact.forUsername(recipient.getId(), contact.getNumber())); - contact.setChecked(true); + SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber()); + markContactSelected(selected, contact); if (onContactSelectedListener != null) { onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null); @@ -381,24 +393,66 @@ public final class ContactSelectionListFragment extends Fragment } }); } else { - selectedContacts.add(selectedContact); - contact.setChecked(true); + markContactSelected(selectedContact, contact); if (onContactSelectedListener != null) { onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber()); } } } else { - selectedContacts.remove(selectedContact); - contact.setChecked(false); + markContactUnselected(selectedContact, contact); if (onContactSelectedListener != null) { onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber()); } + }} + } + + private void markContactSelected(@NonNull SelectedContact selectedContact, @NonNull ContactSelectionListItem listItem) { + cursorRecyclerViewAdapter.addSelectedContact(selectedContact); + listItem.setChecked(true); + if (isMulti() && FeatureFlags.newGroupUI()) { + chipGroup.addView(newChipForContact(listItem, selectedContact)); + } + } + + private void markContactUnselected(@NonNull SelectedContact selectedContact, @NonNull ContactSelectionListItem listItem) { + cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact); + listItem.setChecked(false); + removeChipForContact(selectedContact); + } + + private void removeChipForContact(@NonNull SelectedContact contact) { + for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) { + View v = chipGroup.getChildAt(i); + if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) { + chipGroup.removeView(v); } } } + private View newChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) { + final ContactChip chip = new ContactChip(requireContext()); + chip.setText(contact.getChipName()); + chip.setContact(selectedContact); + + LiveRecipient recipient = contact.getRecipient(); + if (recipient != null) { + recipient.observe(getViewLifecycleOwner(), resolved -> { + chip.setAvatar(glideRequests, resolved); + chip.setText(resolved.getShortDisplayName(chip.getContext())); + } + ); + } + + chip.setCloseIconVisible(true); + chip.setOnCloseIconClickListener(view -> { + markContactUnselected(selectedContact, contact); + chipGroup.removeView(chip); + }); + return chip; + } + public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) { this.onContactSelectedListener = onContactSelectedListener; } @@ -407,6 +461,19 @@ public final class ContactSelectionListFragment extends Fragment this.swipeRefresh.setOnRefreshListener(onRefreshListener); } + private void autoScrollOnNewItem() { + chipGroup.addOnLayoutChangeListener((view1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (right > oldRight) { + scrollDebounce.publish(this::smoothScrollChipsToEnd); + } + }); + } + + private void smoothScrollChipsToEnd() { + int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0; + chipGroupScrollContainer.smoothScrollTo(x, 0); + } + public interface OnContactSelectedListener { void onContactSelected(Optional recipientId, String number); void onContactDeselected(Optional recipientId, String number); diff --git a/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java b/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java index 088579204..15af1ecc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java @@ -22,8 +22,6 @@ import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; -import com.annimon.stream.Stream; - import org.thoughtcrime.securesms.components.ContactFilterToolbar; import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; @@ -42,6 +40,7 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.whispersystems.libsignal.util.guava.Optional; +import java.util.List; import java.util.concurrent.ExecutionException; public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener { @@ -135,14 +134,15 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen new SendSmsInvitesAsyncTask(this, inviteText.getText().toString()) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, contactsFragment.getSelectedContacts() - .toArray(new SelectedContact[contactsFragment.getSelectedContacts().size()])); + .toArray(new SelectedContact[0])); } private void updateSmsButtonText() { + List selectedContacts = contactsFragment.getSelectedContacts(); smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends, - contactsFragment.getSelectedContacts().size(), - contactsFragment.getSelectedContacts().size())); - smsSendButton.setEnabled(!contactsFragment.getSelectedContacts().isEmpty()); + selectedContacts.size(), + selectedContacts.size())); + smsSendButton.setEnabled(!selectedContacts.isEmpty()); } @Override public void onBackPressed() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChip.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChip.java new file mode 100644 index 000000000..d7708fa05 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChip.java @@ -0,0 +1,117 @@ +package org.thoughtcrime.securesms.contacts; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; +import com.google.android.material.chip.Chip; + +import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.recipients.Recipient; + +public final class ContactChip extends Chip { + + @Nullable private SelectedContact contact; + + public ContactChip(Context context) { + super(context); + } + + public ContactChip(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ContactChip(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setContact(@NonNull SelectedContact contact) { + this.contact = contact; + } + + public @Nullable SelectedContact getContact() { + return contact; + } + + public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient) { + if (recipient != null) { + requestManager.clear(this); + + Drawable fallbackContactPhotoDrawable = recipient.getFallbackContactPhotoDrawable(getContext(), false); + ContactPhoto contactPhoto = recipient.getContactPhoto(); + + if (contactPhoto == null) { + setChipIcon(new HalfScaleDrawable(fallbackContactPhotoDrawable)); + } else { + requestManager.load(contactPhoto) + .fallback(fallbackContactPhotoDrawable) + .error(fallbackContactPhotoDrawable) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .circleCrop() + .into(new CustomTarget() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + setChipIcon(resource); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + setChipIcon(placeholder); + } + }); + } + } + } + + private static class HalfScaleDrawable extends Drawable { + + private final Drawable fallbackContactPhotoDrawable; + + HalfScaleDrawable(Drawable fallbackContactPhotoDrawable) { + this.fallbackContactPhotoDrawable = fallbackContactPhotoDrawable; + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + super.setBounds(left, top, right, bottom); + fallbackContactPhotoDrawable.setBounds(left, top, 2 * right - left, 2 * bottom - top); + } + + @Override + public void setBounds(@NonNull Rect bounds) { + super.setBounds(bounds); + } + + @Override + public void draw(@NonNull Canvas canvas) { + canvas.save(); + canvas.scale(0.5f, 0.5f); + fallbackContactPhotoDrawable.draw(canvas); + canvas.restore(); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + } +} 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 7b07b2122..c272a9236 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -43,8 +43,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter; import org.thoughtcrime.securesms.util.Util; -import java.util.HashSet; -import java.util.Set; +import java.util.List; +import java.util.Locale; /** * List adapter to display all contacts and their related information @@ -70,7 +70,26 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter selectedContacts = new HashSet<>(); + private final SelectedContactSet selectedContacts = new SelectedContactSet(); + + public void clearSelectedContacts() { + selectedContacts.clear(); + } + + public boolean isSelectedContact(@NonNull SelectedContact contact) { + return selectedContacts.contains(contact); + } + + public void addSelectedContact(@NonNull SelectedContact contact) { + if (!selectedContacts.add(contact)) { + Log.i(TAG, "Contact was already selected, possibly by another identifier"); + } + } + + public void removeFromSelectedContacts(@NonNull SelectedContact selectedContact) { + int removed = selectedContacts.remove(selectedContact); + Log.i(TAG, String.format(Locale.US, "Removed %d selected contacts that matched", removed)); + } public abstract static class ViewHolder extends RecyclerView.ViewHolder { @@ -227,8 +246,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter getSelectedContacts() { - return selectedContacts; + public List getSelectedContacts() { + return selectedContacts.getContacts(); } private CharSequence getSpannedHeaderString(int position) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java index 81bd8a83d..d6044dd23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java @@ -35,6 +35,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF private CheckBox checkBox; private String number; + private String chipName; private int contactType; private LiveRecipient recipient; private GlideRequests glideRequests; @@ -128,8 +129,10 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF if (recipient != null) { this.nameView.setText(recipient); + chipName = recipient.getShortDisplayName(getContext()); } else { this.nameView.setText(name); + chipName = name; } } @@ -137,6 +140,14 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF return number; } + public String getChipName() { + return chipName; + } + + public @Nullable LiveRecipient getRecipient() { + return recipient; + } + public boolean isUsernameType() { return contactType == ContactRepository.NEW_USERNAME_TYPE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContact.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContact.java index 55a23e06a..407df2b89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContact.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContact.java @@ -8,16 +8,12 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import java.util.Objects; - /** * Model for a contact and the various ways it could be represented. Used in situations where we * don't want to create Recipients for the wrapped data (like a custom-entered phone number for * someone you don't yet have a conversation with). - * - * Designed so that two instances will be equal if *any* of its properties match. */ -public class SelectedContact { +public final class SelectedContact { private final RecipientId recipientId; private final String number; private final String username; @@ -46,19 +42,14 @@ public class SelectedContact { } } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SelectedContact that = (SelectedContact) o; + /** + * Returns true iff any non-null property matches one on the other contact. + */ + public boolean matches(@Nullable SelectedContact other) { + if (other == null) return false; - return Objects.equals(recipientId, that.recipientId) || - Objects.equals(number, that.number) || - Objects.equals(username, that.username); - } - - @Override - public int hashCode() { - return Objects.hash(recipientId, number, username); + return recipientId != null && recipientId.equals(other.recipientId) || + number != null && number .equals(other.number) || + username != null && username .equals(other.username); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContactSet.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContactSet.java new file mode 100644 index 000000000..6f14a706b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContactSet.java @@ -0,0 +1,58 @@ +package org.thoughtcrime.securesms.contacts; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Specialised set for {@link SelectedContact} that will not allow more than one entry that + * {@link SelectedContact#matches(SelectedContact)} any other. + */ +public final class SelectedContactSet { + + private final List contacts = new LinkedList<>(); + + public boolean add(@NonNull SelectedContact contact) { + if (contains(contact)) { + return false; + } + + contacts.add(contact); + return true; + } + + public boolean contains(@NonNull SelectedContact otherContact) { + for (SelectedContact contact : contacts) { + if (otherContact.matches(contact)) { + return true; + } + } + return false; + } + + public List getContacts() { + return new ArrayList<>(contacts); + } + + public void clear() { + contacts.clear(); + } + + public int remove(@NonNull SelectedContact otherContact) { + int removeCount = 0; + Iterator iterator = contacts.iterator(); + + while (iterator.hasNext()) { + SelectedContact next = iterator.next(); + if (next.matches(otherContact)) { + iterator.remove(); + removeCount++; + } + } + + return removeCount; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 2ef391458..6727516d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -415,6 +415,12 @@ public class Recipient { context.getString(R.string.Recipient_unknown)); } + public @NonNull String getShortDisplayName(@NonNull Context context) { + return Util.getFirstNonEmpty(getName(context), + getProfileName().getGivenName(), + getDisplayName(context)); + } + public @NonNull MaterialColor getColor() { if (isGroupInternal()) { return MaterialColor.GROUP; @@ -610,6 +616,10 @@ public class Recipient { return getFallbackContactPhotoDrawable(context, inverted, DEFAULT_FALLBACK_PHOTO_PROVIDER); } + public @NonNull Drawable getSmallFallbackContactPhotoDrawable(Context context, boolean inverted) { + return getSmallFallbackContactPhotoDrawable(context, inverted, DEFAULT_FALLBACK_PHOTO_PROVIDER); + } + public @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted, @Nullable FallbackPhotoProvider fallbackPhotoProvider) { return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asDrawable(context, getColor().toAvatarColor(context), inverted); } diff --git a/app/src/main/res/layout/contact_selection_list_fragment.xml b/app/src/main/res/layout/contact_selection_list_fragment.xml index 1393c1a5b..e7d6bbbb0 100644 --- a/app/src/main/res/layout/contact_selection_list_fragment.xml +++ b/app/src/main/res/layout/contact_selection_list_fragment.xml @@ -1,27 +1,34 @@ - - + + + + android:id="@+id/swipe_refresh" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollContainer"> + android:scrollbars="vertical" + tools:listitem="@layout/contact_selection_list_item" /> - @@ -30,64 +37,108 @@ - + + +