Add new call screen for calls tab.

This commit is contained in:
Alex Hart
2023-03-16 12:59:58 -03:00
committed by Greyson Parrelli
parent 1210b2af0f
commit ce3770a0fb
24 changed files with 601 additions and 169 deletions

View File

@@ -354,6 +354,11 @@
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".calls.new.NewCallActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PushContactSelectionActivity"
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden"

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms
import android.content.Context
import android.view.View
import android.widget.TextView
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
@@ -13,17 +14,19 @@ import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
class ContactSelectionListAdapter(
context: Context,
fixedContacts: Set<ContactSearchKey>,
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<NewGroupModel> {
@@ -36,6 +39,17 @@ class ContactSelectionListAdapter(
override fun areContentsTheSame(newItem: InviteToSignalModel): Boolean = true
}
class RefreshContactsModel : MappingModel<RefreshContactsModel> {
override fun areItemsTheSame(newItem: RefreshContactsModel): Boolean = true
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
}
class MoreHeaderModel : MappingModel<MoreHeaderModel> {
override fun areItemsTheSame(newItem: MoreHeaderModel): Boolean = true
override fun areContentsTheSame(newItem: MoreHeaderModel): Boolean = true
}
private class InviteToSignalViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<InviteToSignalModel>(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<RefreshContactsModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: RefreshContactsModel) = Unit
}
private class MoreHeaderViewHolder(itemView: View) : MappingViewHolder<MoreHeaderModel>(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<EmptyModel>(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<ContactSearchData.Arbitrary> {
@@ -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()
}
}

View File

@@ -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();
}

View File

@@ -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")

View File

@@ -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<Material3OnScrollHelperBinder>().bindScrollHelper(binding.recycler)
binding.fab.setOnClickListener {
startActivity(NewCallActivity.createIntent(requireContext()))
}
initializePullToFilter()
initializeTapToScrollToTop()

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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;
}
}
}

View File

@@ -37,20 +37,19 @@ import org.thoughtcrime.securesms.util.visible
open class ContactSearchAdapter(
private val context: Context,
fixedContacts: Set<ContactSearchKey>,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
displaySecondaryInformation: DisplaySecondaryInformation,
displayOptions: DisplayOptions,
onClickCallbacks: ClickCallbacks,
longClickCallbacks: LongClickCallbacks,
storyContextMenuCallbacks: StoryContextMenuCallbacks
storyContextMenuCallbacks: StoryContextMenuCallbacks,
callButtonClickCallbacks: CallButtonClickCallbacks
) : PagingMappingAdapter<ContactSearchKey>(), 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<ContactSearchKey>,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
displaySecondaryInformation: DisplaySecondaryInformation,
displayOptions: DisplayOptions,
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>,
recipientLongClickCallback: OnLongClickedCallback<ContactSearchData.KnownRecipient>
recipientLongClickCallback: OnLongClickedCallback<ContactSearchData.KnownRecipient>,
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<ContactSearchData.Story>,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, onClick) {
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(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<ContactSearchKey>,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
private val displaySecondaryInformation: DisplaySecondaryInformation,
displayOptions: DisplayOptions,
onClick: OnClickedCallback<ContactSearchData.KnownRecipient>,
private val onLongClick: OnLongClickedCallback<ContactSearchData.KnownRecipient>
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
private val onLongClick: OnLongClickedCallback<ContactSearchData.KnownRecipient>,
callButtonClickCallbacks: CallButtonClickCallbacks
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(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<T : MappingModel<T>, D : ContactSearchData>(
itemView: View,
private val displayCheckBox: Boolean,
private val displaySmsTag: DisplaySmsTag,
val onClick: OnClickedCallback<D>
val displayOptions: DisplayOptions,
val onClick: OnClickedCallback<D>,
val onCallButtonClickCallbacks: CallButtonClickCallbacks
) : MappingViewHolder<T>(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<D : ContactSearchData> {
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
}

View File

@@ -7,8 +7,8 @@ import org.thoughtcrime.securesms.contacts.HeaderAction
*/
class ContactSearchConfiguration private constructor(
val query: String?,
val hasEmptyState: Boolean,
val sections: List<Section>
val sections: List<Section>,
val emptyStateSections: List<Section>
) {
/**
@@ -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<Section> = 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<Section> {
return sections
}
}
/**
* Internal builder class with build method.
*/
private class ConfigurationBuilder : Builder {
private val sections: MutableList<Section> = 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)
}
}

View File

@@ -42,9 +42,7 @@ class ContactSearchMediator(
private val fragment: Fragment,
private val fixedContacts: Set<ContactSearchKey> = 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<ContactSearchKey>,
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<ContactSearchKey>
}
@@ -243,14 +239,13 @@ class ContactSearchMediator(
override fun create(
context: Context,
fixedContacts: Set<ContactSearchKey>,
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<ContactSearchKey> {
return ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, callbacks, longClickCallbacks, storyContextMenuCallbacks)
return ContactSearchAdapter(context, fixedContacts, displayOptions, callbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks)
}
}
}

View File

@@ -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<ContactSearchData> {
if (searchSize == 0 && contactConfiguration.hasEmptyState) {
return mutableListOf(ContactSearchData.Empty(contactConfiguration.query))
val sections: List<ContactSearchConfiguration.Section> = if (displayEmptyState) {
contactConfiguration.emptyStateSections
} else {
contactConfiguration.sections
}
val sizeMap: Map<ContactSearchConfiguration.Section, Int> = contactConfiguration.sections.associateWith { getSectionSize(it, contactConfiguration.query) }
val sizeMap: Map<ContactSearchConfiguration.Section, Int> = 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<List<ContactSearchData>> = contactConfiguration.sections.mapIndexed { index, section ->
val results: List<List<ContactSearchData>> = 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))
}
}

View File

@@ -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<ContactSearchKey>): Set<ContactSearchKey> {

View File

@@ -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

View File

@@ -26,15 +26,14 @@ import java.util.Locale
class ConversationListSearchAdapter(
context: Context,
fixedContacts: Set<ContactSearchKey>,
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(

View File

@@ -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

View File

@@ -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
)

View File

@@ -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" />
<ImageView
android:id="@+id/start_video"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_video_call_24"
android:visibility="gone"
android:padding="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/sms_tag"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/signal_colorOnSurface"
tools:visibility="visible" />
<ImageView
android:id="@+id/start_audio"
android:layout_width="48dp"
android:layout_height="48dp"
android:importantForAccessibility="no"
android:src="@drawable/symbol_phone_24"
android:visibility="gone"
android:padding="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/start_video"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/signal_colorOnSurface"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/search_no_results"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="64dp"
android:textAppearance="@style/Signal.Text.BodyLarge"
tools:text="@string/SearchFragment_no_results" />

View File

@@ -1,24 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/rounded_inset_ripple_background"
android:minHeight="@dimen/contact_selection_item_height"
android:paddingStart="@dimen/dsl_settings_gutter"
android:paddingEnd="@dimen/dsl_settings_gutter"
android:background="@drawable/rounded_inset_ripple_background">
tools:viewBindingIgnore="true">
<ImageView
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/invite_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@color/signal_colorSurfaceVariant"
android:importantForAccessibility="no"
android:src="@drawable/ic_invite_circle"
android:scaleType="centerInside"
android:src="@drawable/symbol_invite_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle" />
<TextView
android:id="@+id/invite_text"

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/rounded_inset_ripple_background"
android:minHeight="@dimen/contact_selection_item_height"
android:paddingStart="@dimen/dsl_settings_gutter"
android:paddingEnd="@dimen/dsl_settings_gutter"
tools:viewBindingIgnore="true">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@color/signal_colorSurfaceVariant"
android:importantForAccessibility="no"
android:scaleType="centerInside"
android:src="@drawable/symbol_refresh_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="marquee"
android:labelFor="@id/action_icon"
android:singleLine="true"
android:text="@string/contact_selection_activity__refresh_contacts"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintBottom_toTopOf="@id/description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/image"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="marquee"
android:labelFor="@id/action_icon"
android:singleLine="true"
android:text="@string/contact_selection_activity__missing_someone"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/image"
app:layout_constraintTop_toBottomOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_refresh"
android:title="@string/new_conversation_activity__refresh" />
<item
android:id="@+id/menu_invite"
android:title="@string/text_secure_normal__invite_friends" />
</menu>

View File

@@ -2268,6 +2268,12 @@
<!-- contact_selection_activity -->
<string name="contact_selection_activity__invite_to_signal">Invite to Signal</string>
<string name="contact_selection_activity__new_group">New group</string>
<!-- Row item title for refreshing contacts -->
<string name="contact_selection_activity__refresh_contacts">Refresh contacts</string>
<!-- Row item description for refreshing contacts -->
<string name="contact_selection_activity__missing_someone">Missing someone? Try refreshing</string>
<!-- Row header title for more section -->
<string name="contact_selection_activity__more">More</string>
<!-- contact_filter_toolbar -->
<string name="contact_filter_toolbar__clear_entered_text_description">Clear entered text</string>
@@ -3113,6 +3119,8 @@
<!-- **************************************** -->
<!-- contact_selection_list -->
<!-- Displayed in a row on the new call screen when searching by phone number. -->
<string name="contact_selection_list__new_call">New call to…</string>
<string name="contact_selection_list__unknown_contact">New message to…</string>
<string name="contact_selection_list__unknown_contact_block">Block user</string>
<string name="contact_selection_list__unknown_contact_add_to_group">Add to group</string>
@@ -5728,5 +5736,9 @@
<!-- Call log new call content description -->
<string name="CallLogFragment__start_a_new_call">Start a new call</string>
<!-- New call activity -->
<!-- Activity title in title bar -->
<string name="NewCallActivity__new_call">New call</string>
<!-- EOF -->
</resources>

View File

@@ -148,6 +148,14 @@ class ContactSearchPagedDataSourceTest {
"two",
"three"
)
withEmptyState {
arbitrary(
"one",
"two",
"three"
)
}
}
return ContactSearchPagedDataSource(configuration, repository, ArbitraryRepoFake())