Refactor ContactSelectionListFragment to use ContactSearch infrastructure.
This commit is contained in:
parent
0f6bc0471c
commit
7fbfc09a89
39 changed files with 1088 additions and 1249 deletions
|
@ -26,7 +26,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -71,7 +71,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
|||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
int displayMode = includeSms ? DisplayMode.FLAG_ALL : DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
int displayMode = includeSms ? ContactSelectionDisplayMode.FLAG_ALL : ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
class ContactSelectionListAdapter(
|
||||
context: Context,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
displayPhoneNumber: DisplayPhoneNumber,
|
||||
onClickCallbacks: OnContactSelectionClick,
|
||||
longClickCallbacks: LongClickCallbacks,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks
|
||||
) : ContactSearchAdapter(context, emptySet(), displayCheckBox, displaySmsTag, displayPhoneNumber, onClickCallbacks, longClickCallbacks, storyContextMenuCallbacks) {
|
||||
|
||||
init {
|
||||
registerFactory(NewGroupModel::class.java, LayoutFactory({ StaticMappingViewHolder(it, onClickCallbacks::onNewGroupClicked) }, R.layout.contact_selection_new_group_item))
|
||||
registerFactory(InviteToSignalModel::class.java, LayoutFactory({ StaticMappingViewHolder(it, onClickCallbacks::onInviteToSignalClicked) }, R.layout.contact_selection_invite_action_item))
|
||||
}
|
||||
|
||||
class NewGroupModel : MappingModel<NewGroupModel> {
|
||||
override fun areItemsTheSame(newItem: NewGroupModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: NewGroupModel): Boolean = true
|
||||
}
|
||||
|
||||
class InviteToSignalModel : MappingModel<InviteToSignalModel> {
|
||||
override fun areItemsTheSame(newItem: InviteToSignalModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: InviteToSignalModel): Boolean = true
|
||||
}
|
||||
|
||||
class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
|
||||
|
||||
enum class ArbitraryRow(val code: String) {
|
||||
NEW_GROUP("new-group"),
|
||||
INVITE_TO_SIGNAL("invite-to-signal");
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String) = values().first { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSize(section: ContactSearchConfiguration.Section.Arbitrary, query: String?): Int {
|
||||
return if (query.isNullOrEmpty()) section.types.size else 0
|
||||
}
|
||||
|
||||
override fun getData(section: ContactSearchConfiguration.Section.Arbitrary, query: String?, startIndex: Int, endIndex: Int, totalSearchSize: Int): List<ContactSearchData.Arbitrary> {
|
||||
check(section.types.size == 1)
|
||||
return listOf(ContactSearchData.Arbitrary(section.types.first()))
|
||||
}
|
||||
|
||||
override fun getMappingModel(arbitrary: ContactSearchData.Arbitrary): MappingModel<*> {
|
||||
val code = ArbitraryRow.fromCode(arbitrary.type)
|
||||
return when (code) {
|
||||
ArbitraryRow.NEW_GROUP -> NewGroupModel()
|
||||
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OnContactSelectionClick : ClickCallbacks {
|
||||
fun onNewGroupClicked()
|
||||
fun onInviteToSignalClicked()
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ import android.Manifest;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Rect;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
@ -40,10 +39,7 @@ import androidx.annotation.Px;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
@ -59,32 +55,29 @@ import com.pnikosis.materialishprogress.ProgressWheel;
|
|||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
|
||||
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.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContacts;
|
||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||
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;
|
||||
|
@ -105,7 +98,6 @@ import kotlin.Unit;
|
|||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public final class ContactSelectionListFragment extends LoggingFragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||
|
@ -137,27 +129,23 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private RecyclerView chipRecycler;
|
||||
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
||||
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
||||
private MappingAdapter contactChipAdapter;
|
||||
private ContactChipViewModel contactChipViewModel;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
private HeaderActionProvider headerActionProvider;
|
||||
private TextView headerActionView;
|
||||
private ContactSearchMediator contactSearchMediator;
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@Nullable private ListCallback listCallback;
|
||||
@Nullable private ScrollCallback scrollCallback;
|
||||
@Nullable private OnItemLongClickListener onItemLongClickListener;
|
||||
private GlideRequests glideRequests;
|
||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
private boolean canSelectSelf;
|
||||
private ListClickListener listClickListener = new ListClickListener();
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
|
@ -191,14 +179,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) getParentFragment();
|
||||
}
|
||||
|
||||
if (context instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
|
||||
}
|
||||
|
||||
if (context instanceof HeaderActionProvider) {
|
||||
headerActionProvider = (HeaderActionProvider) context;
|
||||
}
|
||||
|
@ -234,16 +214,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
||||
handleContactPermissionGranted();
|
||||
} else {
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
contactSearchMediator.refresh();
|
||||
}
|
||||
})
|
||||
.onAnyDenied(() -> {
|
||||
FragmentActivity activity = requireActivity();
|
||||
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
|
||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
|
||||
if (safeArguments().getBoolean(RECENTS, activity.getIntent().getBooleanExtra(RECENTS, false))) {
|
||||
LoaderManager.getInstance(this).initLoader(0, null, ContactSelectionListFragment.this);
|
||||
if (safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false))) {
|
||||
contactSearchMediator.refresh();
|
||||
} else {
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
|
@ -305,7 +283,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
swipeRefresh.setNestedScrollingEnabled(isRefreshable);
|
||||
swipeRefresh.setEnabled(isRefreshable);
|
||||
|
||||
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
|
||||
selectionLimit = arguments.getParcelable(SELECTION_LIMITS);
|
||||
if (selectionLimit == null) {
|
||||
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
||||
|
@ -353,6 +330,65 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
headerActionView.setEnabled(false);
|
||||
}
|
||||
|
||||
contactSearchMediator = new ContactSearchMediator(
|
||||
this,
|
||||
currentSelection.stream()
|
||||
.map(r -> new ContactSearchKey.RecipientSearchKey(r, false))
|
||||
.collect(java.util.stream.Collectors.toSet()),
|
||||
selectionLimit,
|
||||
isMulti,
|
||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||
ContactSearchAdapter.DisplayPhoneNumber.ALWAYS,
|
||||
this::mapStateToConfiguration,
|
||||
new ContactSearchMediator.SimpleCallbacks() {
|
||||
@Override
|
||||
public void onAdapterListCommitted(int size) {
|
||||
onLoadFinished(size);
|
||||
}
|
||||
},
|
||||
false,
|
||||
(context, fixedContacts, displayCheckBox, displaySmsTag, displayPhoneNumber, callbacks, longClickCallbacks, storyContextMenuCallbacks) -> new ContactSelectionListAdapter(
|
||||
context,
|
||||
displayCheckBox,
|
||||
displaySmsTag,
|
||||
displayPhoneNumber,
|
||||
new ContactSelectionListAdapter.OnContactSelectionClick() {
|
||||
@Override
|
||||
public void onNewGroupClicked() {
|
||||
listCallback.onNewGroup(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInviteToSignalClicked() {
|
||||
listCallback.onInvite();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoryClicked(@NonNull View view1, @NonNull ContactSearchData.Story story, boolean isSelected) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKnownRecipientClicked(@NonNull View view1, @NonNull ContactSearchData.KnownRecipient knownRecipient, boolean isSelected) {
|
||||
listClickListener.onItemClick(knownRecipient.getContactSearchKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExpandClicked(@NonNull ContactSearchData.Expand expand) {
|
||||
callbacks.onExpandClicked(expand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknownRecipientClicked(@NonNull View view, @NonNull ContactSearchData.UnknownRecipient unknownRecipient, boolean isSelected) {
|
||||
listClickListener.onItemClick(unknownRecipient.getContactSearchKey());
|
||||
}
|
||||
},
|
||||
(anchorView, data) -> listClickListener.onItemLongClick(anchorView, data.getContactSearchKey()),
|
||||
storyContextMenuCallbacks
|
||||
),
|
||||
new ContactSelectionListAdapter.ArbitraryRepository()
|
||||
);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
@ -372,27 +408,30 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
}
|
||||
|
||||
public @NonNull List<SelectedContact> getSelectedContacts() {
|
||||
if (cursorRecyclerViewAdapter == null) {
|
||||
if (contactSearchMediator == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return cursorRecyclerViewAdapter.getSelectedContacts();
|
||||
return contactSearchMediator.getSelectedContacts()
|
||||
.stream()
|
||||
.map(ContactSearchKey::requireSelectedContact)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
public int getSelectedContactsCount() {
|
||||
if (cursorRecyclerViewAdapter == null) {
|
||||
if (contactSearchMediator == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
||||
return contactSearchMediator.getSelectedContacts().size();
|
||||
}
|
||||
|
||||
public int getTotalMemberCount() {
|
||||
if (cursorRecyclerViewAdapter == null) {
|
||||
if (contactSearchMediator == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return cursorRecyclerViewAdapter.getSelectedContactsCount() + cursorRecyclerViewAdapter.getCurrentContactsCount();
|
||||
return getSelectedContactsCount() + contactSearchMediator.getFixedContactsSize();
|
||||
}
|
||||
|
||||
private Set<RecipientId> getCurrentSelection() {
|
||||
|
@ -410,34 +449,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
}
|
||||
|
||||
private void initializeCursor() {
|
||||
glideRequests = GlideApp.with(this);
|
||||
|
||||
cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(),
|
||||
glideRequests,
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti,
|
||||
currentSelection,
|
||||
safeArguments().getInt(ContactSelectionArguments.CHECKBOX_RESOURCE, R.drawable.contact_selection_checkbox));
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
if (listCallback != null) {
|
||||
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
|
||||
headerAdapter.hide();
|
||||
concatenateAdapter.addAdapter(headerAdapter);
|
||||
}
|
||||
|
||||
concatenateAdapter.addAdapter(cursorRecyclerViewAdapter);
|
||||
|
||||
if (listCallback != null) {
|
||||
footerAdapter = new FixedViewsAdapter(createInviteActionView(listCallback));
|
||||
footerAdapter.hide();
|
||||
concatenateAdapter.addAdapter(footerAdapter);
|
||||
}
|
||||
|
||||
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
|
||||
recyclerView.setAdapter(concatenateAdapter);
|
||||
recyclerView.setAdapter(contactSearchMediator.getAdapter());
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
|
@ -458,20 +471,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
return hasQueryFilter() || shouldDisplayRecents();
|
||||
}
|
||||
|
||||
private View createInviteActionView(@NonNull ListCallback listCallback) {
|
||||
View view = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.contact_selection_invite_action_item, (ViewGroup) requireView(), false);
|
||||
view.setOnClickListener(v -> listCallback.onInvite());
|
||||
return view;
|
||||
}
|
||||
|
||||
private View createNewGroupItem(@NonNull ListCallback listCallback) {
|
||||
View view = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.contact_selection_new_group_item, (ViewGroup) requireView(), false);
|
||||
view.setOnClickListener(v -> listCallback.onNewGroup(false));
|
||||
return view;
|
||||
}
|
||||
|
||||
private void initializeNoContactsPermission() {
|
||||
swipeRefresh.setVisibility(View.GONE);
|
||||
|
||||
|
@ -496,7 +495,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
|
||||
public void setQueryFilter(String filter) {
|
||||
this.cursorFilter = filter;
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
contactSearchMediator.onFilterChanged(filter);
|
||||
}
|
||||
|
||||
public void resetQueryFilter() {
|
||||
|
@ -513,51 +512,21 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
}
|
||||
|
||||
public void reset() {
|
||||
cursorRecyclerViewAdapter.clearSelectedContacts();
|
||||
|
||||
if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
}
|
||||
contactSearchMediator.clearSelection();
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
headerActionView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setRecyclerViewPaddingBottom(@Px int paddingBottom) {
|
||||
ViewUtil.setPaddingBottom(recyclerView, paddingBottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
FragmentActivity activity = requireActivity();
|
||||
int displayMode = safeArguments().getInt(DISPLAY_MODE, activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL));
|
||||
boolean displayRecents = shouldDisplayRecents();
|
||||
|
||||
if (cursorFactoryProvider != null) {
|
||||
return cursorFactoryProvider.get().create();
|
||||
} else {
|
||||
return new ContactsCursorLoader.Factory(activity, displayMode, cursorFilter, displayRecents).create();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, @Nullable Cursor data) {
|
||||
private void onLoadFinished(int count) {
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
showContactsLayout.setVisibility(View.GONE);
|
||||
|
||||
cursorRecyclerViewAdapter.changeCursor(data);
|
||||
|
||||
if (footerAdapter != null) {
|
||||
footerAdapter.show();
|
||||
}
|
||||
|
||||
if (headerAdapter != null) {
|
||||
if (TextUtils.isEmpty(cursorFilter)) {
|
||||
headerAdapter.show();
|
||||
} else {
|
||||
headerAdapter.hide();
|
||||
}
|
||||
}
|
||||
|
||||
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
||||
boolean useFastScroller = data != null && data.getCount() > 20;
|
||||
boolean useFastScroller = count > 20;
|
||||
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
||||
if (useFastScroller) {
|
||||
fastScroller.setVisibility(View.VISIBLE);
|
||||
|
@ -574,13 +543,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
cursorRecyclerViewAdapter.changeCursor(null);
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
headerActionView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private boolean shouldDisplayRecents() {
|
||||
return safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
|
||||
}
|
||||
|
@ -634,20 +596,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
*
|
||||
* @param contacts List of the contacts to select. This will not overwrite the current selection, but append to it.
|
||||
*/
|
||||
public void markSelected(@NonNull Set<ShareContact> contacts) {
|
||||
public void markSelected(@NonNull Set<RecipientId> contacts) {
|
||||
if (contacts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<SelectedContact> toMarkSelected = contacts.stream()
|
||||
.map(contact -> {
|
||||
if (contact.getRecipientId().isPresent()) {
|
||||
return SelectedContact.forRecipientId(contact.getRecipientId().get());
|
||||
} else {
|
||||
return SelectedContact.forPhone(null, contact.getNumber());
|
||||
}
|
||||
})
|
||||
.filter(c -> !cursorRecyclerViewAdapter.isSelectedContact(c))
|
||||
.filter(r -> !contactSearchMediator.getSelectedContacts()
|
||||
.contains(new ContactSearchKey.RecipientSearchKey(r, false)))
|
||||
.map(SelectedContact::forRecipientId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
if (toMarkSelected.isEmpty()) {
|
||||
|
@ -657,22 +614,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
for (final SelectedContact selectedContact : toMarkSelected) {
|
||||
markContactSelected(selectedContact);
|
||||
}
|
||||
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount());
|
||||
}
|
||||
|
||||
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(ContactSelectionListItem contact) {
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
|
||||
private class ListClickListener {
|
||||
public void onItemClick(ContactSearchKey contact) {
|
||||
SelectedContact selectedContact = contact.requireSelectedContact();
|
||||
|
||||
if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (!isMulti || !contactSearchMediator.getSelectedContacts().contains(selectedContact.toContactSearchKey())) {
|
||||
if (selectionHardLimitReached()) {
|
||||
if (onSelectionLimitReachedListener != null) {
|
||||
onSelectionLimitReachedListener.onHardLimitReached(selectionLimit.getHardLimit());
|
||||
|
@ -682,63 +635,58 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
return;
|
||||
}
|
||||
|
||||
if (contact.isUsernameType()) {
|
||||
if (contact instanceof ContactSearchKey.UnknownRecipientKey && ((ContactSearchKey.UnknownRecipientKey) contact).getSectionKey() == ContactSearchConfiguration.SectionKey.USERNAME) {
|
||||
String username = ((ContactSearchKey.UnknownRecipientKey) contact).getQuery();
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return UsernameUtil.fetchAciForUsername(contact.getNumber());
|
||||
return UsernameUtil.fetchAciForUsername(username);
|
||||
}, uuid -> {
|
||||
loadingDialog.dismiss();
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
Recipient recipient = Recipient.externalUsername(uuid.get(), username);
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), username);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
|
||||
if (allowed) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, username))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber(), allowed -> {
|
||||
onContactSelectedListener.onBeforeContactSelected(Optional.ofNullable(selectedContact.getRecipientId()), selectedContact.getNumber(), allowed -> {
|
||||
if (allowed) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
markContactUnselected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||
onContactSelectedListener.onContactDeselected(Optional.ofNullable(selectedContact.getRecipientId()), selectedContact.getNumber());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(ContactSelectionListItem item) {
|
||||
public boolean onItemLongClick(View anchorView, ContactSearchKey item) {
|
||||
if (onItemLongClickListener != null) {
|
||||
return onItemLongClickListener.onLongClick(item, recyclerView);
|
||||
return onItemLongClickListener.onLongClick(anchorView, item, recyclerView);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -758,7 +706,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
}
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
||||
contactSearchMediator.setKeysSelected(Collections.singleton(selectedContact.toContactSearchKey()));
|
||||
if (isMulti) {
|
||||
addChipForSelectedContact(selectedContact);
|
||||
}
|
||||
|
@ -768,8 +716,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
}
|
||||
|
||||
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
contactSearchMediator.setKeysNotSelected(Collections.singleton(selectedContact.toContactSearchKey()));
|
||||
contactChipViewModel.remove(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
|
@ -842,6 +789,116 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
chipRecycler.smoothScrollBy(x, 0);
|
||||
}
|
||||
|
||||
private @NonNull ContactSearchConfiguration mapStateToConfiguration(@NonNull ContactSearchState contactSearchState) {
|
||||
int displayMode = safeArguments().getInt(DISPLAY_MODE, requireActivity().getIntent().getIntExtra(DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_ALL));
|
||||
|
||||
boolean includeRecents = safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
|
||||
boolean includePushContacts = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_PUSH);
|
||||
boolean includeSmsContacts = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_SMS);
|
||||
boolean includeActiveGroups = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS);
|
||||
boolean includeInactiveGroups = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS);
|
||||
boolean includeSelf = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_SELF);
|
||||
boolean includeV1Groups = !flagSet(displayMode, ContactSelectionDisplayMode.FLAG_HIDE_GROUPS_V1);
|
||||
boolean includeNew = !flagSet(displayMode, ContactSelectionDisplayMode.FLAG_HIDE_NEW);
|
||||
boolean includeRecentsHeader = !flagSet(displayMode, ContactSelectionDisplayMode.FLAG_HIDE_RECENT_HEADER);
|
||||
boolean includeGroupsAfterContacts = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS);
|
||||
boolean blocked = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_BLOCK);
|
||||
|
||||
ContactSearchConfiguration.TransportType transportType = resolveTransportType(includePushContacts, includeSmsContacts);
|
||||
ContactSearchConfiguration.Section.Recents.Mode mode = resolveRecentsMode(transportType, includeActiveGroups);
|
||||
ContactSearchConfiguration.NewRowMode newRowMode = resolveNewRowMode(blocked, includeActiveGroups);
|
||||
|
||||
return ContactSearchConfiguration.build(builder -> {
|
||||
builder.setQuery(contactSearchState.getQuery());
|
||||
|
||||
if (listCallback != null) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
|
||||
}
|
||||
|
||||
if (transportType != null) {
|
||||
if (TextUtils.isEmpty(contactSearchState.getQuery()) && includeRecents) {
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Recents(
|
||||
25,
|
||||
mode,
|
||||
includeInactiveGroups,
|
||||
includeV1Groups,
|
||||
includeSmsContacts,
|
||||
includeSelf,
|
||||
includeRecentsHeader,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
|
||||
includeSelf,
|
||||
transportType,
|
||||
true,
|
||||
null,
|
||||
!hideLetterHeaders()
|
||||
));
|
||||
}
|
||||
|
||||
if ((includeGroupsAfterContacts || !TextUtils.isEmpty(contactSearchState.getQuery())) && includeActiveGroups) {
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Groups(
|
||||
includeSmsContacts,
|
||||
includeV1Groups,
|
||||
includeInactiveGroups,
|
||||
false,
|
||||
ContactSearchSortOrder.NATURAL,
|
||||
false,
|
||||
true,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
if (listCallback != null) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
|
||||
}
|
||||
|
||||
if (includeNew) {
|
||||
builder.phone(newRowMode);
|
||||
builder.username(newRowMode);
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private static @Nullable ContactSearchConfiguration.TransportType resolveTransportType(boolean includePushContacts, boolean includeSmsContacts) {
|
||||
if (includePushContacts && includeSmsContacts) {
|
||||
return ContactSearchConfiguration.TransportType.ALL;
|
||||
} else if (includePushContacts) {
|
||||
return ContactSearchConfiguration.TransportType.PUSH;
|
||||
} else if (includeSmsContacts) {
|
||||
return ContactSearchConfiguration.TransportType.SMS;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull ContactSearchConfiguration.Section.Recents.Mode resolveRecentsMode(ContactSearchConfiguration.TransportType transportType, boolean includeGroupContacts) {
|
||||
if (transportType != null && includeGroupContacts) {
|
||||
return ContactSearchConfiguration.Section.Recents.Mode.ALL;
|
||||
} else if (includeGroupContacts) {
|
||||
return ContactSearchConfiguration.Section.Recents.Mode.GROUPS;
|
||||
} else {
|
||||
return ContactSearchConfiguration.Section.Recents.Mode.INDIVIDUALS;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull ContactSearchConfiguration.NewRowMode resolveNewRowMode(boolean isBlocked, boolean isActiveGroups) {
|
||||
if (isBlocked) {
|
||||
return ContactSearchConfiguration.NewRowMode.BLOCK;
|
||||
} else if (isActiveGroups) {
|
||||
return ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION;
|
||||
} else {
|
||||
return ContactSearchConfiguration.NewRowMode.ADD_TO_GROUP;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean flagSet(int mode, int flag) {
|
||||
return (mode & flag) > 0;
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
/**
|
||||
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
|
||||
|
@ -874,10 +931,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||
}
|
||||
|
||||
public interface OnItemLongClickListener {
|
||||
boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView);
|
||||
}
|
||||
|
||||
public interface AbstractContactsCursorLoaderFactoryProvider {
|
||||
@NonNull AbstractContactsCursorLoader.Factory get();
|
||||
boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
|||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
|
@ -62,7 +62,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
|||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_SMS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_SMS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Intent;
|
|||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
|
@ -42,6 +43,8 @@ import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
|
|||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
|
||||
import org.thoughtcrime.securesms.contacts.management.ContactsManagementViewModel;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
|
@ -233,18 +236,14 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView) {
|
||||
RecipientId recipientId = contactSelectionListItem.getRecipientId().orElse(null);
|
||||
if (recipientId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView) {
|
||||
RecipientId recipientId = contactSearchKey.requireRecipientSearchKey().getRecipientId();
|
||||
List<ActionItem> actions = generateContextualActionsForRecipient(recipientId);
|
||||
if (actions.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
new SignalContextMenu.Builder(contactSelectionListItem, (ViewGroup) contactSelectionListItem.getRootView())
|
||||
new SignalContextMenu.Builder(anchorView, (ViewGroup) anchorView.getRootView())
|
||||
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW)
|
||||
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
|
||||
.offsetX((int) DimensionUnit.DP.toPixels(12))
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
class StaticMappingViewHolder<T : MappingModel<T>>(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<T>(itemView) {
|
||||
init {
|
||||
itemView.setOnClickListener { onClickListener() }
|
||||
}
|
||||
|
||||
override fun bind(model: T) = Unit
|
||||
}
|
|
@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
|||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
|
@ -143,11 +143,11 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
|||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, 1);
|
||||
intent.putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE,
|
||||
ContactsCursorLoader.DisplayMode.FLAG_PUSH |
|
||||
ContactsCursorLoader.DisplayMode.FLAG_SMS |
|
||||
ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS |
|
||||
ContactsCursorLoader.DisplayMode.FLAG_INACTIVE_GROUPS |
|
||||
ContactsCursorLoader.DisplayMode.FLAG_BLOCK);
|
||||
ContactSelectionDisplayMode.FLAG_PUSH |
|
||||
ContactSelectionDisplayMode.FLAG_SMS |
|
||||
ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS |
|
||||
ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS |
|
||||
ContactSelectionDisplayMode.FLAG_BLOCK);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment, CONTACT_SELECTION_FRAGMENT)
|
||||
|
|
|
@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.ContactSelectionListFragment
|
|||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
@ -100,17 +100,17 @@ class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment
|
|||
}
|
||||
|
||||
private fun getDefaultDisplayMode(): Int {
|
||||
var mode = ContactsCursorLoader.DisplayMode.FLAG_PUSH or
|
||||
ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS or
|
||||
ContactsCursorLoader.DisplayMode.FLAG_HIDE_NEW or
|
||||
ContactsCursorLoader.DisplayMode.FLAG_HIDE_RECENT_HEADER or
|
||||
ContactsCursorLoader.DisplayMode.FLAG_GROUPS_AFTER_CONTACTS
|
||||
var mode = ContactSelectionDisplayMode.FLAG_PUSH or
|
||||
ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS or
|
||||
ContactSelectionDisplayMode.FLAG_HIDE_NEW or
|
||||
ContactSelectionDisplayMode.FLAG_HIDE_RECENT_HEADER or
|
||||
ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS
|
||||
|
||||
if (Util.isDefaultSmsProvider(requireContext()) && SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
||||
mode = mode or ContactsCursorLoader.DisplayMode.FLAG_SMS
|
||||
mode = mode or ContactSelectionDisplayMode.FLAG_SMS
|
||||
}
|
||||
|
||||
return mode or ContactsCursorLoader.DisplayMode.FLAG_HIDE_GROUPS_V1
|
||||
return mode or ContactSelectionDisplayMode.FLAG_HIDE_GROUPS_V1
|
||||
}
|
||||
|
||||
override fun onBeforeContactSelected(recipientId: Optional<RecipientId>, number: String?, callback: Consumer<Boolean>) {
|
||||
|
|
|
@ -54,7 +54,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.L
|
|||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
|
@ -768,7 +768,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
AddMembersActivity.createIntent(
|
||||
requireContext(),
|
||||
addMembersToGroup.groupId,
|
||||
ContactsCursorLoader.DisplayMode.FLAG_PUSH,
|
||||
ContactSelectionDisplayMode.FLAG_PUSH,
|
||||
addMembersToGroup.selectionWarning,
|
||||
addMembersToGroup.selectionLimit,
|
||||
addMembersToGroup.isAnnouncementGroup,
|
||||
|
|
|
@ -37,7 +37,7 @@ public class ContactRepository {
|
|||
private final Context context;
|
||||
|
||||
public static final String ID_COLUMN = "id";
|
||||
static final String NAME_COLUMN = "name";
|
||||
public static final String NAME_COLUMN = "name";
|
||||
static final String NUMBER_COLUMN = "number";
|
||||
static final String NUMBER_TYPE_COLUMN = "number_type";
|
||||
static final String LABEL_COLUMN = "label";
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
public final class ContactSelectionDisplayMode {
|
||||
public static final int FLAG_PUSH = 1;
|
||||
public static final int FLAG_SMS = 1 << 1;
|
||||
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
|
||||
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
|
||||
public static final int FLAG_SELF = 1 << 4;
|
||||
public static final int FLAG_BLOCK = 1 << 5;
|
||||
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
|
||||
public static final int FLAG_HIDE_NEW = 1 << 6;
|
||||
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
|
||||
public static final int FLAG_GROUPS_AFTER_CONTACTS = 1 << 8;
|
||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
||||
}
|
|
@ -1,448 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
* <p>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.provider.ContactsContract;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScrollAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.HeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.signal.core.util.CharacterIterable;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* List adapter to display all contacts and their related information
|
||||
*
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewHolder>
|
||||
implements FastScrollAdapter,
|
||||
StickyHeaderAdapter<HeaderViewHolder>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = Log.tag(ContactSelectionListAdapter.class);
|
||||
|
||||
private static final int VIEW_TYPE_CONTACT = 0;
|
||||
private static final int VIEW_TYPE_DIVIDER = 1;
|
||||
|
||||
public static final int PAYLOAD_SELECTION_CHANGE = 1;
|
||||
|
||||
private final boolean multiSelect;
|
||||
private final LayoutInflater layoutInflater;
|
||||
private final ItemClickListener clickListener;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Set<RecipientId> currentContacts;
|
||||
private final int checkboxResource;
|
||||
|
||||
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 {
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, boolean checkboxVisible);
|
||||
|
||||
public abstract void unbind(@NonNull GlideRequests glideRequests);
|
||||
|
||||
public abstract void setChecked(boolean checked);
|
||||
|
||||
public void animateChecked(boolean checked) {
|
||||
// Intentionally empty.
|
||||
}
|
||||
|
||||
public abstract void setEnabled(boolean enabled);
|
||||
|
||||
public void setLetterHeaderCharacter(@Nullable String letterHeaderCharacter) {
|
||||
// Intentionally empty.
|
||||
}
|
||||
}
|
||||
|
||||
public static class ContactViewHolder extends ViewHolder implements LetterHeaderDecoration.LetterHeaderItem {
|
||||
|
||||
private String letterHeader;
|
||||
|
||||
ContactViewHolder(@NonNull final View itemView,
|
||||
@Nullable final ItemClickListener clickListener)
|
||||
{
|
||||
super(itemView);
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (clickListener != null) clickListener.onItemClick(getView());
|
||||
});
|
||||
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
if (clickListener != null) {
|
||||
return clickListener.onItemLongClick(getView());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ContactSelectionListItem getView() {
|
||||
return (ContactSelectionListItem) itemView;
|
||||
}
|
||||
|
||||
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, boolean checkBoxVisible) {
|
||||
getView().set(glideRequests, recipientId, type, name, number, label, about, checkBoxVisible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbind(@NonNull GlideRequests glideRequests) {
|
||||
getView().unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
getView().setChecked(checked, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animateChecked(boolean checked) {
|
||||
getView().setChecked(checked, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
getView().setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getHeaderLetter() {
|
||||
return letterHeader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLetterHeaderCharacter(@Nullable String letterHeaderCharacter) {
|
||||
this.letterHeader = letterHeaderCharacter;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DividerViewHolder extends ViewHolder {
|
||||
|
||||
private final TextView label;
|
||||
|
||||
DividerViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
this.label = itemView.findViewById(R.id.label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, boolean checkboxVisible) {
|
||||
this.label.setText(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbind(@NonNull GlideRequests glideRequests) {}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {}
|
||||
}
|
||||
|
||||
static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
HeaderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
public ContactSelectionListAdapter(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable Cursor cursor,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
boolean multiSelect,
|
||||
@NonNull Set<RecipientId> currentContacts,
|
||||
int checkboxResource)
|
||||
{
|
||||
super(context, cursor);
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.currentContacts = currentContacts;
|
||||
this.checkboxResource = checkboxResource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getHeaderId(int i) {
|
||||
if (!isActiveCursor()) return -1;
|
||||
else if (i == -1) return -1;
|
||||
|
||||
int contactType = getContactType(i);
|
||||
|
||||
if (contactType == ContactRepository.DIVIDER_TYPE) return -1;
|
||||
return Util.hashCode(getHeaderString(i), getContactType(i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
if (viewType == VIEW_TYPE_CONTACT) {
|
||||
View view = layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false);
|
||||
view.findViewById(R.id.check_box).setBackgroundResource(checkboxResource);
|
||||
return new ContactViewHolder(view, clickListener);
|
||||
} else {
|
||||
return new DividerViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
String rawId = CursorUtil.requireString(cursor, ContactRepository.ID_COLUMN);
|
||||
RecipientId id = rawId != null ? RecipientId.from(rawId) : null;
|
||||
int contactType = CursorUtil.requireInt(cursor, ContactRepository.CONTACT_TYPE_COLUMN);
|
||||
String name = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
|
||||
String number = CursorUtil.requireString(cursor, ContactRepository.NUMBER_COLUMN);
|
||||
int numberType = CursorUtil.requireInt(cursor, ContactRepository.NUMBER_TYPE_COLUMN);
|
||||
String about = CursorUtil.requireString(cursor, ContactRepository.ABOUT_COLUMN);
|
||||
String label = CursorUtil.requireString(cursor, ContactRepository.LABEL_COLUMN);
|
||||
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
|
||||
numberType, label).toString();
|
||||
boolean currentContact = currentContacts.contains(id);
|
||||
|
||||
viewHolder.unbind(glideRequests);
|
||||
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, about, multiSelect || currentContact);
|
||||
viewHolder.setEnabled(true);
|
||||
|
||||
if (currentContact) {
|
||||
viewHolder.setChecked(true);
|
||||
viewHolder.setEnabled(false);
|
||||
} else if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forUsername(id, number)));
|
||||
} else {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
|
||||
}
|
||||
|
||||
if (isContactRow(contactType)) {
|
||||
int position = cursor.getPosition();
|
||||
if (position == 0) {
|
||||
viewHolder.setLetterHeaderCharacter(getHeaderLetterForDisplayName(cursor));
|
||||
} else {
|
||||
cursor.moveToPrevious();
|
||||
|
||||
int previousRowContactType = CursorUtil.requireInt(cursor, ContactRepository.CONTACT_TYPE_COLUMN);
|
||||
|
||||
if (!isContactRow(previousRowContactType)) {
|
||||
cursor.moveToNext();
|
||||
viewHolder.setLetterHeaderCharacter(getHeaderLetterForDisplayName(cursor));
|
||||
} else {
|
||||
String previousHeaderLetter = getHeaderLetterForDisplayName(cursor);
|
||||
cursor.moveToNext();
|
||||
String newHeaderLetter = getHeaderLetterForDisplayName(cursor);
|
||||
|
||||
if (Objects.equals(previousHeaderLetter, newHeaderLetter)) {
|
||||
viewHolder.setLetterHeaderCharacter(null);
|
||||
} else {
|
||||
viewHolder.setLetterHeaderCharacter(newHeaderLetter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isContactRow(int contactType) {
|
||||
return (contactType & (ContactRepository.NEW_PHONE_TYPE | ContactRepository.NEW_USERNAME_TYPE | ContactRepository.DIVIDER_TYPE)) == 0;
|
||||
}
|
||||
|
||||
private @Nullable String getHeaderLetterForDisplayName(@NonNull Cursor cursor) {
|
||||
String name = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
|
||||
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Iterator<String> characterIterator = new CharacterIterable(name).iterator();
|
||||
|
||||
if (!TextUtils.isEmpty(name) && characterIterator.hasNext()) {
|
||||
String next = characterIterator.next();
|
||||
|
||||
if (Character.isLetter(next.codePointAt(0))) {
|
||||
return next.toUpperCase();
|
||||
} else {
|
||||
return "#";
|
||||
}
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor, @NonNull List<Object> payloads) {
|
||||
if (!arePayloadsValid(payloads)) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
String rawId = CursorUtil.requireString(cursor, ContactRepository.ID_COLUMN);
|
||||
RecipientId id = rawId != null ? RecipientId.from(rawId) : null;
|
||||
int numberType = CursorUtil.requireInt(cursor, ContactRepository.NUMBER_TYPE_COLUMN);
|
||||
String number = CursorUtil.requireString(cursor, ContactRepository.NUMBER_COLUMN);
|
||||
|
||||
viewHolder.setEnabled(true);
|
||||
|
||||
if (currentContacts.contains(id)) {
|
||||
viewHolder.animateChecked(true);
|
||||
viewHolder.setEnabled(false);
|
||||
} else if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
viewHolder.animateChecked(selectedContacts.contains(SelectedContact.forUsername(id, number)));
|
||||
} else {
|
||||
viewHolder.animateChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(@NonNull Cursor cursor) {
|
||||
if (CursorUtil.requireInt(cursor, ContactRepository.CONTACT_TYPE_COLUMN) == ContactRepository.DIVIDER_TYPE) {
|
||||
return VIEW_TYPE_DIVIDER;
|
||||
} else {
|
||||
return VIEW_TYPE_CONTACT;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) {
|
||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position, int type) {
|
||||
((TextView) viewHolder.itemView).setText(getSpannedHeaderString(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean arePayloadsValid(@NonNull List<Object> payloads) {
|
||||
return payloads.size() == 1 && payloads.get(0).equals(PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemViewRecycled(ViewHolder holder) {
|
||||
holder.unbind(glideRequests);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getBubbleText(int position) {
|
||||
return getHeaderString(position);
|
||||
}
|
||||
|
||||
public List<SelectedContact> getSelectedContacts() {
|
||||
return selectedContacts.getContacts();
|
||||
}
|
||||
|
||||
public int getSelectedContactsCount() {
|
||||
return selectedContacts.size();
|
||||
}
|
||||
|
||||
public int getCurrentContactsCount() {
|
||||
return currentContacts.size();
|
||||
}
|
||||
|
||||
private CharSequence getSpannedHeaderString(int position) {
|
||||
final String headerString = getHeaderString(position);
|
||||
if (isPush(position)) {
|
||||
SpannableString spannable = new SpannableString(headerString);
|
||||
spannable.setSpan(new ForegroundColorSpan(getContext().getResources().getColor(R.color.core_ultramarine)), 0, headerString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
} else {
|
||||
return headerString;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String getHeaderString(int position) {
|
||||
int contactType = getContactType(position);
|
||||
|
||||
if ((contactType & ContactRepository.RECENT_TYPE) > 0 || contactType == ContactRepository.DIVIDER_TYPE) {
|
||||
return " ";
|
||||
}
|
||||
|
||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
||||
String letter = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
|
||||
|
||||
if (letter != null) {
|
||||
letter = letter.trim();
|
||||
if (letter.length() > 0) {
|
||||
char firstChar = letter.charAt(0);
|
||||
if (Character.isLetterOrDigit(firstChar)) {
|
||||
return String.valueOf(Character.toUpperCase(firstChar));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "#";
|
||||
}
|
||||
|
||||
private int getContactType(int position) {
|
||||
final Cursor cursor = getCursorAtPositionOrThrow(position);
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.CONTACT_TYPE_COLUMN));
|
||||
}
|
||||
|
||||
private boolean isPush(int position) {
|
||||
return getContactType(position) == ContactRepository.PUSH_TYPE;
|
||||
}
|
||||
|
||||
public interface ItemClickListener {
|
||||
void onItemClick(ContactSelectionListItem item);
|
||||
boolean onItemLongClick(ContactSelectionListItem item);
|
||||
}
|
||||
}
|
|
@ -1,351 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* CursorLoader that initializes a ContactsDatabase instance
|
||||
*
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||
|
||||
private static final String TAG = Log.tag(ContactsCursorLoader.class);
|
||||
|
||||
public static final class DisplayMode {
|
||||
public static final int FLAG_PUSH = 1;
|
||||
public static final int FLAG_SMS = 1 << 1;
|
||||
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
|
||||
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
|
||||
public static final int FLAG_SELF = 1 << 4;
|
||||
public static final int FLAG_BLOCK = 1 << 5;
|
||||
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
|
||||
public static final int FLAG_HIDE_NEW = 1 << 6;
|
||||
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
|
||||
public static final int FLAG_GROUPS_AFTER_CONTACTS = 1 << 8;
|
||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
||||
}
|
||||
|
||||
private static final int RECENT_CONVERSATION_MAX = 25;
|
||||
|
||||
private final int mode;
|
||||
private final boolean recents;
|
||||
|
||||
private final ContactRepository contactRepository;
|
||||
|
||||
private ContactsCursorLoader(@NonNull Context context, int mode, String filter, boolean recents)
|
||||
{
|
||||
super(context, filter);
|
||||
|
||||
if (flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS) && !flagSet(mode, DisplayMode.FLAG_ACTIVE_GROUPS)) {
|
||||
throw new AssertionError("Inactive group flag set, but the active group flag isn't!");
|
||||
}
|
||||
|
||||
this.mode = mode;
|
||||
this.recents = recents;
|
||||
this.contactRepository = new ContactRepository(context, context.getString(R.string.note_to_self));
|
||||
}
|
||||
|
||||
protected final List<Cursor> getUnfilteredResults() {
|
||||
ArrayList<Cursor> cursorList = new ArrayList<>();
|
||||
|
||||
if (groupsOnly(mode)) {
|
||||
addRecentGroupsSection(cursorList);
|
||||
addGroupsSection(cursorList);
|
||||
} else {
|
||||
addRecentsSection(cursorList);
|
||||
addContactsSection(cursorList);
|
||||
if (addGroupsAfterContacts(mode)) {
|
||||
addGroupsSection(cursorList);
|
||||
}
|
||||
}
|
||||
|
||||
return cursorList;
|
||||
}
|
||||
|
||||
protected final List<Cursor> getFilteredResults() {
|
||||
ArrayList<Cursor> cursorList = new ArrayList<>();
|
||||
|
||||
addContactsSection(cursorList);
|
||||
addGroupsSection(cursorList);
|
||||
|
||||
if (!hideNewNumberOrUsername(mode)) {
|
||||
addNewNumberSection(cursorList);
|
||||
addUsernameSearchSection(cursorList);
|
||||
}
|
||||
|
||||
return cursorList;
|
||||
}
|
||||
|
||||
private void addRecentsSection(@NonNull List<Cursor> cursorList) {
|
||||
if (!recents) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor recentConversations = getRecentConversationsCursor();
|
||||
|
||||
if (recentConversations.getCount() > 0) {
|
||||
if (!hideRecentsHeader(mode)) {
|
||||
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
|
||||
}
|
||||
cursorList.add(recentConversations);
|
||||
}
|
||||
}
|
||||
|
||||
private void addContactsSection(@NonNull List<Cursor> cursorList) {
|
||||
List<Cursor> contacts = getContactsCursors();
|
||||
|
||||
if (!isCursorListEmpty(contacts)) {
|
||||
if (!getFilter().isEmpty() || recents) {
|
||||
cursorList.add(ContactsCursorRows.forContactsHeader(getContext()));
|
||||
}
|
||||
cursorList.addAll(contacts);
|
||||
}
|
||||
}
|
||||
|
||||
private void addRecentGroupsSection(@NonNull List<Cursor> cursorList) {
|
||||
if (!groupsEnabled(mode) || !recents) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor groups = getRecentConversationsCursor(true);
|
||||
|
||||
if (groups.getCount() > 0) {
|
||||
if (!hideRecentsHeader(mode)) {
|
||||
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
|
||||
}
|
||||
cursorList.add(groups);
|
||||
}
|
||||
}
|
||||
|
||||
private void addGroupsSection(@NonNull List<Cursor> cursorList) {
|
||||
if (!groupsEnabled(mode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor groups = getGroupsCursor();
|
||||
|
||||
if (groups.getCount() > 0) {
|
||||
cursorList.add(ContactsCursorRows.forGroupsHeader(getContext()));
|
||||
cursorList.add(groups);
|
||||
}
|
||||
}
|
||||
|
||||
private void addNewNumberSection(@NonNull List<Cursor> cursorList) {
|
||||
if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(getFilter())) {
|
||||
cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext()));
|
||||
cursorList.add(getNewNumberCursor());
|
||||
} else if (!FeatureFlags.usernames() && NumberUtil.isValidSmsOrEmail(getFilter())) {
|
||||
cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext()));
|
||||
cursorList.add(getNewNumberCursor());
|
||||
}
|
||||
}
|
||||
|
||||
private void addUsernameSearchSection(@NonNull List<Cursor> cursorList) {
|
||||
if (FeatureFlags.usernames() && UsernameUtil.isValidUsernameForSearch(getFilter())) {
|
||||
cursorList.add(ContactsCursorRows.forUsernameSearchHeader(getContext()));
|
||||
cursorList.add(getUsernameSearchCursor());
|
||||
}
|
||||
}
|
||||
|
||||
private Cursor getRecentConversationsCursor() {
|
||||
return getRecentConversationsCursor(false);
|
||||
}
|
||||
|
||||
private Cursor getRecentConversationsCursor(boolean groupsOnly) {
|
||||
ThreadTable threadTable = SignalDatabase.threads();
|
||||
|
||||
MatrixCursor recentConversations = ContactsCursorRows.createMatrixCursor(RECENT_CONVERSATION_MAX);
|
||||
try (Cursor rawConversations = threadTable.getRecentConversationList(RECENT_CONVERSATION_MAX, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), false, groupsOnly, hideGroupsV1(mode), !smsEnabled(mode), false)) {
|
||||
ThreadTable.Reader reader = threadTable.readerFor(rawConversations);
|
||||
ThreadRecord threadRecord;
|
||||
while ((threadRecord = reader.getNext()) != null) {
|
||||
recentConversations.addRow(ContactsCursorRows.forRecipient(getContext(), threadRecord.getRecipient()));
|
||||
}
|
||||
}
|
||||
return recentConversations;
|
||||
}
|
||||
|
||||
private List<Cursor> getContactsCursors() {
|
||||
List<Cursor> cursorList = new ArrayList<>(2);
|
||||
|
||||
if (pushEnabled(mode) && smsEnabled(mode)) {
|
||||
cursorList.add(contactRepository.queryNonGroupContacts(getFilter(), selfEnabled(mode)));
|
||||
} else if (pushEnabled(mode)) {
|
||||
cursorList.add(contactRepository.querySignalContacts(getFilter(), selfEnabled(mode)));
|
||||
} else if (smsEnabled(mode)) {
|
||||
cursorList.add(contactRepository.queryNonSignalContacts(getFilter()));
|
||||
}
|
||||
|
||||
return cursorList;
|
||||
}
|
||||
|
||||
private Cursor getGroupsCursor() {
|
||||
MatrixCursor groupContacts = ContactsCursorRows.createMatrixCursor();
|
||||
Map<RecipientId, GroupRecord> groups = new LinkedHashMap<>();
|
||||
|
||||
try (GroupTable.Reader reader = SignalDatabase.groups().queryGroupsByTitle(getFilter(), flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode), !smsEnabled(mode))) {
|
||||
GroupRecord groupRecord;
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
groups.put(groupRecord.getRecipientId(), groupRecord);
|
||||
}
|
||||
}
|
||||
|
||||
if (getFilter() != null && !Util.isEmpty(getFilter())) {
|
||||
Set<RecipientId> filteredContacts = new HashSet<>();
|
||||
try (Cursor cursor = SignalDatabase.recipients().queryAllContacts(getFilter())) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
filteredContacts.add(RecipientId.from(CursorUtil.requireString(cursor, RecipientTable.ID)));
|
||||
}
|
||||
}
|
||||
|
||||
try (GroupTable.Reader reader = SignalDatabase.groups().queryGroupsByMembership(filteredContacts, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode), !smsEnabled(mode))) {
|
||||
GroupRecord groupRecord;
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
groups.put(groupRecord.getRecipientId(), groupRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (GroupRecord groupRecord : groups.values()) {
|
||||
groupContacts.addRow(ContactsCursorRows.forGroup(groupRecord));
|
||||
}
|
||||
|
||||
return groupContacts;
|
||||
}
|
||||
|
||||
private Cursor getNewNumberCursor() {
|
||||
return ContactsCursorRows.forNewNumber(getUnknownContactTitle(), getFilter());
|
||||
}
|
||||
|
||||
private Cursor getUsernameSearchCursor() {
|
||||
return ContactsCursorRows.forUsernameSearch(getFilter());
|
||||
}
|
||||
|
||||
private String getUnknownContactTitle() {
|
||||
if (blockUser(mode)) {
|
||||
return getContext().getString(R.string.contact_selection_list__unknown_contact_block);
|
||||
} else if (newConversation(mode)) {
|
||||
return getContext().getString(R.string.contact_selection_list__unknown_contact);
|
||||
} else {
|
||||
return getContext().getString(R.string.contact_selection_list__unknown_contact_add_to_group);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isCursorListEmpty(List<Cursor> list) {
|
||||
int sum = 0;
|
||||
for (Cursor cursor : list) {
|
||||
sum += cursor.getCount();
|
||||
}
|
||||
return sum == 0;
|
||||
}
|
||||
|
||||
private static boolean selfEnabled(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_SELF);
|
||||
}
|
||||
|
||||
private static boolean blockUser(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_BLOCK);
|
||||
}
|
||||
|
||||
private static boolean newConversation(int mode) {
|
||||
return groupsEnabled(mode);
|
||||
}
|
||||
|
||||
private static boolean pushEnabled(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_PUSH);
|
||||
}
|
||||
|
||||
private static boolean smsEnabled(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_SMS);
|
||||
}
|
||||
|
||||
private static boolean groupsEnabled(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_ACTIVE_GROUPS);
|
||||
}
|
||||
|
||||
private static boolean groupsOnly(int mode) {
|
||||
return mode == DisplayMode.FLAG_ACTIVE_GROUPS;
|
||||
}
|
||||
|
||||
private static boolean hideGroupsV1(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_HIDE_GROUPS_V1);
|
||||
}
|
||||
|
||||
private static boolean hideNewNumberOrUsername(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_HIDE_NEW);
|
||||
}
|
||||
|
||||
private static boolean hideRecentsHeader(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_HIDE_RECENT_HEADER);
|
||||
}
|
||||
|
||||
private static boolean addGroupsAfterContacts(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_GROUPS_AFTER_CONTACTS);
|
||||
}
|
||||
|
||||
private static boolean flagSet(int mode, int flag) {
|
||||
return (mode & flag) > 0;
|
||||
}
|
||||
|
||||
public static class Factory implements AbstractContactsCursorLoader.Factory {
|
||||
|
||||
private final Context context;
|
||||
private final int displayMode;
|
||||
private final String cursorFilter;
|
||||
private final boolean displayRecents;
|
||||
|
||||
public Factory(Context context, int displayMode, String cursorFilter, boolean displayRecents) {
|
||||
this.context = context;
|
||||
this.displayMode = displayMode;
|
||||
this.cursorFilter = cursorFilter;
|
||||
this.displayRecents = displayRecents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull AbstractContactsCursorLoader create() {
|
||||
return new ContactsCursorLoader(context, displayMode, cursorFilter, displayRecents);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.provider.ContactsContract;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
/**
|
||||
* Helper utility for generating cursors and cursor rows for subclasses of {@link AbstractContactsCursorLoader}.
|
||||
*/
|
||||
public final class ContactsCursorRows {
|
||||
|
||||
private static final String[] CONTACT_PROJECTION = new String[]{ContactRepository.ID_COLUMN,
|
||||
ContactRepository.NAME_COLUMN,
|
||||
ContactRepository.NUMBER_COLUMN,
|
||||
ContactRepository.NUMBER_TYPE_COLUMN,
|
||||
ContactRepository.LABEL_COLUMN,
|
||||
ContactRepository.CONTACT_TYPE_COLUMN,
|
||||
ContactRepository.ABOUT_COLUMN};
|
||||
|
||||
/**
|
||||
* Create a {@link MatrixCursor} with the proper projection for a subclass of {@link AbstractContactsCursorLoader}
|
||||
*/
|
||||
public static @NonNull MatrixCursor createMatrixCursor() {
|
||||
return new MatrixCursor(CONTACT_PROJECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link MatrixCursor} with the proper projection for a subclass of {@link AbstractContactsCursorLoader}
|
||||
*
|
||||
* @param initialCapacity The initial capacity to hand to the {@link MatrixCursor}
|
||||
*/
|
||||
public static @NonNull MatrixCursor createMatrixCursor(int initialCapacity) {
|
||||
return new MatrixCursor(CONTACT_PROJECTION, initialCapacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a row for a contacts cursor based off the given recipient.
|
||||
*/
|
||||
public static @NonNull Object[] forRecipient(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
String stringId = recipient.isGroup() ? recipient.requireGroupId().toString()
|
||||
: OptionalUtil.or(recipient.getE164().map(PhoneNumberFormatter::prettyPrint), recipient.getEmail()).orElse("");
|
||||
|
||||
return new Object[]{recipient.getId().serialize(),
|
||||
recipient.getDisplayName(context),
|
||||
stringId,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
|
||||
"",
|
||||
ContactRepository.RECENT_TYPE | (recipient.isRegistered() && !recipient.isForceSmsSelection() ? ContactRepository.PUSH_TYPE : 0),
|
||||
recipient.getCombinedAboutAndEmoji()};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a row for a contacts cursor based off the given system contact.
|
||||
*/
|
||||
public static @NonNull Object[] forNonPushContact(@NonNull Cursor systemContactCursor) {
|
||||
return new Object[]{systemContactCursor.getLong(systemContactCursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN)),
|
||||
systemContactCursor.getString(systemContactCursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN)),
|
||||
systemContactCursor.getString(systemContactCursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN)),
|
||||
systemContactCursor.getString(systemContactCursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN)),
|
||||
systemContactCursor.getString(systemContactCursor.getColumnIndexOrThrow(ContactRepository.LABEL_COLUMN)),
|
||||
ContactRepository.NORMAL_TYPE,
|
||||
""};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a row for a contacts cursor based off the given group record.
|
||||
*/
|
||||
public static @NonNull Object[] forGroup(@NonNull GroupRecord groupRecord) {
|
||||
return new Object[]{groupRecord.getRecipientId().serialize(),
|
||||
groupRecord.getTitle(),
|
||||
groupRecord.getId(),
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
||||
"",
|
||||
ContactRepository.NORMAL_TYPE,
|
||||
""};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a row for a contacts cursor for a new number the user is entering or has entered.
|
||||
*/
|
||||
public static @NonNull MatrixCursor forNewNumber(@NonNull String unknownContactTitle, @NonNull String filter) {
|
||||
MatrixCursor matrixCursor = createMatrixCursor(1);
|
||||
|
||||
matrixCursor.addRow(new Object[]{null,
|
||||
unknownContactTitle,
|
||||
filter,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
||||
"\u21e2",
|
||||
ContactRepository.NEW_PHONE_TYPE,
|
||||
""});
|
||||
|
||||
return matrixCursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a row for a contacts cursor for a username the user is entering or has entered.
|
||||
*/
|
||||
public static @NonNull MatrixCursor forUsernameSearch(@NonNull String filter) {
|
||||
MatrixCursor matrixCursor = createMatrixCursor(1);
|
||||
|
||||
matrixCursor.addRow(new Object[]{null,
|
||||
null,
|
||||
filter,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
||||
"\u21e2",
|
||||
ContactRepository.NEW_USERNAME_TYPE,
|
||||
""});
|
||||
|
||||
return matrixCursor;
|
||||
}
|
||||
|
||||
public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) {
|
||||
return forHeader(context.getString(R.string.ContactsCursorLoader_find_by_username));
|
||||
}
|
||||
|
||||
public static @NonNull MatrixCursor forPhoneNumberSearchHeader(@NonNull Context context) {
|
||||
return forHeader(context.getString(R.string.ContactsCursorLoader_phone_number_search));
|
||||
}
|
||||
|
||||
public static @NonNull MatrixCursor forGroupsHeader(@NonNull Context context) {
|
||||
return forHeader(context.getString(R.string.ContactsCursorLoader_groups));
|
||||
}
|
||||
|
||||
public static @NonNull MatrixCursor forRecentsHeader(@NonNull Context context) {
|
||||
return forHeader(context.getString(R.string.ContactsCursorLoader_recent_chats));
|
||||
}
|
||||
|
||||
public static @NonNull MatrixCursor forContactsHeader(@NonNull Context context) {
|
||||
return forHeader(context.getString(R.string.ContactsCursorLoader_contacts));
|
||||
}
|
||||
|
||||
public static @NonNull MatrixCursor forHeader(@NonNull String name) {
|
||||
MatrixCursor matrixCursor = createMatrixCursor(1);
|
||||
|
||||
matrixCursor.addRow(new Object[]{null,
|
||||
name,
|
||||
"",
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
|
||||
"",
|
||||
ContactRepository.DIVIDER_TYPE,
|
||||
""});
|
||||
|
||||
return matrixCursor;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import android.content.Context;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
|
@ -46,10 +48,30 @@ public final class SelectedContact {
|
|||
}
|
||||
}
|
||||
|
||||
public @Nullable RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public @Nullable String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public boolean hasUsername() {
|
||||
return username != null;
|
||||
}
|
||||
|
||||
public @NonNull ContactSearchKey toContactSearchKey() {
|
||||
if (recipientId != null) {
|
||||
return new ContactSearchKey.RecipientSearchKey(recipientId, false);
|
||||
} else if (number != null) {
|
||||
return new ContactSearchKey.UnknownRecipientKey(ContactSearchConfiguration.SectionKey.PHONE_NUMBER, number);
|
||||
} else if (username != null) {
|
||||
return new ContactSearchKey.UnknownRecipientKey(ContactSearchConfiguration.SectionKey.USERNAME, username);
|
||||
} else {
|
||||
throw new IllegalStateException("Nothing to map!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when non-null recipient ids match, and false if not.
|
||||
* <p>
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.BreakIteratorCompat
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.FromTextView
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScrollAdapter
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration
|
||||
|
@ -31,17 +34,35 @@ import org.thoughtcrime.securesms.util.visible
|
|||
*/
|
||||
@Suppress("LeakingThis")
|
||||
open class ContactSearchAdapter(
|
||||
private val context: Context,
|
||||
fixedContacts: Set<ContactSearchKey>,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
displayPhoneNumber: DisplayPhoneNumber,
|
||||
onClickCallbacks: ClickCallbacks,
|
||||
longClickCallbacks: LongClickCallbacks,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks
|
||||
) : PagingMappingAdapter<ContactSearchKey>() {
|
||||
) : PagingMappingAdapter<ContactSearchKey>(), FastScrollAdapter {
|
||||
|
||||
init {
|
||||
registerStoryItems(this, displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
|
||||
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, onClickCallbacks::onKnownRecipientClicked)
|
||||
registerKnownRecipientItems(this, fixedContacts, displayCheckBox, displaySmsTag, displayPhoneNumber, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick)
|
||||
registerHeaders(this)
|
||||
registerExpands(this, onClickCallbacks::onExpandClicked)
|
||||
registerFactory(UnknownRecipientModel::class.java, LayoutFactory({ UnknownRecipientViewHolder(it, onClickCallbacks::onUnknownRecipientClicked, displayCheckBox) }, R.layout.contact_search_unknown_item))
|
||||
}
|
||||
|
||||
override fun getBubbleText(position: Int): CharSequence {
|
||||
val model = getItem(position)
|
||||
return if (model is FastScrollCharacterProvider) {
|
||||
model.getFastScrollCharacter(context)
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
}
|
||||
|
||||
interface FastScrollCharacterProvider {
|
||||
fun getFastScrollCharacter(context: Context): CharSequence
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -59,13 +80,16 @@ open class ContactSearchAdapter(
|
|||
|
||||
fun registerKnownRecipientItems(
|
||||
mappingAdapter: MappingAdapter,
|
||||
fixedContacts: Set<ContactSearchKey>,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>
|
||||
displayPhoneNumber: DisplayPhoneNumber,
|
||||
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>,
|
||||
recipientLongClickCallback: OnLongClickedCallback<ContactSearchData.KnownRecipient>
|
||||
) {
|
||||
mappingAdapter.registerFactory(
|
||||
RecipientModel::class.java,
|
||||
LayoutFactory({ KnownRecipientViewHolder(it, displayCheckBox, displaySmsTag, recipientListener) }, R.layout.contact_search_item)
|
||||
LayoutFactory({ KnownRecipientViewHolder(it, fixedContacts, displayCheckBox, displaySmsTag, displayPhoneNumber, recipientListener, recipientLongClickCallback) }, R.layout.contact_search_item)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -97,6 +121,7 @@ open class ContactSearchAdapter(
|
|||
is ContactSearchData.Thread -> ThreadModel(it)
|
||||
is ContactSearchData.Empty -> EmptyModel(it)
|
||||
is ContactSearchData.GroupWithMembers -> GroupWithMembersModel(it)
|
||||
is ContactSearchData.UnknownRecipient -> UnknownRecipientModel(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -161,6 +186,7 @@ open class ContactSearchAdapter(
|
|||
context.resources.getQuantityString(R.plurals.ContactSearchItems__my_story_s_dot_d_viewers, count, presentPrivacyMode(model.story.privacyMode), count)
|
||||
}
|
||||
}
|
||||
|
||||
else -> context.resources.getQuantityString(R.plurals.ContactSearchItems__custom_story_d_viewers, count, count)
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +268,32 @@ open class ContactSearchAdapter(
|
|||
/**
|
||||
* Recipient model
|
||||
*/
|
||||
class RecipientModel(val knownRecipient: ContactSearchData.KnownRecipient, val isSelected: Boolean, val shortSummary: Boolean) : MappingModel<RecipientModel> {
|
||||
class RecipientModel(
|
||||
val knownRecipient: ContactSearchData.KnownRecipient,
|
||||
val isSelected: Boolean,
|
||||
val shortSummary: Boolean
|
||||
) : MappingModel<RecipientModel>, FastScrollCharacterProvider {
|
||||
|
||||
override fun getFastScrollCharacter(context: Context): CharSequence {
|
||||
val name = if (knownRecipient.recipient.isSelf) {
|
||||
context.getString(R.string.note_to_self)
|
||||
} else {
|
||||
knownRecipient.recipient.getDisplayName(context)
|
||||
}
|
||||
|
||||
var letter = BreakIteratorCompat.getInstance().apply { setText(name) }.take(1)
|
||||
if (letter != null) {
|
||||
letter = letter.trim { it <= ' ' }
|
||||
if (letter.isNotEmpty()) {
|
||||
val firstChar = letter[0]
|
||||
if (Character.isLetterOrDigit(firstChar)) {
|
||||
return firstChar.uppercaseChar().toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "#"
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: RecipientModel): Boolean {
|
||||
return newItem.knownRecipient == knownRecipient
|
||||
|
@ -261,11 +312,47 @@ open class ContactSearchAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
class UnknownRecipientModel(val data: ContactSearchData.UnknownRecipient) : MappingModel<UnknownRecipientModel> {
|
||||
override fun areItemsTheSame(newItem: UnknownRecipientModel): Boolean = true
|
||||
|
||||
override fun areContentsTheSame(newItem: UnknownRecipientModel): Boolean = data == newItem.data
|
||||
}
|
||||
|
||||
private class UnknownRecipientViewHolder(
|
||||
itemView: View,
|
||||
private val onClick: OnClickedCallback<ContactSearchData.UnknownRecipient>,
|
||||
private val displayCheckBox: Boolean
|
||||
) : MappingViewHolder<UnknownRecipientModel>(itemView) {
|
||||
|
||||
private val checkbox: CheckBox = itemView.findViewById(R.id.check_box)
|
||||
private val name: FromTextView = itemView.findViewById(R.id.name)
|
||||
private val number: TextView = itemView.findViewById(R.id.number)
|
||||
|
||||
override fun bind(model: UnknownRecipientModel) {
|
||||
checkbox.visible = displayCheckBox
|
||||
checkbox.isSelected = false
|
||||
name.setText(
|
||||
when (model.data.mode) {
|
||||
ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION -> R.string.contact_selection_list__unknown_contact
|
||||
ContactSearchConfiguration.NewRowMode.BLOCK -> R.string.contact_selection_list__unknown_contact_block
|
||||
ContactSearchConfiguration.NewRowMode.ADD_TO_GROUP -> R.string.contact_selection_list__unknown_contact_add_to_group
|
||||
}
|
||||
)
|
||||
number.text = model.data.query
|
||||
itemView.setOnClickListener {
|
||||
onClick.onClicked(itemView, model.data, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class KnownRecipientViewHolder(
|
||||
itemView: View,
|
||||
private val fixedContacts: Set<ContactSearchKey>,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
onClick: OnClickedCallback<ContactSearchData.KnownRecipient>
|
||||
private val displayPhoneNumber: DisplayPhoneNumber,
|
||||
onClick: OnClickedCallback<ContactSearchData.KnownRecipient>,
|
||||
private val onLongClick: OnLongClickedCallback<ContactSearchData.KnownRecipient>
|
||||
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
|
||||
|
||||
private var headerLetter: String? = null
|
||||
|
@ -282,6 +369,9 @@ open class ContactSearchAdapter(
|
|||
val count = recipient.participantIds.size
|
||||
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
|
||||
number.visible = true
|
||||
} else if (displayPhoneNumber == DisplayPhoneNumber.ALWAYS && recipient.hasE164()) {
|
||||
number.text = recipient.requireE164()
|
||||
number.visible = true
|
||||
} else {
|
||||
super.bindNumberField(model)
|
||||
}
|
||||
|
@ -289,15 +379,22 @@ open class ContactSearchAdapter(
|
|||
headerLetter = model.knownRecipient.headerLetter
|
||||
}
|
||||
|
||||
override fun bindCheckbox(model: RecipientModel) {
|
||||
checkbox.isEnabled = !fixedContacts.contains(model.knownRecipient.contactSearchKey)
|
||||
}
|
||||
|
||||
override fun getHeaderLetter(): String? {
|
||||
return headerLetter
|
||||
}
|
||||
|
||||
override fun bindLongPress(model: RecipientModel) {
|
||||
itemView.setOnLongClickListener { onLongClick.onLongClicked(itemView, model.knownRecipient) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base Recipient View Holder
|
||||
*/
|
||||
|
||||
abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(
|
||||
itemView: View,
|
||||
private val displayCheckBox: Boolean,
|
||||
|
@ -316,6 +413,7 @@ open class ContactSearchAdapter(
|
|||
override fun bind(model: T) {
|
||||
checkbox.visible = displayCheckBox
|
||||
checkbox.isChecked = isSelected(model)
|
||||
|
||||
itemView.setOnClickListener { onClick.onClicked(avatar, getData(model), isSelected(model)) }
|
||||
bindLongPress(model)
|
||||
|
||||
|
@ -332,6 +430,8 @@ open class ContactSearchAdapter(
|
|||
bindSmsTagField(model)
|
||||
}
|
||||
|
||||
protected open fun bindCheckbox(model: T) = Unit
|
||||
|
||||
protected open fun bindAvatar(model: T) {
|
||||
avatar.setAvatar(getRecipient(model))
|
||||
}
|
||||
|
@ -444,12 +544,12 @@ open class ContactSearchAdapter(
|
|||
ContactSearchConfiguration.SectionKey.RECENTS -> R.string.ContactsCursorLoader_recent_chats
|
||||
ContactSearchConfiguration.SectionKey.INDIVIDUALS -> R.string.ContactsCursorLoader_contacts
|
||||
ContactSearchConfiguration.SectionKey.GROUPS -> R.string.ContactsCursorLoader_groups
|
||||
ContactSearchConfiguration.SectionKey.ARBITRARY -> error("This section does not support HEADER")
|
||||
ContactSearchConfiguration.SectionKey.GROUP_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
||||
ContactSearchConfiguration.SectionKey.CHATS -> R.string.ContactsCursorLoader__chats
|
||||
ContactSearchConfiguration.SectionKey.MESSAGES -> R.string.ContactsCursorLoader__messages
|
||||
ContactSearchConfiguration.SectionKey.GROUPS_WITH_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
||||
ContactSearchConfiguration.SectionKey.CONTACTS_WITHOUT_THREADS -> R.string.ContactsCursorLoader_contacts
|
||||
else -> error("This section does not support HEADER")
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -507,13 +607,33 @@ open class ContactSearchAdapter(
|
|||
NEVER
|
||||
}
|
||||
|
||||
enum class DisplayPhoneNumber {
|
||||
NEVER,
|
||||
ALWAYS
|
||||
}
|
||||
|
||||
fun interface OnClickedCallback<D : ContactSearchData> {
|
||||
fun onClicked(view: View, data: D, isSelected: Boolean)
|
||||
}
|
||||
|
||||
fun interface OnLongClickedCallback<D : ContactSearchData> {
|
||||
fun onLongClicked(view: View, data: D): Boolean
|
||||
}
|
||||
|
||||
interface ClickCallbacks {
|
||||
fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean)
|
||||
fun onKnownRecipientClicked(view: View, knownRecipient: ContactSearchData.KnownRecipient, isSelected: Boolean)
|
||||
fun onExpandClicked(expand: ContactSearchData.Expand)
|
||||
fun onUnknownRecipientClicked(view: View, unknownRecipient: ContactSearchData.UnknownRecipient, isSelected: Boolean) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
|
||||
interface LongClickCallbacks {
|
||||
fun onKnownRecipientLongClick(view: View, data: ContactSearchData.KnownRecipient): Boolean
|
||||
}
|
||||
|
||||
class LongClickCallbacksAdapter : LongClickCallbacks {
|
||||
override fun onKnownRecipientLongClick(view: View, data: ContactSearchData.KnownRecipient): Boolean = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,6 +172,16 @@ class ContactSearchConfiguration private constructor(
|
|||
override val includeHeader: Boolean = true,
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
) : Section(SectionKey.CONTACTS_WITHOUT_THREADS)
|
||||
|
||||
data class Username(val newRowMode: NewRowMode) : Section(SectionKey.USERNAME) {
|
||||
override val includeHeader: Boolean = false
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
}
|
||||
|
||||
data class PhoneNumber(val newRowMode: NewRowMode) : Section(SectionKey.PHONE_NUMBER) {
|
||||
override val includeHeader: Boolean = false
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -227,7 +237,17 @@ class ContactSearchConfiguration private constructor(
|
|||
/**
|
||||
* Messages from 1:1 and Group chats
|
||||
*/
|
||||
MESSAGES
|
||||
MESSAGES,
|
||||
|
||||
/**
|
||||
* A row representing the search query as a phone number
|
||||
*/
|
||||
PHONE_NUMBER,
|
||||
|
||||
/**
|
||||
* A row representing the search query as a username
|
||||
*/
|
||||
USERNAME
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -247,6 +267,15 @@ class ContactSearchConfiguration private constructor(
|
|||
ALL
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the mode for 'Username' or 'PhoneNumber'
|
||||
*/
|
||||
enum class NewRowMode {
|
||||
NEW_CONVERSATION,
|
||||
BLOCK,
|
||||
ADD_TO_GROUP
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* DSL Style builder function. Example:
|
||||
|
@ -296,11 +325,12 @@ class ContactSearchConfiguration private constructor(
|
|||
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
||||
}
|
||||
|
||||
fun groupsWithMembers(
|
||||
includeHeader: Boolean = true,
|
||||
expandConfig: ExpandConfig? = null
|
||||
) {
|
||||
addSection(Section.GroupsWithMembers(includeHeader, expandConfig))
|
||||
fun username(newRowMode: NewRowMode) {
|
||||
addSection(Section.Username(newRowMode))
|
||||
}
|
||||
|
||||
fun phone(newRowMode: NewRowMode) {
|
||||
addSection(Section.PhoneNumber(newRowMode))
|
||||
}
|
||||
|
||||
fun addSection(section: Section)
|
||||
|
|
|
@ -89,4 +89,13 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
|
|||
*/
|
||||
@VisibleForTesting
|
||||
class TestRow(val value: Int) : ContactSearchData(ContactSearchKey.Expand(ContactSearchConfiguration.SectionKey.RECENTS))
|
||||
|
||||
/**
|
||||
* A row displaying an unknown phone number or username
|
||||
*/
|
||||
data class UnknownRecipient(
|
||||
val sectionKey: ContactSearchConfiguration.SectionKey,
|
||||
val mode: ContactSearchConfiguration.NewRowMode,
|
||||
val query: String
|
||||
) : ContactSearchData(ContactSearchKey.UnknownRecipientKey(sectionKey, query))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.contacts.paged
|
|||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
||||
|
@ -20,11 +21,23 @@ sealed class ContactSearchKey {
|
|||
|
||||
open fun requireRecipientSearchKey(): RecipientSearchKey = error("This key cannot be parcelized")
|
||||
|
||||
open fun requireSelectedContact(): SelectedContact = error("This key cannot be converted into a SelectedContact")
|
||||
|
||||
@Parcelize
|
||||
data class RecipientSearchKey(val recipientId: RecipientId, val isStory: Boolean) : ContactSearchKey(), Parcelable {
|
||||
override fun requireRecipientSearchKey(): RecipientSearchKey = this
|
||||
|
||||
override fun requireShareContact(): ShareContact = ShareContact(recipientId)
|
||||
|
||||
override fun requireSelectedContact(): SelectedContact = SelectedContact.forRecipientId(recipientId)
|
||||
}
|
||||
|
||||
data class UnknownRecipientKey(val sectionKey: ContactSearchConfiguration.SectionKey, val query: String) : ContactSearchKey() {
|
||||
override fun requireSelectedContact(): SelectedContact = when (sectionKey) {
|
||||
ContactSearchConfiguration.SectionKey.USERNAME -> SelectedContact.forPhone(null, query)
|
||||
ContactSearchConfiguration.SectionKey.PHONE_NUMBER -> SelectedContact.forPhone(null, query)
|
||||
else -> error("Unexpected section for unknown recipient: $sectionKey")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
|
@ -22,13 +23,30 @@ import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter
|
|||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* This mediator serves as the delegate for interacting with the ContactSearch* framework.
|
||||
*
|
||||
* @param fragment The fragment displaying the content search results.
|
||||
* @param fixedContacts Contacts which are "pre-selected" (for example, already a member of a group we're adding to)
|
||||
* @param selectionLimits [SelectionLimits] describing how large the result set can be.
|
||||
* @param displayCheckBox Whether or not to display checkboxes on items.
|
||||
* @param displaySmsTag Whether or not to display the SMS tag on items.
|
||||
* @param displayPhoneNumber Whether or not to display phone numbers on known contacts.
|
||||
* @param mapStateToConfiguration Maps a [ContactSearchState] to a [ContactSearchConfiguration]
|
||||
* @param callbacks Hooks to help process, filter, and react to selection
|
||||
* @param performSafetyNumberChecks Whether to perform safety number checks for selected users
|
||||
* @param adapterFactory A factory for creating an instance of [PagingMappingAdapter] to display items
|
||||
* @param arbitraryRepository A repository for managing [ContactSearchKey.Arbitrary] data
|
||||
*/
|
||||
class ContactSearchMediator(
|
||||
private val fragment: Fragment,
|
||||
private val fixedContacts: Set<ContactSearchKey> = setOf(),
|
||||
selectionLimits: SelectionLimits,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||
displayPhoneNumber: ContactSearchAdapter.DisplayPhoneNumber,
|
||||
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
|
||||
private val contactSelectionPreFilter: (View?, Set<ContactSearchKey>) -> Set<ContactSearchKey> = { _, s -> s },
|
||||
private val callbacks: Callbacks = SimpleCallbacks(),
|
||||
performSafetyNumberChecks: Boolean = true,
|
||||
adapterFactory: AdapterFactory = DefaultAdapterFactory,
|
||||
arbitraryRepository: ArbitraryRepository? = null,
|
||||
|
@ -49,8 +67,11 @@ class ContactSearchMediator(
|
|||
)[ContactSearchViewModel::class.java]
|
||||
|
||||
val adapter = adapterFactory.create(
|
||||
context = fragment.requireContext(),
|
||||
fixedContacts = fixedContacts,
|
||||
displayCheckBox = displayCheckBox,
|
||||
displaySmsTag = displaySmsTag,
|
||||
displayPhoneNumber = displayPhoneNumber,
|
||||
callbacks = object : ContactSearchAdapter.ClickCallbacks {
|
||||
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
|
||||
toggleStorySelection(view, story, isSelected)
|
||||
|
@ -64,6 +85,7 @@ class ContactSearchMediator(
|
|||
viewModel.expandSection(expand.sectionKey)
|
||||
}
|
||||
},
|
||||
longClickCallbacks = ContactSearchAdapter.LongClickCallbacksAdapter(),
|
||||
storyContextMenuCallbacks = StoryContextMenuCallbacks()
|
||||
)
|
||||
|
||||
|
@ -75,7 +97,9 @@ class ContactSearchMediator(
|
|||
)
|
||||
|
||||
dataAndSelection.observe(fragment.viewLifecycleOwner) { (data, selection) ->
|
||||
adapter.submitList(ContactSearchAdapter.toMappingModelList(data, selection, arbitraryRepository))
|
||||
adapter.submitList(ContactSearchAdapter.toMappingModelList(data, selection, arbitraryRepository), {
|
||||
callbacks.onAdapterListCommitted(data.size)
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.controller.observe(fragment.viewLifecycleOwner) { controller ->
|
||||
|
@ -98,17 +122,28 @@ class ContactSearchMediator(
|
|||
}
|
||||
|
||||
fun setKeysSelected(keys: Set<ContactSearchKey>) {
|
||||
viewModel.setKeysSelected(contactSelectionPreFilter(null, keys))
|
||||
viewModel.setKeysSelected(callbacks.onBeforeContactsSelected(null, keys))
|
||||
}
|
||||
|
||||
fun setKeysNotSelected(keys: Set<ContactSearchKey>) {
|
||||
keys.forEach {
|
||||
callbacks.onContactDeselected(null, it)
|
||||
}
|
||||
viewModel.setKeysNotSelected(keys)
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
viewModel.clearSelection()
|
||||
}
|
||||
|
||||
fun getSelectedContacts(): Set<ContactSearchKey> {
|
||||
return viewModel.getSelectedContacts()
|
||||
}
|
||||
|
||||
fun getFixedContactsSize(): Int {
|
||||
return fixedContacts.size
|
||||
}
|
||||
|
||||
fun getSelectionState(): LiveData<Set<ContactSearchKey>> {
|
||||
return viewModel.selectionState
|
||||
}
|
||||
|
@ -135,9 +170,10 @@ class ContactSearchMediator(
|
|||
|
||||
private fun toggleSelection(view: View, contactSearchData: ContactSearchData, isSelected: Boolean) {
|
||||
return if (isSelected) {
|
||||
callbacks.onContactDeselected(view, contactSearchData.contactSearchKey)
|
||||
viewModel.setKeysNotSelected(setOf(contactSearchData.contactSearchKey))
|
||||
} else {
|
||||
viewModel.setKeysSelected(contactSelectionPreFilter(view, setOf(contactSearchData.contactSearchKey)))
|
||||
viewModel.setKeysSelected(callbacks.onBeforeContactsSelected(view, setOf(contactSearchData.contactSearchKey)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,27 +207,50 @@ class ContactSearchMediator(
|
|||
}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey>
|
||||
fun onContactDeselected(view: View?, contactSearchKey: ContactSearchKey)
|
||||
fun onAdapterListCommitted(size: Int)
|
||||
}
|
||||
|
||||
open class SimpleCallbacks : Callbacks {
|
||||
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
|
||||
return contactSearchKeys
|
||||
}
|
||||
|
||||
override fun onContactDeselected(view: View?, contactSearchKey: ContactSearchKey) = Unit
|
||||
override fun onAdapterListCommitted(size: Int) = Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the construction of a PagingMappingAdapter<ContactSearchKey> so that it can
|
||||
* be swapped for another implementation, allow listeners to be wrapped, etc.
|
||||
*/
|
||||
fun interface AdapterFactory {
|
||||
fun create(
|
||||
context: Context,
|
||||
fixedContacts: Set<ContactSearchKey>,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||
displayPhoneNumber: ContactSearchAdapter.DisplayPhoneNumber,
|
||||
callbacks: ContactSearchAdapter.ClickCallbacks,
|
||||
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
|
||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
||||
): PagingMappingAdapter<ContactSearchKey>
|
||||
}
|
||||
|
||||
private object DefaultAdapterFactory : AdapterFactory {
|
||||
override fun create(
|
||||
context: Context,
|
||||
fixedContacts: Set<ContactSearchKey>,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||
displayPhoneNumber: ContactSearchAdapter.DisplayPhoneNumber,
|
||||
callbacks: ContactSearchAdapter.ClickCallbacks,
|
||||
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
|
||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
||||
): PagingMappingAdapter<ContactSearchKey> {
|
||||
return ContactSearchAdapter(displayCheckBox, displaySmsTag, callbacks, storyContextMenuCallbacks)
|
||||
return ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displayPhoneNumber, callbacks, longClickCallbacks, storyContextMenuCallbacks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.contacts.paged
|
|||
import android.database.Cursor
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.contacts.ContactRepository
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchCollection
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.CursorSearchIterator
|
||||
|
@ -12,12 +13,15 @@ import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
|||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.StorySend
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.search.MessageResult
|
||||
import org.thoughtcrime.securesms.search.MessageSearchResult
|
||||
import org.thoughtcrime.securesms.search.SearchRepository
|
||||
import org.thoughtcrime.securesms.search.ThreadSearchResult
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
|
@ -116,6 +120,8 @@ class ContactSearchPagedDataSource(
|
|||
is ContactSearchConfiguration.Section.Messages -> getMessageData(query).getCollectionSize(section, query, null)
|
||||
is ContactSearchConfiguration.Section.GroupsWithMembers -> getGroupsWithMembersIterator(query).getCollectionSize(section, query, null)
|
||||
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsIterator(query).getCollectionSize(section, query, null)
|
||||
is ContactSearchConfiguration.Section.PhoneNumber -> if (isPossiblyPhoneNumber(query)) 1 else 0
|
||||
is ContactSearchConfiguration.Section.Username -> if (isPossiblyUsername(query)) 1 else 0
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,17 +158,68 @@ class ContactSearchPagedDataSource(
|
|||
is ContactSearchConfiguration.Section.Messages -> getMessageContactData(section, query, startIndex, endIndex)
|
||||
is ContactSearchConfiguration.Section.GroupsWithMembers -> getGroupsWithMembersContactData(section, query, startIndex, endIndex)
|
||||
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsContactData(section, query, startIndex, endIndex)
|
||||
is ContactSearchConfiguration.Section.PhoneNumber -> getPossiblePhoneNumber(section, query)
|
||||
is ContactSearchConfiguration.Section.Username -> getPossibleUsername(section, query)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPossiblyPhoneNumber(query: String?): Boolean {
|
||||
if (query == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return if (FeatureFlags.usernames()) {
|
||||
NumberUtil.isVisuallyValidNumberOrEmail(query)
|
||||
} else {
|
||||
NumberUtil.isValidSmsOrEmail(query)
|
||||
}
|
||||
}
|
||||
private fun isPossiblyUsername(query: String?): Boolean {
|
||||
return query != null && FeatureFlags.usernames() && UsernameUtil.isValidUsernameForSearch(query)
|
||||
}
|
||||
private fun getPossiblePhoneNumber(section: ContactSearchConfiguration.Section.PhoneNumber, query: String?): List<ContactSearchData> {
|
||||
return if (isPossiblyPhoneNumber(query)) {
|
||||
listOf(ContactSearchData.UnknownRecipient(section.sectionKey, section.newRowMode, query!!))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
private fun getPossibleUsername(section: ContactSearchConfiguration.Section.Username, query: String?): List<ContactSearchData> {
|
||||
return if (isPossiblyUsername(query)) {
|
||||
listOf(ContactSearchData.UnknownRecipient(section.sectionKey, section.newRowMode, query!!))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNonGroupSearchIterator(section: ContactSearchConfiguration.Section.Individuals, query: String?): ContactSearchIterator<Cursor> {
|
||||
return when (section.transportType) {
|
||||
ContactSearchConfiguration.TransportType.PUSH -> CursorSearchIterator(contactSearchPagedDataSourceRepository.querySignalContacts(query, section.includeSelf))
|
||||
ContactSearchConfiguration.TransportType.SMS -> CursorSearchIterator(contactSearchPagedDataSourceRepository.queryNonSignalContacts(query))
|
||||
ContactSearchConfiguration.TransportType.ALL -> CursorSearchIterator(contactSearchPagedDataSourceRepository.queryNonGroupContacts(query, section.includeSelf))
|
||||
ContactSearchConfiguration.TransportType.PUSH -> CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.querySignalContacts(query, section.includeSelf)))
|
||||
ContactSearchConfiguration.TransportType.SMS -> CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.queryNonSignalContacts(query)))
|
||||
ContactSearchConfiguration.TransportType.ALL -> CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.queryNonGroupContacts(query, section.includeSelf)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun wrapRecipientCursor(cursor: Cursor?): Cursor? {
|
||||
return if (cursor == null || cursor.count == 0) {
|
||||
null
|
||||
} else {
|
||||
WrapAroundCursor(cursor, offset = getFirstAlphaRecipientPosition(cursor))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFirstAlphaRecipientPosition(cursor: Cursor): Int {
|
||||
cursor.moveToPosition(-1)
|
||||
while (cursor.moveToNext()) {
|
||||
val sortName = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN))
|
||||
if (!sortName.first().isDigit()) {
|
||||
return cursor.position
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun getNonGroupHeaderLetterMap(section: ContactSearchConfiguration.Section.Individuals, query: String?): Map<RecipientId, String> {
|
||||
return when (section.transportType) {
|
||||
ContactSearchConfiguration.TransportType.PUSH -> contactSearchPagedDataSourceRepository.querySignalContactLetterHeaders(query, section.includeSelf)
|
||||
|
|
|
@ -112,6 +112,10 @@ class ContactSearchViewModel(
|
|||
return selectionStore.state
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selectionStore.update { emptySet() }
|
||||
}
|
||||
|
||||
fun addToVisibleGroupStories(groupStories: Set<ContactSearchKey.RecipientSearchKey>) {
|
||||
disposables += contactSearchRepository.markDisplayAsStory(groupStories.map { it.recipientId }).subscribe {
|
||||
configurationStore.update { state ->
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.CursorWrapper
|
||||
import androidx.annotation.IntRange
|
||||
import java.lang.Integer.max
|
||||
import java.lang.Integer.min
|
||||
|
||||
/**
|
||||
* Cursor that takes a start offset and will wrap results around to the other side.
|
||||
*
|
||||
* For example, given a cursor with rows:
|
||||
*
|
||||
* [A, B, C, D, E]
|
||||
*
|
||||
* When I create a wrap-around cursor with a start offset = 2, and I read out all results,
|
||||
* I expect:
|
||||
*
|
||||
* [C, D, E, A, B]
|
||||
*/
|
||||
class WrapAroundCursor(delegate: Cursor, @IntRange(from = 0) private val offset: Int) : CursorWrapper(delegate) {
|
||||
|
||||
init {
|
||||
check(offset < delegate.count && offset >= 0)
|
||||
}
|
||||
|
||||
override fun moveToPosition(position: Int): Boolean {
|
||||
return if (offset == 0) {
|
||||
super.moveToPosition(position)
|
||||
} else {
|
||||
if (position == -1 || position == count) {
|
||||
super.moveToPosition(position)
|
||||
} else {
|
||||
super.moveToPosition((position + offset) % count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveToFirst(): Boolean {
|
||||
return super.moveToPosition(offset)
|
||||
}
|
||||
|
||||
override fun moveToLast(): Boolean {
|
||||
return if (offset == 0) {
|
||||
super.moveToLast()
|
||||
} else {
|
||||
super.moveToPosition(offset - 1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun move(offset: Int): Boolean {
|
||||
return if (offset == 0) {
|
||||
super.move(offset)
|
||||
} else {
|
||||
val position = max(min(offset + position, count), -1)
|
||||
moveToPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveToNext(): Boolean {
|
||||
return move(1)
|
||||
}
|
||||
|
||||
override fun moveToPrevious(): Boolean {
|
||||
return move(-1)
|
||||
}
|
||||
|
||||
override fun isLast(): Boolean {
|
||||
return if (offset == 0) {
|
||||
super.isLast()
|
||||
} else {
|
||||
return position == count - 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun isFirst(): Boolean {
|
||||
return if (offset == 0) {
|
||||
super.isFirst()
|
||||
} else {
|
||||
return position == 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPosition(): Int {
|
||||
return if (offset == 0) {
|
||||
super.getPosition()
|
||||
} else {
|
||||
val position = super.getPosition()
|
||||
if (position < 0 || position == count) {
|
||||
return position
|
||||
}
|
||||
|
||||
val distance = (position - offset) % count
|
||||
if (distance >= 0) {
|
||||
distance
|
||||
} else {
|
||||
count + distance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,12 @@ package org.thoughtcrime.securesms.contacts.selection
|
|||
|
||||
import android.os.Bundle
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
data class ContactSelectionArguments(
|
||||
val displayMode: Int = ContactsCursorLoader.DisplayMode.FLAG_ALL,
|
||||
val displayMode: Int = ContactSelectionDisplayMode.FLAG_ALL,
|
||||
val isRefreshable: Boolean = true,
|
||||
val displayRecents: Boolean = false,
|
||||
val selectionLimits: SelectionLimits? = null,
|
||||
|
|
|
@ -120,11 +120,17 @@ class MultiselectForwardFragment :
|
|||
contactSearchRecycler = view.findViewById(R.id.contact_selection_list)
|
||||
contactSearchMediator = ContactSearchMediator(
|
||||
this,
|
||||
emptySet(),
|
||||
FeatureFlags.shareSelectionLimit(),
|
||||
!args.selectSingleRecipient,
|
||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||
ContactSearchAdapter.DisplayPhoneNumber.NEVER,
|
||||
this::getConfiguration,
|
||||
this::filterContacts
|
||||
object : ContactSearchMediator.SimpleCallbacks() {
|
||||
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
|
||||
return filterContacts(view, contactSearchKeys)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
contactSearchRecycler.adapter = contactSearchMediator.adapter
|
||||
|
|
|
@ -294,22 +294,32 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
cameraFab.setVisibility(View.VISIBLE);
|
||||
|
||||
contactSearchMediator = new ContactSearchMediator(this,
|
||||
Collections.emptySet(),
|
||||
SelectionLimits.NO_LIMITS,
|
||||
false,
|
||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||
ContactSearchAdapter.DisplayPhoneNumber.NEVER,
|
||||
this::mapSearchStateToConfiguration,
|
||||
(v, s) -> s,
|
||||
new ContactSearchMediator.SimpleCallbacks(),
|
||||
false,
|
||||
(displayCheckBox,
|
||||
(context,
|
||||
fixedContacts,
|
||||
displayCheckBox,
|
||||
displaySmsTag,
|
||||
displayPhoneNumber,
|
||||
callbacks,
|
||||
longClickCallbacks,
|
||||
storyContextMenuCallbacks
|
||||
) -> {
|
||||
//noinspection CodeBlock2Expr
|
||||
return new ConversationListSearchAdapter(
|
||||
context,
|
||||
fixedContacts,
|
||||
displayCheckBox,
|
||||
displaySmsTag,
|
||||
displayPhoneNumber,
|
||||
new ContactSearchClickCallbacks(callbacks),
|
||||
longClickCallbacks,
|
||||
storyContextMenuCallbacks,
|
||||
getViewLifecycleOwner(),
|
||||
GlideApp.with(this)
|
||||
|
@ -1894,6 +1904,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
public void onExpandClicked(@NonNull ContactSearchData.Expand expand) {
|
||||
delegate.onExpandClicked(expand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknownRecipientClicked(@NonNull View view, @NonNull ContactSearchData.UnknownRecipient unknownRecipient, boolean isSelected) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback extends Material3OnScrollHelperBinder, SearchBinder {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.conversationlist
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.os.bundleOf
|
||||
|
@ -9,6 +10,7 @@ import org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository
|
|||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
|
@ -22,13 +24,17 @@ import java.util.Locale
|
|||
* as well as ChatFilter row support and empty state handler.
|
||||
*/
|
||||
class ConversationListSearchAdapter(
|
||||
context: Context,
|
||||
fixedContacts: Set<ContactSearchKey>,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
displayPhoneNumber: DisplayPhoneNumber,
|
||||
onClickedCallbacks: ConversationListSearchClickCallbacks,
|
||||
longClickCallbacks: LongClickCallbacks,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
glideRequests: GlideRequests
|
||||
) : ContactSearchAdapter(displayCheckBox, displaySmsTag, onClickedCallbacks, storyContextMenuCallbacks) {
|
||||
) : ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displayPhoneNumber, onClickedCallbacks, longClickCallbacks, storyContextMenuCallbacks) {
|
||||
|
||||
init {
|
||||
registerFactory(
|
||||
|
|
|
@ -4362,7 +4362,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
SELECT 1
|
||||
FROM ${GroupTable.MembershipTable.TABLE_NAME}
|
||||
INNER JOIN ${GroupTable.TABLE_NAME} ON ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID} = ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.GROUP_ID}
|
||||
WHERE ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID} = ${TABLE_NAME}.$ID AND ${GroupTable.TABLE_NAME}.${GroupTable.ACTIVE} = 1 AND ${GroupTable.TABLE_NAME}.${GroupTable.MMS} = 0
|
||||
WHERE ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID} = $TABLE_NAME.$ID AND ${GroupTable.TABLE_NAME}.${GroupTable.ACTIVE} = 1 AND ${GroupTable.TABLE_NAME}.${GroupTable.MMS} = 0
|
||||
)
|
||||
""".toSingleLine()
|
||||
const val FILTER_GROUPS = " AND $GROUP_ID IS NULL"
|
||||
|
|
|
@ -16,7 +16,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|||
import org.thoughtcrime.securesms.ContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupViewModel.Event;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
|
@ -50,7 +50,7 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
|
|||
intent.putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.add_to_group_activity);
|
||||
intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS);
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS);
|
||||
|
||||
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(currentGroupsMemberOf));
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.ContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity;
|
||||
|
@ -51,8 +51,8 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
|||
intent.putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.create_group_activity);
|
||||
|
||||
boolean smsEnabled = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
int displayMode = smsEnabled ? ContactsCursorLoader.DisplayMode.FLAG_SMS | ContactsCursorLoader.DisplayMode.FLAG_PUSH
|
||||
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
|
||||
int displayMode = smsEnabled ? ContactSelectionDisplayMode.FLAG_SMS | ContactSelectionDisplayMode.FLAG_PUSH
|
||||
: ContactSelectionDisplayMode.FLAG_PUSH;
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.groupLimits().excludingSelf());
|
||||
|
|
|
@ -67,6 +67,7 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment(
|
|||
selectionLimits = FeatureFlags.shareSelectionLimit(),
|
||||
displayCheckBox = true,
|
||||
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||
displayPhoneNumber = ContactSearchAdapter.DisplayPhoneNumber.NEVER,
|
||||
mapStateToConfiguration = { state ->
|
||||
ContactSearchConfiguration.build {
|
||||
query = state.query
|
||||
|
|
|
@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
|||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.payments.CanNotSendPaymentDialog;
|
||||
|
@ -49,7 +49,7 @@ public class PaymentRecipientSelectionFragment extends LoggingFragment implement
|
|||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putBoolean(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
arguments.putInt(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH | DisplayMode.FLAG_HIDE_NEW);
|
||||
arguments.putInt(ContactSelectionListFragment.DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_HIDE_NEW);
|
||||
arguments.putBoolean(ContactSelectionListFragment.CAN_SELECT_SELF, false);
|
||||
|
||||
Fragment child = getChildFragmentManager().findFragmentById(R.id.contact_selection_list_fragment_holder);
|
||||
|
|
|
@ -29,6 +29,7 @@ class ViewAllSignalConnectionsFragment : Fragment(R.layout.view_all_signal_conne
|
|||
selectionLimits = SelectionLimits(0, 0),
|
||||
displayCheckBox = false,
|
||||
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.IF_NOT_REGISTERED,
|
||||
displayPhoneNumber = ContactSearchAdapter.DisplayPhoneNumber.NEVER,
|
||||
mapStateToConfiguration = { getConfiguration() },
|
||||
performSafetyNumberChecks = false
|
||||
)
|
||||
|
|
|
@ -12,13 +12,12 @@ import com.google.android.material.button.MaterialButton
|
|||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
import java.util.Optional
|
||||
|
@ -78,7 +77,7 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b
|
|||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
if (it.distributionListId == null || it.privateStory != null) {
|
||||
getAttachedContactSelectionFragment().markSelected(it.selection.map(::ShareContact).toSet())
|
||||
getAttachedContactSelectionFragment().markSelected(it.selection.toSet())
|
||||
presentTitle(toolbar, it.selection.size)
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +140,7 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b
|
|||
private fun initializeContactSelectionFragment() {
|
||||
val contactSelectionListFragment = ContactSelectionListFragment()
|
||||
val arguments = ContactSelectionArguments(
|
||||
displayMode = ContactsCursorLoader.DisplayMode.FLAG_PUSH or ContactsCursorLoader.DisplayMode.FLAG_HIDE_NEW,
|
||||
displayMode = ContactSelectionDisplayMode.FLAG_PUSH or ContactSelectionDisplayMode.FLAG_HIDE_NEW,
|
||||
isRefreshable = false,
|
||||
displayRecents = false,
|
||||
selectionLimits = SelectionLimits.NO_LIMITS,
|
||||
|
|
|
@ -80,6 +80,6 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/blocked_users_header"
|
||||
tools:listitem="@layout/contact_selection_list_item" />
|
||||
tools:listitem="@layout/contact_search_item" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,46 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/rounded_inset_ripple_background"
|
||||
android:background="@drawable/conversation_item_background"
|
||||
android:focusable="true"
|
||||
android:minHeight="@dimen/contact_selection_item_height"
|
||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
||||
android:paddingEnd="@dimen/dsl_settings_gutter">
|
||||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/contact_photo_image"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
|
||||
android:cropToPadding="true"
|
||||
android:foreground="@drawable/contact_photo_background"
|
||||
android:background="@color/signal_colorSurfaceVariant"
|
||||
android:importantForAccessibility="no"
|
||||
app:contentPadding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:src="@color/blue_600" />
|
||||
|
||||
<org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
android:id="@+id/contact_badge"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="22dp"
|
||||
android:contentDescription="@string/ImageView__badge"
|
||||
app:badge_size="medium"
|
||||
app:layout_constraintStart_toStartOf="@id/contact_photo_image"
|
||||
app:layout_constraintTop_toTopOf="@id/contact_photo_image"
|
||||
tools:visibility="visible" />
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle"
|
||||
app:srcCompat="@drawable/ic_search_24"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
android:id="@+id/check_box"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="@drawable/contact_selection_checkbox"
|
||||
android:background="?contactCheckboxBackground"
|
||||
android:button="@null"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
|
@ -58,10 +47,10 @@
|
|||
android:drawablePadding="4dp"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:layout_constraintBottom_toTopOf="@id/number"
|
||||
app:layout_constraintEnd_toStartOf="@id/sms_tag"
|
||||
app:layout_constraintEnd_toStartOf="@id/check_box"
|
||||
app:layout_constraintStart_toEndOf="@id/contact_photo_image"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
|
@ -76,47 +65,17 @@
|
|||
android:layout_marginStart="16dp"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:textDirection="ltr"
|
||||
app:emoji_forceCustom="true"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/label"
|
||||
app:layout_constraintEnd_toStartOf="@id/check_box"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/contact_photo_image"
|
||||
app:layout_constraintTop_toBottomOf="@id/name"
|
||||
tools:text="@sample/contacts.json/data/number" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:paddingStart="4dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="@id/name"
|
||||
app:layout_constraintStart_toEndOf="@id/number"
|
||||
app:layout_constraintTop_toTopOf="@id/number"
|
||||
tools:ignore="RtlSymmetry"
|
||||
tools:text="· Mobile" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sms_tag"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/ContactSelectionListItem__sms"
|
||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/check_box"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginEnd="0dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</org.thoughtcrime.securesms.contacts.ContactSelectionListItem>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -24,7 +24,7 @@
|
|||
android:clipToPadding="false"
|
||||
android:scrollbarThumbVertical="@drawable/contact_selection_scrollbar_thumb"
|
||||
android:scrollbars="vertical"
|
||||
tools:listitem="@layout/contact_selection_list_item" />
|
||||
tools:listitem="@layout/contact_search_item" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/empty"
|
||||
|
|
|
@ -30,12 +30,13 @@ class ContactSearchPagedDataSourceTest {
|
|||
@Before
|
||||
fun setUp() {
|
||||
whenever(repository.getRecipientFromGroupRecord(any())).thenReturn(Recipient.UNKNOWN)
|
||||
whenever(repository.getRecipientFromSearchCursor(cursor)).thenReturn(Recipient.UNKNOWN)
|
||||
whenever(repository.getRecipientFromSearchCursor(any())).thenReturn(Recipient.UNKNOWN)
|
||||
whenever(repository.getRecipientFromThreadCursor(cursor)).thenReturn(Recipient.UNKNOWN)
|
||||
whenever(repository.getRecipientFromDistributionListCursor(cursor)).thenReturn(Recipient.UNKNOWN)
|
||||
whenever(repository.getPrivacyModeFromDistributionListCursor(cursor)).thenReturn(DistributionListPrivacyMode.ALL)
|
||||
whenever(repository.getGroupStories()).thenReturn(emptySet())
|
||||
whenever(repository.getLatestStorySends(any())).thenReturn(emptyList())
|
||||
whenever(cursor.getString(any())).thenReturn("A")
|
||||
whenever(cursor.moveToPosition(any())).thenCallRealMethod()
|
||||
whenever(cursor.moveToNext()).thenCallRealMethod()
|
||||
whenever(cursor.position).thenCallRealMethod()
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.app.Application
|
||||
import android.database.MatrixCursor
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.util.readToList
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class WrapAroundCursorTest {
|
||||
|
||||
private val values = listOf("Cereal", "Marshmallows", "Toast", "Meatballs", "Oatmeal")
|
||||
|
||||
private val delegate = MatrixCursor(arrayOf("Breakfast")).apply {
|
||||
values.forEach {
|
||||
addRow(arrayOf(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of zero, then I expect the original order`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 0)
|
||||
val actual = testSubject.readToList { cursor -> cursor.getString(0) }
|
||||
assertEquals(values, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, then I expect wrap around`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
val expected = values.drop(2) + values.dropLast(3)
|
||||
val actual = testSubject.readToList { cursor -> cursor.getString(0) }
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and internal position of 1, when I getPosition, then I expect an offset position`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(1)
|
||||
val actual = testSubject.position
|
||||
assertEquals(4, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and internal position of 4, when I getPosition, then I expect an offset position`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(4)
|
||||
val actual = testSubject.position
|
||||
assertEquals(2, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and internal position of 2, when I getPosition, then I expect an offset position`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(2)
|
||||
val actual = testSubject.position
|
||||
assertEquals(0, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and internal position of -1, when I getPosition, then I expect -1`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(-1)
|
||||
val actual = testSubject.position
|
||||
assertEquals(-1, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and internal position of 5, when I getPosition, then I expect 5`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(5)
|
||||
val actual = testSubject.position
|
||||
assertEquals(5, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I set internal cursor position to 2 and isFirst, then I expect true`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(2)
|
||||
val actual = testSubject.isFirst
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I set internal cursor position to 0 and isFirst, then I expect false`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(0)
|
||||
val actual = testSubject.isFirst
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I set internal cursor position to 1 and isLast, then I expect true`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(1)
|
||||
val actual = testSubject.isLast
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I set internal cursor position to 4 and isLast, then I expect false`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(4)
|
||||
val actual = testSubject.isLast
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I moveToPosition to 0, then I expect the internal cursor to be at position 2`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToPosition(0)
|
||||
val actual = delegate.position
|
||||
assertEquals(2, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I moveToPosition to 4, then I expect the internal cursor to be at position 1`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToPosition(4)
|
||||
val actual = delegate.position
|
||||
assertEquals(1, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I moveToPosition to -1, then I expect the internal cursor to be at position -1`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToPosition(-1)
|
||||
val actual = delegate.position
|
||||
assertEquals(-1, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I moveToPosition to 5, then I expect the internal cursor to be at position 5`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToPosition(5)
|
||||
val actual = delegate.position
|
||||
assertEquals(5, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I moveToFirst, then I expect the internal cursor to be at position 2`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToFirst()
|
||||
val actual = delegate.position
|
||||
assertEquals(2, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two, when I moveToLast, then I expect the internal cursor to be at position 1`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToLast()
|
||||
val actual = delegate.position
|
||||
assertEquals(1, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at first position, when I move 4, then I expect the internal cursor to be at position 1`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToFirst()
|
||||
testSubject.move(4)
|
||||
val actual = delegate.position
|
||||
assertEquals(1, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at first position, when I move 6, then I expect the internal cursor to be at position 5`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToFirst()
|
||||
testSubject.move(6)
|
||||
val actual = delegate.position
|
||||
assertEquals(5, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at first position, when I move -1, then I expect the internal cursor to be at position -1`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToFirst()
|
||||
testSubject.move(-1)
|
||||
val actual = delegate.position
|
||||
assertEquals(-1, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at last position, when I move -1, then I expect the internal cursor to be at position 0`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToLast()
|
||||
testSubject.move(-1)
|
||||
val actual = delegate.position
|
||||
assertEquals(0, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at last position, when I move 1, then I expect the internal cursor to be at position 5`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToLast()
|
||||
testSubject.move(1)
|
||||
val actual = delegate.position
|
||||
assertEquals(5, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at last position, when I moveToNext, then I expect the internal cursor to be at position 5`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToLast()
|
||||
testSubject.moveToNext()
|
||||
val actual = delegate.position
|
||||
assertEquals(5, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at first position, when I moveToPrevious, then I expect the internal cursor to be at position -1`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
testSubject.moveToFirst()
|
||||
testSubject.moveToPrevious()
|
||||
val actual = delegate.position
|
||||
assertEquals(-1, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at internal position 4, when I moveToNext, then I expect the internal cursor to be at position 0`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(4)
|
||||
testSubject.moveToNext()
|
||||
val actual = delegate.position
|
||||
assertEquals(0, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an offset of two and at internal position 0, when I moveToPrevious, then I expect the internal cursor to be at position 4`() {
|
||||
val testSubject = WrapAroundCursor(delegate, offset = 2)
|
||||
delegate.moveToPosition(0)
|
||||
testSubject.moveToPrevious()
|
||||
val actual = delegate.position
|
||||
assertEquals(4, actual)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue