From 74dc222a542de078c08354df13dbcc5e8e59b368 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 6 Mar 2024 13:35:08 -0400 Subject: [PATCH] Add Recency support for contact search ordering. --- .../securesms/database/RecipientTableTest.kt | 4 +- .../ContactSelectionListFragment.java | 3 +- .../securesms/contacts/ContactRepository.java | 11 +++--- .../paged/ContactSearchConfiguration.kt | 5 ++- .../paged/ContactSearchPagedDataSource.kt | 6 ++- .../ContactSearchPagedDataSourceRepository.kt | 4 +- .../securesms/database/RecipientTable.kt | 38 ++++++++++++++++--- 7 files changed, 53 insertions(+), 18 deletions(-) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt index ad246536b0..64d480e6d4 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt @@ -57,7 +57,7 @@ class RecipientTableTest { SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person")) SignalDatabase.recipients.markHidden(hiddenRecipient) - val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!! + val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Hidden", false))!! assertEquals(0, results.count) } @@ -128,7 +128,7 @@ class RecipientTableTest { SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person")) SignalDatabase.recipients.setBlocked(blockedRecipient, true) - val results = SignalDatabase.recipients.querySignalContacts("Blocked", false)!! + val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", false))!! assertEquals(0, results.count) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 43b045c83a..4a15618ab8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -919,7 +919,8 @@ public final class ContactSelectionListFragment extends LoggingFragment { transportType, !hideHeader, null, - !hideLetterHeaders() + !hideLetterHeaders(), + newConversationCallback != null ? ContactSearchSortOrder.RECENCY : ContactSearchSortOrder.NATURAL )); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java index fc11ee6071..957f4e732d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java @@ -11,6 +11,7 @@ import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.signal.libsignal.protocol.util.Pair; +import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; @@ -108,15 +109,15 @@ public class ContactRepository { @WorkerThread public @NonNull Cursor querySignalContacts(@NonNull String query) { - return querySignalContacts(query, true); + return querySignalContacts(new RecipientTable.ContactSearchQuery(query, true, ContactSearchSortOrder.NATURAL)); } @WorkerThread - public @NonNull Cursor querySignalContacts(@NonNull String query, boolean includeSelf) { - Cursor cursor = TextUtils.isEmpty(query) ? recipientTable.getSignalContacts(includeSelf) - : recipientTable.querySignalContacts(query, includeSelf); + public @NonNull Cursor querySignalContacts(@NonNull RecipientTable.ContactSearchQuery contactSearchQuery) { + Cursor cursor = TextUtils.isEmpty(contactSearchQuery.getQuery()) ? recipientTable.getSignalContacts(contactSearchQuery.getIncludeSelf()) + : recipientTable.querySignalContacts(contactSearchQuery); - cursor = handleNoteToSelfQuery(query, includeSelf, cursor); + cursor = handleNoteToSelfQuery(contactSearchQuery.getQuery(), contactSearchQuery.getIncludeSelf(), cursor); return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt index b9b0861e77..e3006d7e63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchConfiguration.kt @@ -69,6 +69,8 @@ class ContactSearchConfiguration private constructor( /** * 1:1 Recipients with whom the user has started a conversation. * + * Note that sort order is only respected when returning a query result for signal-only contacts. In all other cases, natural ordering is used. + * * Key: [ContactSearchKey.RecipientSearchKey] * Data: [ContactSearchData.KnownRecipient] * Model: [ContactSearchAdapter.RecipientModel] @@ -78,7 +80,8 @@ class ContactSearchConfiguration private constructor( val transportType: TransportType, override val includeHeader: Boolean, override val expandConfig: ExpandConfig? = null, - val includeLetterHeaders: Boolean = false + val includeLetterHeaders: Boolean = false, + val pushSearchResultsSortOrder: ContactSearchSortOrder = ContactSearchSortOrder.NATURAL ) : Section(SectionKey.INDIVIDUALS) /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt index 6ca89f4ff1..e13e486af6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterat 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.RecipientTable import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode import org.thoughtcrime.securesms.database.model.GroupRecord import org.thoughtcrime.securesms.database.model.ThreadRecord @@ -210,7 +211,10 @@ class ContactSearchPagedDataSource( private fun getNonGroupSearchIterator(section: ContactSearchConfiguration.Section.Individuals, query: String?): ContactSearchIterator { return when (section.transportType) { - ContactSearchConfiguration.TransportType.PUSH -> CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.querySignalContacts(query, section.includeSelf))) + ContactSearchConfiguration.TransportType.PUSH -> { + val searchQuery = RecipientTable.ContactSearchQuery(query ?: "", section.includeSelf, section.pushSearchResultsSortOrder) + CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.querySignalContacts(searchQuery))) + } ContactSearchConfiguration.TransportType.SMS -> CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.queryNonSignalContacts(query))) ContactSearchConfiguration.TransportType.ALL -> CursorSearchIterator(wrapRecipientCursor(contactSearchPagedDataSourceRepository.queryNonGroupContacts(query, section.includeSelf))) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt index daa527e3d3..fc6978783c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt @@ -34,8 +34,8 @@ open class ContactSearchPagedDataSourceRepository( .getLatestActiveStorySendTimestamps(System.currentTimeMillis() - activeStoryCutoffDuration) } - open fun querySignalContacts(query: String?, includeSelf: Boolean): Cursor? { - return contactRepository.querySignalContacts(query ?: "", includeSelf) + open fun querySignalContacts(contactsSearchQuery: RecipientTable.ContactSearchQuery): Cursor? { + return contactRepository.querySignalContacts(contactsSearchQuery) } open fun querySignalContactLetterHeaders(query: String?, includeSelf: Boolean, includePush: Boolean, includeSms: Boolean): Map { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 28232029ec..0819fd4583 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.badges.Badges.toDatabaseBadge import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.color.MaterialColor import org.thoughtcrime.securesms.color.MaterialColor.UnknownColorException +import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash import org.thoughtcrime.securesms.conversation.colors.ChatColors @@ -3238,7 +3239,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return getSignalContacts(includeSelf)?.count ?: 0 } - fun getSignalContacts(includeSelf: Boolean, orderBy: String? = null): Cursor? { + private fun getSignalContacts(includeSelf: Boolean, orderBy: String? = null): Cursor? { val searchSelection = ContactSearchSelection.Builder() .withRegistered(true) .withGroups(false) @@ -3249,20 +3250,39 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) } - fun querySignalContacts(inputQuery: String, includeSelf: Boolean): Cursor? { - val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery) + fun querySignalContacts(contactSearchQuery: ContactSearchQuery): Cursor? { + val query = SqlUtil.buildCaseInsensitiveGlobPattern(contactSearchQuery.query) val searchSelection = ContactSearchSelection.Builder() .withRegistered(true) .withGroups(false) - .excludeId(if (includeSelf) null else Recipient.self().id) + .excludeId(if (contactSearchQuery.includeSelf) null else Recipient.self().id) .withSearchQuery(query) .build() val selection = searchSelection.where val args = searchSelection.args - val orderBy = "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $E164" + val orderBy = "${if (contactSearchQuery.contactSearchSortOrder == ContactSearchSortOrder.RECENCY) "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC, " else ""}$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $E164" - return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) + return if (contactSearchQuery.contactSearchSortOrder == ContactSearchSortOrder.RECENCY) { + val ambiguous = listOf(ID) + val projection = SEARCH_PROJECTION.map { + if (it in ambiguous) "$TABLE_NAME.$it" else it + } + "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE}" + + //language=roomsql + readableDatabase.query( + """ + SELECT ${projection.joinToString(",")} + FROM $TABLE_NAME + JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$ID + WHERE $selection + ORDER BY $orderBy + """.trimIndent(), + args + ) + } else { + readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy) + } } fun querySignalContactLetterHeaders(inputQuery: String, includeSelf: Boolean, includePush: Boolean, includeSms: Boolean): Map { @@ -4337,6 +4357,12 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da private class GetOrInsertResult(val recipientId: RecipientId, val neededInsert: Boolean) + data class ContactSearchQuery( + val query: String, + val includeSelf: Boolean, + val contactSearchSortOrder: ContactSearchSortOrder = ContactSearchSortOrder.NATURAL + ) + @VisibleForTesting internal class ContactSearchSelection private constructor(val where: String, val args: Array) {