Add group member results to contact search.

main
Alex Hart 2023-01-24 10:50:28 -04:00 zatwierdzone przez Greyson Parrelli
rodzic eaeeb08987
commit c022172ace
10 zmienionych plików z 184 dodań i 9 usunięć

Wyświetl plik

@ -131,6 +131,14 @@ public class ContactRepository {
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS); 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) { private @NonNull Cursor handleNoteToSelfQuery(@NonNull String query, boolean includeSelf, Cursor cursor) {
if (includeSelf && noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) { if (includeSelf && noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) {
Recipient self = Recipient.self(); Recipient self = Recipient.self();

Wyświetl plik

@ -272,10 +272,13 @@ open class ContactSearchAdapter(
override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient
override fun bindNumberField(model: RecipientModel) { override fun bindNumberField(model: RecipientModel) {
val recipient = getRecipient(model) val recipient = getRecipient(model)
if (model.knownRecipient.sectionKey == ContactSearchConfiguration.SectionKey.GROUP_MEMBERS) {
if (model.shortSummary && recipient.isGroup) { number.text = model.knownRecipient.groupsInCommon.toDisplayText(context)
number.visible = true
} else if (model.shortSummary && recipient.isGroup) {
val count = recipient.participantIds.size val count = recipient.participantIds.size
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count) number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
number.visible = true
} else { } else {
super.bindNumberField(model) super.bindNumberField(model)
} }
@ -404,6 +407,7 @@ open class ContactSearchAdapter(
ContactSearchConfiguration.SectionKey.INDIVIDUALS -> R.string.ContactsCursorLoader_contacts ContactSearchConfiguration.SectionKey.INDIVIDUALS -> R.string.ContactsCursorLoader_contacts
ContactSearchConfiguration.SectionKey.GROUPS -> R.string.ContactsCursorLoader_groups ContactSearchConfiguration.SectionKey.GROUPS -> R.string.ContactsCursorLoader_groups
ContactSearchConfiguration.SectionKey.ARBITRARY -> error("This section does not support HEADER") ContactSearchConfiguration.SectionKey.ARBITRARY -> error("This section does not support HEADER")
ContactSearchConfiguration.SectionKey.GROUP_MEMBERS -> R.string.ContactsCursorLoader_group_members
} }
) )

Wyświetl plik

@ -76,17 +76,47 @@ class ContactSearchConfiguration private constructor(
override val includeHeader: Boolean = false override val includeHeader: Boolean = false
override val expandConfig: ExpandConfig? = null 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. * Describes a given section. Useful for labeling sections and managing expansion state.
*/ */
enum class SectionKey { enum class SectionKey {
/**
* Lists My Stories, distribution lists, as well as group stories.
*/
STORIES, STORIES,
/**
* Recent chats.
*/
RECENTS, RECENTS,
/**
* 1:1 Contacts with whom I've started a chat.
*/
INDIVIDUALS, INDIVIDUALS,
/**
* Active groups the user is a member of
*/
GROUPS, 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
} }
/** /**

Wyświetl plik

@ -25,9 +25,11 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
* A row displaying a known recipient. * A row displaying a known recipient.
*/ */
data class KnownRecipient( data class KnownRecipient(
val sectionKey: ContactSearchConfiguration.SectionKey,
val recipient: Recipient, val recipient: Recipient,
val shortSummary: Boolean = false, val shortSummary: Boolean = false,
val headerLetter: String? = null val headerLetter: String? = null,
val groupsInCommon: GroupsInCommon = GroupsInCommon(0, listOf())
) : ContactSearchData(ContactSearchKey.RecipientSearchKey(recipient.id, false)) ) : ContactSearchData(ContactSearchKey.RecipientSearchKey(recipient.id, false))
/** /**

Wyświetl plik

@ -91,6 +91,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.Recents -> getRecentsSearchIterator(section, query).getCollectionSize(section, query, null) is ContactSearchConfiguration.Section.Recents -> getRecentsSearchIterator(section, query).getCollectionSize(section, query, null)
is ContactSearchConfiguration.Section.Stories -> getStoriesSearchIterator(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.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.Recents -> getRecentsContactData(section, query, startIndex, endIndex)
is ContactSearchConfiguration.Section.Stories -> getStoriesContactData(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.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)) return CursorSearchIterator(contactSearchPagedDataSourceRepository.getRecents(section))
} }
private fun getGroupMembersSearchIterator(query: String?): ContactSearchIterator<Cursor> {
return CursorSearchIterator(contactSearchPagedDataSourceRepository.queryGroupMemberContacts(query))
}
private fun <R> readContactData( private fun <R> readContactData(
records: ContactSearchIterator<R>, records: ContactSearchIterator<R>,
recordsPredicate: ((R) -> Boolean)?, recordsPredicate: ((R) -> Boolean)?,
@ -197,7 +203,7 @@ class ContactSearchPagedDataSource(
startIndex = startIndex, startIndex = startIndex,
endIndex = endIndex, endIndex = endIndex,
recordMapper = { recordMapper = {
ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromThreadCursor(it)) ContactSearchData.KnownRecipient(section.sectionKey, contactSearchPagedDataSourceRepository.getRecipientFromThreadCursor(it))
} }
) )
} }
@ -219,7 +225,7 @@ class ContactSearchPagedDataSource(
endIndex = endIndex, endIndex = endIndex,
recordMapper = { recordMapper = {
val recipient = contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it) 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) { if (section.returnAsGroupStories) {
ContactSearchData.Story(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it), 0, DistributionListPrivacyMode.ALL) ContactSearchData.Story(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it), 0, DistributionListPrivacyMode.ALL)
} else { } 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( private fun <R> createResultsCollection(
section: ContactSearchConfiguration.Section, section: ContactSearchConfiguration.Section,
records: ContactSearchIterator<R>, records: ContactSearchIterator<R>,

Wyświetl plik

@ -48,6 +48,10 @@ open class ContactSearchPagedDataSourceRepository(
return contactRepository.queryNonGroupContacts(query ?: "", includeSelf) return contactRepository.queryNonGroupContacts(query ?: "", includeSelf)
} }
open fun queryGroupMemberContacts(query: String?): Cursor? {
return contactRepository.queryGroupMemberContacts(query ?: "")
}
open fun getGroupSearchIterator( open fun getGroupSearchIterator(
section: ContactSearchConfiguration.Section.Groups, section: ContactSearchConfiguration.Section.Groups,
query: String? query: String?
@ -95,6 +99,20 @@ open class ContactSearchPagedDataSourceRepository(
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, ContactRepository.ID_COLUMN))) 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 { open fun getRecipientFromGroupRecord(groupRecord: GroupRecord): Recipient {
return Recipient.resolved(groupRecord.recipientId) return Recipient.resolved(groupRecord.recipientId)
} }

Wyświetl plik

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

Wyświetl plik

@ -454,6 +454,14 @@ class MultiselectForwardFragment :
) )
) )
if (!query.isNullOrEmpty()) {
addSection(
ContactSearchConfiguration.Section.GroupMembers(
includeHeader = true
)
)
}
addSection( addSection(
ContactSearchConfiguration.Section.Groups( ContactSearchConfiguration.Section.Groups(
includeHeader = true, includeHeader = true,

Wyświetl plik

@ -3120,6 +3120,31 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) 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? { fun queryAllContacts(inputQuery: String): Cursor? {
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery) val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
val selection = val selection =
@ -4126,6 +4151,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
internal class Builder { internal class Builder {
private var includeRegistered = false private var includeRegistered = false
private var includeNonRegistered = false private var includeNonRegistered = false
private var includeGroupMembers = false
private var excludeId: RecipientId? = null private var excludeId: RecipientId? = null
private var excludeGroups = false private var excludeGroups = false
private var searchQuery: String? = null private var searchQuery: String? = null
@ -4140,6 +4166,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return this return this
} }
fun withGroupMembers(includeGroupMembers: Boolean): Builder {
this.includeGroupMembers = includeGroupMembers
return this
}
fun excludeId(recipientId: RecipientId?): Builder { fun excludeId(recipientId: RecipientId?): Builder {
excludeId = recipientId excludeId = recipientId
return this return this
@ -4156,11 +4187,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
} }
fun build(): ContactSearchSelection { 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 stringBuilder = StringBuilder("(")
val args: MutableList<Any?> = LinkedList() val args: MutableList<Any?> = LinkedList()
var hasPreceedingSection = false
if (includeRegistered) { if (includeRegistered) {
hasPreceedingSection = true
stringBuilder.append("(") stringBuilder.append("(")
args.add(RegisteredState.REGISTERED.id) args.add(RegisteredState.REGISTERED.id)
args.add(1) args.add(1)
@ -4175,11 +4208,12 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
stringBuilder.append(")") stringBuilder.append(")")
} }
if (includeRegistered && includeNonRegistered) { if (hasPreceedingSection && includeNonRegistered) {
stringBuilder.append(" OR ") stringBuilder.append(" OR ")
} }
if (includeNonRegistered) { if (includeNonRegistered) {
hasPreceedingSection = true
stringBuilder.append("(") stringBuilder.append("(")
args.add(RegisteredState.REGISTERED.id) args.add(RegisteredState.REGISTERED.id)
@ -4195,6 +4229,26 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
stringBuilder.append(")") 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(")")
stringBuilder.append(FILTER_BLOCKED) stringBuilder.append(FILTER_BLOCKED)
args.add(0) args.add(0)
@ -4216,6 +4270,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
} }
companion object { 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_GROUPS = " AND $GROUP_ID IS NULL"
const val FILTER_ID = " AND $ID != ?" const val FILTER_ID = " AND $ID != ?"
const val FILTER_BLOCKED = " AND $BLOCKED = ?" 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 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 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 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 ?)"
} }
} }

Wyświetl plik

@ -224,6 +224,8 @@
<string name="ContactsCursorLoader_recent_chats">Recent chats</string> <string name="ContactsCursorLoader_recent_chats">Recent chats</string>
<string name="ContactsCursorLoader_contacts">Contacts</string> <string name="ContactsCursorLoader_contacts">Contacts</string>
<string name="ContactsCursorLoader_groups">Groups</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> <string name="ContactsCursorLoader_phone_number_search">Phone number search</string>
<!-- Header for username search --> <!-- Header for username search -->
<string name="ContactsCursorLoader_find_by_username">Find by username</string> <string name="ContactsCursorLoader_find_by_username">Find by username</string>