mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Add "contacts without threads" section to Conversation List Search.
This commit is contained in:
committed by
Greyson Parrelli
parent
09902e5d11
commit
36dfa19aec
@@ -33,24 +33,22 @@ import org.thoughtcrime.securesms.util.visible
|
||||
open class ContactSearchAdapter(
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
recipientListener: Listener<ContactSearchData.KnownRecipient>,
|
||||
storyListener: Listener<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit
|
||||
onClickCallbacks: ClickCallbacks,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks
|
||||
) : PagingMappingAdapter<ContactSearchKey>() {
|
||||
|
||||
init {
|
||||
registerStoryItems(this, displayCheckBox, storyListener, storyContextMenuCallbacks)
|
||||
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, recipientListener)
|
||||
registerStoryItems(this, displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
|
||||
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, onClickCallbacks::onKnownRecipientClicked)
|
||||
registerHeaders(this)
|
||||
registerExpands(this, expandListener)
|
||||
registerExpands(this, onClickCallbacks::onExpandClicked)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun registerStoryItems(
|
||||
mappingAdapter: MappingAdapter,
|
||||
displayCheckBox: Boolean = false,
|
||||
storyListener: Listener<ContactSearchData.Story>,
|
||||
storyListener: OnClickedCallback<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
|
||||
) {
|
||||
mappingAdapter.registerFactory(
|
||||
@@ -63,7 +61,7 @@ open class ContactSearchAdapter(
|
||||
mappingAdapter: MappingAdapter,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
recipientListener: Listener<ContactSearchData.KnownRecipient>
|
||||
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>
|
||||
) {
|
||||
mappingAdapter.registerFactory(
|
||||
RecipientModel::class.java,
|
||||
@@ -135,7 +133,7 @@ open class ContactSearchAdapter(
|
||||
private class StoryViewHolder(
|
||||
itemView: View,
|
||||
displayCheckBox: Boolean,
|
||||
onClick: Listener<ContactSearchData.Story>,
|
||||
onClick: OnClickedCallback<ContactSearchData.Story>,
|
||||
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
|
||||
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, onClick) {
|
||||
override fun isSelected(model: StoryModel): Boolean = model.isSelected
|
||||
@@ -267,7 +265,7 @@ open class ContactSearchAdapter(
|
||||
itemView: View,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
onClick: Listener<ContactSearchData.KnownRecipient>
|
||||
onClick: OnClickedCallback<ContactSearchData.KnownRecipient>
|
||||
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
|
||||
|
||||
private var headerLetter: String? = null
|
||||
@@ -304,7 +302,7 @@ open class ContactSearchAdapter(
|
||||
itemView: View,
|
||||
private val displayCheckBox: Boolean,
|
||||
private val displaySmsTag: DisplaySmsTag,
|
||||
val onClick: Listener<D>
|
||||
val onClick: OnClickedCallback<D>
|
||||
) : MappingViewHolder<T>(itemView) {
|
||||
|
||||
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
|
||||
@@ -318,7 +316,7 @@ open class ContactSearchAdapter(
|
||||
override fun bind(model: T) {
|
||||
checkbox.visible = displayCheckBox
|
||||
checkbox.isChecked = isSelected(model)
|
||||
itemView.setOnClickListener { onClick.listen(avatar, getData(model), isSelected(model)) }
|
||||
itemView.setOnClickListener { onClick.onClicked(avatar, getData(model), isSelected(model)) }
|
||||
bindLongPress(model)
|
||||
|
||||
if (payload.isNotEmpty()) {
|
||||
@@ -451,6 +449,7 @@ open class ContactSearchAdapter(
|
||||
ContactSearchConfiguration.SectionKey.CHATS -> R.string.ContactsCursorLoader__chats
|
||||
ContactSearchConfiguration.SectionKey.MESSAGES -> R.string.ContactsCursorLoader__messages
|
||||
ContactSearchConfiguration.SectionKey.GROUPS_WITH_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
||||
ContactSearchConfiguration.SectionKey.CONTACTS_WITHOUT_THREADS -> R.string.ContactsCursorLoader_contacts
|
||||
}
|
||||
)
|
||||
|
||||
@@ -508,7 +507,13 @@ open class ContactSearchAdapter(
|
||||
NEVER
|
||||
}
|
||||
|
||||
fun interface Listener<D : ContactSearchData> {
|
||||
fun listen(view: View, data: D, isSelected: Boolean)
|
||||
fun interface OnClickedCallback<D : ContactSearchData> {
|
||||
fun onClicked(view: View, data: D, isSelected: Boolean)
|
||||
}
|
||||
|
||||
interface ClickCallbacks {
|
||||
fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean)
|
||||
fun onKnownRecipientClicked(view: View, knownRecipient: ContactSearchData.KnownRecipient, isSelected: Boolean)
|
||||
fun onExpandClicked(expand: ContactSearchData.Expand)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ class ContactSearchConfiguration private constructor(
|
||||
val hasEmptyState: Boolean,
|
||||
val sections: List<Section>
|
||||
) {
|
||||
|
||||
/**
|
||||
* Describes the configuration for a given section of content in search results.
|
||||
*/
|
||||
sealed class Section(val sectionKey: SectionKey) {
|
||||
|
||||
abstract val includeHeader: Boolean
|
||||
@@ -18,6 +22,10 @@ class ContactSearchConfiguration private constructor(
|
||||
|
||||
/**
|
||||
* Distribution lists and group stories.
|
||||
*
|
||||
* Key: [ContactSearchKey.RecipientSearchKey]
|
||||
* Data: [ContactSearchData.Story]
|
||||
* Model: [ContactSearchAdapter.StoryModel]
|
||||
*/
|
||||
data class Stories(
|
||||
val groupStories: Set<ContactSearchData.Story> = emptySet(),
|
||||
@@ -28,6 +36,10 @@ class ContactSearchConfiguration private constructor(
|
||||
|
||||
/**
|
||||
* Recent contacts
|
||||
*
|
||||
* Key: [ContactSearchKey.RecipientSearchKey]
|
||||
* Data: [ContactSearchData.KnownRecipient]
|
||||
* Model: [ContactSearchAdapter.RecipientModel]
|
||||
*/
|
||||
data class Recents(
|
||||
val limit: Int = 25,
|
||||
@@ -47,7 +59,11 @@ class ContactSearchConfiguration private constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* 1:1 Recipients
|
||||
* 1:1 Recipients with whom the user has started a conversation.
|
||||
*
|
||||
* Key: [ContactSearchKey.RecipientSearchKey]
|
||||
* Data: [ContactSearchData.KnownRecipient]
|
||||
* Model: [ContactSearchAdapter.RecipientModel]
|
||||
*/
|
||||
data class Individuals(
|
||||
val includeSelf: Boolean,
|
||||
@@ -59,6 +75,10 @@ class ContactSearchConfiguration private constructor(
|
||||
|
||||
/**
|
||||
* Group Recipients
|
||||
*
|
||||
* Key: [ContactSearchKey.RecipientSearchKey]
|
||||
* Data: [ContactSearchData.KnownRecipient]
|
||||
* Model: [ContactSearchAdapter.RecipientModel]
|
||||
*/
|
||||
data class Groups(
|
||||
val includeMms: Boolean = false,
|
||||
@@ -71,6 +91,14 @@ class ContactSearchConfiguration private constructor(
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
) : Section(SectionKey.GROUPS)
|
||||
|
||||
/**
|
||||
* A set of arbitrary rows, in the order given in the builder. Usage requires
|
||||
* an implementation of [ArbitraryRepository] to be passed into [ContactSearchMediator]
|
||||
*
|
||||
* Key: [ContactSearchKey.Arbitrary]
|
||||
* Data: [ContactSearchData.Arbitrary]
|
||||
* Model: To be provided by an instance of [ArbitraryRepository]
|
||||
*/
|
||||
data class Arbitrary(
|
||||
val types: Set<String>
|
||||
) : Section(SectionKey.ARBITRARY) {
|
||||
@@ -78,6 +106,14 @@ class ContactSearchConfiguration private constructor(
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Individuals who you have not started a conversation with, but are members of shared
|
||||
* groups.
|
||||
*
|
||||
* Key: [ContactSearchKey.RecipientSearchKey]
|
||||
* Data: [ContactSearchData.KnownRecipient]
|
||||
* Model: [ContactSearchAdapter.RecipientModel]
|
||||
*/
|
||||
data class GroupMembers(
|
||||
override val includeHeader: Boolean = true,
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
@@ -89,22 +125,53 @@ class ContactSearchConfiguration private constructor(
|
||||
*
|
||||
* Key: [ContactSearchKey.GroupWithMembers]
|
||||
* Data: [ContactSearchData.GroupWithMembers]
|
||||
* Model: [ContactSearchAdapter.GroupWithMembersModel]
|
||||
*/
|
||||
data class GroupsWithMembers(
|
||||
override val includeHeader: Boolean = true,
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
) : Section(SectionKey.GROUPS_WITH_MEMBERS)
|
||||
|
||||
/**
|
||||
* 1:1 and Group chat search results, whose data contains
|
||||
* a ThreadRecord. Only displayed when there is a search query.
|
||||
*
|
||||
* Key: [ContactSearchKey.Thread]
|
||||
* Data: [ContactSearchData.Thread]
|
||||
* Model: [ContactSearchAdapter.ThreadModel]
|
||||
*/
|
||||
data class Chats(
|
||||
val isUnreadOnly: Boolean = false,
|
||||
override val includeHeader: Boolean = true,
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
) : Section(SectionKey.CHATS)
|
||||
|
||||
/**
|
||||
* Message search results, only displayed when there
|
||||
* is a search query.
|
||||
*
|
||||
* Key: [ContactSearchKey.Message]
|
||||
* Data: [ContactSearchData.Message]
|
||||
* Model: [ContactSearchAdapter.MessageModel]
|
||||
*/
|
||||
data class Messages(
|
||||
override val includeHeader: Boolean = true,
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
) : Section(SectionKey.MESSAGES)
|
||||
|
||||
/**
|
||||
* Contacts that the user has shared profile key data with or
|
||||
* that exist in system contacts, but that do not have an associated
|
||||
* thread.
|
||||
*
|
||||
* Key: [ContactSearchKey.RecipientSearchKey]
|
||||
* Data: [ContactSearchData.KnownRecipient]
|
||||
* Model: [ContactSearchAdapter.RecipientModel]
|
||||
*/
|
||||
data class ContactsWithoutThreads(
|
||||
override val includeHeader: Boolean = true,
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
) : Section(SectionKey.CONTACTS_WITHOUT_THREADS)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,6 +203,11 @@ class ContactSearchConfiguration private constructor(
|
||||
*/
|
||||
GROUPS_WITH_MEMBERS,
|
||||
|
||||
/**
|
||||
* Section Key for [Section.ContactsWithoutThreads]
|
||||
*/
|
||||
CONTACTS_WITHOUT_THREADS,
|
||||
|
||||
/**
|
||||
* Arbitrary row (think new group button, username row, etc)
|
||||
*/
|
||||
|
||||
@@ -51,10 +51,21 @@ class ContactSearchMediator(
|
||||
val adapter = adapterFactory.create(
|
||||
displayCheckBox = displayCheckBox,
|
||||
displaySmsTag = displaySmsTag,
|
||||
recipientListener = this::toggleSelection,
|
||||
storyListener = this::toggleStorySelection,
|
||||
callbacks = object : ContactSearchAdapter.ClickCallbacks {
|
||||
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
|
||||
toggleStorySelection(view, story, isSelected)
|
||||
}
|
||||
|
||||
override fun onKnownRecipientClicked(view: View, knownRecipient: ContactSearchData.KnownRecipient, isSelected: Boolean) {
|
||||
toggleSelection(view, knownRecipient, isSelected)
|
||||
}
|
||||
|
||||
override fun onExpandClicked(expand: ContactSearchData.Expand) {
|
||||
viewModel.expandSection(expand.sectionKey)
|
||||
}
|
||||
},
|
||||
storyContextMenuCallbacks = StoryContextMenuCallbacks()
|
||||
) { viewModel.expandSection(it.sectionKey) }
|
||||
)
|
||||
|
||||
init {
|
||||
val dataAndSelection: LiveData<Pair<List<ContactSearchData>, Set<ContactSearchKey>>> = LiveDataUtil.combineLatest(
|
||||
@@ -168,10 +179,8 @@ class ContactSearchMediator(
|
||||
fun create(
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
|
||||
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit
|
||||
callbacks: ContactSearchAdapter.ClickCallbacks,
|
||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
||||
): PagingMappingAdapter<ContactSearchKey>
|
||||
}
|
||||
|
||||
@@ -179,12 +188,10 @@ class ContactSearchMediator(
|
||||
override fun create(
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
|
||||
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit
|
||||
callbacks: ContactSearchAdapter.ClickCallbacks,
|
||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
|
||||
): PagingMappingAdapter<ContactSearchKey> {
|
||||
return ContactSearchAdapter(displayCheckBox, displaySmsTag, recipientListener, storyListener, storyContextMenuCallbacks, expandListener)
|
||||
return ContactSearchAdapter(displayCheckBox, displaySmsTag, callbacks, storyContextMenuCallbacks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ class ContactSearchPagedDataSource(
|
||||
is ContactSearchConfiguration.Section.Chats -> getThreadData(query, section.isUnreadOnly).getCollectionSize(section, query, null)
|
||||
is ContactSearchConfiguration.Section.Messages -> getMessageData(query).getCollectionSize(section, query, null)
|
||||
is ContactSearchConfiguration.Section.GroupsWithMembers -> getGroupsWithMembersIterator(query).getCollectionSize(section, query, null)
|
||||
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsIterator(query).getCollectionSize(section, query, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +151,7 @@ class ContactSearchPagedDataSource(
|
||||
is ContactSearchConfiguration.Section.Chats -> getThreadContactData(section, query, startIndex, endIndex)
|
||||
is ContactSearchConfiguration.Section.Messages -> getMessageContactData(section, query, startIndex, endIndex)
|
||||
is ContactSearchConfiguration.Section.GroupsWithMembers -> getGroupsWithMembersContactData(section, query, startIndex, endIndex)
|
||||
is ContactSearchConfiguration.Section.ContactsWithoutThreads -> getContactsWithoutThreadsContactData(section, query, startIndex, endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +182,14 @@ class ContactSearchPagedDataSource(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContactsWithoutThreadsIterator(query: String?): ContactSearchIterator<Cursor> {
|
||||
return if (query.isNullOrEmpty()) {
|
||||
CursorSearchIterator(null)
|
||||
} else {
|
||||
CursorSearchIterator(contactSearchPagedDataSourceRepository.getContactsWithoutThreads(query))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRecentsSearchIterator(section: ContactSearchConfiguration.Section.Recents, query: String?): ContactSearchIterator<Cursor> {
|
||||
if (!query.isNullOrEmpty()) {
|
||||
throw IllegalArgumentException("Searching Recents is not supported")
|
||||
@@ -259,6 +269,21 @@ class ContactSearchPagedDataSource(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContactsWithoutThreadsContactData(section: ContactSearchConfiguration.Section.ContactsWithoutThreads, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
|
||||
return getContactsWithoutThreadsIterator(query).use { records ->
|
||||
readContactData(
|
||||
records = records,
|
||||
recordsPredicate = null,
|
||||
section = section,
|
||||
startIndex = startIndex,
|
||||
endIndex = endIndex,
|
||||
recordMapper = {
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNonGroupContactsData(section: ContactSearchConfiguration.Section.Individuals, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
|
||||
val headerMap: Map<RecipientId, String> = if (section.includeLetterHeaders) {
|
||||
getNonGroupHeaderLetterMap(section, query)
|
||||
@@ -274,7 +299,7 @@ class ContactSearchPagedDataSource(
|
||||
startIndex = startIndex,
|
||||
endIndex = endIndex,
|
||||
recordMapper = {
|
||||
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it)
|
||||
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromSearchCursor(it)
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, recipient, headerLetter = headerMap[recipient.id])
|
||||
}
|
||||
)
|
||||
@@ -319,7 +344,7 @@ class ContactSearchPagedDataSource(
|
||||
startIndex = startIndex,
|
||||
endIndex = endIndex,
|
||||
recordMapper = {
|
||||
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it)
|
||||
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromSearchCursor(it)
|
||||
val groupsInCommon = contactSearchPagedDataSourceRepository.getGroupsInCommon(recipient)
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, recipient, groupsInCommon = groupsInCommon)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.contacts.ContactRepository
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
|
||||
import org.thoughtcrime.securesms.database.DistributionListTables
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
@@ -88,6 +89,10 @@ open class ContactSearchPagedDataSourceRepository(
|
||||
return SignalDatabase.groups.queryGroupsByMemberName(query)
|
||||
}
|
||||
|
||||
open fun getContactsWithoutThreads(query: String): Cursor {
|
||||
return SignalDatabase.recipients.getAllContactsWithoutThreads(query)
|
||||
}
|
||||
|
||||
open fun getRecipientFromDistributionListCursor(cursor: Cursor): Recipient {
|
||||
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, DistributionListTables.RECIPIENT_ID)))
|
||||
}
|
||||
@@ -100,10 +105,14 @@ open class ContactSearchPagedDataSourceRepository(
|
||||
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)))
|
||||
}
|
||||
|
||||
open fun getRecipientFromRecipientCursor(cursor: Cursor): Recipient {
|
||||
open fun getRecipientFromSearchCursor(cursor: Cursor): Recipient {
|
||||
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, ContactRepository.ID_COLUMN)))
|
||||
}
|
||||
|
||||
open fun getRecipientFromRecipientCursor(cursor: Cursor): Recipient {
|
||||
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
open fun getGroupsInCommon(recipient: Recipient): GroupsInCommon {
|
||||
val groupsInCommon = SignalDatabase.groups.getPushGroupsContainingMember(recipient.id)
|
||||
val groupRecipientIds = groupsInCommon.take(2).map { it.recipientId }
|
||||
|
||||
Reference in New Issue
Block a user