From ce3770a0fb19ae31ff88ee0a60f0c6eb0b8509ab Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 16 Mar 2023 12:59:58 -0300 Subject: [PATCH] Add new call screen for calls tab. --- app/src/main/AndroidManifest.xml | 5 + .../securesms/ContactSelectionListAdapter.kt | 62 ++++++- .../ContactSelectionListFragment.java | 165 ++++++++++++------ .../securesms/NewConversationActivity.java | 2 +- .../securesms/calls/log/CallLogFragment.kt | 4 + .../securesms/calls/new/NewCallActivity.kt | 64 ++++++- .../securesms/calls/new/NewCallFragment.kt | 7 - .../contacts/ContactSelectionDisplayMode.java | 47 +++++ .../contacts/paged/ContactSearchAdapter.kt | 89 +++++++--- .../paged/ContactSearchConfiguration.kt | 59 ++++++- .../contacts/paged/ContactSearchMediator.kt | 27 ++- .../paged/ContactSearchPagedDataSource.kt | 41 +++-- .../forward/MultiselectForwardFragment.kt | 8 +- .../ConversationListFragment.java | 26 +-- .../ConversationListSearchAdapter.kt | 7 +- .../v2/stories/ChooseGroupStoryBottomSheet.kt | 8 +- .../ViewAllSignalConnectionsFragment.kt | 8 +- .../main/res/layout/contact_search_item.xml | 31 +++- .../layout/contact_selection_empty_state.xml | 10 ++ .../contact_selection_invite_action_item.xml | 15 +- .../contact_selection_refresh_action_item.xml | 56 ++++++ app/src/main/res/menu/new_call_menu.xml | 9 + app/src/main/res/values/strings.xml | 12 ++ .../paged/ContactSearchPagedDataSourceTest.kt | 8 + 24 files changed, 601 insertions(+), 169 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallFragment.kt create mode 100644 app/src/main/res/layout/contact_selection_empty_state.xml create mode 100644 app/src/main/res/layout/contact_selection_refresh_action_item.xml create mode 100644 app/src/main/res/menu/new_call_menu.xml 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())