Add mentions for v2 group chats.

This commit is contained in:
Cody Henthorne
2020-08-05 16:45:52 -04:00
committed by Greyson Parrelli
parent 0bb9c1d650
commit b2d4c5d14b
90 changed files with 2279 additions and 372 deletions

View File

@@ -41,6 +41,8 @@ import android.provider.Browser;
import android.provider.ContactsContract;
import android.provider.Telephony;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -72,6 +74,7 @@ import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.ViewModelProviders;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
@@ -111,6 +114,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.Reminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
@@ -124,6 +128,7 @@ import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
@@ -136,12 +141,14 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
@@ -196,7 +203,6 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
@@ -221,6 +227,7 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.thoughtcrime.securesms.util.CommunicationActions;
@@ -248,10 +255,12 @@ import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -648,6 +657,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
boolean initiating = threadId == -1;
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
SlideDeck slideDeck = new SlideDeck();
List<Mention> mentions = new ArrayList<>(result.getMentions());
for (Media mediaItem : result.getNonUploadedMedia()) {
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
@@ -669,6 +679,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
quote,
Collections.emptyList(),
Collections.emptyList(),
mentions,
expiresIn,
result.isViewOnce(),
subscriptionId,
@@ -1373,7 +1384,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private ListenableFuture<Boolean> initializeDraft() {
final SettableFuture<Boolean> result = new SettableFuture<>();
final String draftText = getIntent().getStringExtra(TEXT_EXTRA);
final CharSequence draftText = getIntent().getCharSequenceExtra(TEXT_EXTRA);
final Uri draftMedia = getIntent().getData();
final String draftContentType = getIntent().getType();
final MediaType draftMediaType = MediaType.from(draftContentType);
@@ -1437,19 +1448,34 @@ public class ConversationActivity extends PassphraseRequiredActivity
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
SettableFuture<Boolean> future = new SettableFuture<>();
new AsyncTask<Void, Void, List<Draft>>() {
new AsyncTask<Void, Void, Pair<Drafts, CharSequence>>() {
@Override
protected List<Draft> doInBackground(Void... params) {
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
List<Draft> results = draftDatabase.getDrafts(threadId);
protected Pair<Drafts, CharSequence> doInBackground(Void... params) {
Context context = ConversationActivity.this;
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(context);
Drafts results = draftDatabase.getDrafts(threadId);
Draft mentionsDraft = results.getDraftOfType(Draft.MENTION);
Spannable updatedText = null;
if (mentionsDraft != null) {
String text = results.getDraftOfType(Draft.TEXT).getValue();
List<Mention> mentions = MentionUtil.bodyRangeListToMentions(context, Base64.decodeOrThrow(mentionsDraft.getValue()));
UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, text, mentions);
updatedText = new SpannableString(updated.getBody());
MentionAnnotation.setMentionAnnotations(updatedText, updated.getMentions());
}
draftDatabase.clearDrafts(threadId);
return results;
return new Pair<>(results, updatedText);
}
@Override
protected void onPostExecute(List<Draft> drafts) {
protected void onPostExecute(Pair<Drafts, CharSequence> draftsWithUpdatedMentions) {
Drafts drafts = Objects.requireNonNull(draftsWithUpdatedMentions.first());
CharSequence updatedText = draftsWithUpdatedMentions.second();
if (drafts.isEmpty()) {
future.set(false);
updateToggleButtonState();
@@ -1473,7 +1499,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
try {
switch (draft.getType()) {
case Draft.TEXT:
composeText.setText(draft.getValue());
composeText.setText(updatedText == null ? draft.getValue() : updatedText);
listener.onSuccess(true);
break;
case Draft.LOCATION:
@@ -1874,8 +1900,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
recipient.observe(this, mentionsViewModel::onRecipientChange);
composeText.setMentionQueryChangedListener(query -> {
if (getRecipient().isGroup()) {
if (getRecipient().isPushV2Group()) {
if (!mentionsSuggestions.resolved()) {
mentionsSuggestions.get();
}
@@ -1883,12 +1910,26 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
});
composeText.setMentionValidator(annotations -> {
if (!getRecipient().isPushV2Group()) {
return annotations;
}
Set<String> validRecipientIds = Stream.of(getRecipient().getParticipants())
.map(r -> MentionAnnotation.idToMentionAnnotationValue(r.getId()))
.collect(Collectors.toSet());
return Stream.of(annotations)
.filterNot(a -> validRecipientIds.contains(a.getValue()))
.toList();
});
mentionsViewModel.getSelectedRecipient().observe(this, recipient -> {
String replacementDisplayName = recipient.getDisplayName(this);
if (replacementDisplayName.equals(recipient.getDisplayUsername())) {
replacementDisplayName = recipient.getUsername().or(replacementDisplayName);
}
composeText.replaceTextWithMention(replacementDisplayName, recipient.requireUuid());
composeText.replaceTextWithMention(replacementDisplayName, recipient.getId());
});
}
@@ -2073,7 +2114,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
long expiresIn = recipient.get().getExpireMessages() * 1000L;
boolean initiating = threadId == -1;
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), expiresIn, false, subscriptionId, initiating, false);
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, false);
}
private void selectContactInfo(ContactData contactData) {
@@ -2097,7 +2138,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
Drafts drafts = new Drafts();
if (!Util.isEmpty(composeText)) {
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed()));
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed().toString()));
List<Mention> draftMentions = composeText.getMentions();
if (!draftMentions.isEmpty()) {
drafts.add(new Draft(Draft.MENTION, Base64.encodeBytes(MentionUtil.mentionsToBodyRangeList(draftMentions).toByteArray())));
}
}
for (Slide slide : attachmentManager.buildSlideDeck().getSlides()) {
@@ -2187,7 +2232,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private void calculateCharactersRemaining() {
String messageBody = composeText.getTextTrimmed();
String messageBody = composeText.getTextTrimmed().toString();
TransportOption transportOption = sendButton.getSelectedTransport();
CharacterState characterState = transportOption.calculateCharacters(messageBody);
@@ -2270,7 +2315,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private String getMessage() throws InvalidMessageException {
String rawText = composeText.getTextTrimmed();
String rawText = composeText.getTextTrimmed().toString();
if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent())
throw new InvalidMessageException(getString(R.string.ConversationActivity_message_is_empty_exclamation));
@@ -2339,6 +2384,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
recipient.isGroup() ||
recipient.getEmail().isPresent() ||
inputPanel.getQuote().isPresent() ||
composeText.hasMentions() ||
linkPreviewViewModel.hasLinkPreview() ||
needsSplit;
@@ -2369,9 +2415,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void sendMediaMessage(@NonNull MediaSendActivityResult result) {
long expiresIn = recipient.get().getExpireMessages() * 1000L;
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
List<Mention> mentions = new ArrayList<>(result.getMentions());
boolean initiating = threadId == -1;
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList());
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message );
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList(), mentions);
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
ApplicationContext.getInstance(this).getTypingStatusSender().onTypingStopped(threadId);
@@ -2395,7 +2442,18 @@ public class ConversationActivity extends PassphraseRequiredActivity
throws InvalidMessageException
{
Log.i(TAG, "Sending media message...");
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), Collections.emptyList(), linkPreviewViewModel.getActiveLinkPreviews(), expiresIn, viewOnce, subscriptionId, initiating, true);
sendMediaMessage(forceSms,
getMessage(),
attachmentManager.buildSlideDeck(),
inputPanel.getQuote().orNull(),
Collections.emptyList(),
linkPreviewViewModel.getActiveLinkPreviews(),
composeText.getMentions(),
expiresIn,
viewOnce,
subscriptionId,
initiating,
true);
}
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms,
@@ -2404,6 +2462,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
QuoteModel quote,
List<Contact> contacts,
List<LinkPreview> previews,
List<Mention> mentions,
final long expiresIn,
final boolean viewOnce,
final int subscriptionId,
@@ -2424,7 +2483,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
}
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient.get(), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews);
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient.get(), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, mentions);
final SettableFuture<Void> future = new SettableFuture<>();
final Context context = getApplicationContext();
@@ -2543,7 +2602,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void updateLinkPreviewState() {
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
linkPreviewViewModel.onEnabled();
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed().toString(), composeText.getSelectionStart(), composeText.getSelectionEnd());
} else {
linkPreviewViewModel.onUserCancel();
}
@@ -2611,7 +2670,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
sendMediaMessage(forceSms, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, true).addListener(new AssertedSuccessListener<Void>() {
ListenableFuture<Void> sendResult = sendMediaMessage(forceSms,
"",
slideDeck,
inputPanel.getQuote().orNull(),
Collections.emptyList(),
Collections.emptyList(),
composeText.getMentions(),
expiresIn,
false,
subscriptionId,
initiating,
true);
sendResult.addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask<Void, Void, Void>() {
@@ -2700,7 +2772,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void onCursorPositionChanged(int start, int end) {
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), start, end);
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed().toString(), start, end);
}
@Override
@@ -2740,7 +2812,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
slideDeck.addSlide(stickerSlide);
sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
}
private void silentlySetComposeText(String text) {
@@ -2969,7 +3041,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
@Override
public void handleReplyMessage(MessageRecord messageRecord) {
public void handleReplyMessage(ConversationMessage conversationMessage) {
MessageRecord messageRecord = conversationMessage.getMessageRecord();
Recipient author;
if (messageRecord.isOutgoing()) {
@@ -3005,7 +3079,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
inputPanel.setQuote(GlideApp.with(this),
messageRecord.getDateSent(),
author,
messageRecord.getBody(),
conversationMessage.getDisplayBody(this),
slideDeck);
} else {
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
@@ -3019,7 +3093,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
inputPanel.setQuote(GlideApp.with(this),
messageRecord.getDateSent(),
author,
messageRecord.getBody(),
conversationMessage.getDisplayBody(this),
slideDeck);
}
@@ -3186,6 +3260,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
quote,
Collections.emptyList(),
Collections.emptyList(),
composeText.getMentions(),
expiresIn,
false,
subscriptionId,
@@ -3244,7 +3319,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
}
private class QuoteRestorationTask extends AsyncTask<Void, Void, MessageRecord> {
private class QuoteRestorationTask extends AsyncTask<Void, Void, ConversationMessage> {
private final String serialized;
private final SettableFuture<Boolean> future;
@@ -3255,20 +3330,27 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
@Override
protected MessageRecord doInBackground(Void... voids) {
protected ConversationMessage doInBackground(Void... voids) {
QuoteId quoteId = QuoteId.deserialize(ConversationActivity.this, serialized);
if (quoteId != null) {
return DatabaseFactory.getMmsSmsDatabase(getApplicationContext()).getMessageFor(quoteId.getId(), quoteId.getAuthor());
if (quoteId == null) {
return null;
}
return null;
Context context = getApplicationContext();
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteId.getId(), quoteId.getAuthor());
if (messageRecord == null) {
return null;
}
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord);
}
@Override
protected void onPostExecute(MessageRecord messageRecord) {
if (messageRecord != null) {
handleReplyMessage(messageRecord);
protected void onPostExecute(ConversationMessage conversationMessage) {
if (conversationMessage != null) {
handleReplyMessage(conversationMessage);
future.set(true);
} else {
Log.e(TAG, "Failed to restore a quote from a draft. No matching message record.");

View File

@@ -4,14 +4,17 @@ import android.content.Context;
import android.database.ContentObserver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@@ -19,7 +22,11 @@ import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -66,19 +73,24 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
int totalCount = db.getConversationCount(threadId);
int effectiveCount = params.requestedStartPosition;
MentionHelper mentionHelper = new MentionHelper();
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.requestedStartPosition, params.requestedLoadSize))) {
MessageRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
records.add(record);
mentionHelper.add(record);
effectiveCount++;
}
}
mentionHelper.fetchMentions(context);
if (!isInvalid()) {
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
List<ConversationMessage> items = Stream.of(result.getItems())
.map(ConversationMessage::new)
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
.toList();
callback.onResult(items, params.requestedStartPosition, result.getTotal());
@@ -92,24 +104,48 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<ConversationMessage> callback) {
long start = System.currentTimeMillis();
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
List<MessageRecord> records = new ArrayList<>(params.loadSize);
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
List<MessageRecord> records = new ArrayList<>(params.loadSize);
MentionHelper mentionHelper = new MentionHelper();
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.startPosition, params.loadSize))) {
MessageRecord record;
while ((record = reader.getNext()) != null && !isInvalid()) {
records.add(record);
mentionHelper.add(record);
}
}
mentionHelper.fetchMentions(context);
List<ConversationMessage> items = Stream.of(records)
.map(ConversationMessage::new)
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
.toList();
callback.onResult(items);
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
}
private static class MentionHelper {
private Collection<Long> messageIds = new LinkedList<>();
private Map<Long, List<Mention>> messageIdToMentions = new HashMap<>();
void add(MessageRecord record) {
if (record.isMms()) {
messageIds.add(record.getId());
}
}
void fetchMentions(Context context) {
messageIdToMentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessages(messageIds);
}
@Nullable List<Mention> getMentions(long id) {
return messageIdToMentions.get(id);
}
}
static class Factory extends DataSource.Factory<Integer, ConversationMessage> {
private final Context context;

View File

@@ -18,6 +18,8 @@ package org.thoughtcrime.securesms.conversation;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -25,7 +27,7 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.ClipboardManager;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -72,6 +74,7 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@@ -128,7 +131,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -595,33 +597,25 @@ public class ConversationFragment extends LoggingFragment {
}
private void handleCopyMessage(final Set<ConversationMessage> conversationMessages) {
List<MessageRecord> messageList = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).toList();
Collections.sort(messageList, new Comparator<MessageRecord>() {
@Override
public int compare(MessageRecord lhs, MessageRecord rhs) {
if (lhs.getDateReceived() < rhs.getDateReceived()) return -1;
else if (lhs.getDateReceived() == rhs.getDateReceived()) return 0;
else return 1;
}
});
List<ConversationMessage> messageList = new ArrayList<>(conversationMessages);
Collections.sort(messageList, (lhs, rhs) -> Long.compare(lhs.getMessageRecord().getDateReceived(), rhs.getMessageRecord().getDateReceived()));
StringBuilder bodyBuilder = new StringBuilder();
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
SpannableStringBuilder bodyBuilder = new SpannableStringBuilder();
ClipboardManager clipboard = (ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE);
for (MessageRecord messageRecord : messageList) {
String body = messageRecord.getDisplayBody(requireContext()).toString();
for (ConversationMessage message : messageList) {
CharSequence body = message.getDisplayBody(requireContext());
if (!TextUtils.isEmpty(body)) {
bodyBuilder.append(body).append('\n');
if (bodyBuilder.length() > 0) {
bodyBuilder.append('\n');
}
bodyBuilder.append(body);
}
}
if (bodyBuilder.length() > 0 && bodyBuilder.charAt(bodyBuilder.length() - 1) == '\n') {
bodyBuilder.deleteCharAt(bodyBuilder.length() - 1);
if (!TextUtils.isEmpty(bodyBuilder)) {
clipboard.setPrimaryClip(ClipData.newPlainText(null, bodyBuilder));
}
String result = bodyBuilder.toString();
if (!TextUtils.isEmpty(result))
clipboard.setText(result);
}
private void handleDeleteMessages(final Set<ConversationMessage> conversationMessages) {
@@ -746,8 +740,7 @@ public class ConversationFragment extends LoggingFragment {
}
private void handleForwardMessage(ConversationMessage conversationMessage) {
MessageRecord message = conversationMessage.getMessageRecord();
if (message.isViewOnce()) {
if (conversationMessage.getMessageRecord().isViewOnce()) {
throw new AssertionError("Cannot forward a view-once message.");
}
@@ -755,10 +748,10 @@ public class ConversationFragment extends LoggingFragment {
SimpleTask.run(getLifecycle(), () -> {
Intent composeIntent = new Intent(getActivity(), ShareActivity.class);
composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody(requireContext()).toString());
composeIntent.putExtra(Intent.EXTRA_TEXT, conversationMessage.getDisplayBody(requireContext()));
if (message.isMms()) {
MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
if (conversationMessage.getMessageRecord().isMms()) {
MmsMessageRecord mediaMessage = (MmsMessageRecord) conversationMessage.getMessageRecord();
boolean isAlbum = mediaMessage.containsMediaSlide() &&
mediaMessage.getSlideDeck().getSlides().size() > 1 &&
mediaMessage.getSlideDeck().getAudioSlide() == null &&
@@ -788,7 +781,7 @@ public class ConversationFragment extends LoggingFragment {
Optional.fromNullable(attachment.getCaption()),
Optional.absent()));
}
};
}
if (!mediaList.isEmpty()) {
composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
@@ -835,7 +828,7 @@ public class ConversationFragment extends LoggingFragment {
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
}
listener.handleReplyMessage(message.getMessageRecord());
listener.handleReplyMessage(message);
}
private void handleSaveAttachment(final MediaMmsMessageRecord message) {
@@ -875,7 +868,7 @@ public class ConversationFragment extends LoggingFragment {
if (getListAdapter() != null) {
clearHeaderIfNotTyping(getListAdapter());
setLastSeen(0);
getListAdapter().addFastRecord(new ConversationMessage(messageRecord));
getListAdapter().addFastRecord(ConversationMessageFactory.createWithResolvedData(messageRecord, messageRecord.getDisplayBody(requireContext()), message.getMentions()));
list.post(() -> list.scrollToPosition(0));
}
@@ -888,7 +881,7 @@ public class ConversationFragment extends LoggingFragment {
if (getListAdapter() != null) {
clearHeaderIfNotTyping(getListAdapter());
setLastSeen(0);
getListAdapter().addFastRecord(new ConversationMessage(messageRecord));
getListAdapter().addFastRecord(ConversationMessageFactory.createWithResolvedData(messageRecord));
list.post(() -> list.scrollToPosition(0));
}
@@ -1017,7 +1010,7 @@ public class ConversationFragment extends LoggingFragment {
public interface ConversationFragmentListener {
void setThreadId(long threadId);
void handleReplyMessage(MessageRecord messageRecord);
void handleReplyMessage(ConversationMessage conversationMessage);
void onMessageActionToolbarOpened();
void onForwardClicked();
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
@@ -1306,7 +1299,7 @@ public class ConversationFragment extends LoggingFragment {
}
@Override
public void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId) {
public void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId) {
if (getContext() == null) return;
RecipientBottomSheetDialogFragment.create(recipientId, groupId).show(requireFragmentManager(), "BOTTOM");

View File

@@ -26,6 +26,7 @@ import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.net.Uri;
import android.text.Annotation;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@@ -71,6 +72,7 @@ import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.SharedContactView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -552,7 +554,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
} else if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE);
} else {
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty());
Spannable styledText = linkifyMessageBody(conversationMessage.getDisplayBody(getContext()), batchSelected.isEmpty());
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
@@ -855,7 +857,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
contactPhoto.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onGroupMemberAvatarClicked(recipientId, conversationRecipient.get().requireGroupId());
eventListener.onGroupMemberClicked(recipientId, conversationRecipient.get().requireGroupId());
}
});
@@ -879,6 +881,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(messageBody);
for (Annotation annotation : mentionAnnotations) {
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return messageBody;
}
@@ -901,7 +909,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
//noinspection ConstantConditions
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment());
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getDisplayText(), quote.isOriginalMissing(), quote.getAttachment());
quoteView.setVisibility(View.VISIBLE);
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -1405,6 +1413,24 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
}
private class MentionClickableSpan extends ClickableSpan {
private final RecipientId mentionedRecipientId;
MentionClickableSpan(RecipientId mentionedRecipientId) {
this.mentionedRecipientId = mentionedRecipientId;
}
@Override
public void onClick(@NonNull View widget) {
if (eventListener != null && !Recipient.resolved(mentionedRecipientId).isLocalNumber()) {
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
}
}
@Override
public void updateDrawState(@NonNull TextPaint ds) { }
}
private void handleMessageApproval() {
final int title;
final int message;

View File

@@ -1,27 +1,58 @@
package org.thoughtcrime.securesms.conversation;
import androidx.annotation.NonNull;
import android.content.Context;
import android.text.SpannableString;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.util.Conversions;
import java.security.MessageDigest;
import java.util.Collections;
import java.util.List;
/**
* A view level model used to pass arbitrary message related information needed
* for various presentations.
*/
public class ConversationMessage {
private final MessageRecord messageRecord;
@NonNull private final MessageRecord messageRecord;
@NonNull private final List<Mention> mentions;
@Nullable private final SpannableString body;
public ConversationMessage(@NonNull MessageRecord messageRecord) {
private ConversationMessage(@NonNull MessageRecord messageRecord) {
this(messageRecord, null, null);
}
private ConversationMessage(@NonNull MessageRecord messageRecord,
@Nullable CharSequence body,
@Nullable List<Mention> mentions)
{
this.messageRecord = messageRecord;
this.body = body != null ? SpannableString.valueOf(body) : null;
this.mentions = mentions != null ? mentions : Collections.emptyList();
if (!this.mentions.isEmpty() && this.body != null) {
MentionAnnotation.setMentionAnnotations(this.body, this.mentions);
}
}
public @NonNull MessageRecord getMessageRecord() {
return messageRecord;
}
public @NonNull List<Mention> getMentions() {
return mentions;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -41,4 +72,74 @@ public class ConversationMessage {
return Conversions.byteArrayToLong(bytes);
}
public @NonNull SpannableString getDisplayBody(Context context) {
if (mentions.isEmpty() || body == null) {
return messageRecord.getDisplayBody(context);
}
return body;
}
/**
* Factory providing multiple ways of creating {@link ConversationMessage}s.
*/
public static class ConversationMessageFactory {
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord. No database or
* heavy work performed as the message is assumed to not have any mentions.
*/
@AnyThread
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord) {
return new ConversationMessage(messageRecord);
}
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, potentially annotated body, and
* list of actual mentions. No database or heavy work performed as the body and mentions are assumed to be
* fully updated with display names.
*
* @param body Contains appropriate {@link MentionAnnotation}s and is updated with actual profile names.
* @param mentions List of actual mentions (i.e., not placeholder) matching annotation ranges in body.
*/
@AnyThread
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @Nullable List<Mention> mentions) {
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
return new ConversationMessage(messageRecord, body, mentions);
}
return createWithResolvedData(messageRecord);
}
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord and will update and modify the provided
* mentions from placeholder to actual. This method may perform database operations to resolve mentions to display names.
*
* @param mentions List of placeholder mentions to be used to update the body in the provided MessageRecord.
*/
@WorkerThread
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @Nullable List<Mention> mentions) {
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
}
return createWithResolvedData(messageRecord);
}
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, and will query for potential mentions. If mentions
* are found, the body of the provided message will be updated and modified to match actual mentions. This will perform
* database operations to query for mentions and then to resolve mentions to display names.
*/
@WorkerThread
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) {
List<Mention> mentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessage(messageRecord.getId());
if (!mentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
}
}
return createWithResolvedData(messageRecord);
}
}
}

View File

@@ -16,20 +16,24 @@ public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
private final AvatarImageView avatar;
private final TextView name;
private final TextView username;
@Nullable private final MentionEventsListener mentionEventsListener;
public MentionViewHolder(@NonNull View itemView, @Nullable MentionEventsListener mentionEventsListener) {
super(itemView);
this.mentionEventsListener = mentionEventsListener;
avatar = findViewById(R.id.mention_recipient_avatar);
name = findViewById(R.id.mention_recipient_name);
avatar = findViewById(R.id.mention_recipient_avatar);
name = findViewById(R.id.mention_recipient_name);
username = findViewById(R.id.mention_recipient_username);
}
@Override
public void bind(@NonNull MentionViewState model) {
avatar.setRecipient(model.getRecipient());
name.setText(model.getName(context));
username.setText(model.getUsername());
itemView.setOnClickListener(v -> {
if (mentionEventsListener != null) {
mentionEventsListener.onMentionClicked(model.getRecipient());

View File

@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MappingModel;
import org.thoughtcrime.securesms.util.Util;
import java.util.Objects;
@@ -26,6 +27,10 @@ public final class MentionViewState implements MappingModel<MentionViewState> {
return recipient;
}
@NonNull String getUsername() {
return Util.emptyIfNull(recipient.getDisplayUsername());
}
@Override
public boolean areItemsTheSame(@NonNull MentionViewState newItem) {
return recipient.getId().equals(newItem.recipient.getId());

View File

@@ -27,13 +27,15 @@ public class MentionsPickerFragment extends LoggingFragment {
private RecyclerView list;
private BottomSheetBehavior<View> behavior;
private MentionsPickerViewModel viewModel;
private int defaultPeekHeight;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.mentions_picker_fragment, container, false);
list = view.findViewById(R.id.mentions_picker_list);
behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet));
list = view.findViewById(R.id.mentions_picker_list);
behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet));
defaultPeekHeight = view.getContext().getResources().getDimensionPixelSize(R.dimen.mentions_picker_peek_height);
return view;
}
@@ -72,13 +74,16 @@ public class MentionsPickerFragment extends LoggingFragment {
if (mappingModels.isEmpty()) {
updateBottomSheetBehavior(0);
}
list.scrollToPosition(0);
}
private void updateBottomSheetBehavior(int count) {
if (count > 0) {
if (behavior.getPeekHeight() == 0) {
behavior.setPeekHeight(ViewUtil.dpToPx(240), true);
behavior.setPeekHeight(defaultPeekHeight, true);
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
list.scrollToPosition(0);
}
} else {
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);

View File

@@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Collections;
import java.util.List;
final class MentionsPickerRepository {
private final RecipientDatabase recipientDatabase;
MentionsPickerRepository(@NonNull Context context) {
recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
}
@WorkerThread
@NonNull List<Recipient> search(MentionQuery mentionQuery) {
if (TextUtils.isEmpty(mentionQuery.query)) {
return Collections.emptyList();
}
List<RecipientId> recipientIds = Stream.of(mentionQuery.members)
.filterNot(m -> m.getMember().isLocalNumber())
.map(m -> m.getMember().getId())
.toList();
return recipientDatabase.queryRecipientsForMentions(mentionQuery.query, recipientIds);
}
static class MentionQuery {
private final String query;
private final List<GroupMemberEntry.FullMember> members;
MentionQuery(@NonNull String query, @NonNull List<GroupMemberEntry.FullMember> members) {
this.query = query;
this.members = members;
}
}
}

View File

@@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -11,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
@@ -20,7 +19,6 @@ import org.thoughtcrime.securesms.util.MappingModel;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Collections;
import java.util.List;
public class MentionsPickerViewModel extends ViewModel {
@@ -28,17 +26,18 @@ public class MentionsPickerViewModel extends ViewModel {
private final SingleLiveEvent<Recipient> selectedRecipient;
private final LiveData<List<MappingModel<?>>> mentionList;
private final MutableLiveData<LiveGroup> group;
private final MutableLiveData<CharSequence> liveQuery;
private final MutableLiveData<String> liveQuery;
MentionsPickerViewModel() {
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
group = new MutableLiveData<>();
liveQuery = new MutableLiveData<>();
selectedRecipient = new SingleLiveEvent<>();
// TODO [cody] [mentions] simple query support implement for building UI/UX, to be replaced with better search before launch
LiveData<List<FullMember>> members = Transformations.distinctUntilChanged(Transformations.switchMap(group, LiveGroup::getFullMembers));
LiveData<List<FullMember>> fullMembers = Transformations.distinctUntilChanged(Transformations.switchMap(group, LiveGroup::getFullMembers));
LiveData<String> query = Transformations.distinctUntilChanged(liveQuery);
LiveData<MentionQuery> mentionQuery = LiveDataUtil.combineLatest(query, fullMembers, MentionQuery::new);
mentionList = LiveDataUtil.combineLatest(Transformations.distinctUntilChanged(liveQuery), members, this::filterMembers);
mentionList = LiveDataUtil.mapAsync(mentionQuery, q -> Stream.of(mentionsPickerRepository.search(q)).<MappingModel<?>>map(MentionViewState::new).toList());
}
@NonNull LiveData<List<MappingModel<?>>> getMentionList() {
@@ -54,7 +53,7 @@ public class MentionsPickerViewModel extends ViewModel {
}
public void onQueryChange(@NonNull CharSequence query) {
liveQuery.setValue(query);
liveQuery.setValue(query.toString());
}
public void onRecipientChange(@NonNull Recipient recipient) {
@@ -65,22 +64,11 @@ public class MentionsPickerViewModel extends ViewModel {
}
}
private @NonNull List<MappingModel<?>> filterMembers(@NonNull CharSequence query, @NonNull List<FullMember> members) {
if (TextUtils.isEmpty(query)) {
return Collections.emptyList();
}
return Stream.of(members)
.filter(m -> m.getMember().getDisplayName(ApplicationDependencies.getApplication()).toLowerCase().replaceAll("\\s", "").startsWith(query.toString()))
.<MappingModel<?>>map(m -> new MentionViewState(m.getMember()))
.toList();
}
public static final class Factory implements ViewModelProvider.Factory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new MentionsPickerViewModel());
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication())));
}
}
}