Add "Group Members" section to ConversationList search results.

This commit is contained in:
Alex Hart
2023-01-26 14:10:38 -04:00
committed by Greyson Parrelli
parent e84c6187b9
commit 09902e5d11
14 changed files with 265 additions and 39 deletions

View File

@@ -33,11 +33,12 @@ import org.thoughtcrime.securesms.util.visible
open class ContactSearchAdapter(
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
recipientListener: Listener<ContactSearchData.KnownRecipient>,
storyListener: Listener<ContactSearchData.Story>,
storyContextMenuCallbacks: StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
) : PagingMappingAdapter<ContactSearchKey>() {
init {
registerStoryItems(this, displayCheckBox, storyListener, storyContextMenuCallbacks)
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, recipientListener)
@@ -49,7 +50,7 @@ open class ContactSearchAdapter(
fun registerStoryItems(
mappingAdapter: MappingAdapter,
displayCheckBox: Boolean = false,
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
storyListener: Listener<ContactSearchData.Story>,
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
) {
mappingAdapter.registerFactory(
@@ -62,7 +63,7 @@ open class ContactSearchAdapter(
mappingAdapter: MappingAdapter,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
recipientListener: Listener<ContactSearchData.KnownRecipient>
) {
mappingAdapter.registerFactory(
RecipientModel::class.java,
@@ -97,6 +98,7 @@ open class ContactSearchAdapter(
is ContactSearchData.Message -> MessageModel(it)
is ContactSearchData.Thread -> ThreadModel(it)
is ContactSearchData.Empty -> EmptyModel(it)
is ContactSearchData.GroupWithMembers -> GroupWithMembersModel(it)
}
}
)
@@ -133,7 +135,7 @@ open class ContactSearchAdapter(
private class StoryViewHolder(
itemView: View,
displayCheckBox: Boolean,
onClick: (View, ContactSearchData.Story, Boolean) -> Unit,
onClick: Listener<ContactSearchData.Story>,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, onClick) {
override fun isSelected(model: StoryModel): Boolean = model.isSelected
@@ -265,7 +267,7 @@ open class ContactSearchAdapter(
itemView: View,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
onClick: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
onClick: Listener<ContactSearchData.KnownRecipient>
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
private var headerLetter: String? = null
@@ -302,7 +304,7 @@ open class ContactSearchAdapter(
itemView: View,
private val displayCheckBox: Boolean,
private val displaySmsTag: DisplaySmsTag,
val onClick: (View, D, Boolean) -> Unit
val onClick: Listener<D>
) : MappingViewHolder<T>(itemView) {
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
@@ -316,7 +318,7 @@ open class ContactSearchAdapter(
override fun bind(model: T) {
checkbox.visible = displayCheckBox
checkbox.isChecked = isSelected(model)
itemView.setOnClickListener { onClick(avatar, getData(model), isSelected(model)) }
itemView.setOnClickListener { onClick.listen(avatar, getData(model), isSelected(model)) }
bindLongPress(model)
if (payload.isNotEmpty()) {
@@ -420,6 +422,15 @@ open class ContactSearchAdapter(
override fun areContentsTheSame(newItem: EmptyModel): Boolean = newItem.empty == empty
}
/**
* Mapping Model for [ContactSearchData.GroupWithMembers]
*/
class GroupWithMembersModel(val groupWithMembers: ContactSearchData.GroupWithMembers) : MappingModel<GroupWithMembersModel> {
override fun areContentsTheSame(newItem: GroupWithMembersModel): Boolean = newItem.groupWithMembers == groupWithMembers
override fun areItemsTheSame(newItem: GroupWithMembersModel): Boolean = newItem.groupWithMembers.contactSearchKey == groupWithMembers.contactSearchKey
}
/**
* View Holder for section headers
*/
@@ -439,6 +450,7 @@ open class ContactSearchAdapter(
ContactSearchConfiguration.SectionKey.GROUP_MEMBERS -> R.string.ContactsCursorLoader_group_members
ContactSearchConfiguration.SectionKey.CHATS -> R.string.ContactsCursorLoader__chats
ContactSearchConfiguration.SectionKey.MESSAGES -> R.string.ContactsCursorLoader__messages
ContactSearchConfiguration.SectionKey.GROUPS_WITH_MEMBERS -> R.string.ContactsCursorLoader_group_members
}
)
@@ -495,4 +507,8 @@ open class ContactSearchAdapter(
IF_NOT_REGISTERED,
NEVER
}
fun interface Listener<D : ContactSearchData> {
fun listen(view: View, data: D, isSelected: Boolean)
}
}

View File

@@ -83,6 +83,18 @@ class ContactSearchConfiguration private constructor(
override val expandConfig: ExpandConfig? = null
) : Section(SectionKey.GROUP_MEMBERS)
/**
* Includes a list of groups with members whose search name match the search query.
* This section will only be rendered if there is a non-null, non-empty query present.
*
* Key: [ContactSearchKey.GroupWithMembers]
* Data: [ContactSearchData.GroupWithMembers]
*/
data class GroupsWithMembers(
override val includeHeader: Boolean = true,
override val expandConfig: ExpandConfig? = null
) : Section(SectionKey.GROUPS_WITH_MEMBERS)
data class Chats(
val isUnreadOnly: Boolean = false,
override val includeHeader: Boolean = true,
@@ -119,6 +131,11 @@ class ContactSearchConfiguration private constructor(
*/
GROUPS,
/**
* Section Key for [Section.GroupsWithMembers]
*/
GROUPS_WITH_MEMBERS,
/**
* Arbitrary row (think new group button, username row, etc)
*/
@@ -207,6 +224,13 @@ class ContactSearchConfiguration private constructor(
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
}
fun groupsWithMembers(
includeHeader: Boolean = true,
expandConfig: ExpandConfig? = null
) {
addSection(Section.GroupsWithMembers(includeHeader, expandConfig))
}
fun addSection(section: Section)
}
}

View File

@@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.annotation.VisibleForTesting
import org.thoughtcrime.securesms.contacts.HeaderAction
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.search.MessageResult
@@ -50,6 +51,16 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
val threadRecord: ThreadRecord
) : ContactSearchData(ContactSearchKey.Thread(threadRecord.threadId))
/**
* A row displaying a group which has members that match the given query.
* Rows of this type are only present if the query is non-empty and non-null.
*/
data class GroupWithMembers(
val query: String,
val groupRecord: GroupRecord,
val date: Long
) : ContactSearchData(ContactSearchKey.GroupWithMembers(groupRecord.id))
/**
* A row containing a title for a given section
*/

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.contacts.paged
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sharing.ShareContact
@@ -48,6 +49,11 @@ sealed class ContactSearchKey {
*/
data class Thread(val threadId: Long) : ContactSearchKey()
/**
* Search key for [ContactSearchData.GroupWithMembers]
*/
data class GroupWithMembers(val groupId: GroupId) : ContactSearchKey()
/**
* Search key for a MessageRecord
*/

View File

@@ -168,8 +168,8 @@ class ContactSearchMediator(
fun create(
displayCheckBox: Boolean,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
): PagingMappingAdapter<ContactSearchKey>
@@ -179,8 +179,8 @@ class ContactSearchMediator(
override fun create(
displayCheckBox: Boolean,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
): PagingMappingAdapter<ContactSearchKey> {

View File

@@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.contacts.paged
import android.database.Cursor
import org.signal.core.util.requireLong
import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchCollection
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
import org.thoughtcrime.securesms.contacts.paged.collections.CursorSearchIterator
import org.thoughtcrime.securesms.contacts.paged.collections.StoriesSearchCollection
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
@@ -112,6 +114,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.GroupMembers -> getGroupMembersSearchIterator(query).getCollectionSize(section, query, null)
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)
}
}
@@ -146,6 +149,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.GroupMembers -> getGroupMembersContactData(section, query, startIndex, endIndex)
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)
}
}
@@ -168,6 +172,14 @@ class ContactSearchPagedDataSource(
return CursorSearchIterator(contactSearchPagedDataSourceRepository.getStories(query))
}
private fun getGroupsWithMembersIterator(query: String?): ContactSearchIterator<Cursor> {
return if (query.isNullOrEmpty()) {
CursorSearchIterator(null)
} else {
CursorSearchIterator(contactSearchPagedDataSourceRepository.getGroupsWithMembers(query))
}
}
private fun getRecentsSearchIterator(section: ContactSearchConfiguration.Section.Recents, query: String?): ContactSearchIterator<Cursor> {
if (!query.isNullOrEmpty()) {
throw IllegalArgumentException("Searching Recents is not supported")
@@ -216,6 +228,22 @@ class ContactSearchPagedDataSource(
}
}
private fun getGroupsWithMembersContactData(section: ContactSearchConfiguration.Section.GroupsWithMembers, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
return getGroupsWithMembersIterator(query).use { records ->
readContactData(
records = records,
recordsPredicate = null,
section = section,
startIndex = startIndex,
endIndex = endIndex,
recordMapper = { cursor ->
val record = GroupTable.Reader(cursor).getCurrent()
ContactSearchData.GroupWithMembers(query!!, record!!, cursor.requireLong(GroupTable.THREAD_DATE))
}
)
}
}
private fun getRecentsContactData(section: ContactSearchConfiguration.Section.Recents, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
return getRecentsSearchIterator(section, query).use { records ->
readContactData(

View File

@@ -84,6 +84,10 @@ open class ContactSearchPagedDataSourceRepository(
return SignalDatabase.distributionLists.getAllListsForContactSelectionUiCursor(query, myStoryContainsQuery(query ?: ""))
}
open fun getGroupsWithMembers(query: String): Cursor {
return SignalDatabase.groups.queryGroupsByMemberName(query)
}
open fun getRecipientFromDistributionListCursor(cursor: Cursor): Recipient {
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, DistributionListTables.RECIPIENT_ID)))
}