diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 31b45f2783..d2127f978d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -354,6 +354,11 @@
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+
+
,
- displayCheckBox: Boolean,
- displaySmsTag: DisplaySmsTag,
- displaySecondaryInformation: DisplaySecondaryInformation,
+ displayOptions: DisplayOptions,
onClickCallbacks: OnContactSelectionClick,
longClickCallbacks: LongClickCallbacks,
- storyContextMenuCallbacks: StoryContextMenuCallbacks
-) : ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, onClickCallbacks, longClickCallbacks, storyContextMenuCallbacks) {
+ storyContextMenuCallbacks: StoryContextMenuCallbacks,
+ callButtonClickCallbacks: CallButtonClickCallbacks
+) : ContactSearchAdapter(context, fixedContacts, displayOptions, onClickCallbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) {
init {
registerFactory(NewGroupModel::class.java, LayoutFactory({ NewGroupViewHolder(it, onClickCallbacks::onNewGroupClicked) }, R.layout.contact_selection_new_group_item))
registerFactory(InviteToSignalModel::class.java, LayoutFactory({ InviteToSignalViewHolder(it, onClickCallbacks::onInviteToSignalClicked) }, R.layout.contact_selection_invite_action_item))
+ registerFactory(RefreshContactsModel::class.java, LayoutFactory({ RefreshContactsViewHolder(it, onClickCallbacks::onRefreshContactsClicked) }, R.layout.contact_selection_refresh_action_item))
+ registerFactory(MoreHeaderModel::class.java, LayoutFactory({ MoreHeaderViewHolder(it) }, R.layout.contact_search_section_header))
+ registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.contact_selection_empty_state))
}
class NewGroupModel : MappingModel {
@@ -36,6 +39,17 @@ class ContactSelectionListAdapter(
override fun areContentsTheSame(newItem: InviteToSignalModel): Boolean = true
}
+ class RefreshContactsModel : MappingModel {
+ override fun areItemsTheSame(newItem: RefreshContactsModel): Boolean = true
+ override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
+ }
+
+ class MoreHeaderModel : MappingModel {
+ override fun areItemsTheSame(newItem: MoreHeaderModel): Boolean = true
+
+ override fun areContentsTheSame(newItem: MoreHeaderModel): Boolean = true
+ }
+
private class InviteToSignalViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
@@ -52,11 +66,39 @@ class ContactSelectionListAdapter(
override fun bind(model: NewGroupModel) = Unit
}
+ private class RefreshContactsViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder(itemView) {
+ init {
+ itemView.setOnClickListener { onClickListener() }
+ }
+
+ override fun bind(model: RefreshContactsModel) = Unit
+ }
+
+ private class MoreHeaderViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val headerTextView: TextView = itemView.findViewById(R.id.section_header)
+
+ override fun bind(model: MoreHeaderModel) {
+ headerTextView.setText(R.string.contact_selection_activity__more)
+ }
+ }
+
+ private class EmptyViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val emptyText: TextView = itemView.findViewById(R.id.search_no_results)
+
+ override fun bind(model: EmptyModel) {
+ emptyText.text = context.getString(R.string.SearchFragment_no_results, model.empty.query)
+ }
+ }
+
class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
enum class ArbitraryRow(val code: String) {
NEW_GROUP("new-group"),
- INVITE_TO_SIGNAL("invite-to-signal");
+ INVITE_TO_SIGNAL("invite-to-signal"),
+ MORE_HEADING("more-heading"),
+ REFRESH_CONTACTS("refresh-contacts");
companion object {
fun fromCode(code: String) = values().first { it.code == code }
@@ -64,7 +106,7 @@ class ContactSelectionListAdapter(
}
override fun getSize(section: ContactSearchConfiguration.Section.Arbitrary, query: String?): Int {
- return if (query.isNullOrEmpty()) section.types.size else 0
+ return section.types.size
}
override fun getData(section: ContactSearchConfiguration.Section.Arbitrary, query: String?, startIndex: Int, endIndex: Int, totalSearchSize: Int): List {
@@ -73,10 +115,11 @@ class ContactSelectionListAdapter(
}
override fun getMappingModel(arbitrary: ContactSearchData.Arbitrary): MappingModel<*> {
- val code = ArbitraryRow.fromCode(arbitrary.type)
- return when (code) {
+ return when (ArbitraryRow.fromCode(arbitrary.type)) {
ArbitraryRow.NEW_GROUP -> NewGroupModel()
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
+ ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
+ ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
}
}
}
@@ -84,5 +127,6 @@ class ContactSelectionListAdapter(
interface OnContactSelectionClick : ClickCallbacks {
fun onNewGroupClicked()
fun onInviteToSignalClicked()
+ fun onRefreshContactsClicked()
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
index 39b285d8a2..aebef2833f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
@@ -47,8 +47,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.transition.AutoTransition;
import androidx.transition.TransitionManager;
-import com.annimon.stream.Collectors;
-import com.annimon.stream.Stream;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel;
@@ -74,6 +72,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
@@ -98,8 +97,7 @@ import kotlin.Unit;
*
* @author Moxie Marlinspike
*/
-public final class ContactSelectionListFragment extends LoggingFragment
-{
+public final class ContactSelectionListFragment extends LoggingFragment {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
@@ -119,27 +117,28 @@ public final class ContactSelectionListFragment extends LoggingFragment
public static final String RV_PADDING_BOTTOM = "recycler_view_padding_bottom";
public static final String RV_CLIP = "recycler_view_clipping";
- private ConstraintLayout constraintLayout;
- private TextView emptyText;
- private OnContactSelectedListener onContactSelectedListener;
- private SwipeRefreshLayout swipeRefresh;
- private View showContactsLayout;
- private Button showContactsButton;
- private TextView showContactsDescription;
- private ProgressWheel showContactsProgress;
- private String cursorFilter;
- private RecyclerView recyclerView;
- private RecyclerViewFastScroller fastScroller;
- private RecyclerView chipRecycler;
- private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
- private MappingAdapter contactChipAdapter;
- private ContactChipViewModel contactChipViewModel;
- private LifecycleDisposable lifecycleDisposable;
- private HeaderActionProvider headerActionProvider;
- private TextView headerActionView;
- private ContactSearchMediator contactSearchMediator;
+ private ConstraintLayout constraintLayout;
+ private TextView emptyText;
+ private OnContactSelectedListener onContactSelectedListener;
+ private SwipeRefreshLayout swipeRefresh;
+ private View showContactsLayout;
+ private Button showContactsButton;
+ private TextView showContactsDescription;
+ private ProgressWheel showContactsProgress;
+ private String cursorFilter;
+ private RecyclerView recyclerView;
+ private RecyclerViewFastScroller fastScroller;
+ private RecyclerView chipRecycler;
+ private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
+ private MappingAdapter contactChipAdapter;
+ private ContactChipViewModel contactChipViewModel;
+ private LifecycleDisposable lifecycleDisposable;
+ private HeaderActionProvider headerActionProvider;
+ private TextView headerActionView;
+ private ContactSearchMediator contactSearchMediator;
- @Nullable private ListCallback listCallback;
+ @Nullable private NewConversationCallback newConversationCallback;
+ @Nullable private NewCallCallback newCallCallback;
@Nullable private ScrollCallback scrollCallback;
@Nullable private OnItemLongClickListener onItemLongClickListener;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
@@ -152,8 +151,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
public void onAttach(@NonNull Context context) {
super.onAttach(context);
- if (context instanceof ListCallback) {
- listCallback = (ListCallback) context;
+ if (context instanceof NewConversationCallback) {
+ newConversationCallback = (NewConversationCallback) context;
+ }
+
+ if (context instanceof NewCallCallback) {
+ newCallCallback = (NewCallCallback) context;
}
if (getParentFragment() instanceof ScrollCallback) {
@@ -234,17 +237,17 @@ public final class ContactSelectionListFragment extends LoggingFragment
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
- emptyText = view.findViewById(android.R.id.empty);
- recyclerView = view.findViewById(R.id.recycler_view);
- swipeRefresh = view.findViewById(R.id.swipe_refresh);
- fastScroller = view.findViewById(R.id.fast_scroller);
- showContactsLayout = view.findViewById(R.id.show_contacts_container);
- showContactsButton = view.findViewById(R.id.show_contacts_button);
- showContactsDescription = view.findViewById(R.id.show_contacts_description);
- showContactsProgress = view.findViewById(R.id.progress);
- chipRecycler = view.findViewById(R.id.chipRecycler);
- constraintLayout = view.findViewById(R.id.container);
- headerActionView = view.findViewById(R.id.header_action);
+ emptyText = view.findViewById(android.R.id.empty);
+ recyclerView = view.findViewById(R.id.recycler_view);
+ swipeRefresh = view.findViewById(R.id.swipe_refresh);
+ fastScroller = view.findViewById(R.id.fast_scroller);
+ showContactsLayout = view.findViewById(R.id.show_contacts_container);
+ showContactsButton = view.findViewById(R.id.show_contacts_button);
+ showContactsDescription = view.findViewById(R.id.show_contacts_description);
+ showContactsProgress = view.findViewById(R.id.progress);
+ chipRecycler = view.findViewById(R.id.chipRecycler);
+ constraintLayout = view.findViewById(R.id.container);
+ headerActionView = view.findViewById(R.id.header_action);
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
@@ -337,9 +340,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
.map(r -> new ContactSearchKey.RecipientSearchKey(r, false))
.collect(java.util.stream.Collectors.toSet()),
selectionLimit,
- isMulti,
- ContactSearchAdapter.DisplaySmsTag.DEFAULT,
- ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
+ new ContactSearchAdapter.DisplayOptions(
+ isMulti,
+ ContactSearchAdapter.DisplaySmsTag.DEFAULT,
+ ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
+ newCallCallback != null
+ ),
this::mapStateToConfiguration,
new ContactSearchMediator.SimpleCallbacks() {
@Override
@@ -348,21 +354,30 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
},
false,
- (context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, callbacks, longClickCallbacks, storyContextMenuCallbacks) -> new ContactSelectionListAdapter(
+ (context, fixedContacts, displayOptions, callbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) -> new ContactSelectionListAdapter(
context,
fixedContacts,
- displayCheckBox,
- displaySmsTag,
- displaySecondaryInformation,
+ displayOptions,
new ContactSelectionListAdapter.OnContactSelectionClick() {
+ @Override
+ public void onRefreshContactsClicked() {
+ newCallCallback.onRefresh();
+ }
+
@Override
public void onNewGroupClicked() {
- listCallback.onNewGroup(false);
+ newConversationCallback.onNewGroup(false);
}
@Override
public void onInviteToSignalClicked() {
- listCallback.onInvite();
+ if (newConversationCallback != null) {
+ newConversationCallback.onInvite();
+ }
+
+ if (newCallCallback != null) {
+ newCallCallback.onInvite();
+ }
}
@Override
@@ -386,7 +401,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
},
(anchorView, data) -> listClickListener.onItemLongClick(anchorView, data.getContactSearchKey()),
- storyContextMenuCallbacks
+ storyContextMenuCallbacks,
+ new CallButtonClickCallbacks()
+
),
new ContactSelectionListAdapter.ArbitraryRepository()
);
@@ -805,6 +822,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
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);
+ boolean includeGroupMembers = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_GROUP_MEMBERS);
+ boolean hasQuery = !TextUtils.isEmpty(contactSearchState.getQuery());
ContactSearchConfiguration.TransportType transportType = resolveTransportType(includePushContacts, includeSmsContacts);
ContactSearchConfiguration.Section.Recents.Mode mode = resolveRecentsMode(transportType, includeActiveGroups);
@@ -813,12 +832,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
return ContactSearchConfiguration.build(builder -> {
builder.setQuery(contactSearchState.getQuery());
- if (listCallback != null) {
+ if (newConversationCallback != null) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
}
if (transportType != null) {
- if (TextUtils.isEmpty(contactSearchState.getQuery()) && includeRecents) {
+ if (!hasQuery && includeRecents) {
builder.addSection(new ContactSearchConfiguration.Section.Recents(
25,
mode,
@@ -834,13 +853,13 @@ public final class ContactSelectionListFragment extends LoggingFragment
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
includeSelf,
transportType,
- true,
+ newCallCallback == null,
null,
!hideLetterHeaders()
));
}
- if ((includeGroupsAfterContacts || !TextUtils.isEmpty(contactSearchState.getQuery())) && includeActiveGroups) {
+ if ((includeGroupsAfterContacts || hasQuery) && includeActiveGroups) {
builder.addSection(new ContactSearchConfiguration.Section.Groups(
includeSmsContacts,
includeV1Groups,
@@ -853,18 +872,34 @@ public final class ContactSelectionListFragment extends LoggingFragment
));
}
- if (listCallback != null) {
- builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
+ if (hasQuery && includeGroupMembers) {
+ builder.addSection(new ContactSearchConfiguration.Section.GroupMembers());
}
if (includeNew) {
builder.phone(newRowMode);
builder.username(newRowMode);
}
+
+ if (newCallCallback != null || newConversationCallback != null) {
+ addMoreSection(builder);
+ builder.withEmptyState(emptyBuilder -> {
+ emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
+ addMoreSection(emptyBuilder);
+ return Unit.INSTANCE;
+ });
+ }
+
return Unit.INSTANCE;
});
}
+ private void addMoreSection(@NonNull ContactSearchConfiguration.Builder builder) {
+ builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.MORE_HEADING.getCode());
+ builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode());
+ builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
+ }
+
private static @Nullable ContactSearchConfiguration.TransportType resolveTransportType(boolean includePushContacts, boolean includeSmsContacts) {
if (includePushContacts && includeSmsContacts) {
return ContactSearchConfiguration.TransportType.ALL;
@@ -887,9 +922,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
}
- private static @NonNull ContactSearchConfiguration.NewRowMode resolveNewRowMode(boolean isBlocked, boolean isActiveGroups) {
+ private @NonNull ContactSearchConfiguration.NewRowMode resolveNewRowMode(boolean isBlocked, boolean isActiveGroups) {
if (isBlocked) {
return ContactSearchConfiguration.NewRowMode.BLOCK;
+ } else if (newCallCallback != null) {
+ return ContactSearchConfiguration.NewRowMode.NEW_CALL;
} else if (isActiveGroups) {
return ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION;
} else {
@@ -901,6 +938,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
return (mode & flag) > 0;
}
+ private class CallButtonClickCallbacks implements ContactSearchAdapter.CallButtonClickCallbacks {
+ @Override
+ public void onVideoCallButtonClicked(@NonNull Recipient recipient) {
+ CommunicationActions.startVideoCall(ContactSelectionListFragment.this, recipient);
+ }
+
+ @Override
+ public void onAudioCallButtonClicked(@NonNull Recipient recipient) {
+ CommunicationActions.startVoiceCall(ContactSelectionListFragment.this, recipient);
+ }
+ }
+
public interface OnContactSelectedListener {
/**
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
@@ -918,12 +967,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
void onHardLimitReached(int limit);
}
- public interface ListCallback {
+ public interface NewConversationCallback {
void onInvite();
void onNewGroup(boolean forceV1);
}
+ public interface NewCallCallback {
+ void onInvite();
+
+ void onRefresh();
+ }
+
public interface ScrollCallback {
void onBeginScroll();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
index 8ec57f1409..ae0f022bae 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
@@ -69,7 +69,7 @@ import java.util.stream.Stream;
* @author Moxie Marlinspike
*/
public class NewConversationActivity extends ContactSelectionActivity
- implements ContactSelectionListFragment.ListCallback, ContactSelectionListFragment.OnItemLongClickListener
+ implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener
{
@SuppressWarnings("unused")
diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt
index 4ff66601d8..0fb8982c43 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt
@@ -18,6 +18,7 @@ import io.reactivex.rxjava3.kotlin.Observables
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.calls.new.NewCallActivity
import org.thoughtcrime.securesms.components.Material3SearchToolbar
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
@@ -114,6 +115,9 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
binding.recycler.adapter = adapter
requireListener().bindScrollHelper(binding.recycler)
+ binding.fab.setOnClickListener {
+ startActivity(NewCallActivity.createIntent(requireContext()))
+ }
initializePullToFilter()
initializeTapToScrollToTop()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt
index 6d5832c008..f72d8f2a05 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt
@@ -1,10 +1,62 @@
package org.thoughtcrime.securesms.calls.new
-import android.annotation.SuppressLint
-import androidx.fragment.app.Fragment
-import org.thoughtcrime.securesms.components.FragmentWrapperActivity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import androidx.core.app.ActivityCompat
+import androidx.core.view.MenuProvider
+import org.thoughtcrime.securesms.ContactSelectionActivity
+import org.thoughtcrime.securesms.ContactSelectionListFragment
+import org.thoughtcrime.securesms.InviteActivity
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
-class NewCallActivity : FragmentWrapperActivity() {
- @SuppressLint("DiscouragedApi")
- override fun getFragment(): Fragment = NewCallFragment()
+class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment.NewCallCallback {
+
+ override fun onCreate(icicle: Bundle?, ready: Boolean) {
+ super.onCreate(icicle, ready)
+ requireNotNull(supportActionBar)
+ supportActionBar?.setTitle(R.string.NewCallActivity__new_call)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ addMenuProvider(NewCallMenuProvider())
+ }
+
+ override fun onSelectionChanged() = Unit
+
+ companion object {
+ fun createIntent(context: Context): Intent {
+ return Intent(context, NewCallActivity::class.java)
+ .putExtra(
+ ContactSelectionListFragment.DISPLAY_MODE,
+ ContactSelectionDisplayMode.none()
+ .withPush()
+ .withActiveGroups()
+ .withGroupMembers()
+ .build()
+ )
+ }
+ }
+
+ override fun onInvite() {
+ startActivity(Intent(this, InviteActivity::class.java))
+ }
+
+ private inner class NewCallMenuProvider : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.new_call_menu, menu)
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ when (menuItem.itemId) {
+ android.R.id.home -> ActivityCompat.finishAfterTransition(this@NewCallActivity)
+ R.id.menu_refresh -> onRefresh()
+ R.id.menu_invite -> startActivity(Intent(this@NewCallActivity, InviteActivity::class.java))
+ }
+
+ return true
+ }
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallFragment.kt
deleted file mode 100644
index efa838163c..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallFragment.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.thoughtcrime.securesms.calls.new
-
-import android.annotation.SuppressLint
-import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
-
-@SuppressLint("DiscouragedApi")
-class NewCallFragment : DSLSettingsFragment()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionDisplayMode.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionDisplayMode.java
index a4b4789735..2c0170125f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionDisplayMode.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionDisplayMode.java
@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.contacts;
+import androidx.annotation.NonNull;
+
public final class ContactSelectionDisplayMode {
public static final int FLAG_PUSH = 1;
public static final int FLAG_SMS = 1 << 1;
@@ -11,5 +13,50 @@ public final class ContactSelectionDisplayMode {
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_GROUP_MEMBERS = 1 << 9;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
+
+ public static Builder all() {
+ return new Builder(FLAG_ALL);
+ }
+
+ public static Builder none() {
+ return new Builder(0);
+ }
+
+ public static class Builder {
+ int displayMode = 0;
+
+ public Builder(int displayMode) {
+ this.displayMode = displayMode;
+ }
+
+ public @NonNull Builder withPush() {
+ displayMode = setFlag(displayMode, FLAG_PUSH);
+ return this;
+ }
+
+ public @NonNull Builder withActiveGroups() {
+ displayMode = setFlag(displayMode, FLAG_ACTIVE_GROUPS);
+ return this;
+ }
+
+ public @NonNull Builder withGroupMembers() {
+ displayMode = setFlag(displayMode, FLAG_GROUP_MEMBERS);
+ return this;
+ }
+
+ public int build() {
+ return displayMode;
+ }
+
+ private static int setFlag(int displayMode, int flag) {
+ return displayMode | flag;
+ }
+
+ private static int clearFlag(int displayMode, int flag) {
+ return displayMode & ~flag;
+ }
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt
index be65b4feae..12a93c5c5a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt
@@ -37,20 +37,19 @@ import org.thoughtcrime.securesms.util.visible
open class ContactSearchAdapter(
private val context: Context,
fixedContacts: Set,
- displayCheckBox: Boolean,
- displaySmsTag: DisplaySmsTag,
- displaySecondaryInformation: DisplaySecondaryInformation,
+ displayOptions: DisplayOptions,
onClickCallbacks: ClickCallbacks,
longClickCallbacks: LongClickCallbacks,
- storyContextMenuCallbacks: StoryContextMenuCallbacks
+ storyContextMenuCallbacks: StoryContextMenuCallbacks,
+ callButtonClickCallbacks: CallButtonClickCallbacks
) : PagingMappingAdapter(), FastScrollAdapter {
init {
- registerStoryItems(this, displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
- registerKnownRecipientItems(this, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick)
+ registerStoryItems(this, displayOptions.displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
+ registerKnownRecipientItems(this, fixedContacts, displayOptions, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick, callButtonClickCallbacks)
registerHeaders(this)
registerExpands(this, onClickCallbacks::onExpandClicked)
- registerFactory(UnknownRecipientModel::class.java, LayoutFactory({ UnknownRecipientViewHolder(it, onClickCallbacks::onUnknownRecipientClicked, displayCheckBox) }, R.layout.contact_search_unknown_item))
+ registerFactory(UnknownRecipientModel::class.java, LayoutFactory({ UnknownRecipientViewHolder(it, onClickCallbacks::onUnknownRecipientClicked, displayOptions.displayCheckBox) }, R.layout.contact_search_unknown_item))
}
override fun getBubbleText(position: Int): CharSequence {
@@ -82,15 +81,16 @@ open class ContactSearchAdapter(
fun registerKnownRecipientItems(
mappingAdapter: MappingAdapter,
fixedContacts: Set,
- displayCheckBox: Boolean,
- displaySmsTag: DisplaySmsTag,
- displaySecondaryInformation: DisplaySecondaryInformation,
+ displayOptions: DisplayOptions,
recipientListener: OnClickedCallback,
- recipientLongClickCallback: OnLongClickedCallback
+ recipientLongClickCallback: OnLongClickedCallback,
+ recipientCallButtonClickCallbacks: CallButtonClickCallbacks
) {
mappingAdapter.registerFactory(
RecipientModel::class.java,
- LayoutFactory({ KnownRecipientViewHolder(it, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, recipientListener, recipientLongClickCallback) }, R.layout.contact_search_item)
+ LayoutFactory({
+ KnownRecipientViewHolder(it, fixedContacts, displayOptions, recipientListener, recipientLongClickCallback, recipientCallButtonClickCallbacks)
+ }, R.layout.contact_search_item)
)
}
@@ -161,7 +161,7 @@ open class ContactSearchAdapter(
displayCheckBox: Boolean,
onClick: OnClickedCallback,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
- ) : BaseRecipientViewHolder(itemView, displayCheckBox, DisplaySmsTag.NEVER, onClick) {
+ ) : BaseRecipientViewHolder(itemView, DisplayOptions(displayCheckBox = displayCheckBox), onClick, EmptyCallButtonClickCallbacks) {
override fun isSelected(model: StoryModel): Boolean = model.isSelected
override fun getData(model: StoryModel): ContactSearchData.Story = model.story
override fun getRecipient(model: StoryModel): Recipient = model.story.recipient
@@ -334,6 +334,7 @@ open class ContactSearchAdapter(
checkbox.isSelected = false
name.setText(
when (model.data.mode) {
+ ContactSearchConfiguration.NewRowMode.NEW_CALL -> R.string.contact_selection_list__new_call
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
@@ -349,12 +350,11 @@ open class ContactSearchAdapter(
private class KnownRecipientViewHolder(
itemView: View,
private val fixedContacts: Set,
- displayCheckBox: Boolean,
- displaySmsTag: DisplaySmsTag,
- private val displaySecondaryInformation: DisplaySecondaryInformation,
+ displayOptions: DisplayOptions,
onClick: OnClickedCallback,
- private val onLongClick: OnLongClickedCallback
- ) : BaseRecipientViewHolder(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
+ private val onLongClick: OnLongClickedCallback,
+ callButtonClickCallbacks: CallButtonClickCallbacks
+ ) : BaseRecipientViewHolder(itemView, displayOptions, onClick, callButtonClickCallbacks), LetterHeaderDecoration.LetterHeaderItem {
private var headerLetter: String? = null
@@ -370,10 +370,10 @@ open class ContactSearchAdapter(
val count = recipient.participantIds.size
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
number.visible = true
- } else if (displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.combinedAboutAndEmoji != null) {
+ } else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.combinedAboutAndEmoji != null) {
number.text = recipient.combinedAboutAndEmoji
number.visible = true
- } else if (displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.hasE164()) {
+ } else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.hasE164()) {
number.text = PhoneNumberFormatter.prettyPrint(recipient.requireE164())
number.visible = true
} else {
@@ -410,9 +410,9 @@ open class ContactSearchAdapter(
*/
abstract class BaseRecipientViewHolder, D : ContactSearchData>(
itemView: View,
- private val displayCheckBox: Boolean,
- private val displaySmsTag: DisplaySmsTag,
- val onClick: OnClickedCallback
+ val displayOptions: DisplayOptions,
+ val onClick: OnClickedCallback,
+ val onCallButtonClickCallbacks: CallButtonClickCallbacks
) : MappingViewHolder(itemView) {
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
@@ -422,6 +422,8 @@ open class ContactSearchAdapter(
protected val number: TextView = itemView.findViewById(R.id.number)
protected val label: TextView = itemView.findViewById(R.id.label)
protected val smsTag: View = itemView.findViewById(R.id.sms_tag)
+ private val startAudio: View = itemView.findViewById(R.id.start_audio)
+ private val startVideo: View = itemView.findViewById(R.id.start_video)
override fun bind(model: T) {
if (isEnabled(model)) {
@@ -442,10 +444,11 @@ open class ContactSearchAdapter(
bindNumberField(model)
bindLabelField(model)
bindSmsTagField(model)
+ bindCallButtons(model)
}
protected open fun bindCheckbox(model: T) {
- checkbox.visible = displayCheckBox
+ checkbox.visible = displayOptions.displayCheckBox
checkbox.isChecked = isSelected(model)
}
@@ -476,7 +479,7 @@ open class ContactSearchAdapter(
}
protected open fun bindSmsTagField(model: T) {
- smsTag.visible = when (displaySmsTag) {
+ smsTag.visible = when (displayOptions.displaySmsTag) {
DisplaySmsTag.DEFAULT -> isSmsContact(model)
DisplaySmsTag.IF_NOT_REGISTERED -> isNotRegistered(model)
DisplaySmsTag.NEVER -> false
@@ -485,6 +488,25 @@ open class ContactSearchAdapter(
protected open fun bindLongPress(model: T) = Unit
+ private fun bindCallButtons(model: T) {
+ val recipient = getRecipient(model)
+ if (displayOptions.displayCallButtons && (recipient.isPushGroup || recipient.isRegistered)) {
+ startVideo.visible = true
+ startAudio.visible = !recipient.isPushGroup
+
+ startVideo.setOnClickListener {
+ onCallButtonClickCallbacks.onVideoCallButtonClicked(recipient)
+ }
+
+ startAudio.setOnClickListener {
+ onCallButtonClickCallbacks.onAudioCallButtonClicked(recipient)
+ }
+ } else {
+ startVideo.visible = false
+ startAudio.visible = false
+ }
+ }
+
private fun isSmsContact(model: T): Boolean {
return (getRecipient(model).isForceSmsSelection || getRecipient(model).isUnregistered) && !getRecipient(model).isDistributionList
}
@@ -635,6 +657,13 @@ open class ContactSearchAdapter(
ALWAYS
}
+ data class DisplayOptions(
+ val displayCheckBox: Boolean = false,
+ val displaySmsTag: DisplaySmsTag = DisplaySmsTag.NEVER,
+ val displaySecondaryInformation: DisplaySecondaryInformation = DisplaySecondaryInformation.NEVER,
+ val displayCallButtons: Boolean = false
+ )
+
fun interface OnClickedCallback {
fun onClicked(view: View, data: D, isSelected: Boolean)
}
@@ -652,6 +681,16 @@ open class ContactSearchAdapter(
}
}
+ interface CallButtonClickCallbacks {
+ fun onVideoCallButtonClicked(recipient: Recipient)
+ fun onAudioCallButtonClicked(recipient: Recipient)
+ }
+
+ object EmptyCallButtonClickCallbacks : CallButtonClickCallbacks {
+ override fun onVideoCallButtonClicked(recipient: Recipient) = Unit
+ override fun onAudioCallButtonClicked(recipient: Recipient) = Unit
+ }
+
interface LongClickCallbacks {
fun onKnownRecipientLongClick(view: View, data: ContactSearchData.KnownRecipient): Boolean
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt
index 837345b1ec..b9b0861e77 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt
@@ -7,8 +7,8 @@ import org.thoughtcrime.securesms.contacts.HeaderAction
*/
class ContactSearchConfiguration private constructor(
val query: String?,
- val hasEmptyState: Boolean,
- val sections: List
+ val sections: List,
+ val emptyStateSections: List
) {
/**
@@ -20,6 +20,14 @@ class ContactSearchConfiguration private constructor(
open val headerAction: HeaderAction? = null
abstract val expandConfig: ExpandConfig?
+ /**
+ * Section representing the "extra" item.
+ */
+ object Empty : Section(SectionKey.EMPTY) {
+ override val includeHeader: Boolean = false
+ override val expandConfig: ExpandConfig? = null
+ }
+
/**
* Distribution lists and group stories.
*
@@ -188,6 +196,11 @@ class ContactSearchConfiguration private constructor(
* Describes a given section. Useful for labeling sections and managing expansion state.
*/
enum class SectionKey {
+ /**
+ * A generic empty item
+ */
+ EMPTY,
+
/**
* Lists My Stories, distribution lists, as well as group stories.
*/
@@ -271,6 +284,7 @@ class ContactSearchConfiguration private constructor(
* Describes the mode for 'Username' or 'PhoneNumber'
*/
enum class NewRowMode {
+ NEW_CALL,
NEW_CONVERSATION,
BLOCK,
ADD_TO_GROUP
@@ -296,21 +310,47 @@ class ContactSearchConfiguration private constructor(
}
}
- /**
- * Internal builder class with build method.
- */
- private class ConfigurationBuilder : Builder {
+ private class EmptyStateBuilder : Builder {
private val sections: MutableList = mutableListOf()
override var query: String? = null
- override var hasEmptyState: Boolean = false
override fun addSection(section: Section) {
sections.add(section)
}
+ override fun withEmptyState(emptyStateBuilderFn: Builder.() -> Unit) {
+ error("Unsupported operation: Already in empty state.")
+ }
+
+ fun build(): List {
+ return sections
+ }
+ }
+
+ /**
+ * Internal builder class with build method.
+ */
+ private class ConfigurationBuilder : Builder {
+ private val sections: MutableList = mutableListOf()
+ private val emptyState = EmptyStateBuilder()
+
+ override var query: String? = null
+
+ override fun addSection(section: Section) {
+ sections.add(section)
+ }
+
+ override fun withEmptyState(emptyStateBuilderFn: Builder.() -> Unit) {
+ emptyState.emptyStateBuilderFn()
+ }
+
fun build(): ContactSearchConfiguration {
- return ContactSearchConfiguration(query, hasEmptyState, sections)
+ return ContactSearchConfiguration(
+ query = query,
+ sections = sections,
+ emptyStateSections = emptyState.build()
+ )
}
}
@@ -319,7 +359,6 @@ class ContactSearchConfiguration private constructor(
*/
interface Builder {
var query: String?
- var hasEmptyState: Boolean
fun arbitrary(first: String, vararg rest: String) {
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
@@ -333,6 +372,8 @@ class ContactSearchConfiguration private constructor(
addSection(Section.PhoneNumber(newRowMode))
}
+ fun withEmptyState(emptyStateBuilderFn: Builder.() -> Unit)
+
fun addSection(section: Section)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt
index d91b345530..1bf592e86b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt
@@ -42,9 +42,7 @@ class ContactSearchMediator(
private val fragment: Fragment,
private val fixedContacts: Set = setOf(),
selectionLimits: SelectionLimits,
- displayCheckBox: Boolean,
- displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
- displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
+ displayOptions: ContactSearchAdapter.DisplayOptions,
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
private val callbacks: Callbacks = SimpleCallbacks(),
performSafetyNumberChecks: Boolean = true,
@@ -69,9 +67,7 @@ class ContactSearchMediator(
val adapter = adapterFactory.create(
context = fragment.requireContext(),
fixedContacts = fixedContacts,
- displayCheckBox = displayCheckBox,
- displaySmsTag = displaySmsTag,
- displaySecondaryInformation = displaySecondaryInformation,
+ displayOptions = displayOptions,
callbacks = object : ContactSearchAdapter.ClickCallbacks {
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
toggleStorySelection(view, story, isSelected)
@@ -86,7 +82,8 @@ class ContactSearchMediator(
}
},
longClickCallbacks = ContactSearchAdapter.LongClickCallbacksAdapter(),
- storyContextMenuCallbacks = StoryContextMenuCallbacks()
+ storyContextMenuCallbacks = StoryContextMenuCallbacks(),
+ callButtonClickCallbacks = ContactSearchAdapter.EmptyCallButtonClickCallbacks
)
init {
@@ -230,12 +227,11 @@ class ContactSearchMediator(
fun create(
context: Context,
fixedContacts: Set,
- displayCheckBox: Boolean,
- displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
- displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
+ displayOptions: ContactSearchAdapter.DisplayOptions,
callbacks: ContactSearchAdapter.ClickCallbacks,
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
- storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
+ storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
+ callButtonClickCallbacks: ContactSearchAdapter.CallButtonClickCallbacks
): PagingMappingAdapter
}
@@ -243,14 +239,13 @@ class ContactSearchMediator(
override fun create(
context: Context,
fixedContacts: Set,
- displayCheckBox: Boolean,
- displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
- displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
+ displayOptions: ContactSearchAdapter.DisplayOptions,
callbacks: ContactSearchAdapter.ClickCallbacks,
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
- storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
+ storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
+ callButtonClickCallbacks: ContactSearchAdapter.CallButtonClickCallbacks
): PagingMappingAdapter {
- return ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, callbacks, longClickCallbacks, storyContextMenuCallbacks)
+ return ContactSearchAdapter(context, fixedContacts, displayOptions, callbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks)
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt
index 9dcef736a2..c6e76b31b0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt
@@ -44,32 +44,51 @@ class ContactSearchPagedDataSource(
private var searchCache = SearchCache()
private var searchSize = -1
+ private var displayEmptyState: Boolean = false
+ /**
+ * When determining when the list is in an empty state, we ignore any arbitrary items, since in general
+ * they are always present. If you'd like arbitrary items to appear even when the list is empty, ensure
+ * they are added to the empty state configuration.
+ */
override fun size(): Int {
- searchSize = contactConfiguration.sections.sumOf {
+ val (arbitrarySections, nonArbitrarySections) = contactConfiguration.sections.partition {
+ it is ContactSearchConfiguration.Section.Arbitrary
+ }
+
+ val sizeOfNonArbitrarySections = nonArbitrarySections.sumOf {
getSectionSize(it, contactConfiguration.query)
}
- return if (searchSize == 0 && contactConfiguration.hasEmptyState) {
- 1
+ displayEmptyState = sizeOfNonArbitrarySections == 0
+ searchSize = if (displayEmptyState) {
+ contactConfiguration.emptyStateSections.sumOf {
+ getSectionSize(it, contactConfiguration.query)
+ }
} else {
- searchSize
+ arbitrarySections.sumOf {
+ getSectionSize(it, contactConfiguration.query)
+ } + sizeOfNonArbitrarySections
}
+
+ return searchSize
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList {
- if (searchSize == 0 && contactConfiguration.hasEmptyState) {
- return mutableListOf(ContactSearchData.Empty(contactConfiguration.query))
+ val sections: List = if (displayEmptyState) {
+ contactConfiguration.emptyStateSections
+ } else {
+ contactConfiguration.sections
}
- val sizeMap: Map = contactConfiguration.sections.associateWith { getSectionSize(it, contactConfiguration.query) }
+ val sizeMap: Map = sections.associateWith { getSectionSize(it, contactConfiguration.query) }
val startIndex: Index = findIndex(sizeMap, start)
val endIndex: Index = findIndex(sizeMap, start + length)
- val indexOfStartSection = contactConfiguration.sections.indexOf(startIndex.category)
- val indexOfEndSection = contactConfiguration.sections.indexOf(endIndex.category)
+ val indexOfStartSection = sections.indexOf(startIndex.category)
+ val indexOfEndSection = sections.indexOf(endIndex.category)
- val results: List> = contactConfiguration.sections.mapIndexed { index, section ->
+ val results: List> = sections.mapIndexed { index, section ->
if (index in indexOfStartSection..indexOfEndSection) {
getSectionData(
section = section,
@@ -122,6 +141,7 @@ class ContactSearchPagedDataSource(
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
+ is ContactSearchConfiguration.Section.Empty -> 1
}
}
@@ -160,6 +180,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsContactData(section, query, startIndex, endIndex)
is ContactSearchConfiguration.Section.PhoneNumber -> getPossiblePhoneNumber(section, query)
is ContactSearchConfiguration.Section.Username -> getPossibleUsername(section, query)
+ is ContactSearchConfiguration.Section.Empty -> listOf(ContactSearchData.Empty(query))
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt
index 7898abc15d..4ca963d5a7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt
@@ -124,9 +124,11 @@ class MultiselectForwardFragment :
this,
emptySet(),
FeatureFlags.shareSelectionLimit(),
- !args.selectSingleRecipient,
- ContactSearchAdapter.DisplaySmsTag.DEFAULT,
- ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
+ ContactSearchAdapter.DisplayOptions(
+ displayCheckBox = !args.selectSingleRecipient,
+ displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
+ displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
+ ),
this::getConfiguration,
object : ContactSearchMediator.SimpleCallbacks() {
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set): Set {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
index e7a5518c78..2d55566b51 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -300,31 +300,32 @@ public class ConversationListFragment extends MainFragment implements ActionMode
contactSearchMediator = new ContactSearchMediator(this,
Collections.emptySet(),
SelectionLimits.NO_LIMITS,
- false,
- ContactSearchAdapter.DisplaySmsTag.DEFAULT,
- ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
+ new ContactSearchAdapter.DisplayOptions(
+ false,
+ ContactSearchAdapter.DisplaySmsTag.DEFAULT,
+ ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
+ false
+ ),
this::mapSearchStateToConfiguration,
new ContactSearchMediator.SimpleCallbacks(),
false,
(context,
fixedContacts,
- displayCheckBox,
- displaySmsTag,
- displaySecondaryInformation,
+ displayOptions,
callbacks,
longClickCallbacks,
- storyContextMenuCallbacks
+ storyContextMenuCallbacks,
+ callButtonClickCallbacks
) -> {
//noinspection CodeBlock2Expr
return new ConversationListSearchAdapter(
context,
fixedContacts,
- displayCheckBox,
- displaySmsTag,
- displaySecondaryInformation,
+ displayOptions,
new ContactSearchClickCallbacks(callbacks),
longClickCallbacks,
storyContextMenuCallbacks,
+ callButtonClickCallbacks,
getViewLifecycleOwner(),
GlideApp.with(this)
);
@@ -655,7 +656,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
null
));
- builder.setHasEmptyState(true);
+ builder.withEmptyState(emptyStateBuilder -> {
+ builder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
+ return Unit.INSTANCE;
+ });
} else {
builder.arbitrary(
conversationFilterRequest.getSource() == ConversationFilterSource.DRAG
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.kt
index 62d72c985d..8c29f85e53 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.kt
@@ -26,15 +26,14 @@ import java.util.Locale
class ConversationListSearchAdapter(
context: Context,
fixedContacts: Set,
- displayCheckBox: Boolean,
- displaySmsTag: DisplaySmsTag,
- displaySecondaryInformation: DisplaySecondaryInformation,
+ displayOptions: DisplayOptions,
onClickedCallbacks: ConversationListSearchClickCallbacks,
longClickCallbacks: LongClickCallbacks,
storyContextMenuCallbacks: StoryContextMenuCallbacks,
+ callButtonClickCallbacks: CallButtonClickCallbacks,
lifecycleOwner: LifecycleOwner,
glideRequests: GlideRequests
-) : ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, onClickedCallbacks, longClickCallbacks, storyContextMenuCallbacks) {
+) : ContactSearchAdapter(context, fixedContacts, displayOptions, onClickedCallbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) {
init {
registerFactory(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt
index 79ba7a32b6..82dcb6aa59 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt
@@ -66,9 +66,11 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment(
mediator = ContactSearchMediator(
fragment = this,
selectionLimits = FeatureFlags.shareSelectionLimit(),
- displayCheckBox = true,
- displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
- displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
+ displayOptions = ContactSearchAdapter.DisplayOptions(
+ displayCheckBox = true,
+ displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
+ displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
+ ),
mapStateToConfiguration = { state ->
ContactSearchConfiguration.build {
query = state.query
diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/connections/ViewAllSignalConnectionsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/connections/ViewAllSignalConnectionsFragment.kt
index 1afbfa8228..37433f78e7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/connections/ViewAllSignalConnectionsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/connections/ViewAllSignalConnectionsFragment.kt
@@ -27,9 +27,11 @@ class ViewAllSignalConnectionsFragment : Fragment(R.layout.view_all_signal_conne
val mediator = ContactSearchMediator(
fragment = this,
selectionLimits = SelectionLimits(0, 0),
- displayCheckBox = false,
- displaySmsTag = ContactSearchAdapter.DisplaySmsTag.IF_NOT_REGISTERED,
- displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
+ displayOptions = ContactSearchAdapter.DisplayOptions(
+ displayCheckBox = false,
+ displaySmsTag = ContactSearchAdapter.DisplaySmsTag.IF_NOT_REGISTERED,
+ displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
+ ),
mapStateToConfiguration = { getConfiguration() },
performSafetyNumberChecks = false
)
diff --git a/app/src/main/res/layout/contact_search_item.xml b/app/src/main/res/layout/contact_search_item.xml
index eecaeeda2b..b37a477e5b 100644
--- a/app/src/main/res/layout/contact_search_item.xml
+++ b/app/src/main/res/layout/contact_search_item.xml
@@ -61,7 +61,7 @@
android:textAppearance="@style/TextAppearance.Signal.Body1"
android:textColor="@color/signal_text_primary"
app:layout_constraintBottom_toTopOf="@id/number"
- app:layout_constraintEnd_toStartOf="@id/sms_tag"
+ app:layout_constraintEnd_toStartOf="@id/start_audio"
app:layout_constraintStart_toEndOf="@id/contact_photo_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
@@ -120,4 +120,33 @@
app:layout_goneMarginEnd="0dp"
tools:visibility="visible" />
+
+
+
+
diff --git a/app/src/main/res/layout/contact_selection_empty_state.xml b/app/src/main/res/layout/contact_selection_empty_state.xml
new file mode 100644
index 0000000000..ed46e6bb81
--- /dev/null
+++ b/app/src/main/res/layout/contact_selection_empty_state.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/contact_selection_invite_action_item.xml b/app/src/main/res/layout/contact_selection_invite_action_item.xml
index eb3fc62c00..18fa45e720 100644
--- a/app/src/main/res/layout/contact_selection_invite_action_item.xml
+++ b/app/src/main/res/layout/contact_selection_invite_action_item.xml
@@ -1,24 +1,27 @@
+ tools:viewBindingIgnore="true">
-
+ app:layout_constraintTop_toTopOf="parent"
+ app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/new_call_menu.xml b/app/src/main/res/menu/new_call_menu.xml
new file mode 100644
index 0000000000..feb43d2b9f
--- /dev/null
+++ b/app/src/main/res/menu/new_call_menu.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ac4eb6a4da..dc64cd106d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2268,6 +2268,12 @@
Invite to Signal
New group
+
+ Refresh contacts
+
+ Missing someone? Try refreshing
+
+ More
Clear entered text
@@ -3113,6 +3119,8 @@
+
+ New call to…
New message to…
Block user
Add to group
@@ -5728,5 +5736,9 @@
Start a new call
+
+
+ New call
+
diff --git a/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt b/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt
index 622a7eaaa6..c3f3c7ab32 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt
+++ b/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt
@@ -148,6 +148,14 @@ class ContactSearchPagedDataSourceTest {
"two",
"three"
)
+
+ withEmptyState {
+ arbitrary(
+ "one",
+ "two",
+ "three"
+ )
+ }
}
return ContactSearchPagedDataSource(configuration, repository, ArbitraryRepoFake())