mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 19:29:54 +01:00
Add mentions for v2 group chats.
This commit is contained in:
committed by
Greyson Parrelli
parent
0bb9c1d650
commit
b2d4c5d14b
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user