Add "contacts without threads" section to Conversation List Search.

This commit is contained in:
Alex Hart
2023-01-26 14:44:55 -04:00
committed by Greyson Parrelli
parent 09902e5d11
commit 36dfa19aec
9 changed files with 252 additions and 75 deletions

View File

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

View File

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

View File

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

View File

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

View File

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