Add group member results to contact search.
This commit is contained in:
parent
eaeeb08987
commit
c022172ace
10 changed files with 184 additions and 9 deletions
|
@ -131,6 +131,14 @@ public class ContactRepository {
|
|||
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull Cursor queryGroupMemberContacts(@NonNull String query) {
|
||||
Cursor cursor = TextUtils.isEmpty(query) ? recipientTable.getGroupMemberContacts()
|
||||
: recipientTable.queryGroupMemberContacts(query);
|
||||
|
||||
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS);
|
||||
}
|
||||
|
||||
private @NonNull Cursor handleNoteToSelfQuery(@NonNull String query, boolean includeSelf, Cursor cursor) {
|
||||
if (includeSelf && noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) {
|
||||
Recipient self = Recipient.self();
|
||||
|
|
|
@ -272,10 +272,13 @@ open class ContactSearchAdapter(
|
|||
override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient
|
||||
override fun bindNumberField(model: RecipientModel) {
|
||||
val recipient = getRecipient(model)
|
||||
|
||||
if (model.shortSummary && recipient.isGroup) {
|
||||
if (model.knownRecipient.sectionKey == ContactSearchConfiguration.SectionKey.GROUP_MEMBERS) {
|
||||
number.text = model.knownRecipient.groupsInCommon.toDisplayText(context)
|
||||
number.visible = true
|
||||
} else if (model.shortSummary && recipient.isGroup) {
|
||||
val count = recipient.participantIds.size
|
||||
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
|
||||
number.visible = true
|
||||
} else {
|
||||
super.bindNumberField(model)
|
||||
}
|
||||
|
@ -404,6 +407,7 @@ open class ContactSearchAdapter(
|
|||
ContactSearchConfiguration.SectionKey.INDIVIDUALS -> R.string.ContactsCursorLoader_contacts
|
||||
ContactSearchConfiguration.SectionKey.GROUPS -> R.string.ContactsCursorLoader_groups
|
||||
ContactSearchConfiguration.SectionKey.ARBITRARY -> error("This section does not support HEADER")
|
||||
ContactSearchConfiguration.SectionKey.GROUP_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -76,17 +76,47 @@ class ContactSearchConfiguration private constructor(
|
|||
override val includeHeader: Boolean = false
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
}
|
||||
|
||||
data class GroupMembers(
|
||||
override val includeHeader: Boolean = true,
|
||||
override val expandConfig: ExpandConfig? = null
|
||||
) : Section(SectionKey.GROUP_MEMBERS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a given section. Useful for labeling sections and managing expansion state.
|
||||
*/
|
||||
enum class SectionKey {
|
||||
/**
|
||||
* Lists My Stories, distribution lists, as well as group stories.
|
||||
*/
|
||||
STORIES,
|
||||
|
||||
/**
|
||||
* Recent chats.
|
||||
*/
|
||||
RECENTS,
|
||||
|
||||
/**
|
||||
* 1:1 Contacts with whom I've started a chat.
|
||||
*/
|
||||
INDIVIDUALS,
|
||||
|
||||
/**
|
||||
* Active groups the user is a member of
|
||||
*/
|
||||
GROUPS,
|
||||
ARBITRARY
|
||||
|
||||
/**
|
||||
* Arbitrary row (think new group button, username row, etc)
|
||||
*/
|
||||
ARBITRARY,
|
||||
|
||||
/**
|
||||
* Contacts that are members of groups user is in that they've not explicitly
|
||||
* started a conversation with.
|
||||
*/
|
||||
GROUP_MEMBERS
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,9 +25,11 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
|
|||
* A row displaying a known recipient.
|
||||
*/
|
||||
data class KnownRecipient(
|
||||
val sectionKey: ContactSearchConfiguration.SectionKey,
|
||||
val recipient: Recipient,
|
||||
val shortSummary: Boolean = false,
|
||||
val headerLetter: String? = null
|
||||
val headerLetter: String? = null,
|
||||
val groupsInCommon: GroupsInCommon = GroupsInCommon(0, listOf())
|
||||
) : ContactSearchData(ContactSearchKey.RecipientSearchKey(recipient.id, false))
|
||||
|
||||
/**
|
||||
|
|
|
@ -91,6 +91,7 @@ class ContactSearchPagedDataSource(
|
|||
is ContactSearchConfiguration.Section.Recents -> getRecentsSearchIterator(section, query).getCollectionSize(section, query, null)
|
||||
is ContactSearchConfiguration.Section.Stories -> getStoriesSearchIterator(query).getCollectionSize(section, query, null)
|
||||
is ContactSearchConfiguration.Section.Arbitrary -> arbitraryRepository?.getSize(section, query) ?: error("Invalid arbitrary section.")
|
||||
is ContactSearchConfiguration.Section.GroupMembers -> getGroupMembersSearchIterator(query).getCollectionSize(section, query, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,6 +123,7 @@ class ContactSearchPagedDataSource(
|
|||
is ContactSearchConfiguration.Section.Recents -> getRecentsContactData(section, query, startIndex, endIndex)
|
||||
is ContactSearchConfiguration.Section.Stories -> getStoriesContactData(section, query, startIndex, endIndex)
|
||||
is ContactSearchConfiguration.Section.Arbitrary -> arbitraryRepository?.getData(section, query, startIndex, endIndex) ?: error("Invalid arbitrary section.")
|
||||
is ContactSearchConfiguration.Section.GroupMembers -> getGroupMembersContactData(section, query, startIndex, endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,6 +154,10 @@ class ContactSearchPagedDataSource(
|
|||
return CursorSearchIterator(contactSearchPagedDataSourceRepository.getRecents(section))
|
||||
}
|
||||
|
||||
private fun getGroupMembersSearchIterator(query: String?): ContactSearchIterator<Cursor> {
|
||||
return CursorSearchIterator(contactSearchPagedDataSourceRepository.queryGroupMemberContacts(query))
|
||||
}
|
||||
|
||||
private fun <R> readContactData(
|
||||
records: ContactSearchIterator<R>,
|
||||
recordsPredicate: ((R) -> Boolean)?,
|
||||
|
@ -197,7 +203,7 @@ class ContactSearchPagedDataSource(
|
|||
startIndex = startIndex,
|
||||
endIndex = endIndex,
|
||||
recordMapper = {
|
||||
ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromThreadCursor(it))
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, contactSearchPagedDataSourceRepository.getRecipientFromThreadCursor(it))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -219,7 +225,7 @@ class ContactSearchPagedDataSource(
|
|||
endIndex = endIndex,
|
||||
recordMapper = {
|
||||
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it)
|
||||
ContactSearchData.KnownRecipient(recipient, headerLetter = headerMap[recipient.id])
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, recipient, headerLetter = headerMap[recipient.id])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -237,7 +243,7 @@ class ContactSearchPagedDataSource(
|
|||
if (section.returnAsGroupStories) {
|
||||
ContactSearchData.Story(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it), 0, DistributionListPrivacyMode.ALL)
|
||||
} else {
|
||||
ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it), shortSummary = section.shortSummary)
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it), shortSummary = section.shortSummary)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -254,6 +260,23 @@ class ContactSearchPagedDataSource(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getGroupMembersContactData(section: ContactSearchConfiguration.Section.GroupMembers, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
|
||||
return getGroupMembersSearchIterator(query).use { records ->
|
||||
readContactData(
|
||||
records = records,
|
||||
recordsPredicate = null,
|
||||
section = section,
|
||||
startIndex = startIndex,
|
||||
endIndex = endIndex,
|
||||
recordMapper = {
|
||||
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it)
|
||||
val groupsInCommon = contactSearchPagedDataSourceRepository.getGroupsInCommon(recipient)
|
||||
ContactSearchData.KnownRecipient(section.sectionKey, recipient, groupsInCommon = groupsInCommon)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R> createResultsCollection(
|
||||
section: ContactSearchConfiguration.Section,
|
||||
records: ContactSearchIterator<R>,
|
||||
|
|
|
@ -48,6 +48,10 @@ open class ContactSearchPagedDataSourceRepository(
|
|||
return contactRepository.queryNonGroupContacts(query ?: "", includeSelf)
|
||||
}
|
||||
|
||||
open fun queryGroupMemberContacts(query: String?): Cursor? {
|
||||
return contactRepository.queryGroupMemberContacts(query ?: "")
|
||||
}
|
||||
|
||||
open fun getGroupSearchIterator(
|
||||
section: ContactSearchConfiguration.Section.Groups,
|
||||
query: String?
|
||||
|
@ -95,6 +99,20 @@ open class ContactSearchPagedDataSourceRepository(
|
|||
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, ContactRepository.ID_COLUMN)))
|
||||
}
|
||||
|
||||
open fun getGroupsInCommon(recipient: Recipient): GroupsInCommon {
|
||||
val groupsInCommon = SignalDatabase.groups.getPushGroupsContainingMember(recipient.id)
|
||||
val groupRecipientIds = groupsInCommon.take(2).map { it.recipientId }
|
||||
val names = Recipient.resolvedList(groupRecipientIds)
|
||||
.map { it.getDisplayName(context) }
|
||||
.sorted()
|
||||
|
||||
return GroupsInCommon(groupsInCommon.size, names)
|
||||
}
|
||||
|
||||
open fun hasGroupsInCommon(recipient: Recipient): Boolean {
|
||||
return SignalDatabase.groups.getPushGroupsContainingMember(recipient.id).isNotEmpty()
|
||||
}
|
||||
|
||||
open fun getRecipientFromGroupRecord(groupRecord: GroupRecord): Recipient {
|
||||
return Recipient.resolved(groupRecord.recipientId)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Groups in common helper class
|
||||
*/
|
||||
data class GroupsInCommon(
|
||||
private val total: Int,
|
||||
private val names: List<String>
|
||||
) {
|
||||
fun toDisplayText(context: Context): String {
|
||||
return when (total) {
|
||||
1 -> context.getString(R.string.MessageRequestProfileView_member_of_one_group, names[0])
|
||||
2 -> context.getString(R.string.MessageRequestProfileView_member_of_two_groups, names[0], names[1])
|
||||
else -> context.getString(
|
||||
R.string.MessageRequestProfileView_member_of_many_groups, names[0], names[1],
|
||||
context.resources.getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, total - 2, total - 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -454,6 +454,14 @@ class MultiselectForwardFragment :
|
|||
)
|
||||
)
|
||||
|
||||
if (!query.isNullOrEmpty()) {
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.GroupMembers(
|
||||
includeHeader = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Groups(
|
||||
includeHeader = true,
|
||||
|
|
|
@ -3120,6 +3120,31 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
|
||||
}
|
||||
|
||||
fun getGroupMemberContacts(): Cursor? {
|
||||
val searchSelection = ContactSearchSelection.Builder()
|
||||
.withGroupMembers(true)
|
||||
.excludeId(Recipient.self().id)
|
||||
.build()
|
||||
|
||||
val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + PHONE
|
||||
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, searchSelection.where, searchSelection.args, null, null, orderBy)
|
||||
}
|
||||
|
||||
fun queryGroupMemberContacts(inputQuery: String): Cursor? {
|
||||
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
|
||||
val searchSelection = ContactSearchSelection.Builder()
|
||||
.withGroupMembers(true)
|
||||
.excludeId(Recipient.self().id)
|
||||
.withSearchQuery(query)
|
||||
.build()
|
||||
|
||||
val selection = searchSelection.where
|
||||
val args = searchSelection.args
|
||||
val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + PHONE
|
||||
|
||||
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
|
||||
}
|
||||
|
||||
fun queryAllContacts(inputQuery: String): Cursor? {
|
||||
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
|
||||
val selection =
|
||||
|
@ -4126,6 +4151,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
internal class Builder {
|
||||
private var includeRegistered = false
|
||||
private var includeNonRegistered = false
|
||||
private var includeGroupMembers = false
|
||||
private var excludeId: RecipientId? = null
|
||||
private var excludeGroups = false
|
||||
private var searchQuery: String? = null
|
||||
|
@ -4140,6 +4166,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
return this
|
||||
}
|
||||
|
||||
fun withGroupMembers(includeGroupMembers: Boolean): Builder {
|
||||
this.includeGroupMembers = includeGroupMembers
|
||||
return this
|
||||
}
|
||||
|
||||
fun excludeId(recipientId: RecipientId?): Builder {
|
||||
excludeId = recipientId
|
||||
return this
|
||||
|
@ -4156,11 +4187,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
}
|
||||
|
||||
fun build(): ContactSearchSelection {
|
||||
check(!(!includeRegistered && !includeNonRegistered)) { "Must include either registered or non-registered recipients in search" }
|
||||
check(!(!includeRegistered && !includeNonRegistered && !includeGroupMembers)) { "Must include either registered, non-registered, or group member recipients in search" }
|
||||
val stringBuilder = StringBuilder("(")
|
||||
val args: MutableList<Any?> = LinkedList()
|
||||
var hasPreceedingSection = false
|
||||
|
||||
if (includeRegistered) {
|
||||
hasPreceedingSection = true
|
||||
stringBuilder.append("(")
|
||||
args.add(RegisteredState.REGISTERED.id)
|
||||
args.add(1)
|
||||
|
@ -4175,11 +4208,12 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
stringBuilder.append(")")
|
||||
}
|
||||
|
||||
if (includeRegistered && includeNonRegistered) {
|
||||
if (hasPreceedingSection && includeNonRegistered) {
|
||||
stringBuilder.append(" OR ")
|
||||
}
|
||||
|
||||
if (includeNonRegistered) {
|
||||
hasPreceedingSection = true
|
||||
stringBuilder.append("(")
|
||||
args.add(RegisteredState.REGISTERED.id)
|
||||
|
||||
|
@ -4195,6 +4229,26 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
stringBuilder.append(")")
|
||||
}
|
||||
|
||||
if (hasPreceedingSection && includeGroupMembers) {
|
||||
stringBuilder.append(" OR ")
|
||||
}
|
||||
|
||||
if (includeGroupMembers) {
|
||||
stringBuilder.append("(")
|
||||
args.add(RegisteredState.REGISTERED.id)
|
||||
args.add(1)
|
||||
if (Util.isEmpty(searchQuery)) {
|
||||
stringBuilder.append(GROUP_MEMBER_CONTACT)
|
||||
} else {
|
||||
stringBuilder.append(QUERY_GROUP_MEMBER_CONTACT)
|
||||
args.add(searchQuery)
|
||||
args.add(searchQuery)
|
||||
args.add(searchQuery)
|
||||
}
|
||||
|
||||
stringBuilder.append(")")
|
||||
}
|
||||
|
||||
stringBuilder.append(")")
|
||||
stringBuilder.append(FILTER_BLOCKED)
|
||||
args.add(0)
|
||||
|
@ -4216,6 +4270,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val HAS_GROUP_IN_COMMON = "EXISTS (SELECT 1 FROM ${GroupTable.MembershipTable.TABLE_NAME} WHERE ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID} = $ID)"
|
||||
const val FILTER_GROUPS = " AND $GROUP_ID IS NULL"
|
||||
const val FILTER_ID = " AND $ID != ?"
|
||||
const val FILTER_BLOCKED = " AND $BLOCKED = ?"
|
||||
|
@ -4224,6 +4279,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
const val QUERY_NON_SIGNAL_CONTACT = "$NON_SIGNAL_CONTACT AND ($PHONE GLOB ? OR $EMAIL GLOB ? OR $SYSTEM_JOINED_NAME GLOB ?)"
|
||||
const val SIGNAL_CONTACT = "$REGISTERED = ? AND (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)"
|
||||
const val QUERY_SIGNAL_CONTACT = "$SIGNAL_CONTACT AND ($PHONE GLOB ? OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)"
|
||||
const val GROUP_MEMBER_CONTACT = "$REGISTERED = ? AND $HAS_GROUP_IN_COMMON AND NOT (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)"
|
||||
const val QUERY_GROUP_MEMBER_CONTACT = "$GROUP_MEMBER_CONTACT AND ($PHONE GLOB ? OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -224,6 +224,8 @@
|
|||
<string name="ContactsCursorLoader_recent_chats">Recent chats</string>
|
||||
<string name="ContactsCursorLoader_contacts">Contacts</string>
|
||||
<string name="ContactsCursorLoader_groups">Groups</string>
|
||||
<!-- Contact search header for individuals who the user has not started a conversation with but is in a group with -->
|
||||
<string name="ContactsCursorLoader_group_members">Group members</string>
|
||||
<string name="ContactsCursorLoader_phone_number_search">Phone number search</string>
|
||||
<!-- Header for username search -->
|
||||
<string name="ContactsCursorLoader_find_by_username">Find by username</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue