mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Add new call screen for calls tab.
This commit is contained in:
committed by
Greyson Parrelli
parent
1210b2af0f
commit
ce3770a0fb
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
10
app/src/main/res/layout/contact_selection_empty_state.xml
Normal file
10
app/src/main/res/layout/contact_selection_empty_state.xml
Normal 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" />
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
9
app/src/main/res/menu/new_call_menu.xml
Normal file
9
app/src/main/res/menu/new_call_menu.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -148,6 +148,14 @@ class ContactSearchPagedDataSourceTest {
|
||||
"two",
|
||||
"three"
|
||||
)
|
||||
|
||||
withEmptyState {
|
||||
arbitrary(
|
||||
"one",
|
||||
"two",
|
||||
"three"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return ContactSearchPagedDataSource(configuration, repository, ArbitraryRepoFake())
|
||||
|
||||
Reference in New Issue
Block a user