Add "Group Members" section to ConversationList search results.
This commit is contained in:
parent
e84c6187b9
commit
09902e5d11
14 changed files with 265 additions and 39 deletions
|
@ -33,11 +33,12 @@ import org.thoughtcrime.securesms.util.visible
|
||||||
open class ContactSearchAdapter(
|
open class ContactSearchAdapter(
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: DisplaySmsTag,
|
displaySmsTag: DisplaySmsTag,
|
||||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
recipientListener: Listener<ContactSearchData.KnownRecipient>,
|
||||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
storyListener: Listener<ContactSearchData.Story>,
|
||||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||||
expandListener: (ContactSearchData.Expand) -> Unit
|
expandListener: (ContactSearchData.Expand) -> Unit
|
||||||
) : PagingMappingAdapter<ContactSearchKey>() {
|
) : PagingMappingAdapter<ContactSearchKey>() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerStoryItems(this, displayCheckBox, storyListener, storyContextMenuCallbacks)
|
registerStoryItems(this, displayCheckBox, storyListener, storyContextMenuCallbacks)
|
||||||
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, recipientListener)
|
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, recipientListener)
|
||||||
|
@ -49,7 +50,7 @@ open class ContactSearchAdapter(
|
||||||
fun registerStoryItems(
|
fun registerStoryItems(
|
||||||
mappingAdapter: MappingAdapter,
|
mappingAdapter: MappingAdapter,
|
||||||
displayCheckBox: Boolean = false,
|
displayCheckBox: Boolean = false,
|
||||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
storyListener: Listener<ContactSearchData.Story>,
|
||||||
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
|
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
|
||||||
) {
|
) {
|
||||||
mappingAdapter.registerFactory(
|
mappingAdapter.registerFactory(
|
||||||
|
@ -62,7 +63,7 @@ open class ContactSearchAdapter(
|
||||||
mappingAdapter: MappingAdapter,
|
mappingAdapter: MappingAdapter,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: DisplaySmsTag,
|
displaySmsTag: DisplaySmsTag,
|
||||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
|
recipientListener: Listener<ContactSearchData.KnownRecipient>
|
||||||
) {
|
) {
|
||||||
mappingAdapter.registerFactory(
|
mappingAdapter.registerFactory(
|
||||||
RecipientModel::class.java,
|
RecipientModel::class.java,
|
||||||
|
@ -97,6 +98,7 @@ open class ContactSearchAdapter(
|
||||||
is ContactSearchData.Message -> MessageModel(it)
|
is ContactSearchData.Message -> MessageModel(it)
|
||||||
is ContactSearchData.Thread -> ThreadModel(it)
|
is ContactSearchData.Thread -> ThreadModel(it)
|
||||||
is ContactSearchData.Empty -> EmptyModel(it)
|
is ContactSearchData.Empty -> EmptyModel(it)
|
||||||
|
is ContactSearchData.GroupWithMembers -> GroupWithMembersModel(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -133,7 +135,7 @@ open class ContactSearchAdapter(
|
||||||
private class StoryViewHolder(
|
private class StoryViewHolder(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
onClick: (View, ContactSearchData.Story, Boolean) -> Unit,
|
onClick: Listener<ContactSearchData.Story>,
|
||||||
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
|
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
|
||||||
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, onClick) {
|
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, onClick) {
|
||||||
override fun isSelected(model: StoryModel): Boolean = model.isSelected
|
override fun isSelected(model: StoryModel): Boolean = model.isSelected
|
||||||
|
@ -265,7 +267,7 @@ open class ContactSearchAdapter(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: DisplaySmsTag,
|
displaySmsTag: DisplaySmsTag,
|
||||||
onClick: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
|
onClick: Listener<ContactSearchData.KnownRecipient>
|
||||||
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
|
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
|
||||||
|
|
||||||
private var headerLetter: String? = null
|
private var headerLetter: String? = null
|
||||||
|
@ -302,7 +304,7 @@ open class ContactSearchAdapter(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
private val displayCheckBox: Boolean,
|
private val displayCheckBox: Boolean,
|
||||||
private val displaySmsTag: DisplaySmsTag,
|
private val displaySmsTag: DisplaySmsTag,
|
||||||
val onClick: (View, D, Boolean) -> Unit
|
val onClick: Listener<D>
|
||||||
) : MappingViewHolder<T>(itemView) {
|
) : MappingViewHolder<T>(itemView) {
|
||||||
|
|
||||||
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
|
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
|
||||||
|
@ -316,7 +318,7 @@ open class ContactSearchAdapter(
|
||||||
override fun bind(model: T) {
|
override fun bind(model: T) {
|
||||||
checkbox.visible = displayCheckBox
|
checkbox.visible = displayCheckBox
|
||||||
checkbox.isChecked = isSelected(model)
|
checkbox.isChecked = isSelected(model)
|
||||||
itemView.setOnClickListener { onClick(avatar, getData(model), isSelected(model)) }
|
itemView.setOnClickListener { onClick.listen(avatar, getData(model), isSelected(model)) }
|
||||||
bindLongPress(model)
|
bindLongPress(model)
|
||||||
|
|
||||||
if (payload.isNotEmpty()) {
|
if (payload.isNotEmpty()) {
|
||||||
|
@ -420,6 +422,15 @@ open class ContactSearchAdapter(
|
||||||
override fun areContentsTheSame(newItem: EmptyModel): Boolean = newItem.empty == empty
|
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
|
* View Holder for section headers
|
||||||
*/
|
*/
|
||||||
|
@ -439,6 +450,7 @@ open class ContactSearchAdapter(
|
||||||
ContactSearchConfiguration.SectionKey.GROUP_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
ContactSearchConfiguration.SectionKey.GROUP_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
||||||
ContactSearchConfiguration.SectionKey.CHATS -> R.string.ContactsCursorLoader__chats
|
ContactSearchConfiguration.SectionKey.CHATS -> R.string.ContactsCursorLoader__chats
|
||||||
ContactSearchConfiguration.SectionKey.MESSAGES -> R.string.ContactsCursorLoader__messages
|
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,
|
IF_NOT_REGISTERED,
|
||||||
NEVER
|
NEVER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun interface Listener<D : ContactSearchData> {
|
||||||
|
fun listen(view: View, data: D, isSelected: Boolean)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,18 @@ class ContactSearchConfiguration private constructor(
|
||||||
override val expandConfig: ExpandConfig? = null
|
override val expandConfig: ExpandConfig? = null
|
||||||
) : Section(SectionKey.GROUP_MEMBERS)
|
) : 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(
|
data class Chats(
|
||||||
val isUnreadOnly: Boolean = false,
|
val isUnreadOnly: Boolean = false,
|
||||||
override val includeHeader: Boolean = true,
|
override val includeHeader: Boolean = true,
|
||||||
|
@ -119,6 +131,11 @@ class ContactSearchConfiguration private constructor(
|
||||||
*/
|
*/
|
||||||
GROUPS,
|
GROUPS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section Key for [Section.GroupsWithMembers]
|
||||||
|
*/
|
||||||
|
GROUPS_WITH_MEMBERS,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arbitrary row (think new group button, username row, etc)
|
* Arbitrary row (think new group button, username row, etc)
|
||||||
*/
|
*/
|
||||||
|
@ -207,6 +224,13 @@ class ContactSearchConfiguration private constructor(
|
||||||
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun groupsWithMembers(
|
||||||
|
includeHeader: Boolean = true,
|
||||||
|
expandConfig: ExpandConfig? = null
|
||||||
|
) {
|
||||||
|
addSection(Section.GroupsWithMembers(includeHeader, expandConfig))
|
||||||
|
}
|
||||||
|
|
||||||
fun addSection(section: Section)
|
fun addSection(section: Section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
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.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.search.MessageResult
|
import org.thoughtcrime.securesms.search.MessageResult
|
||||||
|
@ -50,6 +51,16 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
|
||||||
val threadRecord: ThreadRecord
|
val threadRecord: ThreadRecord
|
||||||
) : ContactSearchData(ContactSearchKey.Thread(threadRecord.threadId))
|
) : 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
|
* A row containing a title for a given section
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.contacts.paged
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
import org.thoughtcrime.securesms.sharing.ShareContact
|
||||||
|
|
||||||
|
@ -48,6 +49,11 @@ sealed class ContactSearchKey {
|
||||||
*/
|
*/
|
||||||
data class Thread(val threadId: Long) : 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
|
* Search key for a MessageRecord
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -168,8 +168,8 @@ class ContactSearchMediator(
|
||||||
fun create(
|
fun create(
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
|
||||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
|
||||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
||||||
expandListener: (ContactSearchData.Expand) -> Unit
|
expandListener: (ContactSearchData.Expand) -> Unit
|
||||||
): PagingMappingAdapter<ContactSearchKey>
|
): PagingMappingAdapter<ContactSearchKey>
|
||||||
|
@ -179,8 +179,8 @@ class ContactSearchMediator(
|
||||||
override fun create(
|
override fun create(
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
|
||||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
|
||||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
||||||
expandListener: (ContactSearchData.Expand) -> Unit
|
expandListener: (ContactSearchData.Expand) -> Unit
|
||||||
): PagingMappingAdapter<ContactSearchKey> {
|
): PagingMappingAdapter<ContactSearchKey> {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package org.thoughtcrime.securesms.contacts.paged
|
package org.thoughtcrime.securesms.contacts.paged
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.paging.PagedDataSource
|
import org.signal.paging.PagedDataSource
|
||||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchCollection
|
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchCollection
|
||||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
|
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
|
||||||
import org.thoughtcrime.securesms.contacts.paged.collections.CursorSearchIterator
|
import org.thoughtcrime.securesms.contacts.paged.collections.CursorSearchIterator
|
||||||
import org.thoughtcrime.securesms.contacts.paged.collections.StoriesSearchCollection
|
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.DistributionListPrivacyMode
|
||||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
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.GroupMembers -> getGroupMembersSearchIterator(query).getCollectionSize(section, query, null)
|
||||||
is ContactSearchConfiguration.Section.Chats -> getThreadData(query, section.isUnreadOnly).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.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.GroupMembers -> getGroupMembersContactData(section, query, startIndex, endIndex)
|
||||||
is ContactSearchConfiguration.Section.Chats -> getThreadContactData(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.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))
|
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> {
|
private fun getRecentsSearchIterator(section: ContactSearchConfiguration.Section.Recents, query: String?): ContactSearchIterator<Cursor> {
|
||||||
if (!query.isNullOrEmpty()) {
|
if (!query.isNullOrEmpty()) {
|
||||||
throw IllegalArgumentException("Searching Recents is not supported")
|
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> {
|
private fun getRecentsContactData(section: ContactSearchConfiguration.Section.Recents, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
|
||||||
return getRecentsSearchIterator(section, query).use { records ->
|
return getRecentsSearchIterator(section, query).use { records ->
|
||||||
readContactData(
|
readContactData(
|
||||||
|
|
|
@ -84,6 +84,10 @@ open class ContactSearchPagedDataSourceRepository(
|
||||||
return SignalDatabase.distributionLists.getAllListsForContactSelectionUiCursor(query, myStoryContainsQuery(query ?: ""))
|
return SignalDatabase.distributionLists.getAllListsForContactSelectionUiCursor(query, myStoryContainsQuery(query ?: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open fun getGroupsWithMembers(query: String): Cursor {
|
||||||
|
return SignalDatabase.groups.queryGroupsByMemberName(query)
|
||||||
|
}
|
||||||
|
|
||||||
open fun getRecipientFromDistributionListCursor(cursor: Cursor): Recipient {
|
open fun getRecipientFromDistributionListCursor(cursor: Cursor): Recipient {
|
||||||
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, DistributionListTables.RECIPIENT_ID)))
|
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, DistributionListTables.RECIPIENT_ID)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,11 +313,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
expandListener,
|
expandListener,
|
||||||
(v, t, b) -> {
|
(v, t, b) -> {
|
||||||
onConversationClicked(t.getThreadRecord());
|
onConversationClicked(t.getThreadRecord());
|
||||||
return Unit.INSTANCE;
|
|
||||||
},
|
},
|
||||||
(v, m, b) -> {
|
(v, m, b) -> {
|
||||||
onMessageClicked(m.getMessageResult());
|
onMessageClicked(m.getMessageResult());
|
||||||
return Unit.INSTANCE;
|
},
|
||||||
|
(v, m, b) -> {
|
||||||
|
onContactClicked(Recipient.resolved(m.getGroupRecord().getRecipientId()));
|
||||||
},
|
},
|
||||||
getViewLifecycleOwner(),
|
getViewLifecycleOwner(),
|
||||||
GlideApp.with(this),
|
GlideApp.with(this),
|
||||||
|
@ -600,12 +601,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
if (TextUtils.isEmpty(state.getQuery())) {
|
if (TextUtils.isEmpty(state.getQuery())) {
|
||||||
return ContactSearchConfiguration.build(b -> Unit.INSTANCE);
|
return ContactSearchConfiguration.build(b -> Unit.INSTANCE);
|
||||||
} else {
|
} else {
|
||||||
return ContactSearchConfiguration.build(b -> {
|
return ContactSearchConfiguration.build(builder -> {
|
||||||
ConversationFilterRequest conversationFilterRequest = state.getConversationFilterRequest();
|
ConversationFilterRequest conversationFilterRequest = state.getConversationFilterRequest();
|
||||||
boolean unreadOnly = conversationFilterRequest != null && conversationFilterRequest.getFilter() == ConversationFilter.UNREAD;
|
boolean unreadOnly = conversationFilterRequest != null && conversationFilterRequest.getFilter() == ConversationFilter.UNREAD;
|
||||||
|
|
||||||
b.setQuery(state.getQuery());
|
builder.setQuery(state.getQuery());
|
||||||
b.addSection(new ContactSearchConfiguration.Section.Chats(
|
builder.addSection(new ContactSearchConfiguration.Section.Chats(
|
||||||
unreadOnly,
|
unreadOnly,
|
||||||
true,
|
true,
|
||||||
new ContactSearchConfiguration.ExpandConfig(
|
new ContactSearchConfiguration.ExpandConfig(
|
||||||
|
@ -615,17 +616,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!unreadOnly) {
|
if (!unreadOnly) {
|
||||||
|
builder.addSection(new ContactSearchConfiguration.Section.GroupsWithMembers(
|
||||||
|
true,
|
||||||
|
new ContactSearchConfiguration.ExpandConfig(
|
||||||
|
state.getExpandedSections().contains(ContactSearchConfiguration.SectionKey.GROUPS_WITH_MEMBERS),
|
||||||
|
(a) -> 5
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
// Groups-with-member-section
|
builder.addSection(new ContactSearchConfiguration.Section.Messages(
|
||||||
|
|
||||||
b.addSection(new ContactSearchConfiguration.Section.Messages(
|
|
||||||
true,
|
true,
|
||||||
null
|
null
|
||||||
));
|
));
|
||||||
|
|
||||||
b.setHasEmptyState(true);
|
builder.setHasEmptyState(true);
|
||||||
} else {
|
} else {
|
||||||
b.arbitrary(
|
builder.arbitrary(
|
||||||
conversationFilterRequest.getSource() == ConversationFilterSource.DRAG
|
conversationFilterRequest.getSource() == ConversationFilterSource.DRAG
|
||||||
? ConversationListSearchAdapter.ChatFilterOptions.WITHOUT_TIP.getCode()
|
? ConversationListSearchAdapter.ChatFilterOptions.WITHOUT_TIP.getCode()
|
||||||
: ConversationListSearchAdapter.ChatFilterOptions.WITH_TIP.getCode()
|
: ConversationListSearchAdapter.ChatFilterOptions.WITH_TIP.getCode()
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.graphics.Typeface;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.text.style.CharacterStyle;
|
import android.text.style.CharacterStyle;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
|
@ -60,6 +61,7 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||||
import org.thoughtcrime.securesms.components.FromTextView;
|
import org.thoughtcrime.securesms.components.FromTextView;
|
||||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||||
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
|
||||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||||
import org.thoughtcrime.securesms.database.MessageTypes;
|
import org.thoughtcrime.securesms.database.MessageTypes;
|
||||||
|
@ -80,10 +82,19 @@ import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync;
|
import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync;
|
||||||
|
|
||||||
|
@ -129,6 +140,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||||
private SearchUtil.StyleFactory searchStyleFactory;
|
private SearchUtil.StyleFactory searchStyleFactory;
|
||||||
|
|
||||||
private LiveData<SpannableString> displayBody;
|
private LiveData<SpannableString> displayBody;
|
||||||
|
private Disposable joinMembersDisposable = Disposable.empty();
|
||||||
|
|
||||||
public ConversationListItem(Context context) {
|
public ConversationListItem(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
|
@ -219,6 +231,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||||
|
|
||||||
observeRecipient(lifecycleOwner, thread.getRecipient().live());
|
observeRecipient(lifecycleOwner, thread.getRecipient().live());
|
||||||
observeDisplayBody(null, null);
|
observeDisplayBody(null, null);
|
||||||
|
joinMembersDisposable.dispose();
|
||||||
|
|
||||||
if (highlightSubstring != null) {
|
if (highlightSubstring != null) {
|
||||||
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
|
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
|
||||||
|
@ -277,6 +290,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||||
|
|
||||||
observeRecipient(lifecycleOwner, contact.live());
|
observeRecipient(lifecycleOwner, contact.live());
|
||||||
observeDisplayBody(null, null);
|
observeDisplayBody(null, null);
|
||||||
|
joinMembersDisposable.dispose();
|
||||||
setSubjectViewText(null);
|
setSubjectViewText(null);
|
||||||
|
|
||||||
fromView.setText(contact, SearchUtil.getHighlightedSpan(locale, searchStyleFactory, new SpannableString(contact.getDisplayName(getContext())), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
|
fromView.setText(contact, SearchUtil.getHighlightedSpan(locale, searchStyleFactory, new SpannableString(contact.getDisplayName(getContext())), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
|
||||||
|
@ -305,6 +319,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||||
|
|
||||||
observeRecipient(lifecycleOwner, messageResult.getConversationRecipient().live());
|
observeRecipient(lifecycleOwner, messageResult.getConversationRecipient().live());
|
||||||
observeDisplayBody(null, null);
|
observeDisplayBody(null, null);
|
||||||
|
joinMembersDisposable.dispose();
|
||||||
setSubjectViewText(null);
|
setSubjectViewText(null);
|
||||||
|
|
||||||
fromView.setText(recipient.get(), false);
|
fromView.setText(recipient.get(), false);
|
||||||
|
@ -321,6 +336,46 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void bindGroupWithMembers(@NonNull LifecycleOwner lifecycleOwner,
|
||||||
|
@NonNull ContactSearchData.GroupWithMembers groupWithMembers,
|
||||||
|
@NonNull GlideRequests glideRequests,
|
||||||
|
@NonNull Locale locale)
|
||||||
|
{
|
||||||
|
this.glideRequests = glideRequests;
|
||||||
|
this.locale = locale;
|
||||||
|
this.highlightSubstring = groupWithMembers.getQuery();
|
||||||
|
|
||||||
|
observeRecipient(lifecycleOwner, Recipient.live(groupWithMembers.getGroupRecord().getRecipientId()));
|
||||||
|
observeDisplayBody(null, null);
|
||||||
|
joinMembersDisposable.dispose();
|
||||||
|
joinMembersDisposable = joinMembersToDisplayBody(groupWithMembers.getGroupRecord().getMembers(), groupWithMembers.getQuery()).subscribe(joined -> {
|
||||||
|
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, searchStyleFactory, joined, highlightSubstring, SearchUtil.MATCH_ALL));
|
||||||
|
});
|
||||||
|
|
||||||
|
fromView.setText(recipient.get(), false);
|
||||||
|
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, groupWithMembers.getDate()));
|
||||||
|
archivedView.setVisibility(GONE);
|
||||||
|
unreadIndicator.setVisibility(GONE);
|
||||||
|
unreadMentions.setVisibility(GONE);
|
||||||
|
deliveryStatusIndicator.setNone();
|
||||||
|
alertView.setNone();
|
||||||
|
|
||||||
|
setSelectedConversations(new ConversationSet());
|
||||||
|
setBadgeFromRecipient(recipient.get());
|
||||||
|
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull Single<String> joinMembersToDisplayBody(@NonNull List<RecipientId> members, @NonNull String highlightSubstring) {
|
||||||
|
return Single.fromCallable(() -> {
|
||||||
|
return Util.join(Recipient.resolvedList(members)
|
||||||
|
.stream()
|
||||||
|
.map(r -> r.getDisplayName(getContext()))
|
||||||
|
.sorted(new JoinMembersComparator(highlightSubstring))
|
||||||
|
.limit(5)
|
||||||
|
.collect(Collectors.toList()), ",");
|
||||||
|
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unbind() {
|
public void unbind() {
|
||||||
if (this.recipient != null) {
|
if (this.recipient != null) {
|
||||||
|
@ -330,6 +385,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||||
}
|
}
|
||||||
|
|
||||||
observeDisplayBody(null, null);
|
observeDisplayBody(null, null);
|
||||||
|
joinMembersDisposable.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -24,12 +24,13 @@ import java.util.Locale
|
||||||
class ConversationListSearchAdapter(
|
class ConversationListSearchAdapter(
|
||||||
displayCheckBox: Boolean,
|
displayCheckBox: Boolean,
|
||||||
displaySmsTag: DisplaySmsTag,
|
displaySmsTag: DisplaySmsTag,
|
||||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
recipientListener: Listener<ContactSearchData.KnownRecipient>,
|
||||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
storyListener: Listener<ContactSearchData.Story>,
|
||||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||||
expandListener: (ContactSearchData.Expand) -> Unit,
|
expandListener: (ContactSearchData.Expand) -> Unit,
|
||||||
threadListener: (View, ContactSearchData.Thread, Boolean) -> Unit,
|
threadListener: Listener<ContactSearchData.Thread>,
|
||||||
messageListener: (View, ContactSearchData.Message, Boolean) -> Unit,
|
messageListener: Listener<ContactSearchData.Message>,
|
||||||
|
groupWithMembersListener: Listener<ContactSearchData.GroupWithMembers>,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
glideRequests: GlideRequests,
|
glideRequests: GlideRequests,
|
||||||
clearFilterListener: () -> Unit
|
clearFilterListener: () -> Unit
|
||||||
|
@ -55,6 +56,10 @@ class ConversationListSearchAdapter(
|
||||||
EmptyModel::class.java,
|
EmptyModel::class.java,
|
||||||
LayoutFactory({ EmptyViewHolder(it) }, R.layout.conversation_list_empty_search_state)
|
LayoutFactory({ EmptyViewHolder(it) }, R.layout.conversation_list_empty_search_state)
|
||||||
)
|
)
|
||||||
|
registerFactory(
|
||||||
|
GroupWithMembersModel::class.java,
|
||||||
|
LayoutFactory({ GroupWithMembersViewHolder(groupWithMembersListener, lifecycleOwner, glideRequests, it) }, R.layout.conversation_list_item_view)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EmptyViewHolder(
|
private class EmptyViewHolder(
|
||||||
|
@ -70,14 +75,14 @@ class ConversationListSearchAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ThreadViewHolder(
|
private class ThreadViewHolder(
|
||||||
private val threadListener: (View, ContactSearchData.Thread, Boolean) -> Unit,
|
private val threadListener: Listener<ContactSearchData.Thread>,
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
private val glideRequests: GlideRequests,
|
private val glideRequests: GlideRequests,
|
||||||
itemView: View
|
itemView: View
|
||||||
) : MappingViewHolder<ThreadModel>(itemView) {
|
) : MappingViewHolder<ThreadModel>(itemView) {
|
||||||
override fun bind(model: ThreadModel) {
|
override fun bind(model: ThreadModel) {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
threadListener(itemView, model.thread, false)
|
threadListener.listen(itemView, model.thread, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
(itemView as ConversationListItem).bindThread(
|
(itemView as ConversationListItem).bindThread(
|
||||||
|
@ -93,14 +98,14 @@ class ConversationListSearchAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MessageViewHolder(
|
private class MessageViewHolder(
|
||||||
private val messageListener: (View, ContactSearchData.Message, Boolean) -> Unit,
|
private val messageListener: Listener<ContactSearchData.Message>,
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
private val glideRequests: GlideRequests,
|
private val glideRequests: GlideRequests,
|
||||||
itemView: View
|
itemView: View
|
||||||
) : MappingViewHolder<MessageModel>(itemView) {
|
) : MappingViewHolder<MessageModel>(itemView) {
|
||||||
override fun bind(model: MessageModel) {
|
override fun bind(model: MessageModel) {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
messageListener(itemView, model.message, false)
|
messageListener.listen(itemView, model.message, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
(itemView as ConversationListItem).bindMessage(
|
(itemView as ConversationListItem).bindMessage(
|
||||||
|
@ -113,6 +118,26 @@ class ConversationListSearchAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class GroupWithMembersViewHolder(
|
||||||
|
private val groupWithMembersListener: Listener<ContactSearchData.GroupWithMembers>,
|
||||||
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
|
private val glideRequests: GlideRequests,
|
||||||
|
itemView: View
|
||||||
|
) : MappingViewHolder<GroupWithMembersModel>(itemView) {
|
||||||
|
override fun bind(model: GroupWithMembersModel) {
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
groupWithMembersListener.listen(itemView, model.groupWithMembers, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
(itemView as ConversationListItem).bindGroupWithMembers(
|
||||||
|
lifecycleOwner,
|
||||||
|
model.groupWithMembers,
|
||||||
|
glideRequests,
|
||||||
|
Locale.getDefault()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private open class BaseChatFilterMappingModel<T : BaseChatFilterMappingModel<T>>(val options: ChatFilterOptions) : MappingModel<T> {
|
private open class BaseChatFilterMappingModel<T : BaseChatFilterMappingModel<T>>(val options: ChatFilterOptions) : MappingModel<T> {
|
||||||
override fun areItemsTheSame(newItem: T): Boolean = true
|
override fun areItemsTheSame(newItem: T): Boolean = true
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.thoughtcrime.securesms.conversationlist
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two display names, preferring the one which includes the given [highlightSubstring].
|
||||||
|
*/
|
||||||
|
class JoinMembersComparator(private val highlightSubstring: String) : Comparator<String> {
|
||||||
|
override fun compare(o1: String, o2: String): Int {
|
||||||
|
val o1ContainsSubstring = o1.contains(highlightSubstring, true)
|
||||||
|
val o2ContainsSubstring = o2.contains(highlightSubstring, true)
|
||||||
|
return if (!(o1ContainsSubstring xor o2ContainsSubstring)) {
|
||||||
|
o1.compareTo(o2)
|
||||||
|
} else if (o1ContainsSubstring) {
|
||||||
|
-1
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,6 +69,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
private val TAG = Log.tag(GroupTable::class.java)
|
private val TAG = Log.tag(GroupTable::class.java)
|
||||||
|
|
||||||
const val MEMBER_GROUP_CONCAT = "member_group_concat"
|
const val MEMBER_GROUP_CONCAT = "member_group_concat"
|
||||||
|
const val THREAD_DATE = "thread_date"
|
||||||
|
|
||||||
const val TABLE_NAME = "groups"
|
const val TABLE_NAME = "groups"
|
||||||
const val ID = "_id"
|
const val ID = "_id"
|
||||||
|
@ -346,6 +347,24 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
return noMetadata && noMembers
|
return noMetadata && noMembers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun queryGroupsByMemberName(inputQuery: String): Cursor {
|
||||||
|
val subquery = recipients.getAllContactsSubquery(inputQuery)
|
||||||
|
val statement = """
|
||||||
|
SELECT
|
||||||
|
DISTINCT $TABLE_NAME.*,
|
||||||
|
GROUP_CONCAT(${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}) as $MEMBER_GROUP_CONCAT,
|
||||||
|
${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} as $THREAD_DATE
|
||||||
|
FROM $TABLE_NAME
|
||||||
|
INNER JOIN ${MembershipTable.TABLE_NAME} ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
|
||||||
|
INNER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$RECIPIENT_ID
|
||||||
|
WHERE $ACTIVE = 1 AND ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} IN (${subquery.where})
|
||||||
|
GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}
|
||||||
|
ORDER BY $TITLE COLLATE NOCASE ASC
|
||||||
|
""".toSingleLine()
|
||||||
|
|
||||||
|
return databaseHelper.signalReadableDatabase.query(statement, subquery.whereArgs)
|
||||||
|
}
|
||||||
|
|
||||||
fun queryGroupsByTitle(inputQuery: String, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): Reader {
|
fun queryGroupsByTitle(inputQuery: String, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): Reader {
|
||||||
val query = getGroupQueryWhereStatement(inputQuery, includeInactive, excludeV1, excludeMms)
|
val query = getGroupQueryWhereStatement(inputQuery, includeInactive, excludeV1, excludeMms)
|
||||||
//language=sql
|
//language=sql
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.requireString
|
import org.signal.core.util.requireString
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.toSingleLine
|
||||||
import org.signal.core.util.update
|
import org.signal.core.util.update
|
||||||
import org.signal.core.util.withinTransaction
|
import org.signal.core.util.withinTransaction
|
||||||
import org.signal.libsignal.protocol.IdentityKey
|
import org.signal.libsignal.protocol.IdentityKey
|
||||||
|
@ -3179,6 +3180,27 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||||
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null)
|
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the query used for performing the all contacts search so that it can be injected as a subquery.
|
||||||
|
*/
|
||||||
|
fun getAllContactsSubquery(inputQuery: String): SqlUtil.Query {
|
||||||
|
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
|
||||||
|
|
||||||
|
//language=sql
|
||||||
|
val subquery = """SELECT $ID FROM (
|
||||||
|
SELECT ${SEARCH_PROJECTION.joinToString(",")} FROM $TABLE_NAME
|
||||||
|
WHERE $BLOCKED = ? AND $HIDDEN = ? AND
|
||||||
|
(
|
||||||
|
$SORT_NAME GLOB ? OR
|
||||||
|
$USERNAME GLOB ? OR
|
||||||
|
$PHONE GLOB ? OR
|
||||||
|
$EMAIL GLOB ?
|
||||||
|
))
|
||||||
|
""".toSingleLine()
|
||||||
|
|
||||||
|
return SqlUtil.Query(subquery, SqlUtil.buildArgs(0, 0, query, query, query, query))
|
||||||
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun queryRecipientsForMentions(inputQuery: String, recipientIds: List<RecipientId>? = null): List<Recipient> {
|
fun queryRecipientsForMentions(inputQuery: String, recipientIds: List<RecipientId>? = null): List<Recipient> {
|
||||||
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
|
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
|
||||||
|
|
|
@ -190,19 +190,10 @@ public class SearchRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<RecipientId> groupsByMemberIds = new LinkedHashSet<>();
|
|
||||||
|
|
||||||
try (GroupTable.Reader reader = SignalDatabase.groups().queryGroupsByMembership(filteredContacts, true, false, false)) {
|
|
||||||
while ((record = reader.getNext()) != null) {
|
|
||||||
groupsByMemberIds.add(record.getRecipientId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LinkedHashSet<ThreadRecord> output = new LinkedHashSet<>();
|
LinkedHashSet<ThreadRecord> output = new LinkedHashSet<>();
|
||||||
|
|
||||||
output.addAll(getMatchingThreads(contactIds, unreadOnly));
|
output.addAll(getMatchingThreads(contactIds, unreadOnly));
|
||||||
output.addAll(getMatchingThreads(groupsByTitleIds, unreadOnly));
|
output.addAll(getMatchingThreads(groupsByTitleIds, unreadOnly));
|
||||||
output.addAll(getMatchingThreads(groupsByMemberIds, unreadOnly));
|
|
||||||
|
|
||||||
return new ArrayList<>(output);
|
return new ArrayList<>(output);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue