Reimplement contact chips with a recyclerview.
This commit is contained in:
parent
4215b0391d
commit
7bd34d2b99
6 changed files with 191 additions and 118 deletions
|
@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.animation.LayoutTransition;
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
@ -32,7 +31,6 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.HorizontalScrollView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
@ -43,6 +41,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.constraintlayout.widget.ConstraintSet;
|
import androidx.constraintlayout.widget.ConstraintSet;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||||
|
@ -54,7 +53,6 @@ import androidx.transition.TransitionManager;
|
||||||
|
|
||||||
import com.annimon.stream.Collectors;
|
import com.annimon.stream.Collectors;
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.google.android.material.chip.ChipGroup;
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||||
|
|
||||||
|
@ -62,7 +60,7 @@ import org.signal.core.util.concurrent.SimpleTask;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||||
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
|
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactChip;
|
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||||
|
@ -70,6 +68,7 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||||
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
||||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||||
|
import org.thoughtcrime.securesms.contacts.SelectedContacts;
|
||||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
|
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
|
||||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||||
|
@ -77,15 +76,17 @@ import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.sharing.ShareContact;
|
import org.thoughtcrime.securesms.sharing.ShareContact;
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -95,6 +96,10 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import kotlin.Unit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment for selecting a one or more contacts from a list.
|
* Fragment for selecting a one or more contacts from a list.
|
||||||
*
|
*
|
||||||
|
@ -106,7 +111,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||||
|
|
||||||
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
|
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 0;
|
||||||
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
|
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
|
||||||
|
|
||||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||||
|
@ -134,10 +139,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private RecyclerViewFastScroller fastScroller;
|
private RecyclerViewFastScroller fastScroller;
|
||||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||||
private ChipGroup chipGroup;
|
private RecyclerView chipRecycler;
|
||||||
private HorizontalScrollView chipGroupScrollContainer;
|
|
||||||
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
||||||
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
||||||
|
private MappingAdapter contactChipAdapter;
|
||||||
|
private ContactChipViewModel contactChipViewModel;
|
||||||
|
private LifecycleDisposable lifecycleDisposable;
|
||||||
|
|
||||||
private HeaderActionProvider headerActionProvider;
|
private HeaderActionProvider headerActionProvider;
|
||||||
private TextView headerActionView;
|
private TextView headerActionView;
|
||||||
|
@ -249,8 +256,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
||||||
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
||||||
showContactsProgress = view.findViewById(R.id.progress);
|
showContactsProgress = view.findViewById(R.id.progress);
|
||||||
chipGroup = view.findViewById(R.id.chipGroup);
|
chipRecycler = view.findViewById(R.id.chipRecycler);
|
||||||
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
|
|
||||||
constraintLayout = view.findViewById(R.id.container);
|
constraintLayout = view.findViewById(R.id.container);
|
||||||
headerActionView = view.findViewById(R.id.header_action);
|
headerActionView = view.findViewById(R.id.header_action);
|
||||||
|
|
||||||
|
@ -264,6 +270,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
|
||||||
|
contactChipAdapter = new MappingAdapter();
|
||||||
|
lifecycleDisposable = new LifecycleDisposable();
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||||
|
SelectedContacts.register(contactChipAdapter, this::onChipCloseIconClicked);
|
||||||
|
chipRecycler.setAdapter(contactChipAdapter);
|
||||||
|
|
||||||
|
Disposable disposable = contactChipViewModel.getState().subscribe(this::handleSelectedContactsChanged);
|
||||||
|
|
||||||
|
lifecycleDisposable.add(disposable);
|
||||||
|
|
||||||
Intent intent = requireActivity().getIntent();
|
Intent intent = requireActivity().getIntent();
|
||||||
Bundle arguments = safeArguments();
|
Bundle arguments = safeArguments();
|
||||||
|
|
||||||
|
@ -730,75 +748,22 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
||||||
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
||||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||||
removeChipForContact(selectedContact);
|
contactChipViewModel.remove(selectedContact);
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
onContactSelectedListener.onSelectionChanged();
|
onContactSelectedListener.onSelectionChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeChipForContact(@NonNull SelectedContact contact) {
|
private void handleSelectedContactsChanged(@NonNull List<SelectedContacts.Model> selectedContacts) {
|
||||||
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) {
|
contactChipAdapter.submitList(new MappingModelList(selectedContacts), this::smoothScrollChipsToEnd);
|
||||||
View v = chipGroup.getChildAt(i);
|
|
||||||
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
|
|
||||||
chipGroup.removeView(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getChipCount() == 0) {
|
if (selectedContacts.isEmpty()) {
|
||||||
setChipGroupVisibility(ConstraintSet.GONE);
|
setChipGroupVisibility(ConstraintSet.GONE);
|
||||||
}
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
|
|
||||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
|
|
||||||
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
|
||||||
resolved -> addChipForRecipient(resolved, selectedContact));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
|
|
||||||
final ContactChip chip = new ContactChip(requireContext());
|
|
||||||
|
|
||||||
if (getChipCount() == 0) {
|
|
||||||
setChipGroupVisibility(ConstraintSet.VISIBLE);
|
setChipGroupVisibility(ConstraintSet.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
chip.setText(recipient.getShortDisplayName(requireContext()));
|
|
||||||
chip.setContact(selectedContact);
|
|
||||||
chip.setCloseIconVisible(true);
|
|
||||||
chip.setOnCloseIconClickListener(view -> {
|
|
||||||
markContactUnselected(selectedContact);
|
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
|
||||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orElse(null));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
|
|
||||||
@Override
|
|
||||||
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
|
|
||||||
if (getView() == null || !requireView().isAttachedToWindow()) {
|
|
||||||
Log.w(TAG, "Fragment's view was detached before the animation completed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (view == chip && transitionType == LayoutTransition.APPEARING) {
|
|
||||||
chipGroup.getLayoutTransition().removeTransitionListener(this);
|
|
||||||
registerChipRecipientObserver(chip, recipient.live());
|
|
||||||
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addChip(@NonNull ContactChip chip) {
|
|
||||||
chipGroup.addView(chip);
|
|
||||||
if (selectionWarningLimitReachedExactly()) {
|
if (selectionWarningLimitReachedExactly()) {
|
||||||
if (onSelectionLimitReachedListener != null) {
|
if (onSelectionLimitReachedListener != null) {
|
||||||
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
|
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
|
||||||
|
@ -808,21 +773,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getChipCount() {
|
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
|
||||||
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
|
||||||
if (count < 0) throw new AssertionError();
|
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||||
return count;
|
resolved -> contactChipViewModel.add(selectedContact));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
|
private Unit onChipCloseIconClicked(SelectedContacts.Model model) {
|
||||||
if (recipient != null) {
|
markContactUnselected(model.getSelectedContact());
|
||||||
recipient.observe(getViewLifecycleOwner(), resolved -> {
|
if (onContactSelectedListener != null) {
|
||||||
if (chip.isAttachedToWindow()) {
|
onContactSelectedListener.onContactDeselected(Optional.of(model.getRecipient().getId()), model.getRecipient().getE164().orElse(null));
|
||||||
chip.setAvatar(glideRequests, resolved, null);
|
|
||||||
chip.setText(resolved.getShortDisplayName(chip.getContext()));
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return Unit.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getChipCount() {
|
||||||
|
int count = contactChipViewModel.getCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
|
||||||
|
if (count < 0) throw new AssertionError();
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setChipGroupVisibility(int visibility) {
|
private void setChipGroupVisibility(int visibility) {
|
||||||
|
@ -838,7 +807,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
|
|
||||||
ConstraintSet constraintSet = new ConstraintSet();
|
ConstraintSet constraintSet = new ConstraintSet();
|
||||||
constraintSet.clone(constraintLayout);
|
constraintSet.clone(constraintLayout);
|
||||||
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility);
|
constraintSet.setVisibility(R.id.chipRecycler, visibility);
|
||||||
constraintSet.applyTo(constraintLayout);
|
constraintSet.applyTo(constraintLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -847,8 +816,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void smoothScrollChipsToEnd() {
|
private void smoothScrollChipsToEnd() {
|
||||||
int x = ViewUtil.isLtr(chipGroupScrollContainer) ? chipGroup.getWidth() : 0;
|
int x = ViewUtil.isLtr(chipRecycler) ? chipRecycler.getWidth() : 0;
|
||||||
chipGroupScrollContainer.smoothScrollTo(x, 0);
|
chipRecycler.smoothScrollBy(x, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnContactSelectedListener {
|
public interface OnContactSelectedListener {
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel expressly for displaying the current state of the contact chips
|
||||||
|
* in the contact selection fragment.
|
||||||
|
*/
|
||||||
|
class ContactChipViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val store = RxStore(emptyList<SelectedContacts.Model>())
|
||||||
|
|
||||||
|
val state: Flowable<List<SelectedContacts.Model>> = store.stateFlowable
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
||||||
|
val count = store.state.size
|
||||||
|
|
||||||
|
private val disposables = CompositeDisposable()
|
||||||
|
private val disposableMap: MutableMap<RecipientId, Disposable> = mutableMapOf()
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
disposables.clear()
|
||||||
|
disposableMap.values.forEach { it.dispose() }
|
||||||
|
disposableMap.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(selectedContact: SelectedContact) {
|
||||||
|
disposables += getOrCreateRecipientId(selectedContact).map { Recipient.resolved(it) }.observeOn(Schedulers.io()).subscribe { recipient ->
|
||||||
|
store.update { it + SelectedContacts.Model(selectedContact, recipient) }
|
||||||
|
disposableMap[recipient.id]?.dispose()
|
||||||
|
disposableMap[recipient.id] = store.update(recipient.live().asObservable().toFlowable(BackpressureStrategy.LATEST)) { changedRecipient, state ->
|
||||||
|
val index = state.indexOfFirst { it.selectedContact.matches(selectedContact) }
|
||||||
|
when {
|
||||||
|
index == 0 -> {
|
||||||
|
listOf(SelectedContacts.Model(selectedContact, changedRecipient)) + state.drop(index + 1)
|
||||||
|
}
|
||||||
|
index > 0 -> {
|
||||||
|
state.take(index) + SelectedContacts.Model(selectedContact, changedRecipient) + state.drop(index + 1)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(selectedContact: SelectedContact) {
|
||||||
|
store.update { list ->
|
||||||
|
list.filterNot { it.selectedContact.matches(selectedContact) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOrCreateRecipientId(selectedContact: SelectedContact): Single<RecipientId> {
|
||||||
|
return Single.fromCallable {
|
||||||
|
selectedContact.getOrCreateRecipientId(ApplicationDependencies.getApplication())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||||
|
|
||||||
|
object SelectedContacts {
|
||||||
|
@JvmStatic
|
||||||
|
fun register(adapter: MappingAdapter, onCloseIconClicked: (Model) -> Unit) {
|
||||||
|
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onCloseIconClicked) }, R.layout.contact_selection_list_chip))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Model(val selectedContact: SelectedContact, val recipient: Recipient) : MappingModel<Model> {
|
||||||
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
|
return newItem.selectedContact.matches(selectedContact) && recipient == newItem.recipient
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
|
return areItemsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ViewHolder(itemView: View, private val onCloseIconClicked: (Model) -> Unit) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
private val chip: ContactChip = itemView.findViewById(R.id.contact_chip)
|
||||||
|
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
chip.text = model.recipient.getShortDisplayName(context)
|
||||||
|
chip.setContact(model.selectedContact)
|
||||||
|
chip.isCloseIconVisible = true
|
||||||
|
chip.setOnCloseIconClickListener {
|
||||||
|
onCloseIconClicked(model)
|
||||||
|
}
|
||||||
|
chip.setAvatar(GlideApp.with(itemView), model.recipient, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util.rx
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Scheduler
|
import io.reactivex.rxjava3.core.Scheduler
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
|
@ -31,4 +32,10 @@ class RxStore<T : Any>(
|
||||||
fun update(transformer: (T) -> T) {
|
fun update(transformer: (T) -> T) {
|
||||||
actionSubject.onNext(transformer)
|
actionSubject.onNext(transformer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <U> update(flowable: Flowable<U>, transformer: (U, T) -> T): Disposable {
|
||||||
|
return flowable.subscribe {
|
||||||
|
actionSubject.onNext { t -> transformer(it, t) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
6
app/src/main/res/layout/contact_selection_list_chip.xml
Normal file
6
app/src/main/res/layout/contact_selection_list_chip.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<org.thoughtcrime.securesms.contacts.ContactChip xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/contact_chip"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp" />
|
|
@ -14,7 +14,7 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollContainer">
|
app:layout_constraintTop_toBottomOf="@+id/chipRecycler">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/recycler_view"
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollContainer"
|
app:layout_constraintTop_toBottomOf="@+id/chipRecycler"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -58,12 +58,12 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollContainer"
|
app:layout_constraintTop_toBottomOf="@+id/chipRecycler"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
style="@style/Signal.Widget.Button.Large.Primary"
|
|
||||||
android:id="@+id/show_contacts_button"
|
android:id="@+id/show_contacts_button"
|
||||||
|
style="@style/Signal.Widget.Button.Large.Primary"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
|
@ -112,46 +112,23 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<HorizontalScrollView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/chipGroupScrollContainer"
|
android:id="@+id/chipRecycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="56dp"
|
android:layout_height="wrap_content"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:minHeight="48sp"
|
||||||
|
android:orientation="horizontal"
|
||||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
android:paddingStart="@dimen/dsl_settings_gutter"
|
||||||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
android:paddingEnd="@dimen/dsl_settings_gutter"
|
||||||
android:scrollbars="none"
|
android:scrollbars="none"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:visibility="visible">
|
tools:listitem="@layout/contact_selection_list_chip"
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.google.android.material.chip.ChipGroup
|
|
||||||
android:id="@+id/chipGroup"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_marginEnd="96dp"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
app:singleLine="true">
|
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="Example"
|
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</com.google.android.material.chip.ChipGroup>
|
|
||||||
</FrameLayout>
|
|
||||||
</HorizontalScrollView>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/header_action"
|
android:id="@+id/header_action"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
Loading…
Add table
Reference in a new issue