diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt index 51e82d3d60..0b4cd8b8e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt @@ -33,24 +33,22 @@ import org.thoughtcrime.securesms.util.visible open class ContactSearchAdapter( displayCheckBox: Boolean, displaySmsTag: DisplaySmsTag, - recipientListener: Listener, - storyListener: Listener, - storyContextMenuCallbacks: StoryContextMenuCallbacks, - expandListener: (ContactSearchData.Expand) -> Unit + onClickCallbacks: ClickCallbacks, + storyContextMenuCallbacks: StoryContextMenuCallbacks ) : PagingMappingAdapter() { 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, + storyListener: OnClickedCallback, storyContextMenuCallbacks: StoryContextMenuCallbacks? = null ) { mappingAdapter.registerFactory( @@ -63,7 +61,7 @@ open class ContactSearchAdapter( mappingAdapter: MappingAdapter, displayCheckBox: Boolean, displaySmsTag: DisplaySmsTag, - recipientListener: Listener + recipientListener: OnClickedCallback ) { mappingAdapter.registerFactory( RecipientModel::class.java, @@ -135,7 +133,7 @@ open class ContactSearchAdapter( private class StoryViewHolder( itemView: View, displayCheckBox: Boolean, - onClick: Listener, + onClick: OnClickedCallback, private val storyContextMenuCallbacks: StoryContextMenuCallbacks? ) : BaseRecipientViewHolder(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 + onClick: OnClickedCallback ) : BaseRecipientViewHolder(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 + val onClick: OnClickedCallback ) : MappingViewHolder(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 { - fun listen(view: View, data: D, isSelected: Boolean) + fun interface OnClickedCallback { + 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) } } 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 a78047e805..45a934e075 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 @@ -10,6 +10,10 @@ class ContactSearchConfiguration private constructor( val hasEmptyState: Boolean, val sections: List
) { + + /** + * 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 = 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 ) : 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) */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt index 2936e7dd8f..3df22a39e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt @@ -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, Set>> = LiveDataUtil.combineLatest( @@ -168,10 +179,8 @@ class ContactSearchMediator( fun create( displayCheckBox: Boolean, displaySmsTag: ContactSearchAdapter.DisplaySmsTag, - recipientListener: ContactSearchAdapter.Listener, - storyListener: ContactSearchAdapter.Listener, - storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks, - expandListener: (ContactSearchData.Expand) -> Unit + callbacks: ContactSearchAdapter.ClickCallbacks, + storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks ): PagingMappingAdapter } @@ -179,12 +188,10 @@ class ContactSearchMediator( override fun create( displayCheckBox: Boolean, displaySmsTag: ContactSearchAdapter.DisplaySmsTag, - recipientListener: ContactSearchAdapter.Listener, - storyListener: ContactSearchAdapter.Listener, - storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks, - expandListener: (ContactSearchData.Expand) -> Unit + callbacks: ContactSearchAdapter.ClickCallbacks, + storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks ): PagingMappingAdapter { - return ContactSearchAdapter(displayCheckBox, displaySmsTag, recipientListener, storyListener, storyContextMenuCallbacks, expandListener) + return ContactSearchAdapter(displayCheckBox, displaySmsTag, callbacks, storyContextMenuCallbacks) } } } 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 de0d78ffdd..7b177b0ccd 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 @@ -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 { + return if (query.isNullOrEmpty()) { + CursorSearchIterator(null) + } else { + CursorSearchIterator(contactSearchPagedDataSourceRepository.getContactsWithoutThreads(query)) + } + } + private fun getRecentsSearchIterator(section: ContactSearchConfiguration.Section.Recents, query: String?): ContactSearchIterator { 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 { + 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 { val headerMap: Map = 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) } 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 6d85c1b0d6..0960be8fe3 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 @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 37f86fe3ca..91e8e1caf7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -114,6 +114,7 @@ import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView; import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter; import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration; +import org.thoughtcrime.securesms.contacts.paged.ContactSearchData; import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey; import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator; import org.thoughtcrime.securesms.contacts.paged.ContactSearchState; @@ -214,6 +215,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode private static final String TAG = Log.tag(ConversationListFragment.class); private static final int MAXIMUM_PINNED_CONVERSATIONS = 4; + private static final int MAX_CHATS_ABOVE_FOLD = 7; + private static final int MAX_CONTACTS_ABOVE_FOLD = 5; + private static final int MAX_GROUP_MEMBERSHIPS_ABOVE_FOLD = 5; private ActionMode actionMode; private View coordinator; @@ -298,34 +302,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode false, (displayCheckBox, displaySmsTag, - recipientListener, - storyListener, - storyContextMenuCallbacks, - expandListener + callbacks, + storyContextMenuCallbacks ) -> { //noinspection CodeBlock2Expr return new ConversationListSearchAdapter( displayCheckBox, displaySmsTag, - recipientListener, - storyListener, + new ContactSearchClickCallbacks(callbacks), storyContextMenuCallbacks, - expandListener, - (v, t, b) -> { - onConversationClicked(t.getThreadRecord()); - }, - (v, m, b) -> { - onMessageClicked(m.getMessageResult()); - }, - (v, m, b) -> { - onContactClicked(Recipient.resolved(m.getGroupRecord().getRecipientId())); - }, getViewLifecycleOwner(), - GlideApp.with(this), - () -> { - onClearFilterClick(); - return Unit.INSTANCE; - } + GlideApp.with(this) ); }, new ConversationListSearchAdapter.ChatFilterRepository() @@ -611,7 +598,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode true, new ContactSearchConfiguration.ExpandConfig( state.getExpandedSections().contains(ContactSearchConfiguration.SectionKey.CHATS), - (a) -> 7 + (a) -> MAX_CHATS_ABOVE_FOLD ) )); @@ -620,7 +607,15 @@ public class ConversationListFragment extends MainFragment implements ActionMode true, new ContactSearchConfiguration.ExpandConfig( state.getExpandedSections().contains(ContactSearchConfiguration.SectionKey.GROUPS_WITH_MEMBERS), - (a) -> 5 + (a) -> MAX_GROUP_MEMBERSHIPS_ABOVE_FOLD + ) + )); + + builder.addSection(new ContactSearchConfiguration.Section.ContactsWithoutThreads( + true, + new ContactSearchConfiguration.ExpandConfig( + state.getExpandedSections().contains(ContactSearchConfiguration.SectionKey.CONTACTS_WITHOUT_THREADS), + (a) -> MAX_CONTACTS_ABOVE_FOLD ) )); @@ -1850,6 +1845,50 @@ public class ConversationListFragment extends MainFragment implements ActionMode } } + private class ContactSearchClickCallbacks implements ConversationListSearchAdapter.ConversationListSearchClickCallbacks { + + private final ContactSearchAdapter.ClickCallbacks delegate; + + private ContactSearchClickCallbacks(@NonNull ContactSearchAdapter.ClickCallbacks delegate) { + this.delegate = delegate; + } + + @Override + public void onThreadClicked(@NonNull View view, @NonNull ContactSearchData.Thread thread, boolean isSelected) { + onConversationClicked(thread.getThreadRecord()); + } + + @Override + public void onMessageClicked(@NonNull View view, @NonNull ContactSearchData.Message thread, boolean isSelected) { + ConversationListFragment.this.onMessageClicked(thread.getMessageResult()); + } + + @Override + public void onGroupWithMembersClicked(@NonNull View view, @NonNull ContactSearchData.GroupWithMembers groupWithMembers, boolean isSelected) { + onContactClicked(Recipient.resolved(groupWithMembers.getGroupRecord().getRecipientId())); + } + + @Override + public void onClearFilterClicked() { + onClearFilterClick(); + } + + @Override + public void onStoryClicked(@NonNull View view, @NonNull ContactSearchData.Story story, boolean isSelected) { + throw new UnsupportedOperationException(); + } + + @Override + public void onKnownRecipientClicked(@NonNull View view, @NonNull ContactSearchData.KnownRecipient knownRecipient, boolean isSelected) { + onContactClicked(knownRecipient.getRecipient()); + } + + @Override + public void onExpandClicked(@NonNull ContactSearchData.Expand expand) { + delegate.onExpandClicked(expand); + } + } + public interface Callback extends Material3OnScrollHelperBinder, SearchBinder { @NonNull Toolbar getToolbar(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.kt index 23efec712b..adc97af7f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.kt @@ -24,33 +24,28 @@ import java.util.Locale class ConversationListSearchAdapter( displayCheckBox: Boolean, displaySmsTag: DisplaySmsTag, - recipientListener: Listener, - storyListener: Listener, + onClickedCallbacks: ConversationListSearchClickCallbacks, storyContextMenuCallbacks: StoryContextMenuCallbacks, - expandListener: (ContactSearchData.Expand) -> Unit, - threadListener: Listener, - messageListener: Listener, - groupWithMembersListener: Listener, lifecycleOwner: LifecycleOwner, - glideRequests: GlideRequests, - clearFilterListener: () -> Unit -) : ContactSearchAdapter(displayCheckBox, displaySmsTag, recipientListener, storyListener, storyContextMenuCallbacks, expandListener) { + glideRequests: GlideRequests +) : ContactSearchAdapter(displayCheckBox, displaySmsTag, onClickedCallbacks, storyContextMenuCallbacks) { + init { registerFactory( ThreadModel::class.java, - LayoutFactory({ ThreadViewHolder(threadListener, lifecycleOwner, glideRequests, it) }, R.layout.conversation_list_item_view) + LayoutFactory({ ThreadViewHolder(onClickedCallbacks::onThreadClicked, lifecycleOwner, glideRequests, it) }, R.layout.conversation_list_item_view) ) registerFactory( MessageModel::class.java, - LayoutFactory({ MessageViewHolder(messageListener, lifecycleOwner, glideRequests, it) }, R.layout.conversation_list_item_view) + LayoutFactory({ MessageViewHolder(onClickedCallbacks::onMessageClicked, lifecycleOwner, glideRequests, it) }, R.layout.conversation_list_item_view) ) registerFactory( ChatFilterMappingModel::class.java, - LayoutFactory({ ChatFilterViewHolder(it, clearFilterListener) }, R.layout.conversation_list_item_clear_filter) + LayoutFactory({ ChatFilterViewHolder(it, onClickedCallbacks::onClearFilterClicked) }, R.layout.conversation_list_item_clear_filter) ) registerFactory( ChatFilterEmptyMappingModel::class.java, - LayoutFactory({ ChatFilterViewHolder(it, clearFilterListener) }, R.layout.conversation_list_item_clear_filter_empty) + LayoutFactory({ ChatFilterViewHolder(it, onClickedCallbacks::onClearFilterClicked) }, R.layout.conversation_list_item_clear_filter_empty) ) registerFactory( EmptyModel::class.java, @@ -58,7 +53,7 @@ class ConversationListSearchAdapter( ) registerFactory( GroupWithMembersModel::class.java, - LayoutFactory({ GroupWithMembersViewHolder(groupWithMembersListener, lifecycleOwner, glideRequests, it) }, R.layout.conversation_list_item_view) + LayoutFactory({ GroupWithMembersViewHolder(onClickedCallbacks::onGroupWithMembersClicked, lifecycleOwner, glideRequests, it) }, R.layout.conversation_list_item_view) ) } @@ -75,14 +70,14 @@ class ConversationListSearchAdapter( } private class ThreadViewHolder( - private val threadListener: Listener, + private val threadListener: OnClickedCallback, private val lifecycleOwner: LifecycleOwner, private val glideRequests: GlideRequests, itemView: View ) : MappingViewHolder(itemView) { override fun bind(model: ThreadModel) { itemView.setOnClickListener { - threadListener.listen(itemView, model.thread, false) + threadListener.onClicked(itemView, model.thread, false) } (itemView as ConversationListItem).bindThread( @@ -98,14 +93,14 @@ class ConversationListSearchAdapter( } private class MessageViewHolder( - private val messageListener: Listener, + private val messageListener: OnClickedCallback, private val lifecycleOwner: LifecycleOwner, private val glideRequests: GlideRequests, itemView: View ) : MappingViewHolder(itemView) { override fun bind(model: MessageModel) { itemView.setOnClickListener { - messageListener.listen(itemView, model.message, false) + messageListener.onClicked(itemView, model.message, false) } (itemView as ConversationListItem).bindMessage( @@ -119,14 +114,14 @@ class ConversationListSearchAdapter( } private class GroupWithMembersViewHolder( - private val groupWithMembersListener: Listener, + private val groupWithMembersListener: OnClickedCallback, private val lifecycleOwner: LifecycleOwner, private val glideRequests: GlideRequests, itemView: View ) : MappingViewHolder(itemView) { override fun bind(model: GroupWithMembersModel) { itemView.setOnClickListener { - groupWithMembersListener.listen(itemView, model.groupWithMembers, false) + groupWithMembersListener.onClicked(itemView, model.groupWithMembers, false) } (itemView as ConversationListItem).bindGroupWithMembers( @@ -197,4 +192,11 @@ class ConversationListSearchAdapter( } } } + + interface ConversationListSearchClickCallbacks : ClickCallbacks { + fun onThreadClicked(view: View, thread: ContactSearchData.Thread, isSelected: Boolean) + fun onMessageClicked(view: View, thread: ContactSearchData.Message, isSelected: Boolean) + fun onGroupWithMembersClicked(view: View, groupWithMembers: ContactSearchData.GroupWithMembers, isSelected: Boolean) + fun onClearFilterClicked() + } } 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 f89f21338d..0770219ee3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -3201,6 +3201,24 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return SqlUtil.Query(subquery, SqlUtil.buildArgs(0, 0, query, query, query, query)) } + fun getAllContactsWithoutThreads(inputQuery: String): Cursor { + val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery) + + //language=sql + val subquery = """ + SELECT ${SEARCH_PROJECTION.joinToString(", ")} FROM $TABLE_NAME + WHERE $BLOCKED = ? AND $HIDDEN = ? AND NOT EXISTS (SELECT 1 FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$ID LIMIT 1) + AND ( + $SORT_NAME GLOB ? OR + $USERNAME GLOB ? OR + $PHONE GLOB ? OR + $EMAIL GLOB ? + ) + """.toSingleLine() + + return readableDatabase.query(subquery, SqlUtil.buildArgs(0, 0, query, query, query, query)) + } + @JvmOverloads fun queryRecipientsForMentions(inputQuery: String, recipientIds: List? = null): List { val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery) diff --git a/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt b/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt index 1c80000752..0e037495f3 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt @@ -30,7 +30,7 @@ class ContactSearchPagedDataSourceTest { @Before fun setUp() { whenever(repository.getRecipientFromGroupRecord(any())).thenReturn(Recipient.UNKNOWN) - whenever(repository.getRecipientFromRecipientCursor(cursor)).thenReturn(Recipient.UNKNOWN) + whenever(repository.getRecipientFromSearchCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromThreadCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromDistributionListCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getPrivacyModeFromDistributionListCursor(cursor)).thenReturn(DistributionListPrivacyMode.ALL)