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:windowSoftInputMode="stateAlwaysVisible"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
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"
|
<activity android:name=".PushContactSelectionActivity"
|
||||||
android:label="@string/AndroidManifest__select_contacts"
|
android:label="@string/AndroidManifest__select_contacts"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
|
||||||
@@ -13,17 +14,19 @@ import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
|||||||
class ContactSelectionListAdapter(
|
class ContactSelectionListAdapter(
|
||||||
context: Context,
|
context: Context,
|
||||||
fixedContacts: Set<ContactSearchKey>,
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayOptions: DisplayOptions,
|
||||||
displaySmsTag: DisplaySmsTag,
|
|
||||||
displaySecondaryInformation: DisplaySecondaryInformation,
|
|
||||||
onClickCallbacks: OnContactSelectionClick,
|
onClickCallbacks: OnContactSelectionClick,
|
||||||
longClickCallbacks: LongClickCallbacks,
|
longClickCallbacks: LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: StoryContextMenuCallbacks
|
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||||
) : ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, onClickCallbacks, longClickCallbacks, storyContextMenuCallbacks) {
|
callButtonClickCallbacks: CallButtonClickCallbacks
|
||||||
|
) : ContactSearchAdapter(context, fixedContacts, displayOptions, onClickCallbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerFactory(NewGroupModel::class.java, LayoutFactory({ NewGroupViewHolder(it, onClickCallbacks::onNewGroupClicked) }, R.layout.contact_selection_new_group_item))
|
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(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> {
|
class NewGroupModel : MappingModel<NewGroupModel> {
|
||||||
@@ -36,6 +39,17 @@ class ContactSelectionListAdapter(
|
|||||||
override fun areContentsTheSame(newItem: InviteToSignalModel): Boolean = true
|
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) {
|
private class InviteToSignalViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<InviteToSignalModel>(itemView) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener { onClickListener() }
|
itemView.setOnClickListener { onClickListener() }
|
||||||
@@ -52,11 +66,39 @@ class ContactSelectionListAdapter(
|
|||||||
override fun bind(model: NewGroupModel) = Unit
|
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 {
|
class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
|
||||||
|
|
||||||
enum class ArbitraryRow(val code: String) {
|
enum class ArbitraryRow(val code: String) {
|
||||||
NEW_GROUP("new-group"),
|
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 {
|
companion object {
|
||||||
fun fromCode(code: String) = values().first { it.code == code }
|
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 {
|
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> {
|
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<*> {
|
override fun getMappingModel(arbitrary: ContactSearchData.Arbitrary): MappingModel<*> {
|
||||||
val code = ArbitraryRow.fromCode(arbitrary.type)
|
return when (ArbitraryRow.fromCode(arbitrary.type)) {
|
||||||
return when (code) {
|
|
||||||
ArbitraryRow.NEW_GROUP -> NewGroupModel()
|
ArbitraryRow.NEW_GROUP -> NewGroupModel()
|
||||||
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
|
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
|
||||||
|
ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
|
||||||
|
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,5 +127,6 @@ class ContactSelectionListAdapter(
|
|||||||
interface OnContactSelectionClick : ClickCallbacks {
|
interface OnContactSelectionClick : ClickCallbacks {
|
||||||
fun onNewGroupClicked()
|
fun onNewGroupClicked()
|
||||||
fun onInviteToSignalClicked()
|
fun onInviteToSignalClicked()
|
||||||
|
fun onRefreshContactsClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|||||||
import androidx.transition.AutoTransition;
|
import androidx.transition.AutoTransition;
|
||||||
import androidx.transition.TransitionManager;
|
import androidx.transition.TransitionManager;
|
||||||
|
|
||||||
import com.annimon.stream.Collectors;
|
|
||||||
import com.annimon.stream.Stream;
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
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.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||||
@@ -98,8 +97,7 @@ import kotlin.Unit;
|
|||||||
*
|
*
|
||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*/
|
*/
|
||||||
public final class ContactSelectionListFragment extends LoggingFragment
|
public final class ContactSelectionListFragment extends LoggingFragment {
|
||||||
{
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||||
|
|
||||||
@@ -139,7 +137,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
private TextView headerActionView;
|
private TextView headerActionView;
|
||||||
private ContactSearchMediator contactSearchMediator;
|
private ContactSearchMediator contactSearchMediator;
|
||||||
|
|
||||||
@Nullable private ListCallback listCallback;
|
@Nullable private NewConversationCallback newConversationCallback;
|
||||||
|
@Nullable private NewCallCallback newCallCallback;
|
||||||
@Nullable private ScrollCallback scrollCallback;
|
@Nullable private ScrollCallback scrollCallback;
|
||||||
@Nullable private OnItemLongClickListener onItemLongClickListener;
|
@Nullable private OnItemLongClickListener onItemLongClickListener;
|
||||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||||
@@ -152,8 +151,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
public void onAttach(@NonNull Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
|
|
||||||
if (context instanceof ListCallback) {
|
if (context instanceof NewConversationCallback) {
|
||||||
listCallback = (ListCallback) context;
|
newConversationCallback = (NewConversationCallback) context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context instanceof NewCallCallback) {
|
||||||
|
newCallCallback = (NewCallCallback) context;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getParentFragment() instanceof ScrollCallback) {
|
if (getParentFragment() instanceof ScrollCallback) {
|
||||||
@@ -337,9 +340,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
.map(r -> new ContactSearchKey.RecipientSearchKey(r, false))
|
.map(r -> new ContactSearchKey.RecipientSearchKey(r, false))
|
||||||
.collect(java.util.stream.Collectors.toSet()),
|
.collect(java.util.stream.Collectors.toSet()),
|
||||||
selectionLimit,
|
selectionLimit,
|
||||||
|
new ContactSearchAdapter.DisplayOptions(
|
||||||
isMulti,
|
isMulti,
|
||||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||||
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
|
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
|
||||||
|
newCallCallback != null
|
||||||
|
),
|
||||||
this::mapStateToConfiguration,
|
this::mapStateToConfiguration,
|
||||||
new ContactSearchMediator.SimpleCallbacks() {
|
new ContactSearchMediator.SimpleCallbacks() {
|
||||||
@Override
|
@Override
|
||||||
@@ -348,21 +354,30 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, callbacks, longClickCallbacks, storyContextMenuCallbacks) -> new ContactSelectionListAdapter(
|
(context, fixedContacts, displayOptions, callbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) -> new ContactSelectionListAdapter(
|
||||||
context,
|
context,
|
||||||
fixedContacts,
|
fixedContacts,
|
||||||
displayCheckBox,
|
displayOptions,
|
||||||
displaySmsTag,
|
|
||||||
displaySecondaryInformation,
|
|
||||||
new ContactSelectionListAdapter.OnContactSelectionClick() {
|
new ContactSelectionListAdapter.OnContactSelectionClick() {
|
||||||
|
@Override
|
||||||
|
public void onRefreshContactsClicked() {
|
||||||
|
newCallCallback.onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewGroupClicked() {
|
public void onNewGroupClicked() {
|
||||||
listCallback.onNewGroup(false);
|
newConversationCallback.onNewGroup(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInviteToSignalClicked() {
|
public void onInviteToSignalClicked() {
|
||||||
listCallback.onInvite();
|
if (newConversationCallback != null) {
|
||||||
|
newConversationCallback.onInvite();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newCallCallback != null) {
|
||||||
|
newCallCallback.onInvite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -386,7 +401,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(anchorView, data) -> listClickListener.onItemLongClick(anchorView, data.getContactSearchKey()),
|
(anchorView, data) -> listClickListener.onItemLongClick(anchorView, data.getContactSearchKey()),
|
||||||
storyContextMenuCallbacks
|
storyContextMenuCallbacks,
|
||||||
|
new CallButtonClickCallbacks()
|
||||||
|
|
||||||
),
|
),
|
||||||
new ContactSelectionListAdapter.ArbitraryRepository()
|
new ContactSelectionListAdapter.ArbitraryRepository()
|
||||||
);
|
);
|
||||||
@@ -805,6 +822,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
boolean includeRecentsHeader = !flagSet(displayMode, ContactSelectionDisplayMode.FLAG_HIDE_RECENT_HEADER);
|
boolean includeRecentsHeader = !flagSet(displayMode, ContactSelectionDisplayMode.FLAG_HIDE_RECENT_HEADER);
|
||||||
boolean includeGroupsAfterContacts = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS);
|
boolean includeGroupsAfterContacts = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS);
|
||||||
boolean blocked = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_BLOCK);
|
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.TransportType transportType = resolveTransportType(includePushContacts, includeSmsContacts);
|
||||||
ContactSearchConfiguration.Section.Recents.Mode mode = resolveRecentsMode(transportType, includeActiveGroups);
|
ContactSearchConfiguration.Section.Recents.Mode mode = resolveRecentsMode(transportType, includeActiveGroups);
|
||||||
@@ -813,12 +832,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
return ContactSearchConfiguration.build(builder -> {
|
return ContactSearchConfiguration.build(builder -> {
|
||||||
builder.setQuery(contactSearchState.getQuery());
|
builder.setQuery(contactSearchState.getQuery());
|
||||||
|
|
||||||
if (listCallback != null) {
|
if (newConversationCallback != null) {
|
||||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
|
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transportType != null) {
|
if (transportType != null) {
|
||||||
if (TextUtils.isEmpty(contactSearchState.getQuery()) && includeRecents) {
|
if (!hasQuery && includeRecents) {
|
||||||
builder.addSection(new ContactSearchConfiguration.Section.Recents(
|
builder.addSection(new ContactSearchConfiguration.Section.Recents(
|
||||||
25,
|
25,
|
||||||
mode,
|
mode,
|
||||||
@@ -834,13 +853,13 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
|
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
|
||||||
includeSelf,
|
includeSelf,
|
||||||
transportType,
|
transportType,
|
||||||
true,
|
newCallCallback == null,
|
||||||
null,
|
null,
|
||||||
!hideLetterHeaders()
|
!hideLetterHeaders()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((includeGroupsAfterContacts || !TextUtils.isEmpty(contactSearchState.getQuery())) && includeActiveGroups) {
|
if ((includeGroupsAfterContacts || hasQuery) && includeActiveGroups) {
|
||||||
builder.addSection(new ContactSearchConfiguration.Section.Groups(
|
builder.addSection(new ContactSearchConfiguration.Section.Groups(
|
||||||
includeSmsContacts,
|
includeSmsContacts,
|
||||||
includeV1Groups,
|
includeV1Groups,
|
||||||
@@ -853,18 +872,34 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (listCallback != null) {
|
if (hasQuery && includeGroupMembers) {
|
||||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
|
builder.addSection(new ContactSearchConfiguration.Section.GroupMembers());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeNew) {
|
if (includeNew) {
|
||||||
builder.phone(newRowMode);
|
builder.phone(newRowMode);
|
||||||
builder.username(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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private static @Nullable ContactSearchConfiguration.TransportType resolveTransportType(boolean includePushContacts, boolean includeSmsContacts) {
|
||||||
if (includePushContacts && includeSmsContacts) {
|
if (includePushContacts && includeSmsContacts) {
|
||||||
return ContactSearchConfiguration.TransportType.ALL;
|
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) {
|
if (isBlocked) {
|
||||||
return ContactSearchConfiguration.NewRowMode.BLOCK;
|
return ContactSearchConfiguration.NewRowMode.BLOCK;
|
||||||
|
} else if (newCallCallback != null) {
|
||||||
|
return ContactSearchConfiguration.NewRowMode.NEW_CALL;
|
||||||
} else if (isActiveGroups) {
|
} else if (isActiveGroups) {
|
||||||
return ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION;
|
return ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION;
|
||||||
} else {
|
} else {
|
||||||
@@ -901,6 +938,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
return (mode & flag) > 0;
|
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 {
|
public interface OnContactSelectedListener {
|
||||||
/**
|
/**
|
||||||
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
|
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
|
||||||
@@ -918,12 +967,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
void onHardLimitReached(int limit);
|
void onHardLimitReached(int limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ListCallback {
|
public interface NewConversationCallback {
|
||||||
void onInvite();
|
void onInvite();
|
||||||
|
|
||||||
void onNewGroup(boolean forceV1);
|
void onNewGroup(boolean forceV1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface NewCallCallback {
|
||||||
|
void onInvite();
|
||||||
|
|
||||||
|
void onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
public interface ScrollCallback {
|
public interface ScrollCallback {
|
||||||
void onBeginScroll();
|
void onBeginScroll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ import java.util.stream.Stream;
|
|||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*/
|
*/
|
||||||
public class NewConversationActivity extends ContactSelectionActivity
|
public class NewConversationActivity extends ContactSelectionActivity
|
||||||
implements ContactSelectionListFragment.ListCallback, ContactSelectionListFragment.OnItemLongClickListener
|
implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener
|
||||||
{
|
{
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import io.reactivex.rxjava3.kotlin.Observables
|
|||||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
import org.signal.core.util.DimensionUnit
|
import org.signal.core.util.DimensionUnit
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||||
import org.thoughtcrime.securesms.components.Material3SearchToolbar
|
import org.thoughtcrime.securesms.components.Material3SearchToolbar
|
||||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
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
|
binding.recycler.adapter = adapter
|
||||||
requireListener<Material3OnScrollHelperBinder>().bindScrollHelper(binding.recycler)
|
requireListener<Material3OnScrollHelperBinder>().bindScrollHelper(binding.recycler)
|
||||||
|
binding.fab.setOnClickListener {
|
||||||
|
startActivity(NewCallActivity.createIntent(requireContext()))
|
||||||
|
}
|
||||||
|
|
||||||
initializePullToFilter()
|
initializePullToFilter()
|
||||||
initializeTapToScrollToTop()
|
initializeTapToScrollToTop()
|
||||||
|
|||||||
@@ -1,10 +1,62 @@
|
|||||||
package org.thoughtcrime.securesms.calls.new
|
package org.thoughtcrime.securesms.calls.new
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.content.Context
|
||||||
import androidx.fragment.app.Fragment
|
import android.content.Intent
|
||||||
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
|
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() {
|
class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment.NewCallCallback {
|
||||||
@SuppressLint("DiscouragedApi")
|
|
||||||
override fun getFragment(): Fragment = NewCallFragment()
|
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;
|
package org.thoughtcrime.securesms.contacts;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public final class ContactSelectionDisplayMode {
|
public final class ContactSelectionDisplayMode {
|
||||||
public static final int FLAG_PUSH = 1;
|
public static final int FLAG_PUSH = 1;
|
||||||
public static final int FLAG_SMS = 1 << 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_NEW = 1 << 6;
|
||||||
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
|
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_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 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(
|
open class ContactSearchAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
fixedContacts: Set<ContactSearchKey>,
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayOptions: DisplayOptions,
|
||||||
displaySmsTag: DisplaySmsTag,
|
|
||||||
displaySecondaryInformation: DisplaySecondaryInformation,
|
|
||||||
onClickCallbacks: ClickCallbacks,
|
onClickCallbacks: ClickCallbacks,
|
||||||
longClickCallbacks: LongClickCallbacks,
|
longClickCallbacks: LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: StoryContextMenuCallbacks
|
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||||
|
callButtonClickCallbacks: CallButtonClickCallbacks
|
||||||
) : PagingMappingAdapter<ContactSearchKey>(), FastScrollAdapter {
|
) : PagingMappingAdapter<ContactSearchKey>(), FastScrollAdapter {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerStoryItems(this, displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
|
registerStoryItems(this, displayOptions.displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
|
||||||
registerKnownRecipientItems(this, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick)
|
registerKnownRecipientItems(this, fixedContacts, displayOptions, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick, callButtonClickCallbacks)
|
||||||
registerHeaders(this)
|
registerHeaders(this)
|
||||||
registerExpands(this, onClickCallbacks::onExpandClicked)
|
registerExpands(this, onClickCallbacks::onExpandClicked)
|
||||||
registerFactory(UnknownRecipientModel::class.java, LayoutFactory({ UnknownRecipientViewHolder(it, onClickCallbacks::onUnknownRecipientClicked, displayCheckBox) }, R.layout.contact_search_unknown_item))
|
registerFactory(UnknownRecipientModel::class.java, LayoutFactory({ UnknownRecipientViewHolder(it, onClickCallbacks::onUnknownRecipientClicked, displayOptions.displayCheckBox) }, R.layout.contact_search_unknown_item))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBubbleText(position: Int): CharSequence {
|
override fun getBubbleText(position: Int): CharSequence {
|
||||||
@@ -82,15 +81,16 @@ open class ContactSearchAdapter(
|
|||||||
fun registerKnownRecipientItems(
|
fun registerKnownRecipientItems(
|
||||||
mappingAdapter: MappingAdapter,
|
mappingAdapter: MappingAdapter,
|
||||||
fixedContacts: Set<ContactSearchKey>,
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayOptions: DisplayOptions,
|
||||||
displaySmsTag: DisplaySmsTag,
|
|
||||||
displaySecondaryInformation: DisplaySecondaryInformation,
|
|
||||||
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>,
|
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>,
|
||||||
recipientLongClickCallback: OnLongClickedCallback<ContactSearchData.KnownRecipient>
|
recipientLongClickCallback: OnLongClickedCallback<ContactSearchData.KnownRecipient>,
|
||||||
|
recipientCallButtonClickCallbacks: CallButtonClickCallbacks
|
||||||
) {
|
) {
|
||||||
mappingAdapter.registerFactory(
|
mappingAdapter.registerFactory(
|
||||||
RecipientModel::class.java,
|
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,
|
displayCheckBox: Boolean,
|
||||||
onClick: OnClickedCallback<ContactSearchData.Story>,
|
onClick: OnClickedCallback<ContactSearchData.Story>,
|
||||||
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
|
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 isSelected(model: StoryModel): Boolean = model.isSelected
|
||||||
override fun getData(model: StoryModel): ContactSearchData.Story = model.story
|
override fun getData(model: StoryModel): ContactSearchData.Story = model.story
|
||||||
override fun getRecipient(model: StoryModel): Recipient = model.story.recipient
|
override fun getRecipient(model: StoryModel): Recipient = model.story.recipient
|
||||||
@@ -334,6 +334,7 @@ open class ContactSearchAdapter(
|
|||||||
checkbox.isSelected = false
|
checkbox.isSelected = false
|
||||||
name.setText(
|
name.setText(
|
||||||
when (model.data.mode) {
|
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.NEW_CONVERSATION -> R.string.contact_selection_list__unknown_contact
|
||||||
ContactSearchConfiguration.NewRowMode.BLOCK -> R.string.contact_selection_list__unknown_contact_block
|
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
|
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(
|
private class KnownRecipientViewHolder(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
private val fixedContacts: Set<ContactSearchKey>,
|
private val fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayOptions: DisplayOptions,
|
||||||
displaySmsTag: DisplaySmsTag,
|
|
||||||
private val displaySecondaryInformation: DisplaySecondaryInformation,
|
|
||||||
onClick: OnClickedCallback<ContactSearchData.KnownRecipient>,
|
onClick: OnClickedCallback<ContactSearchData.KnownRecipient>,
|
||||||
private val onLongClick: OnLongClickedCallback<ContactSearchData.KnownRecipient>
|
private val onLongClick: OnLongClickedCallback<ContactSearchData.KnownRecipient>,
|
||||||
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
|
callButtonClickCallbacks: CallButtonClickCallbacks
|
||||||
|
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayOptions, onClick, callButtonClickCallbacks), LetterHeaderDecoration.LetterHeaderItem {
|
||||||
|
|
||||||
private var headerLetter: String? = null
|
private var headerLetter: String? = null
|
||||||
|
|
||||||
@@ -370,10 +370,10 @@ open class ContactSearchAdapter(
|
|||||||
val count = recipient.participantIds.size
|
val count = recipient.participantIds.size
|
||||||
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
|
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
|
||||||
number.visible = true
|
number.visible = true
|
||||||
} else if (displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.combinedAboutAndEmoji != null) {
|
} else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.combinedAboutAndEmoji != null) {
|
||||||
number.text = recipient.combinedAboutAndEmoji
|
number.text = recipient.combinedAboutAndEmoji
|
||||||
number.visible = true
|
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.text = PhoneNumberFormatter.prettyPrint(recipient.requireE164())
|
||||||
number.visible = true
|
number.visible = true
|
||||||
} else {
|
} else {
|
||||||
@@ -410,9 +410,9 @@ open class ContactSearchAdapter(
|
|||||||
*/
|
*/
|
||||||
abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(
|
abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
private val displayCheckBox: Boolean,
|
val displayOptions: DisplayOptions,
|
||||||
private val displaySmsTag: DisplaySmsTag,
|
val onClick: OnClickedCallback<D>,
|
||||||
val onClick: OnClickedCallback<D>
|
val onCallButtonClickCallbacks: CallButtonClickCallbacks
|
||||||
) : MappingViewHolder<T>(itemView) {
|
) : MappingViewHolder<T>(itemView) {
|
||||||
|
|
||||||
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
|
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 number: TextView = itemView.findViewById(R.id.number)
|
||||||
protected val label: TextView = itemView.findViewById(R.id.label)
|
protected val label: TextView = itemView.findViewById(R.id.label)
|
||||||
protected val smsTag: View = itemView.findViewById(R.id.sms_tag)
|
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) {
|
override fun bind(model: T) {
|
||||||
if (isEnabled(model)) {
|
if (isEnabled(model)) {
|
||||||
@@ -442,10 +444,11 @@ open class ContactSearchAdapter(
|
|||||||
bindNumberField(model)
|
bindNumberField(model)
|
||||||
bindLabelField(model)
|
bindLabelField(model)
|
||||||
bindSmsTagField(model)
|
bindSmsTagField(model)
|
||||||
|
bindCallButtons(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun bindCheckbox(model: T) {
|
protected open fun bindCheckbox(model: T) {
|
||||||
checkbox.visible = displayCheckBox
|
checkbox.visible = displayOptions.displayCheckBox
|
||||||
checkbox.isChecked = isSelected(model)
|
checkbox.isChecked = isSelected(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +479,7 @@ open class ContactSearchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun bindSmsTagField(model: T) {
|
protected open fun bindSmsTagField(model: T) {
|
||||||
smsTag.visible = when (displaySmsTag) {
|
smsTag.visible = when (displayOptions.displaySmsTag) {
|
||||||
DisplaySmsTag.DEFAULT -> isSmsContact(model)
|
DisplaySmsTag.DEFAULT -> isSmsContact(model)
|
||||||
DisplaySmsTag.IF_NOT_REGISTERED -> isNotRegistered(model)
|
DisplaySmsTag.IF_NOT_REGISTERED -> isNotRegistered(model)
|
||||||
DisplaySmsTag.NEVER -> false
|
DisplaySmsTag.NEVER -> false
|
||||||
@@ -485,6 +488,25 @@ open class ContactSearchAdapter(
|
|||||||
|
|
||||||
protected open fun bindLongPress(model: T) = Unit
|
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 {
|
private fun isSmsContact(model: T): Boolean {
|
||||||
return (getRecipient(model).isForceSmsSelection || getRecipient(model).isUnregistered) && !getRecipient(model).isDistributionList
|
return (getRecipient(model).isForceSmsSelection || getRecipient(model).isUnregistered) && !getRecipient(model).isDistributionList
|
||||||
}
|
}
|
||||||
@@ -635,6 +657,13 @@ open class ContactSearchAdapter(
|
|||||||
ALWAYS
|
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 interface OnClickedCallback<D : ContactSearchData> {
|
||||||
fun onClicked(view: View, data: D, isSelected: Boolean)
|
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 {
|
interface LongClickCallbacks {
|
||||||
fun onKnownRecipientLongClick(view: View, data: ContactSearchData.KnownRecipient): Boolean
|
fun onKnownRecipientLongClick(view: View, data: ContactSearchData.KnownRecipient): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import org.thoughtcrime.securesms.contacts.HeaderAction
|
|||||||
*/
|
*/
|
||||||
class ContactSearchConfiguration private constructor(
|
class ContactSearchConfiguration private constructor(
|
||||||
val query: String?,
|
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
|
open val headerAction: HeaderAction? = null
|
||||||
abstract val expandConfig: ExpandConfig?
|
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.
|
* 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.
|
* Describes a given section. Useful for labeling sections and managing expansion state.
|
||||||
*/
|
*/
|
||||||
enum class SectionKey {
|
enum class SectionKey {
|
||||||
|
/**
|
||||||
|
* A generic empty item
|
||||||
|
*/
|
||||||
|
EMPTY,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists My Stories, distribution lists, as well as group stories.
|
* 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'
|
* Describes the mode for 'Username' or 'PhoneNumber'
|
||||||
*/
|
*/
|
||||||
enum class NewRowMode {
|
enum class NewRowMode {
|
||||||
|
NEW_CALL,
|
||||||
NEW_CONVERSATION,
|
NEW_CONVERSATION,
|
||||||
BLOCK,
|
BLOCK,
|
||||||
ADD_TO_GROUP
|
ADD_TO_GROUP
|
||||||
@@ -296,21 +310,47 @@ class ContactSearchConfiguration private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private class EmptyStateBuilder : Builder {
|
||||||
* Internal builder class with build method.
|
|
||||||
*/
|
|
||||||
private class ConfigurationBuilder : Builder {
|
|
||||||
private val sections: MutableList<Section> = mutableListOf()
|
private val sections: MutableList<Section> = mutableListOf()
|
||||||
|
|
||||||
override var query: String? = null
|
override var query: String? = null
|
||||||
override var hasEmptyState: Boolean = false
|
|
||||||
|
|
||||||
override fun addSection(section: Section) {
|
override fun addSection(section: Section) {
|
||||||
sections.add(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 {
|
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 {
|
interface Builder {
|
||||||
var query: String?
|
var query: String?
|
||||||
var hasEmptyState: Boolean
|
|
||||||
|
|
||||||
fun arbitrary(first: String, vararg rest: String) {
|
fun arbitrary(first: String, vararg rest: String) {
|
||||||
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
||||||
@@ -333,6 +372,8 @@ class ContactSearchConfiguration private constructor(
|
|||||||
addSection(Section.PhoneNumber(newRowMode))
|
addSection(Section.PhoneNumber(newRowMode))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun withEmptyState(emptyStateBuilderFn: Builder.() -> Unit)
|
||||||
|
|
||||||
fun addSection(section: Section)
|
fun addSection(section: Section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ class ContactSearchMediator(
|
|||||||
private val fragment: Fragment,
|
private val fragment: Fragment,
|
||||||
private val fixedContacts: Set<ContactSearchKey> = setOf(),
|
private val fixedContacts: Set<ContactSearchKey> = setOf(),
|
||||||
selectionLimits: SelectionLimits,
|
selectionLimits: SelectionLimits,
|
||||||
displayCheckBox: Boolean,
|
displayOptions: ContactSearchAdapter.DisplayOptions,
|
||||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
|
||||||
displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
|
|
||||||
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
|
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
|
||||||
private val callbacks: Callbacks = SimpleCallbacks(),
|
private val callbacks: Callbacks = SimpleCallbacks(),
|
||||||
performSafetyNumberChecks: Boolean = true,
|
performSafetyNumberChecks: Boolean = true,
|
||||||
@@ -69,9 +67,7 @@ class ContactSearchMediator(
|
|||||||
val adapter = adapterFactory.create(
|
val adapter = adapterFactory.create(
|
||||||
context = fragment.requireContext(),
|
context = fragment.requireContext(),
|
||||||
fixedContacts = fixedContacts,
|
fixedContacts = fixedContacts,
|
||||||
displayCheckBox = displayCheckBox,
|
displayOptions = displayOptions,
|
||||||
displaySmsTag = displaySmsTag,
|
|
||||||
displaySecondaryInformation = displaySecondaryInformation,
|
|
||||||
callbacks = object : ContactSearchAdapter.ClickCallbacks {
|
callbacks = object : ContactSearchAdapter.ClickCallbacks {
|
||||||
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
|
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
|
||||||
toggleStorySelection(view, story, isSelected)
|
toggleStorySelection(view, story, isSelected)
|
||||||
@@ -86,7 +82,8 @@ class ContactSearchMediator(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
longClickCallbacks = ContactSearchAdapter.LongClickCallbacksAdapter(),
|
longClickCallbacks = ContactSearchAdapter.LongClickCallbacksAdapter(),
|
||||||
storyContextMenuCallbacks = StoryContextMenuCallbacks()
|
storyContextMenuCallbacks = StoryContextMenuCallbacks(),
|
||||||
|
callButtonClickCallbacks = ContactSearchAdapter.EmptyCallButtonClickCallbacks
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -230,12 +227,11 @@ class ContactSearchMediator(
|
|||||||
fun create(
|
fun create(
|
||||||
context: Context,
|
context: Context,
|
||||||
fixedContacts: Set<ContactSearchKey>,
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayOptions: ContactSearchAdapter.DisplayOptions,
|
||||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
|
||||||
displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
|
|
||||||
callbacks: ContactSearchAdapter.ClickCallbacks,
|
callbacks: ContactSearchAdapter.ClickCallbacks,
|
||||||
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
|
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
||||||
|
callButtonClickCallbacks: ContactSearchAdapter.CallButtonClickCallbacks
|
||||||
): PagingMappingAdapter<ContactSearchKey>
|
): PagingMappingAdapter<ContactSearchKey>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,14 +239,13 @@ class ContactSearchMediator(
|
|||||||
override fun create(
|
override fun create(
|
||||||
context: Context,
|
context: Context,
|
||||||
fixedContacts: Set<ContactSearchKey>,
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayOptions: ContactSearchAdapter.DisplayOptions,
|
||||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
|
||||||
displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
|
|
||||||
callbacks: ContactSearchAdapter.ClickCallbacks,
|
callbacks: ContactSearchAdapter.ClickCallbacks,
|
||||||
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
|
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
||||||
|
callButtonClickCallbacks: ContactSearchAdapter.CallButtonClickCallbacks
|
||||||
): PagingMappingAdapter<ContactSearchKey> {
|
): 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 searchCache = SearchCache()
|
||||||
private var searchSize = -1
|
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 {
|
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)
|
getSectionSize(it, contactConfiguration.query)
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (searchSize == 0 && contactConfiguration.hasEmptyState) {
|
displayEmptyState = sizeOfNonArbitrarySections == 0
|
||||||
1
|
searchSize = if (displayEmptyState) {
|
||||||
} else {
|
contactConfiguration.emptyStateSections.sumOf {
|
||||||
searchSize
|
getSectionSize(it, contactConfiguration.query)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
arbitrarySections.sumOf {
|
||||||
|
getSectionSize(it, contactConfiguration.query)
|
||||||
|
} + sizeOfNonArbitrarySections
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchSize
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<ContactSearchData> {
|
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<ContactSearchData> {
|
||||||
if (searchSize == 0 && contactConfiguration.hasEmptyState) {
|
val sections: List<ContactSearchConfiguration.Section> = if (displayEmptyState) {
|
||||||
return mutableListOf(ContactSearchData.Empty(contactConfiguration.query))
|
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 startIndex: Index = findIndex(sizeMap, start)
|
||||||
val endIndex: Index = findIndex(sizeMap, start + length)
|
val endIndex: Index = findIndex(sizeMap, start + length)
|
||||||
|
|
||||||
val indexOfStartSection = contactConfiguration.sections.indexOf(startIndex.category)
|
val indexOfStartSection = sections.indexOf(startIndex.category)
|
||||||
val indexOfEndSection = contactConfiguration.sections.indexOf(endIndex.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) {
|
if (index in indexOfStartSection..indexOfEndSection) {
|
||||||
getSectionData(
|
getSectionData(
|
||||||
section = section,
|
section = section,
|
||||||
@@ -122,6 +141,7 @@ class ContactSearchPagedDataSource(
|
|||||||
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsIterator(query).getCollectionSize(section, query, null)
|
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsIterator(query).getCollectionSize(section, query, null)
|
||||||
is ContactSearchConfiguration.Section.PhoneNumber -> if (isPossiblyPhoneNumber(query)) 1 else 0
|
is ContactSearchConfiguration.Section.PhoneNumber -> if (isPossiblyPhoneNumber(query)) 1 else 0
|
||||||
is ContactSearchConfiguration.Section.Username -> if (isPossiblyUsername(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.ContactsWithoutThreads -> getContactsWithoutThreadsContactData(section, query, startIndex, endIndex)
|
||||||
is ContactSearchConfiguration.Section.PhoneNumber -> getPossiblePhoneNumber(section, query)
|
is ContactSearchConfiguration.Section.PhoneNumber -> getPossiblePhoneNumber(section, query)
|
||||||
is ContactSearchConfiguration.Section.Username -> getPossibleUsername(section, query)
|
is ContactSearchConfiguration.Section.Username -> getPossibleUsername(section, query)
|
||||||
|
is ContactSearchConfiguration.Section.Empty -> listOf(ContactSearchData.Empty(query))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,9 +124,11 @@ class MultiselectForwardFragment :
|
|||||||
this,
|
this,
|
||||||
emptySet(),
|
emptySet(),
|
||||||
FeatureFlags.shareSelectionLimit(),
|
FeatureFlags.shareSelectionLimit(),
|
||||||
!args.selectSingleRecipient,
|
ContactSearchAdapter.DisplayOptions(
|
||||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
displayCheckBox = !args.selectSingleRecipient,
|
||||||
ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
|
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||||
|
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
|
||||||
|
),
|
||||||
this::getConfiguration,
|
this::getConfiguration,
|
||||||
object : ContactSearchMediator.SimpleCallbacks() {
|
object : ContactSearchMediator.SimpleCallbacks() {
|
||||||
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
|
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,
|
contactSearchMediator = new ContactSearchMediator(this,
|
||||||
Collections.emptySet(),
|
Collections.emptySet(),
|
||||||
SelectionLimits.NO_LIMITS,
|
SelectionLimits.NO_LIMITS,
|
||||||
|
new ContactSearchAdapter.DisplayOptions(
|
||||||
false,
|
false,
|
||||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||||
ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
|
ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
|
||||||
|
false
|
||||||
|
),
|
||||||
this::mapSearchStateToConfiguration,
|
this::mapSearchStateToConfiguration,
|
||||||
new ContactSearchMediator.SimpleCallbacks(),
|
new ContactSearchMediator.SimpleCallbacks(),
|
||||||
false,
|
false,
|
||||||
(context,
|
(context,
|
||||||
fixedContacts,
|
fixedContacts,
|
||||||
displayCheckBox,
|
displayOptions,
|
||||||
displaySmsTag,
|
|
||||||
displaySecondaryInformation,
|
|
||||||
callbacks,
|
callbacks,
|
||||||
longClickCallbacks,
|
longClickCallbacks,
|
||||||
storyContextMenuCallbacks
|
storyContextMenuCallbacks,
|
||||||
|
callButtonClickCallbacks
|
||||||
) -> {
|
) -> {
|
||||||
//noinspection CodeBlock2Expr
|
//noinspection CodeBlock2Expr
|
||||||
return new ConversationListSearchAdapter(
|
return new ConversationListSearchAdapter(
|
||||||
context,
|
context,
|
||||||
fixedContacts,
|
fixedContacts,
|
||||||
displayCheckBox,
|
displayOptions,
|
||||||
displaySmsTag,
|
|
||||||
displaySecondaryInformation,
|
|
||||||
new ContactSearchClickCallbacks(callbacks),
|
new ContactSearchClickCallbacks(callbacks),
|
||||||
longClickCallbacks,
|
longClickCallbacks,
|
||||||
storyContextMenuCallbacks,
|
storyContextMenuCallbacks,
|
||||||
|
callButtonClickCallbacks,
|
||||||
getViewLifecycleOwner(),
|
getViewLifecycleOwner(),
|
||||||
GlideApp.with(this)
|
GlideApp.with(this)
|
||||||
);
|
);
|
||||||
@@ -655,7 +656,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||||||
null
|
null
|
||||||
));
|
));
|
||||||
|
|
||||||
builder.setHasEmptyState(true);
|
builder.withEmptyState(emptyStateBuilder -> {
|
||||||
|
builder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
builder.arbitrary(
|
builder.arbitrary(
|
||||||
conversationFilterRequest.getSource() == ConversationFilterSource.DRAG
|
conversationFilterRequest.getSource() == ConversationFilterSource.DRAG
|
||||||
|
|||||||
@@ -26,15 +26,14 @@ import java.util.Locale
|
|||||||
class ConversationListSearchAdapter(
|
class ConversationListSearchAdapter(
|
||||||
context: Context,
|
context: Context,
|
||||||
fixedContacts: Set<ContactSearchKey>,
|
fixedContacts: Set<ContactSearchKey>,
|
||||||
displayCheckBox: Boolean,
|
displayOptions: DisplayOptions,
|
||||||
displaySmsTag: DisplaySmsTag,
|
|
||||||
displaySecondaryInformation: DisplaySecondaryInformation,
|
|
||||||
onClickedCallbacks: ConversationListSearchClickCallbacks,
|
onClickedCallbacks: ConversationListSearchClickCallbacks,
|
||||||
longClickCallbacks: LongClickCallbacks,
|
longClickCallbacks: LongClickCallbacks,
|
||||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||||
|
callButtonClickCallbacks: CallButtonClickCallbacks,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
glideRequests: GlideRequests
|
glideRequests: GlideRequests
|
||||||
) : ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, onClickedCallbacks, longClickCallbacks, storyContextMenuCallbacks) {
|
) : ContactSearchAdapter(context, fixedContacts, displayOptions, onClickedCallbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerFactory(
|
registerFactory(
|
||||||
|
|||||||
@@ -66,9 +66,11 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment(
|
|||||||
mediator = ContactSearchMediator(
|
mediator = ContactSearchMediator(
|
||||||
fragment = this,
|
fragment = this,
|
||||||
selectionLimits = FeatureFlags.shareSelectionLimit(),
|
selectionLimits = FeatureFlags.shareSelectionLimit(),
|
||||||
|
displayOptions = ContactSearchAdapter.DisplayOptions(
|
||||||
displayCheckBox = true,
|
displayCheckBox = true,
|
||||||
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||||
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
|
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
|
||||||
|
),
|
||||||
mapStateToConfiguration = { state ->
|
mapStateToConfiguration = { state ->
|
||||||
ContactSearchConfiguration.build {
|
ContactSearchConfiguration.build {
|
||||||
query = state.query
|
query = state.query
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ class ViewAllSignalConnectionsFragment : Fragment(R.layout.view_all_signal_conne
|
|||||||
val mediator = ContactSearchMediator(
|
val mediator = ContactSearchMediator(
|
||||||
fragment = this,
|
fragment = this,
|
||||||
selectionLimits = SelectionLimits(0, 0),
|
selectionLimits = SelectionLimits(0, 0),
|
||||||
|
displayOptions = ContactSearchAdapter.DisplayOptions(
|
||||||
displayCheckBox = false,
|
displayCheckBox = false,
|
||||||
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.IF_NOT_REGISTERED,
|
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.IF_NOT_REGISTERED,
|
||||||
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
|
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
|
||||||
|
),
|
||||||
mapStateToConfiguration = { getConfiguration() },
|
mapStateToConfiguration = { getConfiguration() },
|
||||||
performSafetyNumberChecks = false
|
performSafetyNumberChecks = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
android:textColor="@color/signal_text_primary"
|
android:textColor="@color/signal_text_primary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/number"
|
app:layout_constraintBottom_toTopOf="@id/number"
|
||||||
app:layout_constraintEnd_toStartOf="@id/sms_tag"
|
app:layout_constraintEnd_toStartOf="@id/start_audio"
|
||||||
app:layout_constraintStart_toEndOf="@id/contact_photo_image"
|
app:layout_constraintStart_toEndOf="@id/contact_photo_image"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
@@ -120,4 +120,33 @@
|
|||||||
app:layout_goneMarginEnd="0dp"
|
app:layout_goneMarginEnd="0dp"
|
||||||
tools:visibility="visible" />
|
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>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/rounded_inset_ripple_background"
|
||||||
android:minHeight="@dimen/contact_selection_item_height"
|
android:minHeight="@dimen/contact_selection_item_height"
|
||||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
android:paddingStart="@dimen/dsl_settings_gutter"
|
||||||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
android:paddingEnd="@dimen/dsl_settings_gutter"
|
||||||
android:background="@drawable/rounded_inset_ripple_background">
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<ImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/invite_image"
|
android:id="@+id/invite_image"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:background="@color/signal_colorSurfaceVariant"
|
||||||
android:importantForAccessibility="no"
|
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_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/invite_text"
|
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 -->
|
<!-- contact_selection_activity -->
|
||||||
<string name="contact_selection_activity__invite_to_signal">Invite to Signal</string>
|
<string name="contact_selection_activity__invite_to_signal">Invite to Signal</string>
|
||||||
<string name="contact_selection_activity__new_group">New group</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 -->
|
<!-- contact_filter_toolbar -->
|
||||||
<string name="contact_filter_toolbar__clear_entered_text_description">Clear entered text</string>
|
<string name="contact_filter_toolbar__clear_entered_text_description">Clear entered text</string>
|
||||||
@@ -3113,6 +3119,8 @@
|
|||||||
<!-- **************************************** -->
|
<!-- **************************************** -->
|
||||||
|
|
||||||
<!-- contact_selection_list -->
|
<!-- 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">New message to…</string>
|
||||||
<string name="contact_selection_list__unknown_contact_block">Block user</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>
|
<string name="contact_selection_list__unknown_contact_add_to_group">Add to group</string>
|
||||||
@@ -5728,5 +5736,9 @@
|
|||||||
<!-- Call log new call content description -->
|
<!-- Call log new call content description -->
|
||||||
<string name="CallLogFragment__start_a_new_call">Start a new call</string>
|
<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 -->
|
<!-- EOF -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -148,6 +148,14 @@ class ContactSearchPagedDataSourceTest {
|
|||||||
"two",
|
"two",
|
||||||
"three"
|
"three"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
withEmptyState {
|
||||||
|
arbitrary(
|
||||||
|
"one",
|
||||||
|
"two",
|
||||||
|
"three"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ContactSearchPagedDataSource(configuration, repository, ArbitraryRepoFake())
|
return ContactSearchPagedDataSource(configuration, repository, ArbitraryRepoFake())
|
||||||
|
|||||||
Reference in New Issue
Block a user