kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add "contacts without threads" section to Conversation List Search.
rodzic
09902e5d11
commit
36dfa19aec
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue