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(
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
||||
recipientListener: Listener<ContactSearchData.KnownRecipient>,
|
||||
storyListener: Listener<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit
|
||||
) : PagingMappingAdapter<ContactSearchKey>() {
|
||||
|
||||
init {
|
||||
registerStoryItems(this, displayCheckBox, storyListener, storyContextMenuCallbacks)
|
||||
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, recipientListener)
|
||||
|
@ -49,7 +50,7 @@ open class ContactSearchAdapter(
|
|||
fun registerStoryItems(
|
||||
mappingAdapter: MappingAdapter,
|
||||
displayCheckBox: Boolean = false,
|
||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
||||
storyListener: Listener<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
|
||||
) {
|
||||
mappingAdapter.registerFactory(
|
||||
|
@ -62,7 +63,7 @@ open class ContactSearchAdapter(
|
|||
mappingAdapter: MappingAdapter,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
|
||||
recipientListener: Listener<ContactSearchData.KnownRecipient>
|
||||
) {
|
||||
mappingAdapter.registerFactory(
|
||||
RecipientModel::class.java,
|
||||
|
@ -97,6 +98,7 @@ open class ContactSearchAdapter(
|
|||
is ContactSearchData.Message -> MessageModel(it)
|
||||
is ContactSearchData.Thread -> ThreadModel(it)
|
||||
is ContactSearchData.Empty -> EmptyModel(it)
|
||||
is ContactSearchData.GroupWithMembers -> GroupWithMembersModel(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -133,7 +135,7 @@ open class ContactSearchAdapter(
|
|||
private class StoryViewHolder(
|
||||
itemView: View,
|
||||
displayCheckBox: Boolean,
|
||||
onClick: (View, ContactSearchData.Story, Boolean) -> Unit,
|
||||
onClick: Listener<ContactSearchData.Story>,
|
||||
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
|
||||
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, onClick) {
|
||||
override fun isSelected(model: StoryModel): Boolean = model.isSelected
|
||||
|
@ -265,7 +267,7 @@ open class ContactSearchAdapter(
|
|||
itemView: View,
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
onClick: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
|
||||
onClick: Listener<ContactSearchData.KnownRecipient>
|
||||
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
|
||||
|
||||
private var headerLetter: String? = null
|
||||
|
@ -302,7 +304,7 @@ open class ContactSearchAdapter(
|
|||
itemView: View,
|
||||
private val displayCheckBox: Boolean,
|
||||
private val displaySmsTag: DisplaySmsTag,
|
||||
val onClick: (View, D, Boolean) -> Unit
|
||||
val onClick: Listener<D>
|
||||
) : MappingViewHolder<T>(itemView) {
|
||||
|
||||
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
|
||||
|
@ -316,7 +318,7 @@ open class ContactSearchAdapter(
|
|||
override fun bind(model: T) {
|
||||
checkbox.visible = displayCheckBox
|
||||
checkbox.isChecked = isSelected(model)
|
||||
itemView.setOnClickListener { onClick(avatar, getData(model), isSelected(model)) }
|
||||
itemView.setOnClickListener { onClick.listen(avatar, getData(model), isSelected(model)) }
|
||||
bindLongPress(model)
|
||||
|
||||
if (payload.isNotEmpty()) {
|
||||
|
@ -420,6 +422,15 @@ open class ContactSearchAdapter(
|
|||
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
|
||||
*/
|
||||
|
@ -439,6 +450,7 @@ open class ContactSearchAdapter(
|
|||
ContactSearchConfiguration.SectionKey.GROUP_MEMBERS -> R.string.ContactsCursorLoader_group_members
|
||||
ContactSearchConfiguration.SectionKey.CHATS -> R.string.ContactsCursorLoader__chats
|
||||
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,
|
||||
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
|
||||
) : 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(
|
||||
val isUnreadOnly: Boolean = false,
|
||||
override val includeHeader: Boolean = true,
|
||||
|
@ -119,6 +131,11 @@ class ContactSearchConfiguration private constructor(
|
|||
*/
|
||||
GROUPS,
|
||||
|
||||
/**
|
||||
* Section Key for [Section.GroupsWithMembers]
|
||||
*/
|
||||
GROUPS_WITH_MEMBERS,
|
||||
|
||||
/**
|
||||
* Arbitrary row (think new group button, username row, etc)
|
||||
*/
|
||||
|
@ -207,6 +224,13 @@ class ContactSearchConfiguration private constructor(
|
|||
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
|
||||
}
|
||||
|
||||
fun groupsWithMembers(
|
||||
includeHeader: Boolean = true,
|
||||
expandConfig: ExpandConfig? = null
|
||||
) {
|
||||
addSection(Section.GroupsWithMembers(includeHeader, expandConfig))
|
||||
}
|
||||
|
||||
fun addSection(section: Section)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
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.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.search.MessageResult
|
||||
|
@ -50,6 +51,16 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
|
|||
val threadRecord: ThreadRecord
|
||||
) : 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
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.contacts.paged
|
|||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
||||
|
||||
|
@ -48,6 +49,11 @@ sealed class 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
|
||||
*/
|
||||
|
|
|
@ -168,8 +168,8 @@ class ContactSearchMediator(
|
|||
fun create(
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
||||
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
|
||||
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit
|
||||
): PagingMappingAdapter<ContactSearchKey>
|
||||
|
@ -179,8 +179,8 @@ class ContactSearchMediator(
|
|||
override fun create(
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
|
||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
||||
recipientListener: ContactSearchAdapter.Listener<ContactSearchData.KnownRecipient>,
|
||||
storyListener: ContactSearchAdapter.Listener<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit
|
||||
): PagingMappingAdapter<ContactSearchKey> {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.database.Cursor
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchCollection
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.CursorSearchIterator
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.StoriesSearchCollection
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,6 +149,7 @@ class ContactSearchPagedDataSource(
|
|||
is ContactSearchConfiguration.Section.GroupMembers -> getGroupMembersContactData(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.GroupsWithMembers -> getGroupsWithMembersContactData(section, query, startIndex, endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,6 +172,14 @@ class ContactSearchPagedDataSource(
|
|||
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> {
|
||||
if (!query.isNullOrEmpty()) {
|
||||
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> {
|
||||
return getRecentsSearchIterator(section, query).use { records ->
|
||||
readContactData(
|
||||
|
|
|
@ -84,6 +84,10 @@ open class ContactSearchPagedDataSourceRepository(
|
|||
return SignalDatabase.distributionLists.getAllListsForContactSelectionUiCursor(query, myStoryContainsQuery(query ?: ""))
|
||||
}
|
||||
|
||||
open fun getGroupsWithMembers(query: String): Cursor {
|
||||
return SignalDatabase.groups.queryGroupsByMemberName(query)
|
||||
}
|
||||
|
||||
open fun getRecipientFromDistributionListCursor(cursor: Cursor): Recipient {
|
||||
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, DistributionListTables.RECIPIENT_ID)))
|
||||
}
|
||||
|
|
|
@ -313,11 +313,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
expandListener,
|
||||
(v, t, b) -> {
|
||||
onConversationClicked(t.getThreadRecord());
|
||||
return Unit.INSTANCE;
|
||||
},
|
||||
(v, m, b) -> {
|
||||
onMessageClicked(m.getMessageResult());
|
||||
return Unit.INSTANCE;
|
||||
},
|
||||
(v, m, b) -> {
|
||||
onContactClicked(Recipient.resolved(m.getGroupRecord().getRecipientId()));
|
||||
},
|
||||
getViewLifecycleOwner(),
|
||||
GlideApp.with(this),
|
||||
|
@ -600,12 +601,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
if (TextUtils.isEmpty(state.getQuery())) {
|
||||
return ContactSearchConfiguration.build(b -> Unit.INSTANCE);
|
||||
} else {
|
||||
return ContactSearchConfiguration.build(b -> {
|
||||
return ContactSearchConfiguration.build(builder -> {
|
||||
ConversationFilterRequest conversationFilterRequest = state.getConversationFilterRequest();
|
||||
boolean unreadOnly = conversationFilterRequest != null && conversationFilterRequest.getFilter() == ConversationFilter.UNREAD;
|
||||
|
||||
b.setQuery(state.getQuery());
|
||||
b.addSection(new ContactSearchConfiguration.Section.Chats(
|
||||
builder.setQuery(state.getQuery());
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Chats(
|
||||
unreadOnly,
|
||||
true,
|
||||
new ContactSearchConfiguration.ExpandConfig(
|
||||
|
@ -615,17 +616,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
));
|
||||
|
||||
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
|
||||
|
||||
b.addSection(new ContactSearchConfiguration.Section.Messages(
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Messages(
|
||||
true,
|
||||
null
|
||||
));
|
||||
|
||||
b.setHasEmptyState(true);
|
||||
builder.setHasEmptyState(true);
|
||||
} else {
|
||||
b.arbitrary(
|
||||
builder.arbitrary(
|
||||
conversationFilterRequest.getSource() == ConversationFilterSource.DRAG
|
||||
? ConversationListSearchAdapter.ChatFilterOptions.WITHOUT_TIP.getCode()
|
||||
: ConversationListSearchAdapter.ChatFilterOptions.WITH_TIP.getCode()
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.graphics.Typeface;
|
|||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
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.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
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.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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;
|
||||
|
||||
|
@ -129,6 +140,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
private SearchUtil.StyleFactory searchStyleFactory;
|
||||
|
||||
private LiveData<SpannableString> displayBody;
|
||||
private Disposable joinMembersDisposable = Disposable.empty();
|
||||
|
||||
public ConversationListItem(Context context) {
|
||||
this(context, null);
|
||||
|
@ -219,6 +231,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
|
||||
observeRecipient(lifecycleOwner, thread.getRecipient().live());
|
||||
observeDisplayBody(null, null);
|
||||
joinMembersDisposable.dispose();
|
||||
|
||||
if (highlightSubstring != null) {
|
||||
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());
|
||||
observeDisplayBody(null, null);
|
||||
joinMembersDisposable.dispose();
|
||||
setSubjectViewText(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());
|
||||
observeDisplayBody(null, null);
|
||||
joinMembersDisposable.dispose();
|
||||
setSubjectViewText(null);
|
||||
|
||||
fromView.setText(recipient.get(), false);
|
||||
|
@ -321,6 +336,46 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
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
|
||||
public void unbind() {
|
||||
if (this.recipient != null) {
|
||||
|
@ -330,6 +385,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
}
|
||||
|
||||
observeDisplayBody(null, null);
|
||||
joinMembersDisposable.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -24,12 +24,13 @@ import java.util.Locale
|
|||
class ConversationListSearchAdapter(
|
||||
displayCheckBox: Boolean,
|
||||
displaySmsTag: DisplaySmsTag,
|
||||
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
|
||||
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
|
||||
recipientListener: Listener<ContactSearchData.KnownRecipient>,
|
||||
storyListener: Listener<ContactSearchData.Story>,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit,
|
||||
threadListener: (View, ContactSearchData.Thread, Boolean) -> Unit,
|
||||
messageListener: (View, ContactSearchData.Message, Boolean) -> Unit,
|
||||
threadListener: Listener<ContactSearchData.Thread>,
|
||||
messageListener: Listener<ContactSearchData.Message>,
|
||||
groupWithMembersListener: Listener<ContactSearchData.GroupWithMembers>,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
glideRequests: GlideRequests,
|
||||
clearFilterListener: () -> Unit
|
||||
|
@ -55,6 +56,10 @@ class ConversationListSearchAdapter(
|
|||
EmptyModel::class.java,
|
||||
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(
|
||||
|
@ -70,14 +75,14 @@ class ConversationListSearchAdapter(
|
|||
}
|
||||
|
||||
private class ThreadViewHolder(
|
||||
private val threadListener: (View, ContactSearchData.Thread, Boolean) -> Unit,
|
||||
private val threadListener: Listener<ContactSearchData.Thread>,
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val glideRequests: GlideRequests,
|
||||
itemView: View
|
||||
) : MappingViewHolder<ThreadModel>(itemView) {
|
||||
override fun bind(model: ThreadModel) {
|
||||
itemView.setOnClickListener {
|
||||
threadListener(itemView, model.thread, false)
|
||||
threadListener.listen(itemView, model.thread, false)
|
||||
}
|
||||
|
||||
(itemView as ConversationListItem).bindThread(
|
||||
|
@ -93,14 +98,14 @@ class ConversationListSearchAdapter(
|
|||
}
|
||||
|
||||
private class MessageViewHolder(
|
||||
private val messageListener: (View, ContactSearchData.Message, Boolean) -> Unit,
|
||||
private val messageListener: Listener<ContactSearchData.Message>,
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val glideRequests: GlideRequests,
|
||||
itemView: View
|
||||
) : MappingViewHolder<MessageModel>(itemView) {
|
||||
override fun bind(model: MessageModel) {
|
||||
itemView.setOnClickListener {
|
||||
messageListener(itemView, model.message, false)
|
||||
messageListener.listen(itemView, model.message, false)
|
||||
}
|
||||
|
||||
(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> {
|
||||
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)
|
||||
|
||||
const val MEMBER_GROUP_CONCAT = "member_group_concat"
|
||||
const val THREAD_DATE = "thread_date"
|
||||
|
||||
const val TABLE_NAME = "groups"
|
||||
const val ID = "_id"
|
||||
|
@ -346,6 +347,24 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
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 {
|
||||
val query = getGroupQueryWhereStatement(inputQuery, includeInactive, excludeV1, excludeMms)
|
||||
//language=sql
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.signal.core.util.requireLong
|
|||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
fun queryRecipientsForMentions(inputQuery: String, recipientIds: List<RecipientId>? = null): List<Recipient> {
|
||||
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<>();
|
||||
|
||||
output.addAll(getMatchingThreads(contactIds, unreadOnly));
|
||||
output.addAll(getMatchingThreads(groupsByTitleIds, unreadOnly));
|
||||
output.addAll(getMatchingThreads(groupsByMemberIds, unreadOnly));
|
||||
|
||||
return new ArrayList<>(output);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue