Add new call screen for calls tab.

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

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.contacts;
import androidx.annotation.NonNull;
public final class ContactSelectionDisplayMode {
public static final int FLAG_PUSH = 1;
public static final int FLAG_SMS = 1 << 1;
@@ -11,5 +13,50 @@ public final class ContactSelectionDisplayMode {
public static final int FLAG_HIDE_NEW = 1 << 6;
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
public static final int FLAG_GROUPS_AFTER_CONTACTS = 1 << 8;
public static final int FLAG_GROUP_MEMBERS = 1 << 9;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
public static Builder all() {
return new Builder(FLAG_ALL);
}
public static Builder none() {
return new Builder(0);
}
public static class Builder {
int displayMode = 0;
public Builder(int displayMode) {
this.displayMode = displayMode;
}
public @NonNull Builder withPush() {
displayMode = setFlag(displayMode, FLAG_PUSH);
return this;
}
public @NonNull Builder withActiveGroups() {
displayMode = setFlag(displayMode, FLAG_ACTIVE_GROUPS);
return this;
}
public @NonNull Builder withGroupMembers() {
displayMode = setFlag(displayMode, FLAG_GROUP_MEMBERS);
return this;
}
public int build() {
return displayMode;
}
private static int setFlag(int displayMode, int flag) {
return displayMode | flag;
}
private static int clearFlag(int displayMode, int flag) {
return displayMode & ~flag;
}
}
}

View File

@@ -37,20 +37,19 @@ import org.thoughtcrime.securesms.util.visible
open class ContactSearchAdapter(
private val context: Context,
fixedContacts: Set<ContactSearchKey>,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
displaySecondaryInformation: DisplaySecondaryInformation,
displayOptions: DisplayOptions,
onClickCallbacks: ClickCallbacks,
longClickCallbacks: LongClickCallbacks,
storyContextMenuCallbacks: StoryContextMenuCallbacks
storyContextMenuCallbacks: StoryContextMenuCallbacks,
callButtonClickCallbacks: CallButtonClickCallbacks
) : PagingMappingAdapter<ContactSearchKey>(), FastScrollAdapter {
init {
registerStoryItems(this, displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
registerKnownRecipientItems(this, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick)
registerStoryItems(this, displayOptions.displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
registerKnownRecipientItems(this, fixedContacts, displayOptions, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick, callButtonClickCallbacks)
registerHeaders(this)
registerExpands(this, onClickCallbacks::onExpandClicked)
registerFactory(UnknownRecipientModel::class.java, LayoutFactory({ UnknownRecipientViewHolder(it, onClickCallbacks::onUnknownRecipientClicked, displayCheckBox) }, R.layout.contact_search_unknown_item))
registerFactory(UnknownRecipientModel::class.java, LayoutFactory({ UnknownRecipientViewHolder(it, onClickCallbacks::onUnknownRecipientClicked, displayOptions.displayCheckBox) }, R.layout.contact_search_unknown_item))
}
override fun getBubbleText(position: Int): CharSequence {
@@ -82,15 +81,16 @@ open class ContactSearchAdapter(
fun registerKnownRecipientItems(
mappingAdapter: MappingAdapter,
fixedContacts: Set<ContactSearchKey>,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
displaySecondaryInformation: DisplaySecondaryInformation,
displayOptions: DisplayOptions,
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>,
recipientLongClickCallback: OnLongClickedCallback<ContactSearchData.KnownRecipient>
recipientLongClickCallback: OnLongClickedCallback<ContactSearchData.KnownRecipient>,
recipientCallButtonClickCallbacks: CallButtonClickCallbacks
) {
mappingAdapter.registerFactory(
RecipientModel::class.java,
LayoutFactory({ KnownRecipientViewHolder(it, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, recipientListener, recipientLongClickCallback) }, R.layout.contact_search_item)
LayoutFactory({
KnownRecipientViewHolder(it, fixedContacts, displayOptions, recipientListener, recipientLongClickCallback, recipientCallButtonClickCallbacks)
}, R.layout.contact_search_item)
)
}
@@ -161,7 +161,7 @@ open class ContactSearchAdapter(
displayCheckBox: Boolean,
onClick: OnClickedCallback<ContactSearchData.Story>,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, onClick) {
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, DisplayOptions(displayCheckBox = displayCheckBox), onClick, EmptyCallButtonClickCallbacks) {
override fun isSelected(model: StoryModel): Boolean = model.isSelected
override fun getData(model: StoryModel): ContactSearchData.Story = model.story
override fun getRecipient(model: StoryModel): Recipient = model.story.recipient
@@ -334,6 +334,7 @@ open class ContactSearchAdapter(
checkbox.isSelected = false
name.setText(
when (model.data.mode) {
ContactSearchConfiguration.NewRowMode.NEW_CALL -> R.string.contact_selection_list__new_call
ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION -> R.string.contact_selection_list__unknown_contact
ContactSearchConfiguration.NewRowMode.BLOCK -> R.string.contact_selection_list__unknown_contact_block
ContactSearchConfiguration.NewRowMode.ADD_TO_GROUP -> R.string.contact_selection_list__unknown_contact_add_to_group
@@ -349,12 +350,11 @@ open class ContactSearchAdapter(
private class KnownRecipientViewHolder(
itemView: View,
private val fixedContacts: Set<ContactSearchKey>,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
private val displaySecondaryInformation: DisplaySecondaryInformation,
displayOptions: DisplayOptions,
onClick: OnClickedCallback<ContactSearchData.KnownRecipient>,
private val onLongClick: OnLongClickedCallback<ContactSearchData.KnownRecipient>
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
private val onLongClick: OnLongClickedCallback<ContactSearchData.KnownRecipient>,
callButtonClickCallbacks: CallButtonClickCallbacks
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayOptions, onClick, callButtonClickCallbacks), LetterHeaderDecoration.LetterHeaderItem {
private var headerLetter: String? = null
@@ -370,10 +370,10 @@ open class ContactSearchAdapter(
val count = recipient.participantIds.size
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
number.visible = true
} else if (displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.combinedAboutAndEmoji != null) {
} else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.combinedAboutAndEmoji != null) {
number.text = recipient.combinedAboutAndEmoji
number.visible = true
} else if (displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.hasE164()) {
} else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.hasE164()) {
number.text = PhoneNumberFormatter.prettyPrint(recipient.requireE164())
number.visible = true
} else {
@@ -410,9 +410,9 @@ open class ContactSearchAdapter(
*/
abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(
itemView: View,
private val displayCheckBox: Boolean,
private val displaySmsTag: DisplaySmsTag,
val onClick: OnClickedCallback<D>
val displayOptions: DisplayOptions,
val onClick: OnClickedCallback<D>,
val onCallButtonClickCallbacks: CallButtonClickCallbacks
) : MappingViewHolder<T>(itemView) {
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
@@ -422,6 +422,8 @@ open class ContactSearchAdapter(
protected val number: TextView = itemView.findViewById(R.id.number)
protected val label: TextView = itemView.findViewById(R.id.label)
protected val smsTag: View = itemView.findViewById(R.id.sms_tag)
private val startAudio: View = itemView.findViewById(R.id.start_audio)
private val startVideo: View = itemView.findViewById(R.id.start_video)
override fun bind(model: T) {
if (isEnabled(model)) {
@@ -442,10 +444,11 @@ open class ContactSearchAdapter(
bindNumberField(model)
bindLabelField(model)
bindSmsTagField(model)
bindCallButtons(model)
}
protected open fun bindCheckbox(model: T) {
checkbox.visible = displayCheckBox
checkbox.visible = displayOptions.displayCheckBox
checkbox.isChecked = isSelected(model)
}
@@ -476,7 +479,7 @@ open class ContactSearchAdapter(
}
protected open fun bindSmsTagField(model: T) {
smsTag.visible = when (displaySmsTag) {
smsTag.visible = when (displayOptions.displaySmsTag) {
DisplaySmsTag.DEFAULT -> isSmsContact(model)
DisplaySmsTag.IF_NOT_REGISTERED -> isNotRegistered(model)
DisplaySmsTag.NEVER -> false
@@ -485,6 +488,25 @@ open class ContactSearchAdapter(
protected open fun bindLongPress(model: T) = Unit
private fun bindCallButtons(model: T) {
val recipient = getRecipient(model)
if (displayOptions.displayCallButtons && (recipient.isPushGroup || recipient.isRegistered)) {
startVideo.visible = true
startAudio.visible = !recipient.isPushGroup
startVideo.setOnClickListener {
onCallButtonClickCallbacks.onVideoCallButtonClicked(recipient)
}
startAudio.setOnClickListener {
onCallButtonClickCallbacks.onAudioCallButtonClicked(recipient)
}
} else {
startVideo.visible = false
startAudio.visible = false
}
}
private fun isSmsContact(model: T): Boolean {
return (getRecipient(model).isForceSmsSelection || getRecipient(model).isUnregistered) && !getRecipient(model).isDistributionList
}
@@ -635,6 +657,13 @@ open class ContactSearchAdapter(
ALWAYS
}
data class DisplayOptions(
val displayCheckBox: Boolean = false,
val displaySmsTag: DisplaySmsTag = DisplaySmsTag.NEVER,
val displaySecondaryInformation: DisplaySecondaryInformation = DisplaySecondaryInformation.NEVER,
val displayCallButtons: Boolean = false
)
fun interface OnClickedCallback<D : ContactSearchData> {
fun onClicked(view: View, data: D, isSelected: Boolean)
}
@@ -652,6 +681,16 @@ open class ContactSearchAdapter(
}
}
interface CallButtonClickCallbacks {
fun onVideoCallButtonClicked(recipient: Recipient)
fun onAudioCallButtonClicked(recipient: Recipient)
}
object EmptyCallButtonClickCallbacks : CallButtonClickCallbacks {
override fun onVideoCallButtonClicked(recipient: Recipient) = Unit
override fun onAudioCallButtonClicked(recipient: Recipient) = Unit
}
interface LongClickCallbacks {
fun onKnownRecipientLongClick(view: View, data: ContactSearchData.KnownRecipient): Boolean
}

View File

@@ -7,8 +7,8 @@ import org.thoughtcrime.securesms.contacts.HeaderAction
*/
class ContactSearchConfiguration private constructor(
val query: String?,
val hasEmptyState: Boolean,
val sections: List<Section>
val sections: List<Section>,
val emptyStateSections: List<Section>
) {
/**
@@ -20,6 +20,14 @@ class ContactSearchConfiguration private constructor(
open val headerAction: HeaderAction? = null
abstract val expandConfig: ExpandConfig?
/**
* Section representing the "extra" item.
*/
object Empty : Section(SectionKey.EMPTY) {
override val includeHeader: Boolean = false
override val expandConfig: ExpandConfig? = null
}
/**
* Distribution lists and group stories.
*
@@ -188,6 +196,11 @@ class ContactSearchConfiguration private constructor(
* Describes a given section. Useful for labeling sections and managing expansion state.
*/
enum class SectionKey {
/**
* A generic empty item
*/
EMPTY,
/**
* Lists My Stories, distribution lists, as well as group stories.
*/
@@ -271,6 +284,7 @@ class ContactSearchConfiguration private constructor(
* Describes the mode for 'Username' or 'PhoneNumber'
*/
enum class NewRowMode {
NEW_CALL,
NEW_CONVERSATION,
BLOCK,
ADD_TO_GROUP
@@ -296,21 +310,47 @@ class ContactSearchConfiguration private constructor(
}
}
/**
* Internal builder class with build method.
*/
private class ConfigurationBuilder : Builder {
private class EmptyStateBuilder : Builder {
private val sections: MutableList<Section> = mutableListOf()
override var query: String? = null
override var hasEmptyState: Boolean = false
override fun addSection(section: Section) {
sections.add(section)
}
override fun withEmptyState(emptyStateBuilderFn: Builder.() -> Unit) {
error("Unsupported operation: Already in empty state.")
}
fun build(): List<Section> {
return sections
}
}
/**
* Internal builder class with build method.
*/
private class ConfigurationBuilder : Builder {
private val sections: MutableList<Section> = mutableListOf()
private val emptyState = EmptyStateBuilder()
override var query: String? = null
override fun addSection(section: Section) {
sections.add(section)
}
override fun withEmptyState(emptyStateBuilderFn: Builder.() -> Unit) {
emptyState.emptyStateBuilderFn()
}
fun build(): ContactSearchConfiguration {
return ContactSearchConfiguration(query, hasEmptyState, sections)
return ContactSearchConfiguration(
query = query,
sections = sections,
emptyStateSections = emptyState.build()
)
}
}
@@ -319,7 +359,6 @@ class ContactSearchConfiguration private constructor(
*/
interface Builder {
var query: String?
var hasEmptyState: Boolean
fun arbitrary(first: String, vararg rest: String) {
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
@@ -333,6 +372,8 @@ class ContactSearchConfiguration private constructor(
addSection(Section.PhoneNumber(newRowMode))
}
fun withEmptyState(emptyStateBuilderFn: Builder.() -> Unit)
fun addSection(section: Section)
}
}

View File

@@ -42,9 +42,7 @@ class ContactSearchMediator(
private val fragment: Fragment,
private val fixedContacts: Set<ContactSearchKey> = setOf(),
selectionLimits: SelectionLimits,
displayCheckBox: Boolean,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
displayOptions: ContactSearchAdapter.DisplayOptions,
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
private val callbacks: Callbacks = SimpleCallbacks(),
performSafetyNumberChecks: Boolean = true,
@@ -69,9 +67,7 @@ class ContactSearchMediator(
val adapter = adapterFactory.create(
context = fragment.requireContext(),
fixedContacts = fixedContacts,
displayCheckBox = displayCheckBox,
displaySmsTag = displaySmsTag,
displaySecondaryInformation = displaySecondaryInformation,
displayOptions = displayOptions,
callbacks = object : ContactSearchAdapter.ClickCallbacks {
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
toggleStorySelection(view, story, isSelected)
@@ -86,7 +82,8 @@ class ContactSearchMediator(
}
},
longClickCallbacks = ContactSearchAdapter.LongClickCallbacksAdapter(),
storyContextMenuCallbacks = StoryContextMenuCallbacks()
storyContextMenuCallbacks = StoryContextMenuCallbacks(),
callButtonClickCallbacks = ContactSearchAdapter.EmptyCallButtonClickCallbacks
)
init {
@@ -230,12 +227,11 @@ class ContactSearchMediator(
fun create(
context: Context,
fixedContacts: Set<ContactSearchKey>,
displayCheckBox: Boolean,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
displayOptions: ContactSearchAdapter.DisplayOptions,
callbacks: ContactSearchAdapter.ClickCallbacks,
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
callButtonClickCallbacks: ContactSearchAdapter.CallButtonClickCallbacks
): PagingMappingAdapter<ContactSearchKey>
}
@@ -243,14 +239,13 @@ class ContactSearchMediator(
override fun create(
context: Context,
fixedContacts: Set<ContactSearchKey>,
displayCheckBox: Boolean,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
displaySecondaryInformation: ContactSearchAdapter.DisplaySecondaryInformation,
displayOptions: ContactSearchAdapter.DisplayOptions,
callbacks: ContactSearchAdapter.ClickCallbacks,
longClickCallbacks: ContactSearchAdapter.LongClickCallbacks,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
callButtonClickCallbacks: ContactSearchAdapter.CallButtonClickCallbacks
): PagingMappingAdapter<ContactSearchKey> {
return ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, callbacks, longClickCallbacks, storyContextMenuCallbacks)
return ContactSearchAdapter(context, fixedContacts, displayOptions, callbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks)
}
}
}

View File

@@ -44,32 +44,51 @@ class ContactSearchPagedDataSource(
private var searchCache = SearchCache()
private var searchSize = -1
private var displayEmptyState: Boolean = false
/**
* When determining when the list is in an empty state, we ignore any arbitrary items, since in general
* they are always present. If you'd like arbitrary items to appear even when the list is empty, ensure
* they are added to the empty state configuration.
*/
override fun size(): Int {
searchSize = contactConfiguration.sections.sumOf {
val (arbitrarySections, nonArbitrarySections) = contactConfiguration.sections.partition {
it is ContactSearchConfiguration.Section.Arbitrary
}
val sizeOfNonArbitrarySections = nonArbitrarySections.sumOf {
getSectionSize(it, contactConfiguration.query)
}
return if (searchSize == 0 && contactConfiguration.hasEmptyState) {
1
displayEmptyState = sizeOfNonArbitrarySections == 0
searchSize = if (displayEmptyState) {
contactConfiguration.emptyStateSections.sumOf {
getSectionSize(it, contactConfiguration.query)
}
} else {
searchSize
arbitrarySections.sumOf {
getSectionSize(it, contactConfiguration.query)
} + sizeOfNonArbitrarySections
}
return searchSize
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<ContactSearchData> {
if (searchSize == 0 && contactConfiguration.hasEmptyState) {
return mutableListOf(ContactSearchData.Empty(contactConfiguration.query))
val sections: List<ContactSearchConfiguration.Section> = if (displayEmptyState) {
contactConfiguration.emptyStateSections
} else {
contactConfiguration.sections
}
val sizeMap: Map<ContactSearchConfiguration.Section, Int> = contactConfiguration.sections.associateWith { getSectionSize(it, contactConfiguration.query) }
val sizeMap: Map<ContactSearchConfiguration.Section, Int> = sections.associateWith { getSectionSize(it, contactConfiguration.query) }
val startIndex: Index = findIndex(sizeMap, start)
val endIndex: Index = findIndex(sizeMap, start + length)
val indexOfStartSection = contactConfiguration.sections.indexOf(startIndex.category)
val indexOfEndSection = contactConfiguration.sections.indexOf(endIndex.category)
val indexOfStartSection = sections.indexOf(startIndex.category)
val indexOfEndSection = sections.indexOf(endIndex.category)
val results: List<List<ContactSearchData>> = contactConfiguration.sections.mapIndexed { index, section ->
val results: List<List<ContactSearchData>> = sections.mapIndexed { index, section ->
if (index in indexOfStartSection..indexOfEndSection) {
getSectionData(
section = section,
@@ -122,6 +141,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsIterator(query).getCollectionSize(section, query, null)
is ContactSearchConfiguration.Section.PhoneNumber -> if (isPossiblyPhoneNumber(query)) 1 else 0
is ContactSearchConfiguration.Section.Username -> if (isPossiblyUsername(query)) 1 else 0
is ContactSearchConfiguration.Section.Empty -> 1
}
}
@@ -160,6 +180,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsContactData(section, query, startIndex, endIndex)
is ContactSearchConfiguration.Section.PhoneNumber -> getPossiblePhoneNumber(section, query)
is ContactSearchConfiguration.Section.Username -> getPossibleUsername(section, query)
is ContactSearchConfiguration.Section.Empty -> listOf(ContactSearchData.Empty(query))
}
}