Add "contacts without threads" section to Conversation List Search.

main
Alex Hart 2023-01-26 14:44:55 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 09902e5d11
commit 36dfa19aec
9 zmienionych plików z 252 dodań i 75 usunięć

Wyświetl plik

@ -33,24 +33,22 @@ import org.thoughtcrime.securesms.util.visible
open class ContactSearchAdapter(
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
recipientListener: Listener<ContactSearchData.KnownRecipient>,
storyListener: Listener<ContactSearchData.Story>,
storyContextMenuCallbacks: StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
onClickCallbacks: ClickCallbacks,
storyContextMenuCallbacks: StoryContextMenuCallbacks
) : PagingMappingAdapter<ContactSearchKey>() {
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<ContactSearchData.Story>,
storyListener: OnClickedCallback<ContactSearchData.Story>,
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
) {
mappingAdapter.registerFactory(
@ -63,7 +61,7 @@ open class ContactSearchAdapter(
mappingAdapter: MappingAdapter,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
recipientListener: Listener<ContactSearchData.KnownRecipient>
recipientListener: OnClickedCallback<ContactSearchData.KnownRecipient>
) {
mappingAdapter.registerFactory(
RecipientModel::class.java,
@ -135,7 +133,7 @@ open class ContactSearchAdapter(
private class StoryViewHolder(
itemView: View,
displayCheckBox: Boolean,
onClick: Listener<ContactSearchData.Story>,
onClick: OnClickedCallback<ContactSearchData.Story>,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(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<ContactSearchData.KnownRecipient>
onClick: OnClickedCallback<ContactSearchData.KnownRecipient>
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(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<D>
val onClick: OnClickedCallback<D>
) : MappingViewHolder<T>(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<D : ContactSearchData> {
fun listen(view: View, data: D, isSelected: Boolean)
fun interface OnClickedCallback<D : ContactSearchData> {
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)
}
}

Wyświetl plik

@ -10,6 +10,10 @@ class ContactSearchConfiguration private constructor(
val hasEmptyState: Boolean,
val sections: List<Section>
) {
/**
* 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<ContactSearchData.Story> = 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<String>
) : 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)
*/

Wyświetl plik

@ -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<Pair<List<ContactSearchData>, Set<ContactSearchKey>>> = LiveDataUtil.combineLatest(
@ -168,10 +179,8 @@ class ContactSearchMediator(
fun create(
displayCheckBox: Boolean,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
callbacks: ContactSearchAdapter.ClickCallbacks,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
): PagingMappingAdapter<ContactSearchKey>
}
@ -179,12 +188,10 @@ class ContactSearchMediator(
override fun create(
displayCheckBox: Boolean,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
callbacks: ContactSearchAdapter.ClickCallbacks,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks
): PagingMappingAdapter<ContactSearchKey> {
return ContactSearchAdapter(displayCheckBox, displaySmsTag, recipientListener, storyListener, storyContextMenuCallbacks, expandListener)
return ContactSearchAdapter(displayCheckBox, displaySmsTag, callbacks, storyContextMenuCallbacks)
}
}
}

Wyświetl plik

@ -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<Cursor> {
return if (query.isNullOrEmpty()) {
CursorSearchIterator(null)
} else {
CursorSearchIterator(contactSearchPagedDataSourceRepository.getContactsWithoutThreads(query))
}
}
private fun getRecentsSearchIterator(section: ContactSearchConfiguration.Section.Recents, query: String?): ContactSearchIterator<Cursor> {
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<ContactSearchData> {
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<ContactSearchData> {
val headerMap: Map<RecipientId, String> = 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)
}

Wyświetl plik

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

Wyświetl plik

@ -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();

Wyświetl plik

@ -24,33 +24,28 @@ import java.util.Locale
class ConversationListSearchAdapter(
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
recipientListener: Listener<ContactSearchData.KnownRecipient>,
storyListener: Listener<ContactSearchData.Story>,
onClickedCallbacks: ConversationListSearchClickCallbacks,
storyContextMenuCallbacks: StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit,
threadListener: Listener<ContactSearchData.Thread>,
messageListener: Listener<ContactSearchData.Message>,
groupWithMembersListener: Listener<ContactSearchData.GroupWithMembers>,
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<ContactSearchData.Thread>,
private val threadListener: OnClickedCallback<ContactSearchData.Thread>,
private val lifecycleOwner: LifecycleOwner,
private val glideRequests: GlideRequests,
itemView: View
) : MappingViewHolder<ThreadModel>(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<ContactSearchData.Message>,
private val messageListener: OnClickedCallback<ContactSearchData.Message>,
private val lifecycleOwner: LifecycleOwner,
private val glideRequests: GlideRequests,
itemView: View
) : MappingViewHolder<MessageModel>(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<ContactSearchData.GroupWithMembers>,
private val groupWithMembersListener: OnClickedCallback<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)
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()
}
}

Wyświetl plik

@ -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<RecipientId>? = null): List<Recipient> {
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)

Wyświetl plik

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