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.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
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.contacts.sync.ContactDiscovery;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
@ -71,7 +71,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||||
protected void onCreate(Bundle icicle, boolean ready) {
|
protected void onCreate(Bundle icicle, boolean ready) {
|
||||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||||
boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
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);
|
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.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -40,10 +39,7 @@ import androidx.annotation.Px;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.constraintlayout.widget.ConstraintSet;
|
import androidx.constraintlayout.widget.ConstraintSet;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
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.concurrent.SimpleTask;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||||
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
|
|
||||||
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
|
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
|
||||||
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
||||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||||
import org.thoughtcrime.securesms.contacts.SelectedContacts;
|
import org.thoughtcrime.securesms.contacts.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.contacts.sync.ContactDiscovery;
|
||||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
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.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.sharing.ShareContact;
|
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||||
|
@ -105,7 +98,6 @@ import kotlin.Unit;
|
||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*/
|
*/
|
||||||
public final class ContactSelectionListFragment extends LoggingFragment
|
public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
|
||||||
{
|
{
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||||
|
@ -137,27 +129,23 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
private String cursorFilter;
|
private String cursorFilter;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private RecyclerViewFastScroller fastScroller;
|
private RecyclerViewFastScroller fastScroller;
|
||||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
|
||||||
private RecyclerView chipRecycler;
|
private RecyclerView chipRecycler;
|
||||||
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
||||||
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
|
||||||
private MappingAdapter contactChipAdapter;
|
private MappingAdapter contactChipAdapter;
|
||||||
private ContactChipViewModel contactChipViewModel;
|
private ContactChipViewModel contactChipViewModel;
|
||||||
private LifecycleDisposable lifecycleDisposable;
|
private LifecycleDisposable lifecycleDisposable;
|
||||||
private HeaderActionProvider headerActionProvider;
|
private HeaderActionProvider headerActionProvider;
|
||||||
private TextView headerActionView;
|
private TextView headerActionView;
|
||||||
|
private ContactSearchMediator contactSearchMediator;
|
||||||
|
|
||||||
@Nullable private FixedViewsAdapter headerAdapter;
|
|
||||||
@Nullable private FixedViewsAdapter footerAdapter;
|
|
||||||
@Nullable private ListCallback listCallback;
|
@Nullable private ListCallback listCallback;
|
||||||
@Nullable private ScrollCallback scrollCallback;
|
@Nullable private ScrollCallback scrollCallback;
|
||||||
@Nullable private OnItemLongClickListener onItemLongClickListener;
|
@Nullable private OnItemLongClickListener onItemLongClickListener;
|
||||||
private GlideRequests glideRequests;
|
|
||||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||||
private Set<RecipientId> currentSelection;
|
private Set<RecipientId> currentSelection;
|
||||||
private boolean isMulti;
|
private boolean isMulti;
|
||||||
private boolean hideCount;
|
|
||||||
private boolean canSelectSelf;
|
private boolean canSelectSelf;
|
||||||
|
private ListClickListener listClickListener = new ListClickListener();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
|
@ -191,14 +179,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) getParentFragment();
|
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) getParentFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
|
||||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
|
||||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context instanceof HeaderActionProvider) {
|
if (context instanceof HeaderActionProvider) {
|
||||||
headerActionProvider = (HeaderActionProvider) context;
|
headerActionProvider = (HeaderActionProvider) context;
|
||||||
}
|
}
|
||||||
|
@ -234,16 +214,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
||||||
handleContactPermissionGranted();
|
handleContactPermissionGranted();
|
||||||
} else {
|
} else {
|
||||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
contactSearchMediator.refresh();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onAnyDenied(() -> {
|
.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, requireActivity().getIntent().getBooleanExtra(RECENTS, false))) {
|
||||||
|
contactSearchMediator.refresh();
|
||||||
if (safeArguments().getBoolean(RECENTS, activity.getIntent().getBooleanExtra(RECENTS, false))) {
|
|
||||||
LoaderManager.getInstance(this).initLoader(0, null, ContactSelectionListFragment.this);
|
|
||||||
} else {
|
} else {
|
||||||
initializeNoContactsPermission();
|
initializeNoContactsPermission();
|
||||||
}
|
}
|
||||||
|
@ -305,7 +283,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
swipeRefresh.setNestedScrollingEnabled(isRefreshable);
|
swipeRefresh.setNestedScrollingEnabled(isRefreshable);
|
||||||
swipeRefresh.setEnabled(isRefreshable);
|
swipeRefresh.setEnabled(isRefreshable);
|
||||||
|
|
||||||
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
|
|
||||||
selectionLimit = arguments.getParcelable(SELECTION_LIMITS);
|
selectionLimit = arguments.getParcelable(SELECTION_LIMITS);
|
||||||
if (selectionLimit == null) {
|
if (selectionLimit == null) {
|
||||||
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
||||||
|
@ -353,6 +330,65 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
headerActionView.setEnabled(false);
|
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;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,27 +408,30 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull List<SelectedContact> getSelectedContacts() {
|
public @NonNull List<SelectedContact> getSelectedContacts() {
|
||||||
if (cursorRecyclerViewAdapter == null) {
|
if (contactSearchMediator == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return cursorRecyclerViewAdapter.getSelectedContacts();
|
return contactSearchMediator.getSelectedContacts()
|
||||||
|
.stream()
|
||||||
|
.map(ContactSearchKey::requireSelectedContact)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getSelectedContactsCount() {
|
public int getSelectedContactsCount() {
|
||||||
if (cursorRecyclerViewAdapter == null) {
|
if (contactSearchMediator == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
return contactSearchMediator.getSelectedContacts().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getTotalMemberCount() {
|
public int getTotalMemberCount() {
|
||||||
if (cursorRecyclerViewAdapter == null) {
|
if (contactSearchMediator == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cursorRecyclerViewAdapter.getSelectedContactsCount() + cursorRecyclerViewAdapter.getCurrentContactsCount();
|
return getSelectedContactsCount() + contactSearchMediator.getFixedContactsSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<RecipientId> getCurrentSelection() {
|
private Set<RecipientId> getCurrentSelection() {
|
||||||
|
@ -410,34 +449,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeCursor() {
|
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.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
|
||||||
recyclerView.setAdapter(concatenateAdapter);
|
recyclerView.setAdapter(contactSearchMediator.getAdapter());
|
||||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||||
|
@ -458,20 +471,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
return hasQueryFilter() || shouldDisplayRecents();
|
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() {
|
private void initializeNoContactsPermission() {
|
||||||
swipeRefresh.setVisibility(View.GONE);
|
swipeRefresh.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
@ -496,7 +495,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
|
|
||||||
public void setQueryFilter(String filter) {
|
public void setQueryFilter(String filter) {
|
||||||
this.cursorFilter = filter;
|
this.cursorFilter = filter;
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
contactSearchMediator.onFilterChanged(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetQueryFilter() {
|
public void resetQueryFilter() {
|
||||||
|
@ -513,51 +512,21 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
cursorRecyclerViewAdapter.clearSelectedContacts();
|
contactSearchMediator.clearSelection();
|
||||||
|
fastScroller.setVisibility(View.GONE);
|
||||||
if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) {
|
headerActionView.setVisibility(View.GONE);
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRecyclerViewPaddingBottom(@Px int paddingBottom) {
|
public void setRecyclerViewPaddingBottom(@Px int paddingBottom) {
|
||||||
ViewUtil.setPaddingBottom(recyclerView, paddingBottom);
|
ViewUtil.setPaddingBottom(recyclerView, paddingBottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void onLoadFinished(int count) {
|
||||||
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) {
|
|
||||||
swipeRefresh.setVisibility(View.VISIBLE);
|
swipeRefresh.setVisibility(View.VISIBLE);
|
||||||
showContactsLayout.setVisibility(View.GONE);
|
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);
|
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
||||||
boolean useFastScroller = data != null && data.getCount() > 20;
|
boolean useFastScroller = count > 20;
|
||||||
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
||||||
if (useFastScroller) {
|
if (useFastScroller) {
|
||||||
fastScroller.setVisibility(View.VISIBLE);
|
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() {
|
private boolean shouldDisplayRecents() {
|
||||||
return safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
|
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.
|
* @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()) {
|
if (contacts.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<SelectedContact> toMarkSelected = contacts.stream()
|
Set<SelectedContact> toMarkSelected = contacts.stream()
|
||||||
.map(contact -> {
|
.filter(r -> !contactSearchMediator.getSelectedContacts()
|
||||||
if (contact.getRecipientId().isPresent()) {
|
.contains(new ContactSearchKey.RecipientSearchKey(r, false)))
|
||||||
return SelectedContact.forRecipientId(contact.getRecipientId().get());
|
.map(SelectedContact::forRecipientId)
|
||||||
} else {
|
|
||||||
return SelectedContact.forPhone(null, contact.getNumber());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(c -> !cursorRecyclerViewAdapter.isSelectedContact(c))
|
|
||||||
.collect(java.util.stream.Collectors.toSet());
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
|
||||||
if (toMarkSelected.isEmpty()) {
|
if (toMarkSelected.isEmpty()) {
|
||||||
|
@ -657,22 +614,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
for (final SelectedContact selectedContact : toMarkSelected) {
|
for (final SelectedContact selectedContact : toMarkSelected) {
|
||||||
markContactSelected(selectedContact);
|
markContactSelected(selectedContact);
|
||||||
}
|
}
|
||||||
|
|
||||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
|
private class ListClickListener {
|
||||||
@Override
|
public void onItemClick(ContactSearchKey contact) {
|
||||||
public void onItemClick(ContactSelectionListItem contact) {
|
SelectedContact selectedContact = contact.requireSelectedContact();
|
||||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
|
|
||||||
: SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
|
|
||||||
|
|
||||||
if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
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();
|
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
if (!isMulti || !contactSearchMediator.getSelectedContacts().contains(selectedContact.toContactSearchKey())) {
|
||||||
if (selectionHardLimitReached()) {
|
if (selectionHardLimitReached()) {
|
||||||
if (onSelectionLimitReachedListener != null) {
|
if (onSelectionLimitReachedListener != null) {
|
||||||
onSelectionLimitReachedListener.onHardLimitReached(selectionLimit.getHardLimit());
|
onSelectionLimitReachedListener.onHardLimitReached(selectionLimit.getHardLimit());
|
||||||
|
@ -682,63 +635,58 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
return;
|
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());
|
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||||
|
|
||||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||||
return UsernameUtil.fetchAciForUsername(contact.getNumber());
|
return UsernameUtil.fetchAciForUsername(username);
|
||||||
}, uuid -> {
|
}, uuid -> {
|
||||||
loadingDialog.dismiss();
|
loadingDialog.dismiss();
|
||||||
if (uuid.isPresent()) {
|
if (uuid.isPresent()) {
|
||||||
Recipient recipient = Recipient.externalUsername(uuid.get(), contact.getNumber());
|
Recipient recipient = Recipient.externalUsername(uuid.get(), username);
|
||||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), username);
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
|
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
markContactSelected(selected);
|
markContactSelected(selected);
|
||||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
markContactSelected(selected);
|
markContactSelected(selected);
|
||||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
new MaterialAlertDialogBuilder(requireContext())
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
.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())
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber(), allowed -> {
|
onContactSelectedListener.onBeforeContactSelected(Optional.ofNullable(selectedContact.getRecipientId()), selectedContact.getNumber(), allowed -> {
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
markContactSelected(selectedContact);
|
markContactSelected(selectedContact);
|
||||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
markContactSelected(selectedContact);
|
markContactSelected(selectedContact);
|
||||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
markContactUnselected(selectedContact);
|
markContactUnselected(selectedContact);
|
||||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
onContactSelectedListener.onContactDeselected(Optional.ofNullable(selectedContact.getRecipientId()), selectedContact.getNumber());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public boolean onItemLongClick(View anchorView, ContactSearchKey item) {
|
||||||
public boolean onItemLongClick(ContactSelectionListItem item) {
|
|
||||||
if (onItemLongClickListener != null) {
|
if (onItemLongClickListener != null) {
|
||||||
return onItemLongClickListener.onLongClick(item, recyclerView);
|
return onItemLongClickListener.onLongClick(anchorView, item, recyclerView);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -758,7 +706,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||||
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
contactSearchMediator.setKeysSelected(Collections.singleton(selectedContact.toContactSearchKey()));
|
||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
addChipForSelectedContact(selectedContact);
|
addChipForSelectedContact(selectedContact);
|
||||||
}
|
}
|
||||||
|
@ -768,8 +716,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
|
||||||
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
|
contactSearchMediator.setKeysNotSelected(Collections.singleton(selectedContact.toContactSearchKey()));
|
||||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
|
||||||
contactChipViewModel.remove(selectedContact);
|
contactChipViewModel.remove(selectedContact);
|
||||||
|
|
||||||
if (onContactSelectedListener != null) {
|
if (onContactSelectedListener != null) {
|
||||||
|
@ -842,6 +789,116 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||||
chipRecycler.smoothScrollBy(x, 0);
|
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 {
|
public interface OnContactSelectedListener {
|
||||||
/**
|
/**
|
||||||
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
|
* 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 {
|
public interface OnItemLongClickListener {
|
||||||
boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView);
|
boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView);
|
||||||
}
|
|
||||||
|
|
||||||
public interface AbstractContactsCursorLoaderFactoryProvider {
|
|
||||||
@NonNull AbstractContactsCursorLoader.Factory get();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
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.contacts.SelectedContact;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||||
|
@ -62,7 +62,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
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.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
|
||||||
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
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.ContactSelectionListItem;
|
||||||
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
|
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
|
||||||
import org.thoughtcrime.securesms.contacts.management.ContactsManagementViewModel;
|
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.contacts.sync.ContactDiscovery;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
|
@ -233,18 +236,14 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView) {
|
public boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView) {
|
||||||
RecipientId recipientId = contactSelectionListItem.getRecipientId().orElse(null);
|
RecipientId recipientId = contactSearchKey.requireRecipientSearchKey().getRecipientId();
|
||||||
if (recipientId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ActionItem> actions = generateContextualActionsForRecipient(recipientId);
|
List<ActionItem> actions = generateContextualActionsForRecipient(recipientId);
|
||||||
if (actions.isEmpty()) {
|
if (actions.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
new SignalContextMenu.Builder(contactSelectionListItem, (ViewGroup) contactSelectionListItem.getRootView())
|
new SignalContextMenu.Builder(anchorView, (ViewGroup) anchorView.getRootView())
|
||||||
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW)
|
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW)
|
||||||
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
|
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
|
||||||
.offsetX((int) DimensionUnit.DP.toPixels(12))
|
.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.PassphraseRequiredActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
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.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
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.SELECTION_LIMITS, 1);
|
||||||
intent.putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
intent.putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE,
|
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE,
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_PUSH |
|
ContactSelectionDisplayMode.FLAG_PUSH |
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_SMS |
|
ContactSelectionDisplayMode.FLAG_SMS |
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS |
|
ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS |
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_INACTIVE_GROUPS |
|
ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS |
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_BLOCK);
|
ContactSelectionDisplayMode.FLAG_BLOCK);
|
||||||
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.replace(R.id.fragment_container, fragment, CONTACT_SELECTION_FRAGMENT)
|
.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.LoggingFragment
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
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.groups.SelectionLimits
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
@ -100,17 +100,17 @@ class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDefaultDisplayMode(): Int {
|
private fun getDefaultDisplayMode(): Int {
|
||||||
var mode = ContactsCursorLoader.DisplayMode.FLAG_PUSH or
|
var mode = ContactSelectionDisplayMode.FLAG_PUSH or
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS or
|
ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS or
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_HIDE_NEW or
|
ContactSelectionDisplayMode.FLAG_HIDE_NEW or
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_HIDE_RECENT_HEADER or
|
ContactSelectionDisplayMode.FLAG_HIDE_RECENT_HEADER or
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_GROUPS_AFTER_CONTACTS
|
ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS
|
||||||
|
|
||||||
if (Util.isDefaultSmsProvider(requireContext()) && SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
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>) {
|
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.RecipientPreference
|
||||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
|
import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
|
||||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
|
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.conversation.ConversationIntents
|
||||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||||
|
@ -768,7 +768,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||||
AddMembersActivity.createIntent(
|
AddMembersActivity.createIntent(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
addMembersToGroup.groupId,
|
addMembersToGroup.groupId,
|
||||||
ContactsCursorLoader.DisplayMode.FLAG_PUSH,
|
ContactSelectionDisplayMode.FLAG_PUSH,
|
||||||
addMembersToGroup.selectionWarning,
|
addMembersToGroup.selectionWarning,
|
||||||
addMembersToGroup.selectionLimit,
|
addMembersToGroup.selectionLimit,
|
||||||
addMembersToGroup.isAnnouncementGroup,
|
addMembersToGroup.isAnnouncementGroup,
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class ContactRepository {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
public static final String ID_COLUMN = "id";
|
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_COLUMN = "number";
|
||||||
static final String NUMBER_TYPE_COLUMN = "number_type";
|
static final String NUMBER_TYPE_COLUMN = "number_type";
|
||||||
static final String LABEL_COLUMN = "label";
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
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() {
|
public boolean hasUsername() {
|
||||||
return username != null;
|
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.
|
* Returns true when non-null recipient ids match, and false if not.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
package org.thoughtcrime.securesms.contacts.paged
|
package org.thoughtcrime.securesms.contacts.paged
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import org.signal.core.util.BreakIteratorCompat
|
||||||
import org.signal.core.util.dp
|
import org.signal.core.util.dp
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
import org.thoughtcrime.securesms.components.FromTextView
|
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.ActionItem
|
||||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration
|
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration
|
||||||
|
@ -31,17 +34,35 @@ import org.thoughtcrime.securesms.util.visible
|
||||||
*/
|
*/
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
open class ContactSearchAdapter(
|
open class ContactSearchAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: DisplaySmsTag,
|
displaySmsTag: DisplaySmsTag,
|
||||||
|
displayPhoneNumber: DisplayPhoneNumber,
|
||||||
onClickCallbacks: ClickCallbacks,
|
onClickCallbacks: ClickCallbacks,
|
||||||
|
longClickCallbacks: LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: StoryContextMenuCallbacks
|
storyContextMenuCallbacks: StoryContextMenuCallbacks
|
||||||
) : PagingMappingAdapter<ContactSearchKey>() {
|
) : PagingMappingAdapter<ContactSearchKey>(), FastScrollAdapter {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerStoryItems(this, displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
|
registerStoryItems(this, displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
|
||||||
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, onClickCallbacks::onKnownRecipientClicked)
|
registerKnownRecipientItems(this, fixedContacts, displayCheckBox, displaySmsTag, displayPhoneNumber, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick)
|
||||||
registerHeaders(this)
|
registerHeaders(this)
|
||||||
registerExpands(this, onClickCallbacks::onExpandClicked)
|
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 {
|
companion object {
|
||||||
|
@ -59,13 +80,16 @@ open class ContactSearchAdapter(
|
||||||
|
|
||||||
fun registerKnownRecipientItems(
|
fun registerKnownRecipientItems(
|
||||||
mappingAdapter: MappingAdapter,
|
mappingAdapter: MappingAdapter,
|
||||||
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: DisplaySmsTag,
|
displaySmsTag: DisplaySmsTag,
|
||||||
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>
|
displayPhoneNumber: DisplayPhoneNumber,
|
||||||
|
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>,
|
||||||
|
recipientLongClickCallback: OnLongClickedCallback<ContactSearchData.KnownRecipient>
|
||||||
) {
|
) {
|
||||||
mappingAdapter.registerFactory(
|
mappingAdapter.registerFactory(
|
||||||
RecipientModel::class.java,
|
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.Thread -> ThreadModel(it)
|
||||||
is ContactSearchData.Empty -> EmptyModel(it)
|
is ContactSearchData.Empty -> EmptyModel(it)
|
||||||
is ContactSearchData.GroupWithMembers -> GroupWithMembersModel(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)
|
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)
|
else -> context.resources.getQuantityString(R.plurals.ContactSearchItems__custom_story_d_viewers, count, count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -242,7 +268,32 @@ open class ContactSearchAdapter(
|
||||||
/**
|
/**
|
||||||
* Recipient model
|
* 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 {
|
override fun areItemsTheSame(newItem: RecipientModel): Boolean {
|
||||||
return newItem.knownRecipient == knownRecipient
|
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(
|
private class KnownRecipientViewHolder(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
|
private val fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: DisplaySmsTag,
|
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 {
|
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
|
||||||
|
|
||||||
private var headerLetter: String? = null
|
private var headerLetter: String? = null
|
||||||
|
@ -282,6 +369,9 @@ open class ContactSearchAdapter(
|
||||||
val count = recipient.participantIds.size
|
val count = recipient.participantIds.size
|
||||||
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
|
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
|
||||||
number.visible = true
|
number.visible = true
|
||||||
|
} else if (displayPhoneNumber == DisplayPhoneNumber.ALWAYS && recipient.hasE164()) {
|
||||||
|
number.text = recipient.requireE164()
|
||||||
|
number.visible = true
|
||||||
} else {
|
} else {
|
||||||
super.bindNumberField(model)
|
super.bindNumberField(model)
|
||||||
}
|
}
|
||||||
|
@ -289,15 +379,22 @@ open class ContactSearchAdapter(
|
||||||
headerLetter = model.knownRecipient.headerLetter
|
headerLetter = model.knownRecipient.headerLetter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun bindCheckbox(model: RecipientModel) {
|
||||||
|
checkbox.isEnabled = !fixedContacts.contains(model.knownRecipient.contactSearchKey)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getHeaderLetter(): String? {
|
override fun getHeaderLetter(): String? {
|
||||||
return headerLetter
|
return headerLetter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun bindLongPress(model: RecipientModel) {
|
||||||
|
itemView.setOnLongClickListener { onLongClick.onLongClicked(itemView, model.knownRecipient) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base Recipient View Holder
|
* Base Recipient View Holder
|
||||||
*/
|
*/
|
||||||
|
|
||||||
abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(
|
abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
private val displayCheckBox: Boolean,
|
private val displayCheckBox: Boolean,
|
||||||
|
@ -316,6 +413,7 @@ open class ContactSearchAdapter(
|
||||||
override fun bind(model: T) {
|
override fun bind(model: T) {
|
||||||
checkbox.visible = displayCheckBox
|
checkbox.visible = displayCheckBox
|
||||||
checkbox.isChecked = isSelected(model)
|
checkbox.isChecked = isSelected(model)
|
||||||
|
|
||||||
itemView.setOnClickListener { onClick.onClicked(avatar, getData(model), isSelected(model)) }
|
itemView.setOnClickListener { onClick.onClicked(avatar, getData(model), isSelected(model)) }
|
||||||
bindLongPress(model)
|
bindLongPress(model)
|
||||||
|
|
||||||
|
@ -332,6 +430,8 @@ open class ContactSearchAdapter(
|
||||||
bindSmsTagField(model)
|
bindSmsTagField(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun bindCheckbox(model: T) = Unit
|
||||||
|
|
||||||
protected open fun bindAvatar(model: T) {
|
protected open fun bindAvatar(model: T) {
|
||||||
avatar.setAvatar(getRecipient(model))
|
avatar.setAvatar(getRecipient(model))
|
||||||
}
|
}
|
||||||
|
@ -444,12 +544,12 @@ open class ContactSearchAdapter(
|
||||||
ContactSearchConfiguration.SectionKey.RECENTS -> R.string.ContactsCursorLoader_recent_chats
|
ContactSearchConfiguration.SectionKey.RECENTS -> R.string.ContactsCursorLoader_recent_chats
|
||||||
ContactSearchConfiguration.SectionKey.INDIVIDUALS -> R.string.ContactsCursorLoader_contacts
|
ContactSearchConfiguration.SectionKey.INDIVIDUALS -> R.string.ContactsCursorLoader_contacts
|
||||||
ContactSearchConfiguration.SectionKey.GROUPS -> R.string.ContactsCursorLoader_groups
|
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.GROUP_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
||||||
ContactSearchConfiguration.SectionKey.CHATS -> R.string.ContactsCursorLoader__chats
|
ContactSearchConfiguration.SectionKey.CHATS -> R.string.ContactsCursorLoader__chats
|
||||||
ContactSearchConfiguration.SectionKey.MESSAGES -> R.string.ContactsCursorLoader__messages
|
ContactSearchConfiguration.SectionKey.MESSAGES -> R.string.ContactsCursorLoader__messages
|
||||||
ContactSearchConfiguration.SectionKey.GROUPS_WITH_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
ContactSearchConfiguration.SectionKey.GROUPS_WITH_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
||||||
ContactSearchConfiguration.SectionKey.CONTACTS_WITHOUT_THREADS -> R.string.ContactsCursorLoader_contacts
|
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
|
NEVER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class DisplayPhoneNumber {
|
||||||
|
NEVER,
|
||||||
|
ALWAYS
|
||||||
|
}
|
||||||
|
|
||||||
fun interface OnClickedCallback<D : ContactSearchData> {
|
fun interface OnClickedCallback<D : ContactSearchData> {
|
||||||
fun onClicked(view: View, data: D, isSelected: Boolean)
|
fun onClicked(view: View, data: D, isSelected: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun interface OnLongClickedCallback<D : ContactSearchData> {
|
||||||
|
fun onLongClicked(view: View, data: D): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface ClickCallbacks {
|
interface ClickCallbacks {
|
||||||
fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean)
|
fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean)
|
||||||
fun onKnownRecipientClicked(view: View, knownRecipient: ContactSearchData.KnownRecipient, isSelected: Boolean)
|
fun onKnownRecipientClicked(view: View, knownRecipient: ContactSearchData.KnownRecipient, isSelected: Boolean)
|
||||||
fun onExpandClicked(expand: ContactSearchData.Expand)
|
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 includeHeader: Boolean = true,
|
||||||
override val expandConfig: ExpandConfig? = null
|
override val expandConfig: ExpandConfig? = null
|
||||||
) : Section(SectionKey.CONTACTS_WITHOUT_THREADS)
|
) : 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 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
|
ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the mode for 'Username' or 'PhoneNumber'
|
||||||
|
*/
|
||||||
|
enum class NewRowMode {
|
||||||
|
NEW_CONVERSATION,
|
||||||
|
BLOCK,
|
||||||
|
ADD_TO_GROUP
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* DSL Style builder function. Example:
|
* DSL Style builder function. Example:
|
||||||
|
@ -296,11 +325,12 @@ class ContactSearchConfiguration private constructor(
|
||||||
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun groupsWithMembers(
|
fun username(newRowMode: NewRowMode) {
|
||||||
includeHeader: Boolean = true,
|
addSection(Section.Username(newRowMode))
|
||||||
expandConfig: ExpandConfig? = null
|
}
|
||||||
) {
|
|
||||||
addSection(Section.GroupsWithMembers(includeHeader, expandConfig))
|
fun phone(newRowMode: NewRowMode) {
|
||||||
|
addSection(Section.PhoneNumber(newRowMode))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSection(section: Section)
|
fun addSection(section: Section)
|
||||||
|
|
|
@ -89,4 +89,13 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
class TestRow(val value: Int) : ContactSearchData(ContactSearchKey.Expand(ContactSearchConfiguration.SectionKey.RECENTS))
|
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 android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.thoughtcrime.securesms.contacts.SelectedContact
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
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 requireRecipientSearchKey(): RecipientSearchKey = error("This key cannot be parcelized")
|
||||||
|
|
||||||
|
open fun requireSelectedContact(): SelectedContact = error("This key cannot be converted into a SelectedContact")
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RecipientSearchKey(val recipientId: RecipientId, val isStory: Boolean) : ContactSearchKey(), Parcelable {
|
data class RecipientSearchKey(val recipientId: RecipientId, val isStory: Boolean) : ContactSearchKey(), Parcelable {
|
||||||
override fun requireRecipientSearchKey(): RecipientSearchKey = this
|
override fun requireRecipientSearchKey(): RecipientSearchKey = this
|
||||||
|
|
||||||
override fun requireShareContact(): ShareContact = ShareContact(recipientId)
|
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
|
package org.thoughtcrime.securesms.contacts.paged
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
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 org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||||
import java.util.concurrent.TimeUnit
|
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(
|
class ContactSearchMediator(
|
||||||
private val fragment: Fragment,
|
private val fragment: Fragment,
|
||||||
|
private val fixedContacts: Set<ContactSearchKey> = setOf(),
|
||||||
selectionLimits: SelectionLimits,
|
selectionLimits: SelectionLimits,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||||
|
displayPhoneNumber: ContactSearchAdapter.DisplayPhoneNumber,
|
||||||
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
|
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
|
||||||
private val contactSelectionPreFilter: (View?, Set<ContactSearchKey>) -> Set<ContactSearchKey> = { _, s -> s },
|
private val callbacks: Callbacks = SimpleCallbacks(),
|
||||||
performSafetyNumberChecks: Boolean = true,
|
performSafetyNumberChecks: Boolean = true,
|
||||||
adapterFactory: AdapterFactory = DefaultAdapterFactory,
|
adapterFactory: AdapterFactory = DefaultAdapterFactory,
|
||||||
arbitraryRepository: ArbitraryRepository? = null,
|
arbitraryRepository: ArbitraryRepository? = null,
|
||||||
|
@ -49,8 +67,11 @@ class ContactSearchMediator(
|
||||||
)[ContactSearchViewModel::class.java]
|
)[ContactSearchViewModel::class.java]
|
||||||
|
|
||||||
val adapter = adapterFactory.create(
|
val adapter = adapterFactory.create(
|
||||||
|
context = fragment.requireContext(),
|
||||||
|
fixedContacts = fixedContacts,
|
||||||
displayCheckBox = displayCheckBox,
|
displayCheckBox = displayCheckBox,
|
||||||
displaySmsTag = displaySmsTag,
|
displaySmsTag = displaySmsTag,
|
||||||
|
displayPhoneNumber = displayPhoneNumber,
|
||||||
callbacks = object : ContactSearchAdapter.ClickCallbacks {
|
callbacks = object : ContactSearchAdapter.ClickCallbacks {
|
||||||
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
|
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
|
||||||
toggleStorySelection(view, story, isSelected)
|
toggleStorySelection(view, story, isSelected)
|
||||||
|
@ -64,6 +85,7 @@ class ContactSearchMediator(
|
||||||
viewModel.expandSection(expand.sectionKey)
|
viewModel.expandSection(expand.sectionKey)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
longClickCallbacks = ContactSearchAdapter.LongClickCallbacksAdapter(),
|
||||||
storyContextMenuCallbacks = StoryContextMenuCallbacks()
|
storyContextMenuCallbacks = StoryContextMenuCallbacks()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,7 +97,9 @@ class ContactSearchMediator(
|
||||||
)
|
)
|
||||||
|
|
||||||
dataAndSelection.observe(fragment.viewLifecycleOwner) { (data, selection) ->
|
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 ->
|
viewModel.controller.observe(fragment.viewLifecycleOwner) { controller ->
|
||||||
|
@ -98,17 +122,28 @@ class ContactSearchMediator(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setKeysSelected(keys: Set<ContactSearchKey>) {
|
fun setKeysSelected(keys: Set<ContactSearchKey>) {
|
||||||
viewModel.setKeysSelected(contactSelectionPreFilter(null, keys))
|
viewModel.setKeysSelected(callbacks.onBeforeContactsSelected(null, keys))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setKeysNotSelected(keys: Set<ContactSearchKey>) {
|
fun setKeysNotSelected(keys: Set<ContactSearchKey>) {
|
||||||
|
keys.forEach {
|
||||||
|
callbacks.onContactDeselected(null, it)
|
||||||
|
}
|
||||||
viewModel.setKeysNotSelected(keys)
|
viewModel.setKeysNotSelected(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearSelection() {
|
||||||
|
viewModel.clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
fun getSelectedContacts(): Set<ContactSearchKey> {
|
fun getSelectedContacts(): Set<ContactSearchKey> {
|
||||||
return viewModel.getSelectedContacts()
|
return viewModel.getSelectedContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getFixedContactsSize(): Int {
|
||||||
|
return fixedContacts.size
|
||||||
|
}
|
||||||
|
|
||||||
fun getSelectionState(): LiveData<Set<ContactSearchKey>> {
|
fun getSelectionState(): LiveData<Set<ContactSearchKey>> {
|
||||||
return viewModel.selectionState
|
return viewModel.selectionState
|
||||||
}
|
}
|
||||||
|
@ -135,9 +170,10 @@ class ContactSearchMediator(
|
||||||
|
|
||||||
private fun toggleSelection(view: View, contactSearchData: ContactSearchData, isSelected: Boolean) {
|
private fun toggleSelection(view: View, contactSearchData: ContactSearchData, isSelected: Boolean) {
|
||||||
return if (isSelected) {
|
return if (isSelected) {
|
||||||
|
callbacks.onContactDeselected(view, contactSearchData.contactSearchKey)
|
||||||
viewModel.setKeysNotSelected(setOf(contactSearchData.contactSearchKey))
|
viewModel.setKeysNotSelected(setOf(contactSearchData.contactSearchKey))
|
||||||
} else {
|
} 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
|
* Wraps the construction of a PagingMappingAdapter<ContactSearchKey> so that it can
|
||||||
* be swapped for another implementation, allow listeners to be wrapped, etc.
|
* be swapped for another implementation, allow listeners to be wrapped, etc.
|
||||||
*/
|
*/
|
||||||
fun interface AdapterFactory {
|
fun interface AdapterFactory {
|
||||||
fun create(
|
fun create(
|
||||||
|
context: Context,
|
||||||
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||||
|
displayPhoneNumber: ContactSearchAdapter.DisplayPhoneNumber,
|
||||||
callbacks: ContactSearchAdapter.ClickCallbacks,
|
callbacks: ContactSearchAdapter.ClickCallbacks,
|
||||||
|
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
||||||
): PagingMappingAdapter<ContactSearchKey>
|
): PagingMappingAdapter<ContactSearchKey>
|
||||||
}
|
}
|
||||||
|
|
||||||
private object DefaultAdapterFactory : AdapterFactory {
|
private object DefaultAdapterFactory : AdapterFactory {
|
||||||
override fun create(
|
override fun create(
|
||||||
|
context: Context,
|
||||||
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||||
|
displayPhoneNumber: ContactSearchAdapter.DisplayPhoneNumber,
|
||||||
callbacks: ContactSearchAdapter.ClickCallbacks,
|
callbacks: ContactSearchAdapter.ClickCallbacks,
|
||||||
|
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
||||||
): PagingMappingAdapter<ContactSearchKey> {
|
): 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 android.database.Cursor
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.paging.PagedDataSource
|
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.ContactSearchCollection
|
||||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
|
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
|
||||||
import org.thoughtcrime.securesms.contacts.paged.collections.CursorSearchIterator
|
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.GroupRecord
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.keyvalue.StorySend
|
import org.thoughtcrime.securesms.keyvalue.StorySend
|
||||||
|
import org.thoughtcrime.securesms.phonenumbers.NumberUtil
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.search.MessageResult
|
import org.thoughtcrime.securesms.search.MessageResult
|
||||||
import org.thoughtcrime.securesms.search.MessageSearchResult
|
import org.thoughtcrime.securesms.search.MessageSearchResult
|
||||||
import org.thoughtcrime.securesms.search.SearchRepository
|
import org.thoughtcrime.securesms.search.SearchRepository
|
||||||
import org.thoughtcrime.securesms.search.ThreadSearchResult
|
import org.thoughtcrime.securesms.search.ThreadSearchResult
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
|
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -116,6 +120,8 @@ class ContactSearchPagedDataSource(
|
||||||
is ContactSearchConfiguration.Section.Messages -> getMessageData(query).getCollectionSize(section, query, null)
|
is ContactSearchConfiguration.Section.Messages -> getMessageData(query).getCollectionSize(section, query, null)
|
||||||
is ContactSearchConfiguration.Section.GroupsWithMembers -> getGroupsWithMembersIterator(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.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.Messages -> getMessageContactData(section, query, startIndex, endIndex)
|
||||||
is ContactSearchConfiguration.Section.GroupsWithMembers -> getGroupsWithMembersContactData(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.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> {
|
private fun getNonGroupSearchIterator(section: ContactSearchConfiguration.Section.Individuals, query: String?): ContactSearchIterator<Cursor> {
|
||||||
return when (section.transportType) {
|
return when (section.transportType) {
|
||||||
ContactSearchConfiguration.TransportType.PUSH -> CursorSearchIterator(contactSearchPagedDataSourceRepository.querySignalContacts(query, section.includeSelf))
|
ContactSearchConfiguration.TransportType.PUSH -> CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.querySignalContacts(query, section.includeSelf)))
|
||||||
ContactSearchConfiguration.TransportType.SMS -> CursorSearchIterator(contactSearchPagedDataSourceRepository.queryNonSignalContacts(query))
|
ContactSearchConfiguration.TransportType.SMS -> CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.queryNonSignalContacts(query)))
|
||||||
ContactSearchConfiguration.TransportType.ALL -> CursorSearchIterator(contactSearchPagedDataSourceRepository.queryNonGroupContacts(query, section.includeSelf))
|
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> {
|
private fun getNonGroupHeaderLetterMap(section: ContactSearchConfiguration.Section.Individuals, query: String?): Map<RecipientId, String> {
|
||||||
return when (section.transportType) {
|
return when (section.transportType) {
|
||||||
ContactSearchConfiguration.TransportType.PUSH -> contactSearchPagedDataSourceRepository.querySignalContactLetterHeaders(query, section.includeSelf)
|
ContactSearchConfiguration.TransportType.PUSH -> contactSearchPagedDataSourceRepository.querySignalContactLetterHeaders(query, section.includeSelf)
|
||||||
|
|
|
@ -112,6 +112,10 @@ class ContactSearchViewModel(
|
||||||
return selectionStore.state
|
return selectionStore.state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearSelection() {
|
||||||
|
selectionStore.update { emptySet() }
|
||||||
|
}
|
||||||
|
|
||||||
fun addToVisibleGroupStories(groupStories: Set<ContactSearchKey.RecipientSearchKey>) {
|
fun addToVisibleGroupStories(groupStories: Set<ContactSearchKey.RecipientSearchKey>) {
|
||||||
disposables += contactSearchRepository.markDisplayAsStory(groupStories.map { it.recipientId }).subscribe {
|
disposables += contactSearchRepository.markDisplayAsStory(groupStories.map { it.recipientId }).subscribe {
|
||||||
configurationStore.update { state ->
|
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 android.os.Bundle
|
||||||
import org.thoughtcrime.securesms.R
|
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.groups.SelectionLimits
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
data class ContactSelectionArguments(
|
data class ContactSelectionArguments(
|
||||||
val displayMode: Int = ContactsCursorLoader.DisplayMode.FLAG_ALL,
|
val displayMode: Int = ContactSelectionDisplayMode.FLAG_ALL,
|
||||||
val isRefreshable: Boolean = true,
|
val isRefreshable: Boolean = true,
|
||||||
val displayRecents: Boolean = false,
|
val displayRecents: Boolean = false,
|
||||||
val selectionLimits: SelectionLimits? = null,
|
val selectionLimits: SelectionLimits? = null,
|
||||||
|
|
|
@ -120,11 +120,17 @@ class MultiselectForwardFragment :
|
||||||
contactSearchRecycler = view.findViewById(R.id.contact_selection_list)
|
contactSearchRecycler = view.findViewById(R.id.contact_selection_list)
|
||||||
contactSearchMediator = ContactSearchMediator(
|
contactSearchMediator = ContactSearchMediator(
|
||||||
this,
|
this,
|
||||||
|
emptySet(),
|
||||||
FeatureFlags.shareSelectionLimit(),
|
FeatureFlags.shareSelectionLimit(),
|
||||||
!args.selectSingleRecipient,
|
!args.selectSingleRecipient,
|
||||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||||
|
ContactSearchAdapter.DisplayPhoneNumber.NEVER,
|
||||||
this::getConfiguration,
|
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
|
contactSearchRecycler.adapter = contactSearchMediator.adapter
|
||||||
|
|
|
@ -294,22 +294,32 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
cameraFab.setVisibility(View.VISIBLE);
|
cameraFab.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
contactSearchMediator = new ContactSearchMediator(this,
|
contactSearchMediator = new ContactSearchMediator(this,
|
||||||
|
Collections.emptySet(),
|
||||||
SelectionLimits.NO_LIMITS,
|
SelectionLimits.NO_LIMITS,
|
||||||
false,
|
false,
|
||||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||||
|
ContactSearchAdapter.DisplayPhoneNumber.NEVER,
|
||||||
this::mapSearchStateToConfiguration,
|
this::mapSearchStateToConfiguration,
|
||||||
(v, s) -> s,
|
new ContactSearchMediator.SimpleCallbacks(),
|
||||||
false,
|
false,
|
||||||
(displayCheckBox,
|
(context,
|
||||||
|
fixedContacts,
|
||||||
|
displayCheckBox,
|
||||||
displaySmsTag,
|
displaySmsTag,
|
||||||
|
displayPhoneNumber,
|
||||||
callbacks,
|
callbacks,
|
||||||
|
longClickCallbacks,
|
||||||
storyContextMenuCallbacks
|
storyContextMenuCallbacks
|
||||||
) -> {
|
) -> {
|
||||||
//noinspection CodeBlock2Expr
|
//noinspection CodeBlock2Expr
|
||||||
return new ConversationListSearchAdapter(
|
return new ConversationListSearchAdapter(
|
||||||
|
context,
|
||||||
|
fixedContacts,
|
||||||
displayCheckBox,
|
displayCheckBox,
|
||||||
displaySmsTag,
|
displaySmsTag,
|
||||||
|
displayPhoneNumber,
|
||||||
new ContactSearchClickCallbacks(callbacks),
|
new ContactSearchClickCallbacks(callbacks),
|
||||||
|
longClickCallbacks,
|
||||||
storyContextMenuCallbacks,
|
storyContextMenuCallbacks,
|
||||||
getViewLifecycleOwner(),
|
getViewLifecycleOwner(),
|
||||||
GlideApp.with(this)
|
GlideApp.with(this)
|
||||||
|
@ -1894,6 +1904,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
public void onExpandClicked(@NonNull ContactSearchData.Expand expand) {
|
public void onExpandClicked(@NonNull ContactSearchData.Expand expand) {
|
||||||
delegate.onExpandClicked(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 {
|
public interface Callback extends Material3OnScrollHelperBinder, SearchBinder {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.thoughtcrime.securesms.conversationlist
|
package org.thoughtcrime.securesms.conversationlist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.os.bundleOf
|
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.ContactSearchAdapter
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
|
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.conversationlist.model.ConversationSet
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
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.
|
* as well as ChatFilter row support and empty state handler.
|
||||||
*/
|
*/
|
||||||
class ConversationListSearchAdapter(
|
class ConversationListSearchAdapter(
|
||||||
|
context: Context,
|
||||||
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: DisplaySmsTag,
|
displaySmsTag: DisplaySmsTag,
|
||||||
|
displayPhoneNumber: DisplayPhoneNumber,
|
||||||
onClickedCallbacks: ConversationListSearchClickCallbacks,
|
onClickedCallbacks: ConversationListSearchClickCallbacks,
|
||||||
|
longClickCallbacks: LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
glideRequests: GlideRequests
|
glideRequests: GlideRequests
|
||||||
) : ContactSearchAdapter(displayCheckBox, displaySmsTag, onClickedCallbacks, storyContextMenuCallbacks) {
|
) : ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displayPhoneNumber, onClickedCallbacks, longClickCallbacks, storyContextMenuCallbacks) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerFactory(
|
registerFactory(
|
||||||
|
|
|
@ -4362,7 +4362,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM ${GroupTable.MembershipTable.TABLE_NAME}
|
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}
|
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()
|
""".toSingleLine()
|
||||||
const val FILTER_GROUPS = " AND $GROUP_ID IS NULL"
|
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.ContactSelectionActivity;
|
||||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||||
import org.thoughtcrime.securesms.R;
|
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.groups.ui.addtogroup.AddToGroupViewModel.Event;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
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(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.add_to_group_activity);
|
||||||
intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
|
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));
|
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.ContactSelectionActivity;
|
||||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||||
import org.thoughtcrime.securesms.R;
|
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.contacts.sync.ContactDiscovery;
|
||||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||||
import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity;
|
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);
|
intent.putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.create_group_activity);
|
||||||
|
|
||||||
boolean smsEnabled = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
boolean smsEnabled = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||||
int displayMode = smsEnabled ? ContactsCursorLoader.DisplayMode.FLAG_SMS | ContactsCursorLoader.DisplayMode.FLAG_PUSH
|
int displayMode = smsEnabled ? ContactSelectionDisplayMode.FLAG_SMS | ContactSelectionDisplayMode.FLAG_PUSH
|
||||||
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
|
: ContactSelectionDisplayMode.FLAG_PUSH;
|
||||||
|
|
||||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.groupLimits().excludingSelf());
|
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.groupLimits().excludingSelf());
|
||||||
|
|
|
@ -67,6 +67,7 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment(
|
||||||
selectionLimits = FeatureFlags.shareSelectionLimit(),
|
selectionLimits = FeatureFlags.shareSelectionLimit(),
|
||||||
displayCheckBox = true,
|
displayCheckBox = true,
|
||||||
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||||
|
displayPhoneNumber = ContactSearchAdapter.DisplayPhoneNumber.NEVER,
|
||||||
mapStateToConfiguration = { state ->
|
mapStateToConfiguration = { state ->
|
||||||
ContactSearchConfiguration.build {
|
ContactSearchConfiguration.build {
|
||||||
query = state.query
|
query = state.query
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
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.conversation.ConversationIntents;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.payments.CanNotSendPaymentDialog;
|
import org.thoughtcrime.securesms.payments.CanNotSendPaymentDialog;
|
||||||
|
@ -49,7 +49,7 @@ public class PaymentRecipientSelectionFragment extends LoggingFragment implement
|
||||||
|
|
||||||
Bundle arguments = new Bundle();
|
Bundle arguments = new Bundle();
|
||||||
arguments.putBoolean(ContactSelectionListFragment.REFRESHABLE, false);
|
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);
|
arguments.putBoolean(ContactSelectionListFragment.CAN_SELECT_SELF, false);
|
||||||
|
|
||||||
Fragment child = getChildFragmentManager().findFragmentById(R.id.contact_selection_list_fragment_holder);
|
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),
|
selectionLimits = SelectionLimits(0, 0),
|
||||||
displayCheckBox = false,
|
displayCheckBox = false,
|
||||||
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.IF_NOT_REGISTERED,
|
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.IF_NOT_REGISTERED,
|
||||||
|
displayPhoneNumber = ContactSearchAdapter.DisplayPhoneNumber.NEVER,
|
||||||
mapStateToConfiguration = { getConfiguration() },
|
mapStateToConfiguration = { getConfiguration() },
|
||||||
performSafetyNumberChecks = false
|
performSafetyNumberChecks = false
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,13 +12,12 @@ import com.google.android.material.button.MaterialButton
|
||||||
import org.signal.core.util.dp
|
import org.signal.core.util.dp
|
||||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||||
import org.thoughtcrime.securesms.R
|
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.HeaderAction
|
||||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments
|
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
@ -78,7 +77,7 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b
|
||||||
|
|
||||||
viewModel.state.observe(viewLifecycleOwner) {
|
viewModel.state.observe(viewLifecycleOwner) {
|
||||||
if (it.distributionListId == null || it.privateStory != null) {
|
if (it.distributionListId == null || it.privateStory != null) {
|
||||||
getAttachedContactSelectionFragment().markSelected(it.selection.map(::ShareContact).toSet())
|
getAttachedContactSelectionFragment().markSelected(it.selection.toSet())
|
||||||
presentTitle(toolbar, it.selection.size)
|
presentTitle(toolbar, it.selection.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,7 +140,7 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b
|
||||||
private fun initializeContactSelectionFragment() {
|
private fun initializeContactSelectionFragment() {
|
||||||
val contactSelectionListFragment = ContactSelectionListFragment()
|
val contactSelectionListFragment = ContactSelectionListFragment()
|
||||||
val arguments = ContactSelectionArguments(
|
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,
|
isRefreshable = false,
|
||||||
displayRecents = false,
|
displayRecents = false,
|
||||||
selectionLimits = SelectionLimits.NO_LIMITS,
|
selectionLimits = SelectionLimits.NO_LIMITS,
|
||||||
|
|
|
@ -80,6 +80,6 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/blocked_users_header"
|
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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,46 +1,35 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
tools:viewBindingIgnore="true"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/rounded_inset_ripple_background"
|
android:background="@drawable/conversation_item_background"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:minHeight="@dimen/contact_selection_item_height"
|
android:minHeight="@dimen/contact_selection_item_height"
|
||||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
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:id="@+id/contact_photo_image"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
|
android:background="@color/signal_colorSurfaceVariant"
|
||||||
android:cropToPadding="true"
|
android:importantForAccessibility="no"
|
||||||
android:foreground="@drawable/contact_photo_background"
|
app:contentPadding="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:ignore="UnusedAttribute"
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle"
|
||||||
tools:src="@color/blue_600" />
|
app:srcCompat="@drawable/ic_search_24"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
<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" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatCheckBox
|
<androidx.appcompat.widget.AppCompatCheckBox
|
||||||
android:id="@+id/check_box"
|
android:id="@+id/check_box"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:background="@drawable/contact_selection_checkbox"
|
android:background="?contactCheckboxBackground"
|
||||||
android:button="@null"
|
android:button="@null"
|
||||||
android:clickable="false"
|
android:clickable="false"
|
||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
|
@ -58,10 +47,10 @@
|
||||||
android:drawablePadding="4dp"
|
android:drawablePadding="4dp"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
android:textColor="@color/signal_text_primary"
|
android:textColor="@color/signal_text_primary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/number"
|
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_constraintStart_toEndOf="@id/contact_photo_image"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
@ -76,47 +65,17 @@
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
android:textColor="@color/signal_text_secondary"
|
android:textColor="@color/signal_text_secondary"
|
||||||
android:textDirection="ltr"
|
android:textDirection="ltr"
|
||||||
app:emoji_forceCustom="true"
|
app:emoji_forceCustom="true"
|
||||||
app:layout_constrainedWidth="true"
|
app:layout_constrainedWidth="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
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_bias="0"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintStart_toEndOf="@id/contact_photo_image"
|
app:layout_constraintStart_toEndOf="@id/contact_photo_image"
|
||||||
app:layout_constraintTop_toBottomOf="@id/name"
|
app:layout_constraintTop_toBottomOf="@id/name"
|
||||||
tools:text="@sample/contacts.json/data/number" />
|
tools:text="@sample/contacts.json/data/number" />
|
||||||
|
|
||||||
<TextView
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
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>
|
|
|
@ -24,7 +24,7 @@
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:scrollbarThumbVertical="@drawable/contact_selection_scrollbar_thumb"
|
android:scrollbarThumbVertical="@drawable/contact_selection_scrollbar_thumb"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
tools:listitem="@layout/contact_selection_list_item" />
|
tools:listitem="@layout/contact_search_item" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@android:id/empty"
|
android:id="@android:id/empty"
|
||||||
|
|
|
@ -30,12 +30,13 @@ class ContactSearchPagedDataSourceTest {
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
whenever(repository.getRecipientFromGroupRecord(any())).thenReturn(Recipient.UNKNOWN)
|
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.getRecipientFromThreadCursor(cursor)).thenReturn(Recipient.UNKNOWN)
|
||||||
whenever(repository.getRecipientFromDistributionListCursor(cursor)).thenReturn(Recipient.UNKNOWN)
|
whenever(repository.getRecipientFromDistributionListCursor(cursor)).thenReturn(Recipient.UNKNOWN)
|
||||||
whenever(repository.getPrivacyModeFromDistributionListCursor(cursor)).thenReturn(DistributionListPrivacyMode.ALL)
|
whenever(repository.getPrivacyModeFromDistributionListCursor(cursor)).thenReturn(DistributionListPrivacyMode.ALL)
|
||||||
whenever(repository.getGroupStories()).thenReturn(emptySet())
|
whenever(repository.getGroupStories()).thenReturn(emptySet())
|
||||||
whenever(repository.getLatestStorySends(any())).thenReturn(emptyList())
|
whenever(repository.getLatestStorySends(any())).thenReturn(emptyList())
|
||||||
|
whenever(cursor.getString(any())).thenReturn("A")
|
||||||
whenever(cursor.moveToPosition(any())).thenCallRealMethod()
|
whenever(cursor.moveToPosition(any())).thenCallRealMethod()
|
||||||
whenever(cursor.moveToNext()).thenCallRealMethod()
|
whenever(cursor.moveToNext()).thenCallRealMethod()
|
||||||
whenever(cursor.position).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