mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 11:20:47 +01:00
Add mentions for v2 group chats.
This commit is contained in:
committed by
Greyson Parrelli
parent
0bb9c1d650
commit
b2d4c5d14b
@@ -30,6 +30,9 @@ import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
@@ -42,7 +45,9 @@ import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
@@ -55,6 +60,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Function3;
|
||||
import org.thoughtcrime.securesms.util.IOFunction;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
@@ -76,6 +82,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Encompasses the entire flow of sending media, starting from the selection process to the actual
|
||||
@@ -130,6 +137,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
private EmojiEditText captionText;
|
||||
private EmojiToggle emojiToggle;
|
||||
private Stub<MediaKeyboard> emojiDrawer;
|
||||
private Stub<View> mentionSuggestions;
|
||||
private TextView charactersLeft;
|
||||
private RecyclerView mediaRail;
|
||||
private MediaRailAdapter mediaRailAdapter;
|
||||
@@ -142,7 +150,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
/**
|
||||
* Get an intent to launch the media send flow starting with the picker.
|
||||
*/
|
||||
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @Nullable String body, @NonNull TransportOption transport) {
|
||||
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @Nullable CharSequence body, @NonNull TransportOption transport) {
|
||||
Intent intent = new Intent(context, MediaSendActivity.class);
|
||||
intent.putExtra(KEY_RECIPIENT, recipient.getId());
|
||||
intent.putExtra(KEY_TRANSPORT, transport);
|
||||
@@ -174,7 +182,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
public static Intent buildEditorIntent(@NonNull Context context,
|
||||
@NonNull List<Media> media,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull String body,
|
||||
@NonNull CharSequence body,
|
||||
@NonNull TransportOption transport)
|
||||
{
|
||||
Intent intent = buildGalleryIntent(context, recipient, body, transport);
|
||||
@@ -207,6 +215,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
charactersLeft = findViewById(R.id.mediasend_characters_left);
|
||||
mediaRail = findViewById(R.id.mediasend_media_rail);
|
||||
emojiDrawer = new Stub<>(findViewById(R.id.mediasend_emoji_drawer_stub));
|
||||
mentionSuggestions = new Stub<>(findViewById(R.id.mediasend_mention_suggestions_stub));
|
||||
|
||||
RecipientId recipientId = getIntent().getParcelableExtra(KEY_RECIPIENT);
|
||||
if (recipientId != null) {
|
||||
@@ -222,7 +231,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
|
||||
viewModel.setTransport(transport);
|
||||
viewModel.setRecipient(recipient != null ? recipient.get() : null);
|
||||
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
|
||||
viewModel.onBodyChanged(getIntent().getCharSequenceExtra(KEY_BODY));
|
||||
|
||||
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
|
||||
boolean isCamera = getIntent().getBooleanExtra(KEY_IS_CAMERA, false);
|
||||
@@ -309,6 +318,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
}
|
||||
|
||||
initViewModel();
|
||||
if (FeatureFlags.mentions()) initializeMentionsViewModel();
|
||||
|
||||
revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled());
|
||||
continueButton.setOnClickListener(v -> navigateToContactSelect());
|
||||
@@ -525,7 +535,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients).observe(this, result -> {
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()).observe(this, result -> {
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
@@ -546,7 +556,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
|
||||
sendButton.setEnabled(false);
|
||||
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList()).observe(this, this::setActivityResultAndFinish);
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList(), composeText.getMentions()).observe(this, this::setActivityResultAndFinish);
|
||||
}
|
||||
|
||||
private static Map<Media, MediaTransform> buildModelsToTransform(@NonNull MediaSendFragment fragment) {
|
||||
@@ -751,6 +761,46 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeMentionsViewModel() {
|
||||
if (recipient == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||
|
||||
recipient.observe(this, mentionsViewModel::onRecipientChange);
|
||||
composeText.setMentionQueryChangedListener(query -> {
|
||||
if (recipient.get().isPushV2Group()) {
|
||||
if (!mentionSuggestions.resolved()) {
|
||||
mentionSuggestions.get();
|
||||
}
|
||||
mentionsViewModel.onQueryChange(query);
|
||||
}
|
||||
});
|
||||
|
||||
composeText.setMentionValidator(annotations -> {
|
||||
if (!recipient.get().isPushV2Group()) {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
Set<String> validRecipientIds = Stream.of(recipient.get().getParticipants())
|
||||
.map(r -> MentionAnnotation.idToMentionAnnotationValue(r.getId()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return Stream.of(annotations)
|
||||
.filter(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.getId());
|
||||
});
|
||||
}
|
||||
|
||||
private void presentRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient == null) {
|
||||
composeText.setHint(R.string.MediaSendActivity_message);
|
||||
@@ -836,9 +886,9 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
|
||||
|
||||
private void presentCharactersRemaining() {
|
||||
String messageBody = composeText.getTextTrimmed();
|
||||
String messageBody = composeText.getTextTrimmed().toString();
|
||||
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
|
||||
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
|
||||
charactersLeft.setText(String.format(Locale.getDefault(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
@@ -24,36 +25,41 @@ public class MediaSendActivityResult implements Parcelable {
|
||||
private final String body;
|
||||
private final TransportOption transport;
|
||||
private final boolean viewOnce;
|
||||
private final Collection<Mention> mentions;
|
||||
|
||||
static @NonNull MediaSendActivityResult forPreUpload(@NonNull Collection<PreUploadResult> uploadResults,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
boolean viewOnce,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
Preconditions.checkArgument(uploadResults.size() > 0, "Must supply uploadResults!");
|
||||
return new MediaSendActivityResult(uploadResults, Collections.emptyList(), body, transport, viewOnce);
|
||||
return new MediaSendActivityResult(uploadResults, Collections.emptyList(), body, transport, viewOnce, mentions);
|
||||
}
|
||||
|
||||
static @NonNull MediaSendActivityResult forTraditionalSend(@NonNull List<Media> nonUploadedMedia,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
boolean viewOnce,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
Preconditions.checkArgument(nonUploadedMedia.size() > 0, "Must supply media!");
|
||||
return new MediaSendActivityResult(Collections.emptyList(), nonUploadedMedia, body, transport, viewOnce);
|
||||
return new MediaSendActivityResult(Collections.emptyList(), nonUploadedMedia, body, transport, viewOnce, mentions);
|
||||
}
|
||||
|
||||
private MediaSendActivityResult(@NonNull Collection<PreUploadResult> uploadResults,
|
||||
@NonNull List<Media> nonUploadedMedia,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
boolean viewOnce,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
this.uploadResults = uploadResults;
|
||||
this.nonUploadedMedia = nonUploadedMedia;
|
||||
this.body = body;
|
||||
this.transport = transport;
|
||||
this.viewOnce = viewOnce;
|
||||
this.mentions = mentions;
|
||||
}
|
||||
|
||||
private MediaSendActivityResult(Parcel in) {
|
||||
@@ -62,6 +68,7 @@ public class MediaSendActivityResult implements Parcelable {
|
||||
this.body = in.readString();
|
||||
this.transport = in.readParcelable(TransportOption.class.getClassLoader());
|
||||
this.viewOnce = ParcelUtil.readBoolean(in);
|
||||
this.mentions = ParcelUtil.readParcelableCollection(in, Mention.class);
|
||||
}
|
||||
|
||||
public boolean isPushPreUpload() {
|
||||
@@ -88,6 +95,10 @@ public class MediaSendActivityResult implements Parcelable {
|
||||
return viewOnce;
|
||||
}
|
||||
|
||||
public @NonNull Collection<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public static final Creator<MediaSendActivityResult> CREATOR = new Creator<MediaSendActivityResult>() {
|
||||
@Override
|
||||
public MediaSendActivityResult createFromParcel(Parcel in) {
|
||||
@@ -112,5 +123,6 @@ public class MediaSendActivityResult implements Parcelable {
|
||||
dest.writeString(body);
|
||||
dest.writeParcelable(transport, 0);
|
||||
ParcelUtil.writeBoolean(dest, viewOnce);
|
||||
ParcelUtil.writeParcelableCollection(dest, mentions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
@@ -453,7 +454,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
savedDrawState.putAll(state);
|
||||
}
|
||||
|
||||
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, MediaTransform> modelsToTransform, @NonNull List<Recipient> recipients) {
|
||||
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, MediaTransform> modelsToTransform, @NonNull List<Recipient> recipients, @NonNull List<Mention> mentions) {
|
||||
if (isSms && recipients.size() > 0) {
|
||||
throw new IllegalStateException("Provided recipients to send to, but this is SMS!");
|
||||
}
|
||||
@@ -476,7 +477,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
|
||||
if (isSms || MessageSender.isLocalSelfSend(application, recipient, isSms)) {
|
||||
Log.i(TAG, "SMS or local self-send. Skipping pre-upload.");
|
||||
result.postValue(MediaSendActivityResult.forTraditionalSend(updatedMedia, trimmedBody, transport, isViewOnce()));
|
||||
result.postValue(MediaSendActivityResult.forTraditionalSend(updatedMedia, trimmedBody, transport, isViewOnce(), mentions));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -493,12 +494,12 @@ class MediaSendViewModel extends ViewModel {
|
||||
uploadRepository.updateDisplayOrder(updatedMedia);
|
||||
uploadRepository.getPreUploadResults(uploadResults -> {
|
||||
if (recipients.size() > 0) {
|
||||
sendMessages(recipients, splitBody, uploadResults);
|
||||
sendMessages(recipients, splitBody, uploadResults, mentions);
|
||||
uploadRepository.deleteAbandonedAttachments();
|
||||
}
|
||||
|
||||
Util.cancelRunnableOnMain(dialogRunnable);
|
||||
result.postValue(MediaSendActivityResult.forPreUpload(uploadResults, splitBody, transport, isViewOnce()));
|
||||
result.postValue(MediaSendActivityResult.forPreUpload(uploadResults, splitBody, transport, isViewOnce(), mentions));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -632,7 +633,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void sendMessages(@NonNull List<Recipient> recipients, @NonNull String body, @NonNull Collection<PreUploadResult> preUploadResults) {
|
||||
private void sendMessages(@NonNull List<Recipient> recipients, @NonNull String body, @NonNull Collection<PreUploadResult> preUploadResults, @NonNull List<Mention> mentions) {
|
||||
List<OutgoingSecureMediaMessage> messages = new ArrayList<>(recipients.size());
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
@@ -647,6 +648,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
mentions,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user