mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-07 18:58:54 +01:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbecd2a2fc | ||
|
|
3772dd40ac | ||
|
|
f69a0f0261 | ||
|
|
cb323ffb84 | ||
|
|
0db73e71a0 | ||
|
|
eeb0c838db | ||
|
|
dc48ee5aed | ||
|
|
c0acfa57a9 | ||
|
|
3e166ef927 | ||
|
|
4942d83de5 | ||
|
|
4c30b39e71 | ||
|
|
e55f4fe6b6 | ||
|
|
aff74cffa0 | ||
|
|
8b29bb8664 | ||
|
|
3cee57b6c2 | ||
|
|
857f4a4fc8 | ||
|
|
a942293a74 | ||
|
|
550b121990 | ||
|
|
cc84901a49 | ||
|
|
9d3764c5d9 | ||
|
|
0950235ccd | ||
|
|
8ed7fc894e | ||
|
|
e504ffa225 | ||
|
|
9c63b37bb4 | ||
|
|
5c110ca359 | ||
|
|
1ab61beeb9 | ||
|
|
8e45a546c9 | ||
|
|
745a7f76ea | ||
|
|
8cb9ab3204 | ||
|
|
12533d1414 | ||
|
|
bd1c164d57 | ||
|
|
7446c2096d | ||
|
|
8ce5c4b885 | ||
|
|
ab76112f5f | ||
|
|
9c54e39eae | ||
|
|
61eab44474 | ||
|
|
f6285ec710 | ||
|
|
ed878ec4b4 | ||
|
|
e38d41d67a | ||
|
|
3d237d72bd | ||
|
|
8044d2390c |
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 679
|
||||
def canonicalVersionName = "4.67.2"
|
||||
def canonicalVersionCode = 683
|
||||
def canonicalVersionName = "4.68.2"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -304,7 +304,7 @@ dependencies {
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.3.1'
|
||||
implementation 'org.signal:ringrtc-android:2.4.1'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
@@ -359,10 +359,10 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.9.5'
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.5'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.5'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.5'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.5'
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.2') {
|
||||
|
||||
@@ -132,7 +132,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNeccessary(this);
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
RegistrationUtil.maybeMarkRegistrationComplete(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
@@ -21,17 +22,17 @@ import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable {
|
||||
void bind(@NonNull MessageRecord messageRecord,
|
||||
void bind(@NonNull ConversationMessage messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseHighlight);
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<ConversationMessage> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseHighlight);
|
||||
|
||||
MessageRecord getMessageRecord();
|
||||
ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
@@ -45,7 +46,7 @@ public interface BindableConversationItem extends Unbindable {
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,9 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {}
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
||||
|
||||
@@ -473,11 +473,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
|
||||
if (onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null)) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
@@ -488,11 +492,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
|
||||
if (onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -624,7 +631,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
void onContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
/** @return True if the contact is allowed to be selected, otherwise false. */
|
||||
boolean onContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,8 +121,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -60,7 +60,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
@@ -92,6 +92,8 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
launch(Recipient.external(this, number));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
|
||||
@@ -2,40 +2,50 @@ package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
import androidx.core.os.BuildCompat;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextUtils.TruncateAt;
|
||||
import android.text.method.QwertyKeyListener;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.util.AttributeSet;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.os.BuildCompat;
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class ComposeText extends EmojiEditText {
|
||||
|
||||
private CharSequence hint;
|
||||
private SpannableString subHint;
|
||||
private CharSequence hint;
|
||||
private SpannableString subHint;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
|
||||
@Nullable private InputPanel.MediaListener mediaListener;
|
||||
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
||||
@Nullable private MentionQueryChangedListener mentionQueryChangedListener;
|
||||
|
||||
public ComposeText(Context context) {
|
||||
super(context);
|
||||
@@ -75,11 +85,33 @@ public class ComposeText extends EmojiEditText {
|
||||
protected void onSelectionChanged(int selStart, int selEnd) {
|
||||
super.onSelectionChanged(selStart, selEnd);
|
||||
|
||||
if (FeatureFlags.mentions()) {
|
||||
if (selStart == selEnd) {
|
||||
doAfterCursorChange();
|
||||
} else {
|
||||
updateQuery("");
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorPositionChangedListener != null) {
|
||||
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (FeatureFlags.mentions() && getText() != null && getLayout() != null) {
|
||||
int checkpoint = canvas.save();
|
||||
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
|
||||
try {
|
||||
mentionRendererDelegate.draw(canvas, getText(), getLayout());
|
||||
} finally {
|
||||
canvas.restoreToCount(checkpoint);
|
||||
}
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
private CharSequence ellipsizeToWidth(CharSequence text) {
|
||||
return TextUtils.ellipsize(text,
|
||||
getPaint(),
|
||||
@@ -119,6 +151,10 @@ public class ComposeText extends EmojiEditText {
|
||||
this.cursorPositionChangedListener = listener;
|
||||
}
|
||||
|
||||
public void setMentionQueryChangedListener(@Nullable MentionQueryChangedListener listener) {
|
||||
this.mentionQueryChangedListener = listener;
|
||||
}
|
||||
|
||||
private boolean isLandscape() {
|
||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
@@ -169,9 +205,89 @@ public class ComposeText extends EmojiEditText {
|
||||
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
|
||||
setImeOptions(getImeOptions() | 16777216);
|
||||
}
|
||||
|
||||
if (FeatureFlags.mentions()) {
|
||||
mentionRendererDelegate = new MentionRendererDelegate(getContext());
|
||||
}
|
||||
}
|
||||
|
||||
private void doAfterCursorChange() {
|
||||
Editable text = getText();
|
||||
if (text != null && enoughToFilter(text)) {
|
||||
performFiltering(text);
|
||||
} else {
|
||||
updateQuery("");
|
||||
}
|
||||
}
|
||||
|
||||
private void performFiltering(@NonNull Editable text) {
|
||||
int end = getSelectionEnd();
|
||||
int start = findQueryStart(text, end);
|
||||
CharSequence query = text.subSequence(start, end);
|
||||
updateQuery(query);
|
||||
}
|
||||
|
||||
private void updateQuery(@NonNull CharSequence query) {
|
||||
if (mentionQueryChangedListener != null) {
|
||||
mentionQueryChangedListener.onQueryChanged(query);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean enoughToFilter(@NonNull Editable text) {
|
||||
int end = getSelectionEnd();
|
||||
if (end < 0) {
|
||||
return false;
|
||||
}
|
||||
return end - findQueryStart(text, end) >= 1;
|
||||
}
|
||||
|
||||
public void replaceTextWithMention(@NonNull String displayName, @NonNull UUID uuid) {
|
||||
Editable text = getText();
|
||||
if (text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearComposingText();
|
||||
|
||||
int end = getSelectionEnd();
|
||||
int start = findQueryStart(text, end) - 1;
|
||||
String original = TextUtils.substring(text, start, end);
|
||||
|
||||
QwertyKeyListener.markAsReplaced(text, start, end, original);
|
||||
text.replace(start, end, createReplacementToken(displayName, uuid));
|
||||
}
|
||||
|
||||
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull UUID uuid) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder("@");
|
||||
if (text instanceof Spanned) {
|
||||
SpannableString spannableString = new SpannableString(text + " ");
|
||||
TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, spannableString, 0);
|
||||
builder.append(spannableString);
|
||||
} else {
|
||||
builder.append(text).append(" ");
|
||||
}
|
||||
|
||||
builder.setSpan(MentionAnnotation.mentionAnnotationForUuid(uuid), 0, builder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private int findQueryStart(@NonNull CharSequence text, int inputCursorPosition) {
|
||||
if (inputCursorPosition == 0) {
|
||||
return inputCursorPosition;
|
||||
}
|
||||
|
||||
int delimiterSearchIndex = inputCursorPosition - 1;
|
||||
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != '@' && text.charAt(delimiterSearchIndex) != ' ')) {
|
||||
delimiterSearchIndex--;
|
||||
}
|
||||
|
||||
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == '@') {
|
||||
return delimiterSearchIndex + 1;
|
||||
}
|
||||
return inputCursorPosition;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2)
|
||||
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
||||
|
||||
private static final String TAG = CommitContentListener.class.getSimpleName();
|
||||
@@ -207,4 +323,8 @@ public class ComposeText extends EmojiEditText {
|
||||
public interface CursorPositionChangedListener {
|
||||
void onCursorPositionChanged(int start, int end);
|
||||
}
|
||||
|
||||
public interface MentionQueryChangedListener {
|
||||
void onQueryChanged(CharSequence query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ public class MaskView extends View {
|
||||
target.getDrawingRect(drawingRect);
|
||||
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
|
||||
|
||||
drawingRect.bottom = Math.min(drawingRect.bottom, getBottom() - getPaddingBottom());
|
||||
drawingRect.top += targetParentTranslationY;
|
||||
drawingRect.bottom += targetParentTranslationY;
|
||||
|
||||
@@ -88,6 +87,7 @@ public class MaskView extends View {
|
||||
|
||||
target.draw(maskCanvas);
|
||||
|
||||
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
||||
canvas.drawBitmap(mask, 0, drawingRect.top, maskPaint);
|
||||
|
||||
mask.recycle();
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
@@ -48,6 +49,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
recentModel.onCodePointSelected(emoji);
|
||||
SignalStore.emojiValues().setPreferredVariation(emoji);
|
||||
|
||||
if (emojiEventListener != null) {
|
||||
emojiEventListener.onEmojiSelected(emoji);
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.PopupWindow;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -27,7 +28,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow {
|
||||
this.listener = listener;
|
||||
this.list = (ViewGroup) getContentView().findViewById(R.id.emoji_variation_container);
|
||||
|
||||
setBackgroundDrawable(null);
|
||||
setBackgroundDrawable(ThemeUtil.getThemedDrawable(context, R.attr.emoji_variation_selector_background));
|
||||
setOutsideTouchable(true);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@@ -13,6 +15,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -73,6 +76,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onCodePointSelected(String emoji) {
|
||||
recentlyUsed.remove(emoji);
|
||||
recentlyUsed.add(emoji);
|
||||
@@ -84,22 +88,16 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
}
|
||||
|
||||
final LinkedHashSet<String> latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(preferenceName, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(preferenceName, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
}
|
||||
|
||||
private String[] toReversePrimitiveArray(@NonNull LinkedHashSet<String> emojiSet) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.components.mention;
|
||||
|
||||
|
||||
import android.text.Annotation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Factory for creating mention annotation spans.
|
||||
*
|
||||
* Note: This wraps creating an Android standard {@link Annotation} so it can leverage the built in
|
||||
* span parceling for copy/paste. Do not extend Annotation or this will be lost.
|
||||
*/
|
||||
public final class MentionAnnotation {
|
||||
|
||||
public static final String MENTION_ANNOTATION = "mention";
|
||||
|
||||
private MentionAnnotation() {
|
||||
}
|
||||
|
||||
public static Annotation mentionAnnotationForUuid(@NonNull UUID uuid) {
|
||||
return new Annotation(MENTION_ANNOTATION, uuid.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.thoughtcrime.securesms.components.mention;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Layout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.LayoutUtil;
|
||||
|
||||
/**
|
||||
* Handles actually drawing the mention backgrounds for a TextView.
|
||||
* <p>
|
||||
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||
*/
|
||||
public abstract class MentionRenderer {
|
||||
|
||||
protected final int horizontalPadding;
|
||||
protected final int verticalPadding;
|
||||
|
||||
public MentionRenderer(int horizontalPadding, int verticalPadding) {
|
||||
this.horizontalPadding = horizontalPadding;
|
||||
this.verticalPadding = verticalPadding;
|
||||
}
|
||||
|
||||
public abstract void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset);
|
||||
|
||||
protected int getLineTop(@NonNull Layout layout, int line) {
|
||||
return LayoutUtil.getLineTopWithoutPadding(layout, line) - verticalPadding;
|
||||
}
|
||||
|
||||
protected int getLineBottom(@NonNull Layout layout, int line) {
|
||||
return LayoutUtil.getLineBottomWithoutPadding(layout, line) + verticalPadding;
|
||||
}
|
||||
|
||||
public static final class SingleLineMentionRenderer extends MentionRenderer {
|
||||
|
||||
private final Drawable drawable;
|
||||
|
||||
public SingleLineMentionRenderer(int horizontalPadding, int verticalPadding, @NonNull Drawable drawable) {
|
||||
super(horizontalPadding, verticalPadding);
|
||||
this.drawable = drawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset) {
|
||||
int lineTop = getLineTop(layout, startLine);
|
||||
int lineBottom = getLineBottom(layout, startLine);
|
||||
int left = Math.min(startOffset, endOffset);
|
||||
int right = Math.max(startOffset, endOffset);
|
||||
|
||||
drawable.setBounds(left, lineTop, right, lineBottom);
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MultiLineMentionRenderer extends MentionRenderer {
|
||||
|
||||
private final Drawable drawableLeft;
|
||||
private final Drawable drawableMid;
|
||||
private final Drawable drawableRight;
|
||||
|
||||
public MultiLineMentionRenderer(int horizontalPadding, int verticalPadding,
|
||||
@NonNull Drawable drawableLeft,
|
||||
@NonNull Drawable drawableMid,
|
||||
@NonNull Drawable drawableRight)
|
||||
{
|
||||
super(horizontalPadding, verticalPadding);
|
||||
this.drawableLeft = drawableLeft;
|
||||
this.drawableMid = drawableMid;
|
||||
this.drawableRight = drawableRight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset) {
|
||||
int paragraphDirection = layout.getParagraphDirection(startLine);
|
||||
|
||||
float lineEndOffset;
|
||||
if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) {
|
||||
lineEndOffset = layout.getLineLeft(startLine) - horizontalPadding;
|
||||
} else {
|
||||
lineEndOffset = layout.getLineRight(startLine) + horizontalPadding;
|
||||
}
|
||||
|
||||
int lineBottom = getLineBottom(layout, startLine);
|
||||
int lineTop = getLineTop(layout, startLine);
|
||||
drawStart(canvas, startOffset, lineTop, (int) lineEndOffset, lineBottom);
|
||||
|
||||
for (int line = startLine + 1; line < endLine; line++) {
|
||||
int left = (int) layout.getLineLeft(line) - horizontalPadding;
|
||||
int right = (int) layout.getLineRight(line) + horizontalPadding;
|
||||
|
||||
lineTop = getLineTop(layout, line);
|
||||
lineBottom = getLineBottom(layout, line);
|
||||
|
||||
drawableMid.setBounds(left, lineTop, right, lineBottom);
|
||||
drawableMid.draw(canvas);
|
||||
}
|
||||
|
||||
float lineStartOffset;
|
||||
if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) {
|
||||
lineStartOffset = layout.getLineRight(startLine) + horizontalPadding;
|
||||
} else {
|
||||
lineStartOffset = layout.getLineLeft(startLine) - horizontalPadding;
|
||||
}
|
||||
|
||||
lineBottom = getLineBottom(layout, endLine);
|
||||
lineTop = getLineTop(layout, endLine);
|
||||
|
||||
drawEnd(canvas, (int) lineStartOffset, lineTop, endOffset, lineBottom);
|
||||
}
|
||||
|
||||
private void drawStart(@NonNull Canvas canvas, int start, int top, int end, int bottom) {
|
||||
if (start > end) {
|
||||
drawableRight.setBounds(end, top, start, bottom);
|
||||
drawableRight.draw(canvas);
|
||||
} else {
|
||||
drawableLeft.setBounds(start, top, end, bottom);
|
||||
drawableLeft.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawEnd(@NonNull Canvas canvas, int start, int top, int end, int bottom) {
|
||||
if (start > end) {
|
||||
drawableLeft.setBounds(end, top, start, bottom);
|
||||
drawableLeft.draw(canvas);
|
||||
} else {
|
||||
drawableRight.setBounds(start, top, end, bottom);
|
||||
drawableRight.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.components.mention;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Annotation;
|
||||
import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* Encapsulates the logic for determining the type of mention rendering needed (single vs multi-line) and then
|
||||
* passing that information to the appropriate {@link MentionRenderer}.
|
||||
* <p></p>
|
||||
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||
*/
|
||||
public class MentionRendererDelegate {
|
||||
|
||||
private final MentionRenderer single;
|
||||
private final MentionRenderer multi;
|
||||
private final int horizontalPadding;
|
||||
|
||||
public MentionRendererDelegate(@NonNull Context context) {
|
||||
//noinspection ConstantConditions
|
||||
this(ViewUtil.dpToPx(2),
|
||||
ViewUtil.dpToPx(2),
|
||||
ContextCompat.getDrawable(context, R.drawable.mention_text_bg),
|
||||
ContextCompat.getDrawable(context, R.drawable.mention_text_bg_left),
|
||||
ContextCompat.getDrawable(context, R.drawable.mention_text_bg_mid),
|
||||
ContextCompat.getDrawable(context, R.drawable.mention_text_bg_right),
|
||||
ThemeUtil.getThemedColor(context, R.attr.conversation_mention_background_color));
|
||||
}
|
||||
|
||||
public MentionRendererDelegate(int horizontalPadding,
|
||||
int verticalPadding,
|
||||
@NonNull Drawable drawable,
|
||||
@NonNull Drawable drawableLeft,
|
||||
@NonNull Drawable drawableMid,
|
||||
@NonNull Drawable drawableEnd,
|
||||
@ColorInt int tint)
|
||||
{
|
||||
this.horizontalPadding = horizontalPadding;
|
||||
single = new MentionRenderer.SingleLineMentionRenderer(horizontalPadding,
|
||||
verticalPadding,
|
||||
DrawableUtil.tint(drawable, tint));
|
||||
multi = new MentionRenderer.MultiLineMentionRenderer(horizontalPadding,
|
||||
verticalPadding,
|
||||
DrawableUtil.tint(drawableLeft, tint),
|
||||
DrawableUtil.tint(drawableMid, tint),
|
||||
DrawableUtil.tint(drawableEnd, tint));
|
||||
}
|
||||
|
||||
public void draw(@NonNull Canvas canvas, @NonNull Spanned text, @NonNull Layout layout) {
|
||||
Annotation[] spans = text.getSpans(0, text.length(), Annotation.class);
|
||||
for (Annotation span : spans) {
|
||||
if (MentionAnnotation.MENTION_ANNOTATION.equals(span.getKey())) {
|
||||
int spanStart = text.getSpanStart(span);
|
||||
int spanEnd = text.getSpanEnd(span);
|
||||
int startLine = layout.getLineForOffset(spanStart);
|
||||
int endLine = layout.getLineForOffset(spanEnd);
|
||||
|
||||
int startOffset = (int) (layout.getPrimaryHorizontal(spanStart) + -1 * layout.getParagraphDirection(startLine) * horizontalPadding);
|
||||
int endOffset = (int) (layout.getPrimaryHorizontal(spanEnd) + layout.getParagraphDirection(endLine) * horizontalPadding);
|
||||
|
||||
MentionRenderer renderer = (startLine == endLine) ? single : multi;
|
||||
renderer.draw(canvas, layout, startLine, endLine, startOffset, endOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,7 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -149,6 +150,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeResult;
|
||||
@@ -158,6 +160,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
@@ -198,6 +201,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -224,6 +228,7 @@ import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
@@ -277,7 +282,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
AttachmentKeyboard.Callback,
|
||||
ConversationReactionOverlay.OnReactionSelectedListener,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
|
||||
SafetyNumberChangeDialog.Callback
|
||||
SafetyNumberChangeDialog.Callback,
|
||||
ReactionsBottomSheetDialogFragment.Callback
|
||||
{
|
||||
|
||||
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
|
||||
@@ -339,6 +345,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private InputPanel inputPanel;
|
||||
private View panelParent;
|
||||
private View noLongerMemberBanner;
|
||||
private Stub<View> mentionsSuggestions;
|
||||
|
||||
private LinkPreviewViewModel linkPreviewViewModel;
|
||||
private ConversationSearchViewModel searchViewModel;
|
||||
@@ -412,6 +419,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
initializeStickerObserver();
|
||||
initializeViewModel();
|
||||
initializeGroupViewModel();
|
||||
if (FeatureFlags.mentions()) initializeMentionsViewModel();
|
||||
initializeEnabledCheck();
|
||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
@@ -504,7 +512,12 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
calculateCharactersRemaining();
|
||||
|
||||
if (recipientSnapshot.getGroupId().isPresent() && recipientSnapshot.getGroupId().get().isV2()) {
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(recipientSnapshot.getGroupId().get().requireV2()));
|
||||
GroupId.V2 groupId = recipientSnapshot.getGroupId().get().requireV2();
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(new RequestGroupV2InfoJob(groupId))
|
||||
.then(new GroupV2UpdateSelfProfileKeyJob(groupId))
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
@@ -1693,6 +1706,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
|
||||
messageRequestBottomView = ViewUtil.findById(this, R.id.conversation_activity_message_request_bottom_bar);
|
||||
reactionOverlay = ViewUtil.findById(this, R.id.conversation_reaction_scrubber);
|
||||
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
|
||||
|
||||
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
|
||||
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
|
||||
@@ -1857,6 +1871,28 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
|
||||
}
|
||||
|
||||
private void initializeMentionsViewModel() {
|
||||
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||
|
||||
recipient.observe(this, mentionsViewModel::onRecipientChange);
|
||||
composeText.setMentionQueryChangedListener(query -> {
|
||||
if (getRecipient().isGroup()) {
|
||||
if (!mentionsSuggestions.resolved()) {
|
||||
mentionsSuggestions.get();
|
||||
}
|
||||
mentionsViewModel.onQueryChange(query);
|
||||
}
|
||||
});
|
||||
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
private void showStickerIntroductionTooltip() {
|
||||
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
|
||||
inputPanel.setMediaKeyboardToggleMode(true);
|
||||
@@ -2729,6 +2765,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
typingTextWatcher.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactionsDialogDismissed() {
|
||||
reactionOverlay.hideMask();
|
||||
}
|
||||
|
||||
// Listeners
|
||||
|
||||
private class QuickCameraToggleListener implements OnClickListener {
|
||||
@@ -2893,7 +2934,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
{
|
||||
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionOverlay.setOnHideListener(onHideListener);
|
||||
reactionOverlay.show(this, maskTarget, messageRecord, panelParent.getMeasuredHeight());
|
||||
reactionOverlay.show(this, maskTarget, messageRecord, inputAreaHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2916,6 +2957,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReactionDetails(@NonNull View maskTarget) {
|
||||
reactionOverlay.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorChanged() {
|
||||
if (!reactionOverlay.isShowing()) {
|
||||
@@ -3013,6 +3059,19 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
updateLinkPreviewState();
|
||||
}
|
||||
|
||||
private int inputAreaHeight() {
|
||||
int height = panelParent.getMeasuredHeight();
|
||||
|
||||
if (attachmentKeyboardStub.resolved()) {
|
||||
View keyboard = attachmentKeyboardStub.get();
|
||||
if (keyboard.getVisibility() == View.VISIBLE) {
|
||||
return height + keyboard.getMeasuredHeight();
|
||||
}
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
private void onMessageRequestDeleteClicked(@NonNull MessageRequestViewModel requestModel) {
|
||||
Recipient recipient = requestModel.getRecipient().getValue();
|
||||
if (recipient == null) {
|
||||
|
||||
@@ -65,8 +65,8 @@ import java.util.Set;
|
||||
* manager, so position 0 is at the bottom of the screen. That's why the "header" is at the bottom,
|
||||
* the "footer" is at the top, and we refer to the "next" record as having a lower index.
|
||||
*/
|
||||
public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
extends PagedListAdapter<MessageRecord, RecyclerView.ViewHolder>
|
||||
public class ConversationAdapter
|
||||
extends PagedListAdapter<ConversationMessage, RecyclerView.ViewHolder>
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>
|
||||
{
|
||||
|
||||
@@ -89,16 +89,16 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
private final Locale locale;
|
||||
private final Recipient recipient;
|
||||
|
||||
private final Set<MessageRecord> selected;
|
||||
private final List<MessageRecord> fastRecords;
|
||||
private final Set<Long> releasedFastRecords;
|
||||
private final Calendar calendar;
|
||||
private final MessageDigest digest;
|
||||
private final Set<ConversationMessage> selected;
|
||||
private final List<ConversationMessage> fastRecords;
|
||||
private final Set<Long> releasedFastRecords;
|
||||
private final Calendar calendar;
|
||||
private final MessageDigest digest;
|
||||
|
||||
private String searchQuery;
|
||||
private MessageRecord recordToPulseHighlight;
|
||||
private View headerView;
|
||||
private View footerView;
|
||||
private String searchQuery;
|
||||
private ConversationMessage recordToPulseHighlight;
|
||||
private View headerView;
|
||||
private View footerView;
|
||||
|
||||
ConversationAdapter(@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@@ -130,7 +130,8 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
return MESSAGE_TYPE_FOOTER;
|
||||
}
|
||||
|
||||
MessageRecord messageRecord = getItem(position);
|
||||
ConversationMessage conversationMessage = getItem(position);
|
||||
MessageRecord messageRecord = (conversationMessage != null) ? conversationMessage.getMessageRecord() : null;
|
||||
|
||||
if (messageRecord == null) {
|
||||
return MESSAGE_TYPE_PLACEHOLDER;
|
||||
@@ -153,16 +154,13 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
return FOOTER_ID;
|
||||
}
|
||||
|
||||
MessageRecord record = getItem(position);
|
||||
ConversationMessage message = getItem(position);
|
||||
|
||||
if (record == null) {
|
||||
if (message == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
String unique = (record.isMms() ? "MMS::" : "SMS::") + record.getId();
|
||||
byte[] bytes = digest.digest(unique.getBytes());
|
||||
|
||||
return Conversions.byteArrayToLong(bytes);
|
||||
return message.getUniqueId(digest);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -175,22 +173,23 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
V itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
|
||||
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
|
||||
BindableConversationItem bindable = (BindableConversationItem) itemView;
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemClick(itemView.getMessageRecord());
|
||||
clickListener.onItemClick(bindable.getConversationMessage());
|
||||
}
|
||||
});
|
||||
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemLongClick(itemView, itemView.getMessageRecord());
|
||||
clickListener.onItemLongClick(itemView, bindable.getConversationMessage());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
itemView.setEventListener(clickListener);
|
||||
bindable.setEventListener(clickListener);
|
||||
|
||||
Log.d(TAG, String.format(Locale.US, "Inflate time: %d ms for View type: %d", System.currentTimeMillis() - start, viewType));
|
||||
return new ConversationViewHolder(itemView);
|
||||
@@ -215,23 +214,23 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder;
|
||||
MessageRecord messageRecord = Objects.requireNonNull(getItem(position));
|
||||
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
||||
int adapterPosition = holder.getAdapterPosition();
|
||||
|
||||
MessageRecord previousRecord = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
||||
MessageRecord nextRecord = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
||||
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
||||
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
||||
|
||||
conversationViewHolder.getView().bind(messageRecord,
|
||||
Optional.fromNullable(previousRecord),
|
||||
Optional.fromNullable(nextRecord),
|
||||
glideRequests,
|
||||
locale,
|
||||
selected,
|
||||
recipient,
|
||||
searchQuery,
|
||||
messageRecord == recordToPulseHighlight);
|
||||
conversationViewHolder.getBindable().bind(conversationMessage,
|
||||
Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null),
|
||||
Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null),
|
||||
glideRequests,
|
||||
locale,
|
||||
selected,
|
||||
recipient,
|
||||
searchQuery,
|
||||
conversationMessage == recordToPulseHighlight);
|
||||
|
||||
if (messageRecord == recordToPulseHighlight) {
|
||||
if (conversationMessage == recordToPulseHighlight) {
|
||||
recordToPulseHighlight = null;
|
||||
}
|
||||
break;
|
||||
@@ -245,13 +244,13 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submitList(@Nullable PagedList<MessageRecord> pagedList) {
|
||||
public void submitList(@Nullable PagedList<ConversationMessage> pagedList) {
|
||||
cleanFastRecords();
|
||||
super.submitList(pagedList);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable MessageRecord getItem(int position) {
|
||||
protected @Nullable ConversationMessage getItem(int position) {
|
||||
position = hasHeader() ? position - 1 : position;
|
||||
|
||||
if (position < fastRecords.size()) {
|
||||
@@ -272,7 +271,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof ConversationViewHolder) {
|
||||
((ConversationViewHolder) holder).getView().unbind();
|
||||
((ConversationViewHolder) holder).getBindable().unbind();
|
||||
} else if (holder instanceof HeaderFooterViewHolder) {
|
||||
((HeaderFooterViewHolder) holder).unbind();
|
||||
}
|
||||
@@ -285,11 +284,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
if (position >= getItemCount()) return -1;
|
||||
if (position < 0) return -1;
|
||||
|
||||
MessageRecord record = getItem(position);
|
||||
ConversationMessage conversationMessage = getItem(position);
|
||||
|
||||
if (record == null) return -1;
|
||||
if (conversationMessage == null) return -1;
|
||||
|
||||
calendar.setTime(new Date(record.getDateSent()));
|
||||
calendar.setTime(new Date(conversationMessage.getMessageRecord().getDateSent()));
|
||||
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
|
||||
}
|
||||
|
||||
@@ -300,8 +299,8 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position) {
|
||||
MessageRecord messageRecord = Objects.requireNonNull(getItem(position));
|
||||
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, messageRecord.getDateReceived()));
|
||||
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
||||
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived()));
|
||||
}
|
||||
|
||||
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
|
||||
@@ -328,12 +327,12 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
if (position >= getItemCount()) return 0;
|
||||
if (position < 0) return 0;
|
||||
|
||||
MessageRecord messageRecord = getItem(position);
|
||||
ConversationMessage conversationMessage = getItem(position);
|
||||
|
||||
if (messageRecord == null || messageRecord.isOutgoing()) {
|
||||
if (conversationMessage == null || conversationMessage.getMessageRecord().isOutgoing()) {
|
||||
return 0;
|
||||
} else {
|
||||
return messageRecord.getDateReceived();
|
||||
return conversationMessage.getMessageRecord().getDateReceived();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,8 +402,8 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
* for a database change.
|
||||
*/
|
||||
@MainThread
|
||||
void addFastRecord(MessageRecord record) {
|
||||
fastRecords.add(0, record);
|
||||
void addFastRecord(ConversationMessage conversationMessage) {
|
||||
fastRecords.add(0, conversationMessage);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -422,7 +421,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
/**
|
||||
* Returns set of records that are selected in multi-select mode.
|
||||
*/
|
||||
Set<MessageRecord> getSelectedItems() {
|
||||
Set<ConversationMessage> getSelectedItems() {
|
||||
return new HashSet<>(selected);
|
||||
}
|
||||
|
||||
@@ -436,11 +435,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
/**
|
||||
* Toggles the selected state of a record in multi-select mode.
|
||||
*/
|
||||
void toggleSelection(MessageRecord record) {
|
||||
if (selected.contains(record)) {
|
||||
selected.remove(record);
|
||||
void toggleSelection(ConversationMessage conversationMessage) {
|
||||
if (selected.contains(conversationMessage)) {
|
||||
selected.remove(conversationMessage);
|
||||
} else {
|
||||
selected.add(record);
|
||||
selected.add(conversationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,11 +463,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
Util.assertMainThread();
|
||||
|
||||
synchronized (releasedFastRecords) {
|
||||
Iterator<MessageRecord> recordIterator = fastRecords.iterator();
|
||||
while (recordIterator.hasNext()) {
|
||||
long id = recordIterator.next().getId();
|
||||
Iterator<ConversationMessage> messageIterator = fastRecords.iterator();
|
||||
while (messageIterator.hasNext()) {
|
||||
long id = messageIterator.next().getMessageRecord().getId();
|
||||
if (releasedFastRecords.contains(id)) {
|
||||
recordIterator.remove();
|
||||
messageIterator.remove();
|
||||
releasedFastRecords.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -510,18 +509,17 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getLastVisibleMessageRecord(int position) {
|
||||
public @Nullable ConversationMessage getLastVisibleConversationMessage(int position) {
|
||||
return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0));
|
||||
}
|
||||
|
||||
static class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationItem> ConversationViewHolder(final @NonNull V itemView) {
|
||||
public ConversationViewHolder(final @NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public <V extends View & BindableConversationItem> V getView() {
|
||||
//noinspection unchecked
|
||||
return (V)itemView;
|
||||
public BindableConversationItem getBindable() {
|
||||
return (BindableConversationItem) itemView;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +528,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
|
||||
StickyHeaderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = ViewUtil.findById(itemView, R.id.text);
|
||||
textView = itemView.findViewById(R.id.text);
|
||||
}
|
||||
|
||||
StickyHeaderViewHolder(TextView textView) {
|
||||
@@ -571,21 +569,21 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiffCallback extends DiffUtil.ItemCallback<MessageRecord> {
|
||||
private static class DiffCallback extends DiffUtil.ItemCallback<ConversationMessage> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull MessageRecord oldItem, @NonNull MessageRecord newItem) {
|
||||
return oldItem.isMms() == newItem.isMms() && oldItem.getId() == newItem.getId();
|
||||
public boolean areItemsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
|
||||
return oldItem.getMessageRecord().isMms() == newItem.getMessageRecord().isMms() && oldItem.getMessageRecord().getId() == newItem.getMessageRecord().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull MessageRecord oldItem, @NonNull MessageRecord newItem) {
|
||||
public boolean areContentsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
|
||||
// Corner rounding is not part of the model, so we can't use this yet
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||
void onItemClick(MessageRecord item);
|
||||
void onItemLongClick(View maskTarget, MessageRecord item);
|
||||
void onItemClick(ConversationMessage item);
|
||||
void onItemLongClick(View maskTarget, ConversationMessage item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import androidx.annotation.NonNull;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.paging.PositionalDataSource;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
|
||||
@@ -24,7 +25,7 @@ import java.util.concurrent.Executor;
|
||||
/**
|
||||
* Core data source for loading an individual conversation.
|
||||
*/
|
||||
class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationDataSource.class);
|
||||
|
||||
@@ -57,7 +58,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<MessageRecord> callback) {
|
||||
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<ConversationMessage> callback) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
@@ -76,14 +77,19 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
if (!isInvalid()) {
|
||||
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
|
||||
|
||||
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
|
||||
}
|
||||
List<ConversationMessage> items = Stream.of(result.getItems())
|
||||
.map(ConversationMessage::new)
|
||||
.toList();
|
||||
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", size: " + params.requestedLoadSize + (isInvalid() ? " -- invalidated" : ""));
|
||||
callback.onResult(items, params.requestedStartPosition, result.getTotal());
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", actualSize: " + result.getItems().size() + ", totalCount: " + result.getTotal());
|
||||
} else {
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", totalCount: " + totalCount + " -- invalidated");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<MessageRecord> callback) {
|
||||
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<ConversationMessage> callback) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
@@ -96,12 +102,15 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
}
|
||||
}
|
||||
|
||||
callback.onResult(records);
|
||||
List<ConversationMessage> items = Stream.of(records)
|
||||
.map(ConversationMessage::new)
|
||||
.toList();
|
||||
callback.onResult(items);
|
||||
|
||||
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
static class Factory extends DataSource.Factory<Integer, MessageRecord> {
|
||||
static class Factory extends DataSource.Factory<Integer, ConversationMessage> {
|
||||
|
||||
private final Context context;
|
||||
private final long threadId;
|
||||
@@ -114,7 +123,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource<Integer, MessageRecord> create() {
|
||||
public @NonNull DataSource<Integer, ConversationMessage> create() {
|
||||
return new ConversationDataSource(context, threadId, invalidator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
@@ -152,6 +153,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
private Locale locale;
|
||||
private RecyclerView list;
|
||||
private RecyclerView.ItemDecoration lastSeenDecoration;
|
||||
private RecyclerView.ItemDecoration stickyHeaderDecoration;
|
||||
private ViewSwitcher topLoadMoreView;
|
||||
private ViewSwitcher bottomLoadMoreView;
|
||||
private ConversationTypingView typingView;
|
||||
@@ -210,8 +212,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
typingView = (ConversationTypingView) inflater.inflate(R.layout.conversation_typing_view, container, false);
|
||||
|
||||
new ConversationItemSwipeCallback(
|
||||
messageRecord -> actionMode == null &&
|
||||
MenuState.canReplyToMessage(MenuState.isActionMessage(messageRecord), messageRecord, messageRequestViewModel.shouldShowMessageRequest()),
|
||||
conversationMessage -> actionMode == null &&
|
||||
MenuState.canReplyToMessage(MenuState.isActionMessage(conversationMessage.getMessageRecord()), conversationMessage.getMessageRecord(), messageRequestViewModel.shouldShowMessageRequest()),
|
||||
this::handleReplyMessage
|
||||
).attachToRecyclerView(list);
|
||||
|
||||
@@ -288,9 +290,9 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
final long lastVisibleMessageTimestamp;
|
||||
if (firstVisiblePosition > 0 && lastVisiblePosition != RecyclerView.NO_POSITION) {
|
||||
MessageRecord message = getListAdapter().getLastVisibleMessageRecord(lastVisiblePosition);
|
||||
ConversationMessage message = getListAdapter().getLastVisibleConversationMessage(lastVisiblePosition);
|
||||
|
||||
lastVisibleMessageTimestamp = message != null ? message.getDateReceived() : 0;
|
||||
lastVisibleMessageTimestamp = message != null ? message.getMessageRecord().getDateReceived() : 0;
|
||||
} else {
|
||||
lastVisibleMessageTimestamp = 0;
|
||||
}
|
||||
@@ -434,7 +436,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
Log.d(TAG, "Initializing adapter for " + recipient.getId());
|
||||
ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
|
||||
setStickyHeaderDecoration(adapter);
|
||||
ConversationAdapter.initializePool(list.getRecycledViewPool());
|
||||
|
||||
adapter.registerAdapterDataObserver(snapToTopDataObserver);
|
||||
@@ -519,14 +521,14 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
private void setCorrectMenuVisibility(@NonNull Menu menu) {
|
||||
Set<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
|
||||
Set<ConversationMessage> messages = getListAdapter().getSelectedItems();
|
||||
|
||||
if (actionMode != null && messageRecords.size() == 0) {
|
||||
if (actionMode != null && messages.size() == 0) {
|
||||
actionMode.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
MenuState menuState = MenuState.getMenuState(messageRecords, messageRequestViewModel.shouldShowMessageRequest());
|
||||
MenuState menuState = MenuState.getMenuState(Stream.of(messages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()), messageRequestViewModel.shouldShowMessageRequest());
|
||||
|
||||
menu.findItem(R.id.menu_context_forward).setVisible(menuState.shouldShowForwardAction());
|
||||
menu.findItem(R.id.menu_context_reply).setVisible(menuState.shouldShowReplyAction());
|
||||
@@ -544,8 +546,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
return (SmoothScrollingLinearLayoutManager) list.getLayoutManager();
|
||||
}
|
||||
|
||||
private MessageRecord getSelectedMessageRecord() {
|
||||
Set<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
|
||||
private ConversationMessage getSelectedConversationMessage() {
|
||||
Set<ConversationMessage> messageRecords = getListAdapter().getSelectedItems();
|
||||
|
||||
if (messageRecords.size() == 1) return messageRecords.iterator().next();
|
||||
else throw new AssertionError();
|
||||
@@ -572,6 +574,15 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public void setStickyHeaderDecoration(@NonNull ConversationAdapter adapter) {
|
||||
if (stickyHeaderDecoration != null) {
|
||||
list.removeItemDecoration(stickyHeaderDecoration);
|
||||
}
|
||||
|
||||
stickyHeaderDecoration = new StickyHeaderDecoration(adapter, false, false);
|
||||
list.addItemDecoration(stickyHeaderDecoration);
|
||||
}
|
||||
|
||||
public void setLastSeen(long lastSeen) {
|
||||
if (lastSeenDecoration != null) {
|
||||
list.removeItemDecoration(lastSeenDecoration);
|
||||
@@ -581,8 +592,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
list.addItemDecoration(lastSeenDecoration);
|
||||
}
|
||||
|
||||
private void handleCopyMessage(final Set<MessageRecord> messageRecords) {
|
||||
List<MessageRecord> messageList = new LinkedList<>(messageRecords);
|
||||
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) {
|
||||
@@ -611,7 +622,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
clipboard.setText(result);
|
||||
}
|
||||
|
||||
private void handleDeleteMessages(final Set<MessageRecord> messageRecords) {
|
||||
private void handleDeleteMessages(final Set<ConversationMessage> conversationMessages) {
|
||||
Set<MessageRecord> messageRecords = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet());
|
||||
if (FeatureFlags.remoteDelete()) {
|
||||
buildRemoteDeleteConfirmationDialog(messageRecords).show();
|
||||
} else {
|
||||
@@ -725,11 +737,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisplayDetails(MessageRecord message) {
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message, recipient.getId(), threadId));
|
||||
private void handleDisplayDetails(ConversationMessage message) {
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message.getMessageRecord(), recipient.getId(), threadId));
|
||||
}
|
||||
|
||||
private void handleForwardMessage(MessageRecord message) {
|
||||
private void handleForwardMessage(ConversationMessage conversationMessage) {
|
||||
MessageRecord message = conversationMessage.getMessageRecord();
|
||||
if (message.isViewOnce()) {
|
||||
throw new AssertionError("Cannot forward a view-once message.");
|
||||
}
|
||||
@@ -812,13 +825,13 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);
|
||||
}
|
||||
|
||||
private void handleReplyMessage(final MessageRecord message) {
|
||||
private void handleReplyMessage(final ConversationMessage message) {
|
||||
if (getActivity() != null) {
|
||||
//noinspection ConstantConditions
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
|
||||
}
|
||||
|
||||
listener.handleReplyMessage(message);
|
||||
listener.handleReplyMessage(message.getMessageRecord());
|
||||
}
|
||||
|
||||
private void handleSaveAttachment(final MediaMmsMessageRecord message) {
|
||||
@@ -858,7 +871,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
if (getListAdapter() != null) {
|
||||
clearHeaderIfNotTyping(getListAdapter());
|
||||
setLastSeen(0);
|
||||
getListAdapter().addFastRecord(messageRecord);
|
||||
getListAdapter().addFastRecord(new ConversationMessage(messageRecord));
|
||||
list.post(() -> list.scrollToPosition(0));
|
||||
}
|
||||
|
||||
@@ -871,7 +884,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
if (getListAdapter() != null) {
|
||||
clearHeaderIfNotTyping(getListAdapter());
|
||||
setLastSeen(0);
|
||||
getListAdapter().addFastRecord(messageRecord);
|
||||
getListAdapter().addFastRecord(new ConversationMessage(messageRecord));
|
||||
list.post(() -> list.scrollToPosition(0));
|
||||
}
|
||||
|
||||
@@ -1011,6 +1024,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
void onCursorChanged();
|
||||
void onListVerticalTranslationChanged(float translationY);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void handleReactionDetails(@NonNull View maskTarget);
|
||||
}
|
||||
|
||||
private class ConversationScrollListener extends OnScrollListener {
|
||||
@@ -1085,9 +1099,9 @@ public class ConversationFragment extends LoggingFragment {
|
||||
private class ConversationFragmentItemClickListener implements ItemClickListener {
|
||||
|
||||
@Override
|
||||
public void onItemClick(MessageRecord messageRecord) {
|
||||
public void onItemClick(ConversationMessage conversationMessage) {
|
||||
if (actionMode != null) {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
if (getListAdapter().getSelectedItems().size() == 0) {
|
||||
@@ -1100,10 +1114,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemLongClick(View maskTarget, MessageRecord messageRecord) {
|
||||
public void onItemLongClick(View maskTarget, ConversationMessage conversationMessage) {
|
||||
|
||||
if (actionMode != null) return;
|
||||
|
||||
MessageRecord messageRecord = conversationMessage.getMessageRecord();
|
||||
|
||||
if (messageRecord.isSecure() &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
!messageRecord.isUpdate() &&
|
||||
@@ -1113,12 +1129,12 @@ public class ConversationFragment extends LoggingFragment {
|
||||
{
|
||||
isReacting = true;
|
||||
list.setLayoutFrozen(true);
|
||||
listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(messageRecord), () -> {
|
||||
listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(conversationMessage), () -> {
|
||||
isReacting = false;
|
||||
list.setLayoutFrozen(false);
|
||||
});
|
||||
} else {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
||||
@@ -1259,9 +1275,10 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactionClicked(long messageId, boolean isMms) {
|
||||
public void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms) {
|
||||
if (getContext() == null) return;
|
||||
|
||||
listener.handleReactionDetails(reactionTarget);
|
||||
ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(requireFragmentManager(), null);
|
||||
}
|
||||
|
||||
@@ -1287,8 +1304,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEnterMultiSelect(@NonNull MessageRecord messageRecord) {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
|
||||
private void handleEnterMultiSelect(@NonNull ConversationMessage conversationMessage) {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
||||
@@ -1342,23 +1359,23 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
private final ConversationMessage conversationMessage;
|
||||
|
||||
private ReactionsToolbarListener(@NonNull MessageRecord messageRecord) {
|
||||
this.messageRecord = messageRecord;
|
||||
private ReactionsToolbarListener(@NonNull ConversationMessage conversationMessage) {
|
||||
this.conversationMessage = conversationMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_info: handleDisplayDetails(messageRecord); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(messageRecord)); return true;
|
||||
case R.id.action_copy: handleCopyMessage(Sets.newHashSet(messageRecord)); return true;
|
||||
case R.id.action_reply: handleReplyMessage(messageRecord); return true;
|
||||
case R.id.action_multiselect: handleEnterMultiSelect(messageRecord); return true;
|
||||
case R.id.action_forward: handleForwardMessage(messageRecord); return true;
|
||||
case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) messageRecord); return true;
|
||||
default: return false;
|
||||
case R.id.action_info: handleDisplayDetails(conversationMessage); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(conversationMessage)); return true;
|
||||
case R.id.action_copy: handleCopyMessage(Sets.newHashSet(conversationMessage)); return true;
|
||||
case R.id.action_reply: handleReplyMessage(conversationMessage); return true;
|
||||
case R.id.action_multiselect: handleEnterMultiSelect(conversationMessage); return true;
|
||||
case R.id.action_forward: handleForwardMessage(conversationMessage); return true;
|
||||
case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord()); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1417,24 +1434,24 @@ public class ConversationFragment extends LoggingFragment {
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_details:
|
||||
handleDisplayDetails(getSelectedMessageRecord());
|
||||
handleDisplayDetails(getSelectedConversationMessage());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_forward:
|
||||
handleForwardMessage(getSelectedMessageRecord());
|
||||
handleForwardMessage(getSelectedConversationMessage());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_resend:
|
||||
handleResendMessage(getSelectedMessageRecord());
|
||||
handleResendMessage(getSelectedConversationMessage().getMessageRecord());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_save_attachment:
|
||||
handleSaveAttachment((MediaMmsMessageRecord)getSelectedMessageRecord());
|
||||
handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_reply:
|
||||
maybeShowSwipeToReplyTooltip();
|
||||
handleReplyMessage(getSelectedMessageRecord());
|
||||
handleReplyMessage(getSelectedConversationMessage());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -139,11 +139,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
private static final Rect SWIPE_RECT = new Rect();
|
||||
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
|
||||
protected ConversationItemBodyBubble bodyBubble;
|
||||
protected View reply;
|
||||
@@ -160,7 +161,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private ViewGroup container;
|
||||
protected ReactionsConversationView reactionsView;
|
||||
|
||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||
private @NonNull Set<ConversationMessage> batchSelected = new HashSet<>();
|
||||
private @NonNull Outliner outliner = new Outliner();
|
||||
private LiveRecipient conversationRecipient;
|
||||
private Stub<ConversationItemThumbnail> mediaThumbnailStub;
|
||||
@@ -234,22 +235,23 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull MessageRecord messageRecord,
|
||||
public void bind(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseHighlight)
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<ConversationMessage> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseHighlight)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
|
||||
|
||||
conversationRecipient = conversationRecipient.resolve();
|
||||
|
||||
this.messageRecord = messageRecord;
|
||||
this.conversationMessage = conversationMessage;
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.locale = locale;
|
||||
this.glideRequests = glideRequests;
|
||||
this.batchSelected = batchSelected;
|
||||
@@ -263,7 +265,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
setGutterSizes(messageRecord, groupThread);
|
||||
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread);
|
||||
setInteractionState(messageRecord, pulseHighlight);
|
||||
setInteractionState(conversationMessage, pulseHighlight);
|
||||
setBodyText(messageRecord, searchQuery);
|
||||
setBubbleState(messageRecord);
|
||||
setStatusIcons(messageRecord);
|
||||
@@ -381,8 +383,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
}
|
||||
|
||||
public MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
public ConversationMessage getConversationMessage() {
|
||||
return conversationMessage;
|
||||
}
|
||||
|
||||
/// MessageRecord Attribute Parsers
|
||||
@@ -424,8 +426,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
}
|
||||
|
||||
private void setInteractionState(MessageRecord messageRecord, boolean pulseHighlight) {
|
||||
if (batchSelected.contains(messageRecord)) {
|
||||
private void setInteractionState(ConversationMessage conversationMessage, boolean pulseHighlight) {
|
||||
if (batchSelected.contains(conversationMessage)) {
|
||||
setBackgroundResource(R.drawable.conversation_item_background);
|
||||
setSelected(true);
|
||||
} else if (pulseHighlight) {
|
||||
@@ -437,19 +439,19 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
|
||||
if (mediaThumbnailStub.resolved()) {
|
||||
mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
|
||||
mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
|
||||
mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
|
||||
mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
|
||||
mediaThumbnailStub.get().setLongClickable(batchSelected.isEmpty());
|
||||
}
|
||||
|
||||
if (audioViewStub.resolved()) {
|
||||
audioViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
|
||||
audioViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
|
||||
audioViewStub.get().setClickable(batchSelected.isEmpty());
|
||||
audioViewStub.get().setEnabled(batchSelected.isEmpty());
|
||||
}
|
||||
|
||||
if (documentViewStub.resolved()) {
|
||||
documentViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
|
||||
documentViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
|
||||
documentViewStub.get().setClickable(batchSelected.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -968,7 +970,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
reactionsView.setOnClickListener(v -> {
|
||||
if (eventListener == null) return;
|
||||
|
||||
eventListener.onReactionClicked(current.getId(), current.isMms());
|
||||
eventListener.onReactionClicked(this, current.getId(), current.isMms());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -114,8 +114,8 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
private void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (cannotSwipeViewHolder(viewHolder)) return;
|
||||
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
MessageRecord messageRecord = item.getMessageRecord();
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
ConversationMessage messageRecord = item.getConversationMessage();
|
||||
|
||||
onSwipeListener.onSwipe(messageRecord);
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
if (!(viewHolder.itemView instanceof ConversationItem)) return true;
|
||||
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord()) ||
|
||||
return !swipeAvailabilityProvider.isSwipeAvailable(item.getConversationMessage()) ||
|
||||
item.disallowSwipe(latestDownX, latestDownY);
|
||||
}
|
||||
|
||||
@@ -192,10 +192,10 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
}
|
||||
|
||||
interface SwipeAvailabilityProvider {
|
||||
boolean isSwipeAvailable(MessageRecord messageRecord);
|
||||
boolean isSwipeAvailable(ConversationMessage conversationMessage);
|
||||
}
|
||||
|
||||
interface OnSwipeListener {
|
||||
void onSwipe(MessageRecord messageRecord);
|
||||
void onSwipe(ConversationMessage conversationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
/**
|
||||
* A view level model used to pass arbitrary message related information needed
|
||||
* for various presentations.
|
||||
*/
|
||||
public class ConversationMessage {
|
||||
private final MessageRecord messageRecord;
|
||||
|
||||
public ConversationMessage(@NonNull MessageRecord messageRecord) {
|
||||
this.messageRecord = messageRecord;
|
||||
}
|
||||
|
||||
public @NonNull MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final ConversationMessage that = (ConversationMessage) o;
|
||||
return messageRecord.equals(that.messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return messageRecord.hashCode();
|
||||
}
|
||||
|
||||
public long getUniqueId(@NonNull MessageDigest digest) {
|
||||
String unique = (messageRecord.isMms() ? "MMS::" : "SMS::") + messageRecord.getId();
|
||||
byte[] bytes = digest.digest(unique.getBytes());
|
||||
|
||||
return Conversions.byteArrayToLong(bytes);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import android.animation.AnimatorSet;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
@@ -19,7 +21,6 @@ import android.widget.RelativeLayout;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
@@ -32,10 +33,11 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.MaskView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -91,6 +93,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
private OnHideListener onHideListener;
|
||||
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet revealMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAllButMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideMaskAnimatorSet = new AnimatorSet();
|
||||
@@ -185,16 +188,31 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
maskView.setTarget(maskTarget);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
toolbar.setVisibility(VISIBLE);
|
||||
setVisibility(View.VISIBLE);
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.activity = activity;
|
||||
originalStatusBarColor = activity.getWindow().getStatusBarColor();
|
||||
activity.getWindow().setStatusBarColor(ContextCompat.getColor(activity, R.color.action_mode_status_bar));
|
||||
activity.getWindow().setStatusBarColor(ThemeUtil.getThemedColor(getContext(), R.attr.reactions_overlay_toolbar_background_color));
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext()) && Build.VERSION.SDK_INT >= 23) {
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
|
||||
maskView.setPadding(0, maskPaddingTop, 0, maskPaddingBottom);
|
||||
maskView.setTarget(maskTarget);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
toolbar.setVisibility(GONE);
|
||||
setVisibility(VISIBLE);
|
||||
revealMaskAnimatorSet.start();
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
maskView.setTarget(null);
|
||||
hideInternal(hideAnimatorSet, onHideListener);
|
||||
@@ -218,8 +236,9 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
revealAnimatorSet.end();
|
||||
hideAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
if (Build.VERSION.SDK_INT >= 23 && activity != null) {
|
||||
activity.getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
@@ -358,7 +377,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
view.setTranslationY(0);
|
||||
|
||||
boolean isAtCustomIndex = i == customEmojiIndex;
|
||||
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && ReactionEmoji.values()[i].emoji.equals(oldEmoji);
|
||||
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && oldEmoji != null && ReactionEmoji.values()[i].emoji.equals(EmojiUtil.getCanonicalRepresentation(oldEmoji));
|
||||
boolean isAtCustomIndexAndOldEmojiExists = isAtCustomIndex && oldEmoji != null;
|
||||
|
||||
if (!foundSelected &&
|
||||
@@ -379,13 +398,13 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
view.setImageEmoji(oldEmoji);
|
||||
view.setTag(oldEmoji);
|
||||
} else {
|
||||
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
|
||||
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
|
||||
}
|
||||
} else if (isAtCustomIndex) {
|
||||
view.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.ic_any_emoji_32));
|
||||
view.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.reactions_overlay_custom_emoji_icon));
|
||||
view.setTag(null);
|
||||
} else {
|
||||
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
|
||||
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,7 +466,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
if (selected == customEmojiIndex) {
|
||||
onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
|
||||
} else {
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, ReactionEmoji.values()[selected].emoji);
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[selected].emoji));
|
||||
}
|
||||
} else {
|
||||
hide();
|
||||
@@ -534,6 +553,9 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
revealAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealAnimatorSet.playTogether(reveals);
|
||||
|
||||
revealMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealMaskAnimatorSet.playTogether(overlayRevealAnim);
|
||||
|
||||
List<Animator> hides = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.text.SpannableString;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
@@ -13,43 +14,55 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class ConversationUpdateItem extends LinearLayout
|
||||
implements RecipientForeverObserver, BindableConversationItem
|
||||
public final class ConversationUpdateItem extends LinearLayout
|
||||
implements RecipientForeverObserver,
|
||||
BindableConversationItem,
|
||||
Observer<SpannableString>
|
||||
{
|
||||
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
|
||||
|
||||
private Set<MessageRecord> batchSelected;
|
||||
private Set<ConversationMessage> batchSelected;
|
||||
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
private TextView body;
|
||||
private TextView date;
|
||||
private LiveRecipient sender;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
private TextView body;
|
||||
private TextView date;
|
||||
private LiveRecipient sender;
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private LiveData<SpannableString> displayBody;
|
||||
|
||||
private final Debouncer bodyClearDebouncer = new Debouncer(150);
|
||||
|
||||
public ConversationUpdateItem(Context context) {
|
||||
super(context);
|
||||
@@ -72,19 +85,19 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull MessageRecord messageRecord,
|
||||
public void bind(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseUpdate)
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<ConversationMessage> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseUpdate)
|
||||
{
|
||||
this.batchSelected = batchSelected;
|
||||
|
||||
bind(messageRecord, locale);
|
||||
bind(conversationMessage, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,46 +112,73 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
public ConversationMessage getConversationMessage() {
|
||||
return conversationMessage;
|
||||
}
|
||||
|
||||
private void bind(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
||||
private void bind(@NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
|
||||
if (this.sender != null) {
|
||||
this.sender.removeForeverObserver(this);
|
||||
}
|
||||
|
||||
if (this.messageRecord != null && messageRecord.isGroupAction()) {
|
||||
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).removeObserver(this);
|
||||
}
|
||||
observeDisplayBody(null);
|
||||
setBodyText(null);
|
||||
|
||||
this.messageRecord = messageRecord;
|
||||
this.sender = messageRecord.getIndividualRecipient().live();
|
||||
this.locale = locale;
|
||||
this.conversationMessage = conversationMessage;
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.sender = messageRecord.getIndividualRecipient().live();
|
||||
this.locale = locale;
|
||||
|
||||
this.sender.observeForever(this);
|
||||
|
||||
if (this.messageRecord != null && messageRecord.isGroupAction()) {
|
||||
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).addObserver(this);
|
||||
}
|
||||
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
|
||||
LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
|
||||
LiveData<SpannableString> spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new);
|
||||
|
||||
present(messageRecord);
|
||||
present(conversationMessage);
|
||||
|
||||
observeDisplayBody(spannableStringMessage);
|
||||
}
|
||||
|
||||
private void present(MessageRecord messageRecord) {
|
||||
if (messageRecord.isGroupAction()) setGroupRecord(messageRecord);
|
||||
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) {
|
||||
if (this.displayBody != displayBody) {
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.removeObserver(this);
|
||||
}
|
||||
|
||||
this.displayBody = displayBody;
|
||||
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.observeForever(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setBodyText(@Nullable CharSequence text) {
|
||||
if (text == null) {
|
||||
bodyClearDebouncer.publish(() -> body.setText(null));
|
||||
} else {
|
||||
bodyClearDebouncer.clear();
|
||||
body.setText(text);
|
||||
body.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void present(ConversationMessage conversationMessage) {
|
||||
MessageRecord messageRecord = conversationMessage.getMessageRecord();
|
||||
if (messageRecord.isGroupAction()) setGroupRecord();
|
||||
else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
|
||||
else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
|
||||
else if (messageRecord.isJoined()) setJoinedRecord();
|
||||
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
|
||||
else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord);
|
||||
else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord);
|
||||
else if (messageRecord.isEndSession()) setEndSessionRecord();
|
||||
else if (messageRecord.isIdentityUpdate()) setIdentityRecord();
|
||||
else if (messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord);
|
||||
else if (messageRecord.isProfileChange()) setProfileNameChangeRecord(messageRecord);
|
||||
else if (messageRecord.isProfileChange()) setProfileNameChangeRecord();
|
||||
else throw new AssertionError("Neither group nor log nor joined.");
|
||||
|
||||
if (batchSelected.contains(messageRecord)) setSelected(true);
|
||||
else setSelected(false);
|
||||
if (batchSelected.contains(conversationMessage)) setSelected(true);
|
||||
else setSelected(false);
|
||||
}
|
||||
|
||||
private void setCallRecord(MessageRecord messageRecord) {
|
||||
@@ -146,11 +186,9 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
|
||||
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
|
||||
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateReceived()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@@ -163,10 +201,8 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
title.setText(ExpirationUtil.getExpirationDisplayValue(getContext(), (int)(messageRecord.getExpiresIn() / 1000)));
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(VISIBLE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
@@ -174,72 +210,56 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
return new PorterDuffColorFilter(ThemeUtil.getThemedColor(getContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
private void setIdentityRecord(final MessageRecord messageRecord) {
|
||||
private void setIdentityRecord() {
|
||||
icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.safety_number_icon));
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setIdentityVerifyUpdate(final MessageRecord messageRecord) {
|
||||
if (messageRecord.isIdentityVerified()) icon.setImageResource(R.drawable.ic_check_white_24dp);
|
||||
else icon.setImageResource(R.drawable.ic_info_outline_white_24dp);
|
||||
else icon.setImageResource(R.drawable.ic_info_outline_white_24);
|
||||
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setProfileNameChangeRecord(MessageRecord messageRecord) {
|
||||
private void setProfileNameChangeRecord() {
|
||||
icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_outline_20));
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setGroupRecord(MessageRecord messageRecord) {
|
||||
private void setGroupRecord() {
|
||||
icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.menu_group_icon));
|
||||
icon.clearColorFilter();
|
||||
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setJoinedRecord(MessageRecord messageRecord) {
|
||||
private void setJoinedRecord() {
|
||||
icon.setImageResource(R.drawable.ic_favorite_grey600_24dp);
|
||||
icon.clearColorFilter();
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setEndSessionRecord(MessageRecord messageRecord) {
|
||||
private void setEndSessionRecord() {
|
||||
icon.setImageResource(R.drawable.ic_refresh_white_24dp);
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
present(messageRecord);
|
||||
present(conversationMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,9 +272,13 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
if (sender != null) {
|
||||
sender.removeForeverObserver(this);
|
||||
}
|
||||
if (this.messageRecord != null && messageRecord.isGroupAction()) {
|
||||
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).removeObserver(this);
|
||||
}
|
||||
|
||||
observeDisplayBody(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(SpannableString update) {
|
||||
setBodyText(update);
|
||||
}
|
||||
|
||||
private class InternalClickListener implements View.OnClickListener {
|
||||
|
||||
@@ -28,14 +28,14 @@ class ConversationViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationViewModel.class);
|
||||
|
||||
private final Application context;
|
||||
private final MediaRepository mediaRepository;
|
||||
private final ConversationRepository conversationRepository;
|
||||
private final MutableLiveData<List<Media>> recentMedia;
|
||||
private final MutableLiveData<Long> threadId;
|
||||
private final LiveData<PagedList<MessageRecord>> messages;
|
||||
private final LiveData<ConversationData> conversationMetadata;
|
||||
private final Invalidator invalidator;
|
||||
private final Application context;
|
||||
private final MediaRepository mediaRepository;
|
||||
private final ConversationRepository conversationRepository;
|
||||
private final MutableLiveData<List<Media>> recentMedia;
|
||||
private final MutableLiveData<Long> threadId;
|
||||
private final LiveData<PagedList<ConversationMessage>> messages;
|
||||
private final LiveData<ConversationData> conversationMetadata;
|
||||
private final Invalidator invalidator;
|
||||
|
||||
private int jumpToPosition;
|
||||
|
||||
@@ -55,12 +55,12 @@ class ConversationViewModel extends ViewModel {
|
||||
return conversationData;
|
||||
});
|
||||
|
||||
LiveData<Pair<Long, PagedList<MessageRecord>>> messagesForThreadId = Transformations.switchMap(metadata, data -> {
|
||||
DataSource.Factory<Integer, MessageRecord> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator);
|
||||
PagedList.Config config = new PagedList.Config.Builder()
|
||||
.setPageSize(25)
|
||||
.setInitialLoadSizeHint(25)
|
||||
.build();
|
||||
LiveData<Pair<Long, PagedList<ConversationMessage>>> messagesForThreadId = Transformations.switchMap(metadata, data -> {
|
||||
DataSource.Factory<Integer, ConversationMessage> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator);
|
||||
PagedList.Config config = new PagedList.Config.Builder()
|
||||
.setPageSize(25)
|
||||
.setInitialLoadSizeHint(25)
|
||||
.build();
|
||||
|
||||
final int startPosition;
|
||||
if (data.shouldJumpToMessage()) {
|
||||
@@ -109,7 +109,7 @@ class ConversationViewModel extends ViewModel {
|
||||
return conversationMetadata;
|
||||
}
|
||||
|
||||
@NonNull LiveData<PagedList<MessageRecord>> getMessages() {
|
||||
@NonNull LiveData<PagedList<ConversationMessage>> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ final class SafetyNumberChangeRepository {
|
||||
try {
|
||||
switch (messageType) {
|
||||
case MmsSmsDatabase.SMS_TRANSPORT:
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
case MmsSmsDatabase.MMS_TRANSPORT:
|
||||
return DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView name;
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull MentionViewState model) {
|
||||
avatar.setRecipient(model.getRecipient());
|
||||
name.setText(model.getName(context));
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (mentionEventsListener != null) {
|
||||
mentionEventsListener.onMentionClicked(model.getRecipient());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface MentionEventsListener {
|
||||
void onMentionClicked(@NonNull Recipient recipient);
|
||||
}
|
||||
|
||||
public static MappingAdapter.Factory<MentionViewState> createFactory(@Nullable MentionEventsListener mentionEventsListener) {
|
||||
return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_recipient_list_item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class MentionViewState implements MappingModel<MentionViewState> {
|
||||
|
||||
private final Recipient recipient;
|
||||
|
||||
public MentionViewState(@NonNull Recipient recipient) {
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
@NonNull String getName(@NonNull Context context) {
|
||||
return recipient.getDisplayName(context);
|
||||
}
|
||||
|
||||
@NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull MentionViewState newItem) {
|
||||
return recipient.getId().equals(newItem.recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull MentionViewState newItem) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
return recipient.getDisplayName(context).equals(newItem.recipient.getDisplayName(context)) &&
|
||||
Objects.equals(recipient.getProfileAvatar(), newItem.recipient.getProfileAvatar());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewHolder.MentionEventsListener;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
|
||||
public class MentionsPickerAdapter extends MappingAdapter {
|
||||
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener) {
|
||||
registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MentionsPickerFragment extends LoggingFragment {
|
||||
|
||||
private MentionsPickerAdapter adapter;
|
||||
private RecyclerView list;
|
||||
private BottomSheetBehavior<View> behavior;
|
||||
private MentionsPickerViewModel viewModel;
|
||||
|
||||
@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));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
initializeList();
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(MentionsPickerViewModel.class);
|
||||
viewModel.getMentionList().observe(getViewLifecycleOwner(), this::updateList);
|
||||
}
|
||||
|
||||
private void initializeList() {
|
||||
adapter = new MentionsPickerAdapter(this::handleMentionClicked);
|
||||
|
||||
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(requireContext()) {
|
||||
@Override
|
||||
public void onLayoutCompleted(RecyclerView.State state) {
|
||||
super.onLayoutCompleted(state);
|
||||
updateBottomSheetBehavior(adapter.getItemCount());
|
||||
}
|
||||
};
|
||||
|
||||
list.setLayoutManager(layoutManager);
|
||||
list.setAdapter(adapter);
|
||||
list.setItemAnimator(null);
|
||||
}
|
||||
|
||||
private void handleMentionClicked(@NonNull Recipient recipient) {
|
||||
viewModel.onSelectionChange(recipient);
|
||||
}
|
||||
|
||||
private void updateList(@NonNull List<MappingModel<?>> mappingModels) {
|
||||
adapter.submitList(mappingModels);
|
||||
if (mappingModels.isEmpty()) {
|
||||
updateBottomSheetBehavior(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBottomSheetBehavior(int count) {
|
||||
if (count > 0) {
|
||||
if (behavior.getPeekHeight() == 0) {
|
||||
behavior.setPeekHeight(ViewUtil.dpToPx(240), true);
|
||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
} else {
|
||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
behavior.setPeekHeight(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry.FullMember;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
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 {
|
||||
|
||||
private final SingleLiveEvent<Recipient> selectedRecipient;
|
||||
private final LiveData<List<MappingModel<?>>> mentionList;
|
||||
private final MutableLiveData<LiveGroup> group;
|
||||
private final MutableLiveData<CharSequence> liveQuery;
|
||||
|
||||
MentionsPickerViewModel() {
|
||||
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));
|
||||
|
||||
mentionList = LiveDataUtil.combineLatest(Transformations.distinctUntilChanged(liveQuery), members, this::filterMembers);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<MappingModel<?>>> getMentionList() {
|
||||
return mentionList;
|
||||
}
|
||||
|
||||
void onSelectionChange(@NonNull Recipient recipient) {
|
||||
selectedRecipient.setValue(recipient);
|
||||
}
|
||||
|
||||
public @NonNull LiveData<Recipient> getSelectedRecipient() {
|
||||
return selectedRecipient;
|
||||
}
|
||||
|
||||
public void onQueryChange(@NonNull CharSequence query) {
|
||||
liveQuery.setValue(query);
|
||||
}
|
||||
|
||||
public void onRecipientChange(@NonNull Recipient recipient) {
|
||||
GroupId groupId = recipient.getGroupId().orNull();
|
||||
if (groupId != null) {
|
||||
LiveGroup liveGroup = new LiveGroup(groupId);
|
||||
group.setValue(liveGroup);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
|
||||
@@ -84,11 +85,11 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
||||
|
||||
if (!isInvalid()) {
|
||||
SizeFixResult<Conversation> result = SizeFixResult.ensureMultipleOfPageSize(conversations, params.requestedStartPosition, params.pageSize, totalCount);
|
||||
|
||||
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", actualSize: " + result.getItems().size() + ", totalCount: " + result.getTotal() + ", class: " + getClass().getSimpleName());
|
||||
} else {
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", totalCount: " + totalCount + ", class: " + getClass().getSimpleName() + " -- invalidated");
|
||||
}
|
||||
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", size: " + params.requestedLoadSize + ", totalCount: " + totalCount + ", class: " + getClass().getSimpleName() + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -500,9 +500,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class);
|
||||
|
||||
viewModel.getSearchResult().observe(this, this::onSearchResultChanged);
|
||||
viewModel.getMegaphone().observe(this, this::onMegaphoneChanged);
|
||||
viewModel.getConversationList().observe(this, this::onSubmitList);
|
||||
viewModel.getSearchResult().observe(getViewLifecycleOwner(), this::onSearchResultChanged);
|
||||
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
|
||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList);
|
||||
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
||||
@Override
|
||||
@@ -751,6 +751,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
|
||||
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
|
||||
if (conversationList.isEmpty()) {
|
||||
Log.i(TAG, "Received an empty data set.");
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.VISIBLE);
|
||||
emptyImage.setImageResource(EMPTY_IMAGES[(int) (Math.random() * EMPTY_IMAGES.length)]);
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.content.res.ColorStateList;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.StyleSpan;
|
||||
@@ -32,6 +31,9 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -42,42 +44,49 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConversationListItem extends RelativeLayout
|
||||
implements RecipientForeverObserver,
|
||||
BindableConversationListItem, Unbindable
|
||||
import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync;
|
||||
|
||||
public final class ConversationListItem extends RelativeLayout
|
||||
implements RecipientForeverObserver,
|
||||
BindableConversationListItem,
|
||||
Unbindable,
|
||||
Observer<SpannableString>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = ConversationListItem.class.getSimpleName();
|
||||
private final static String TAG = Log.tag(ConversationListItem.class);
|
||||
|
||||
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
||||
|
||||
private static final int MAX_SNIPPET_LENGTH = 500;
|
||||
|
||||
private Set<Long> selectedThreads;
|
||||
private Set<Long> typingThreads;
|
||||
private LiveRecipient recipient;
|
||||
private LiveRecipient groupAddedBy;
|
||||
private long threadId;
|
||||
private GlideRequests glideRequests;
|
||||
private View subjectContainer;
|
||||
@@ -97,13 +106,9 @@ public class ConversationListItem extends RelativeLayout
|
||||
private AvatarImageView contactPhotoImage;
|
||||
private ThumbnailView thumbnailView;
|
||||
|
||||
private int distributionType;
|
||||
private final Debouncer subjectViewClearDebouncer = new Debouncer(150);
|
||||
|
||||
private final RecipientForeverObserver groupAddedByObserver = adder -> {
|
||||
if (isAttachedToWindow() && subjectView != null && thread != null) {
|
||||
subjectView.setText(getThreadDisplayBody(getContext(), thread));
|
||||
}
|
||||
};
|
||||
private LiveData<SpannableString> displayBody;
|
||||
|
||||
public ConversationListItem(Context context) {
|
||||
this(context, null);
|
||||
@@ -153,16 +158,16 @@ public class ConversationListItem extends RelativeLayout
|
||||
@Nullable String highlightSubstring)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
|
||||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = selectedThreads;
|
||||
this.recipient = thread.getRecipient().live();
|
||||
this.threadId = thread.getThreadId();
|
||||
this.glideRequests = glideRequests;
|
||||
this.unreadCount = thread.getUnreadCount();
|
||||
this.distributionType = thread.getDistributionType();
|
||||
this.lastSeen = thread.getLastSeen();
|
||||
this.thread = thread;
|
||||
this.selectedThreads = selectedThreads;
|
||||
this.recipient = thread.getRecipient().live();
|
||||
this.threadId = thread.getThreadId();
|
||||
this.glideRequests = glideRequests;
|
||||
this.unreadCount = thread.getUnreadCount();
|
||||
this.lastSeen = thread.getLastSeen();
|
||||
this.thread = thread;
|
||||
|
||||
this.recipient.observeForever(this);
|
||||
if (highlightSubstring != null) {
|
||||
@@ -173,14 +178,10 @@ public class ConversationListItem extends RelativeLayout
|
||||
this.fromView.setText(recipient.get(), thread.isRead());
|
||||
}
|
||||
|
||||
this.typingThreads = typingThreads;
|
||||
updateTypingIndicator(typingThreads);
|
||||
|
||||
this.subjectView.setText(getTrimmedSnippet(getThreadDisplayBody(getContext(), thread)));
|
||||
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
groupAddedBy = Recipient.live(thread.getGroupAddedBy());
|
||||
groupAddedBy.observeForever(groupAddedByObserver);
|
||||
}
|
||||
observeDisplayBody(getThreadDisplayBody(getContext(), thread));
|
||||
|
||||
this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
|
||||
this.subjectView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)
|
||||
@@ -214,7 +215,8 @@ public class ConversationListItem extends RelativeLayout
|
||||
@Nullable String highlightSubstring)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
|
||||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
this.recipient = contact.live();
|
||||
@@ -243,7 +245,8 @@ public class ConversationListItem extends RelativeLayout
|
||||
@Nullable String highlightSubstring)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
|
||||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
this.recipient = messageResult.conversationRecipient.live();
|
||||
@@ -275,10 +278,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
|
||||
}
|
||||
|
||||
if (this.groupAddedBy != null) {
|
||||
this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
|
||||
this.groupAddedBy = null;
|
||||
}
|
||||
observeDisplayBody(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -318,17 +318,30 @@ public class ConversationListItem extends RelativeLayout
|
||||
return unreadCount;
|
||||
}
|
||||
|
||||
public int getDistributionType() {
|
||||
return distributionType;
|
||||
}
|
||||
|
||||
public long getLastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
private static @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) {
|
||||
return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet
|
||||
: snippet.subSequence(0, MAX_SNIPPET_LENGTH);
|
||||
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) {
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.removeObserver(this);
|
||||
}
|
||||
|
||||
this.displayBody = displayBody;
|
||||
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.observeForever(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSubjectViewText(@Nullable CharSequence text) {
|
||||
if (text == null) {
|
||||
subjectViewClearDebouncer.publish(() -> subjectView.setText(null));
|
||||
} else {
|
||||
subjectViewClearDebouncer.clear();
|
||||
subjectView.setText(text);
|
||||
subjectView.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setThumbnailSnippet(ThreadRecord thread) {
|
||||
@@ -372,7 +385,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
private void setRippleColor(Recipient recipient) {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
||||
if (VERSION.SDK_INT >= 21) {
|
||||
((RippleDrawable)(getBackground()).mutate())
|
||||
.setColor(ColorStateList.valueOf(recipient.getColor().toConversationColor(getContext())));
|
||||
}
|
||||
@@ -395,16 +408,20 @@ public class ConversationListItem extends RelativeLayout
|
||||
setRippleColor(recipient);
|
||||
}
|
||||
|
||||
|
||||
private static SpannableString getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
|
||||
private static @NonNull LiveData<SpannableString> getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
return emphasisAdded(context.getString(thread.isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
|
||||
: R.string.ThreadRecord_s_added_you_to_the_group,
|
||||
Recipient.live(thread.getGroupAddedBy()).get().getDisplayName(context)));
|
||||
return emphasisAdded(recipientToStringAsync(thread.getGroupAddedBy(),
|
||||
r -> context.getString(thread.isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
|
||||
: R.string.ThreadRecord_s_added_you_to_the_group,
|
||||
r.getDisplayName(context))));
|
||||
} else if (!thread.isMessageRequestAccepted()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_message_request));
|
||||
} else if (SmsDatabase.Types.isGroupUpdate(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
if (thread.getRecipient().isPushV2Group()) {
|
||||
return emphasisAdded(MessageRecord.getGv2ChangeDescription(context, thread.getBody()));
|
||||
} else {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
}
|
||||
} else if (SmsDatabase.Types.isGroupQuit(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
|
||||
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
|
||||
@@ -419,15 +436,15 @@ public class ConversationListItem extends RelativeLayout
|
||||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
} else if (MmsSmsColumns.Types.isDraftMessageType(thread.getType())) {
|
||||
String draftText = context.getString(R.string.ThreadRecord_draft);
|
||||
return emphasisAdded(draftText + " " + thread.getBody(), 0, draftText.length());
|
||||
return emphasisAdded(draftText + " " + thread.getBody());
|
||||
} else if (SmsDatabase.Types.isOutgoingCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_called));
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_called));
|
||||
} else if (SmsDatabase.Types.isIncomingCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_called_you));
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_called_you));
|
||||
} else if (SmsDatabase.Types.isMissedCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_missed_call));
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_missed_call));
|
||||
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, thread.getRecipient().getDisplayName(context)));
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context))));
|
||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
|
||||
int seconds = (int)(thread.getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
@@ -439,7 +456,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
if (thread.getRecipient().isGroup()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
|
||||
} else {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, thread.getRecipient().getDisplayName(context)));
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context))));
|
||||
}
|
||||
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
@@ -450,26 +467,44 @@ public class ConversationListItem extends RelativeLayout
|
||||
} else {
|
||||
ThreadDatabase.Extra extra = thread.getExtra();
|
||||
if (extra != null && extra.isViewOnce()) {
|
||||
return new SpannableString(emphasisAdded(getViewOnceDescription(context, thread.getContentType())));
|
||||
return emphasisAdded(getViewOnceDescription(context, thread.getContentType()));
|
||||
} else if (extra != null && extra.isRemoteDelete()) {
|
||||
return new SpannableString(emphasisAdded(context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted)));
|
||||
return emphasisAdded(context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted));
|
||||
} else {
|
||||
return new SpannableString(Util.emptyIfNull(thread.getBody()));
|
||||
return LiveDataUtil.just(new SpannableString(removeNewlines(thread.getBody())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull SpannableString emphasisAdded(String sequence) {
|
||||
return emphasisAdded(sequence, 0, sequence.length());
|
||||
private static @NonNull String removeNewlines(@Nullable String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (text.indexOf('\n') >= 0) {
|
||||
return text.replaceAll("\n", " ");
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull SpannableString emphasisAdded(String sequence, int start, int end) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
||||
start,
|
||||
end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull String string) {
|
||||
return emphasisAdded(UpdateDescription.staticDescription(string));
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull UpdateDescription description) {
|
||||
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(description));
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<String> description) {
|
||||
return Transformations.map(description, sequence -> {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(Typeface.ITALIC),
|
||||
0,
|
||||
sequence.length(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
});
|
||||
}
|
||||
|
||||
private static String getViewOnceDescription(@NonNull Context context, @Nullable String contentType) {
|
||||
@@ -482,6 +517,15 @@ public class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(SpannableString spannableString) {
|
||||
setSubjectViewText(spannableString);
|
||||
|
||||
if (typingThreads != null) {
|
||||
updateTypingIndicator(typingThreads);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThumbnailPositioner implements Runnable {
|
||||
|
||||
private final View thumbnailView;
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.text.TextUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.paging.DataSource;
|
||||
@@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
@@ -31,10 +33,11 @@ import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
|
||||
class ConversationListViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationListViewModel.class);
|
||||
|
||||
private final Application application;
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final MutableLiveData<Integer> archivedCount;
|
||||
private final LiveData<ConversationList> conversationList;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
@@ -48,7 +51,6 @@ class ConversationListViewModel extends ViewModel {
|
||||
this.application = application;
|
||||
this.megaphone = new MutableLiveData<>();
|
||||
this.searchResult = new MutableLiveData<>();
|
||||
this.archivedCount = new MutableLiveData<>();
|
||||
this.searchRepository = searchRepository;
|
||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||
this.debouncer = new Debouncer(300);
|
||||
@@ -59,10 +61,6 @@ class ConversationListViewModel extends ViewModel {
|
||||
if (!TextUtils.isEmpty(getLastQuery())) {
|
||||
searchRepository.query(getLastQuery(), searchResult::postValue);
|
||||
}
|
||||
|
||||
if (!isArchived) {
|
||||
updateArchivedCount();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,15 +75,27 @@ class ConversationListViewModel extends ViewModel {
|
||||
.setInitialLoadKey(0)
|
||||
.build();
|
||||
|
||||
if (isArchived) {
|
||||
this.archivedCount.setValue(0);
|
||||
} else {
|
||||
updateArchivedCount();
|
||||
}
|
||||
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, observer);
|
||||
|
||||
this.conversationList = LiveDataUtil.combineLatest(conversationList, this.archivedCount, ConversationList::new);
|
||||
this.conversationList = Transformations.switchMap(conversationList, conversation -> {
|
||||
if (conversation.getDataSource().isInvalid()) {
|
||||
Log.w(TAG, "Received an invalid conversation list. Ignoring.");
|
||||
return new MutableLiveData<>();
|
||||
}
|
||||
|
||||
MutableLiveData<ConversationList> updated = new MutableLiveData<>();
|
||||
|
||||
if (isArchived) {
|
||||
updated.postValue(new ConversationList(conversation, 0));
|
||||
} else {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
int archiveCount = DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount();
|
||||
updated.postValue(new ConversationList(conversation, archiveCount));
|
||||
});
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull LiveData<SearchResult> getSearchResult() {
|
||||
@@ -140,12 +150,6 @@ class ConversationListViewModel extends ViewModel {
|
||||
application.getContentResolver().unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
private void updateArchivedCount() {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
archivedCount.postValue(DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount());
|
||||
});
|
||||
}
|
||||
|
||||
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final boolean isArchived;
|
||||
|
||||
@@ -286,8 +286,9 @@ public final class GroupDatabase extends Database {
|
||||
List<Recipient> recipients = new ArrayList<>(currentMembers.size());
|
||||
|
||||
for (RecipientId member : currentMembers) {
|
||||
if (memberSet.includeSelf || !Recipient.resolved(member).isLocalNumber()) {
|
||||
recipients.add(Recipient.resolved(member));
|
||||
Recipient resolved = Recipient.resolved(member);
|
||||
if (memberSet.includeSelf || !resolved.isLocalNumber()) {
|
||||
recipients.add(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -817,9 +818,7 @@ public final class GroupDatabase extends Database {
|
||||
}
|
||||
|
||||
public List<Recipient> getMemberRecipients(@NonNull MemberSet memberSet) {
|
||||
return Stream.of(getMemberRecipientIds(memberSet))
|
||||
.map(Recipient::resolved)
|
||||
.toList();
|
||||
return Recipient.resolvedList(getMemberRecipientIds(memberSet));
|
||||
}
|
||||
|
||||
public List<RecipientId> getMemberRecipientIds(@NonNull MemberSet memberSet) {
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.thoughtcrime.securesms.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
@@ -55,6 +57,8 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
public abstract void markAsSending(long messageId);
|
||||
public abstract void markAsRemoteDelete(long messageId);
|
||||
|
||||
public abstract MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException;
|
||||
|
||||
final int getInsecureMessagesSentForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = new String[]{"COUNT(*)"};
|
||||
|
||||
@@ -445,6 +445,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return rawQuery(RAW_ID_WHERE, new String[] {messageId + ""});
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException {
|
||||
try (Cursor cursor = rawQuery(RAW_ID_WHERE, new String[] {messageId + ""})) {
|
||||
MessageRecord record = new Reader(cursor).getNext();
|
||||
|
||||
@@ -56,7 +56,6 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.Closeable;
|
||||
@@ -1392,13 +1391,13 @@ public class RecipientDatabase extends Database {
|
||||
* If from authoritative source, this will overwrite local, otherwise it will only write to the
|
||||
* database if missing.
|
||||
*/
|
||||
public Collection<RecipientId> persistProfileKeySet(@NonNull ProfileKeySet profileKeySet) {
|
||||
public Set<RecipientId> persistProfileKeySet(@NonNull ProfileKeySet profileKeySet) {
|
||||
Map<UUID, ProfileKey> profileKeys = profileKeySet.getProfileKeys();
|
||||
Map<UUID, ProfileKey> authoritativeProfileKeys = profileKeySet.getAuthoritativeProfileKeys();
|
||||
int totalKeys = profileKeys.size() + authoritativeProfileKeys.size();
|
||||
|
||||
if (totalKeys == 0) {
|
||||
return Collections.emptyList();
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Persisting %d Profile keys, %d of which are authoritative", totalKeys, authoritativeProfileKeys.size()));
|
||||
@@ -1832,7 +1831,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
public @Nullable Cursor querySignalContacts(@NonNull String query, boolean includeSelf) {
|
||||
query = buildGlobPattern(query);
|
||||
query = buildCaseInsensitiveGlobPattern(query);
|
||||
|
||||
String selection = BLOCKED + " = ? AND " +
|
||||
REGISTERED + " = ? AND " +
|
||||
@@ -1870,7 +1869,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
public @Nullable Cursor queryNonSignalContacts(@NonNull String query) {
|
||||
query = buildGlobPattern(query);
|
||||
query = buildCaseInsensitiveGlobPattern(query);
|
||||
|
||||
String selection = BLOCKED + " = ? AND " +
|
||||
REGISTERED + " != ? AND " +
|
||||
@@ -1889,8 +1888,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
public @Nullable Cursor queryAllContacts(@NonNull String query) {
|
||||
query = buildGlobPattern(query);
|
||||
query = "*" + query + "*";
|
||||
query = buildCaseInsensitiveGlobPattern(query);
|
||||
|
||||
String selection = BLOCKED + " = ? AND " +
|
||||
"(" +
|
||||
@@ -1904,7 +1902,14 @@ public class RecipientDatabase extends Database {
|
||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null);
|
||||
}
|
||||
|
||||
private static String buildGlobPattern(@NonNull String query) {
|
||||
/**
|
||||
* Builds a case-insensitive GLOB pattern for fuzzy text queries. Works with all unicode
|
||||
* characters.
|
||||
*
|
||||
* Ex:
|
||||
* cat -> [cC][aA][tT]
|
||||
*/
|
||||
private static String buildCaseInsensitiveGlobPattern(@NonNull String query) {
|
||||
if (TextUtils.isEmpty(query)) {
|
||||
return "*";
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class RemappedRecords {
|
||||
*/
|
||||
void addThread(@NonNull Context context, long oldId, long newId) {
|
||||
ensureInTransaction(context);
|
||||
ensureRecipientMapIsPopulated(context);
|
||||
ensureThreadMapIsPopulated(context);
|
||||
threadMap.put(oldId, newId);
|
||||
DatabaseFactory.getRemappedRecordsDatabase(context).addThreadMapping(oldId, newId);
|
||||
}
|
||||
|
||||
@@ -673,7 +673,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
db.insert(TABLE_NAME, null, values);
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
|
||||
notifyConversationListeners(threadId);
|
||||
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
|
||||
});
|
||||
@@ -854,7 +854,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
return db.query(TABLE_NAME, MESSAGE_PROJECTION, where, null, null, null, null);
|
||||
}
|
||||
|
||||
public SmsMessageRecord getMessage(long messageId) throws NoSuchMessageException {
|
||||
@Override
|
||||
public SmsMessageRecord getMessageRecord(long messageId) throws NoSuchMessageException {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""}, null, null, null);
|
||||
Reader reader = new Reader(cursor);
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.model;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
@@ -17,10 +18,13 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -37,8 +41,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
*/
|
||||
GroupsV2UpdateMessageProducer(@NonNull Context context,
|
||||
@NonNull DescribeMemberStrategy descriptionStrategy,
|
||||
@NonNull UUID selfUuid)
|
||||
{
|
||||
@NonNull UUID selfUuid) {
|
||||
this.context = context;
|
||||
this.descriptionStrategy = descriptionStrategy;
|
||||
this.selfUuid = selfUuid;
|
||||
@@ -50,10 +53,10 @@ final class GroupsV2UpdateMessageProducer {
|
||||
* <p>
|
||||
* Invitation and groups you create are the most common cases where no change is available.
|
||||
*/
|
||||
String describeNewGroup(@NonNull DecryptedGroup group) {
|
||||
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group) {
|
||||
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
|
||||
if (selfPending.isPresent()) {
|
||||
return context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(selfPending.get().getAddedByUuid()));
|
||||
return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy));
|
||||
}
|
||||
|
||||
if (group.getRevision() == 0) {
|
||||
@@ -61,26 +64,26 @@ final class GroupsV2UpdateMessageProducer {
|
||||
if (foundingMember.isPresent()) {
|
||||
ByteString foundingMemberUuid = foundingMember.get().getUuid();
|
||||
if (selfUuidBytes.equals(foundingMemberUuid)) {
|
||||
return context.getString(R.string.MessageRecord_you_created_the_group);
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group));
|
||||
} else {
|
||||
return context.getString(R.string.MessageRecord_s_added_you, describe(foundingMemberUuid));
|
||||
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid).isPresent()) {
|
||||
return context.getString(R.string.MessageRecord_you_joined_the_group);
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
} else {
|
||||
return context.getString(R.string.MessageRecord_group_updated);
|
||||
return updateDescription(context.getString(R.string.MessageRecord_group_updated));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> describeChange(@NonNull DecryptedGroupChange change) {
|
||||
List<String> updates = new LinkedList<>();
|
||||
List<UpdateDescription> describeChanges(@NonNull DecryptedGroupChange change) {
|
||||
List<UpdateDescription> updates = new LinkedList<>();
|
||||
|
||||
if (change.getEditor().isEmpty() || UuidUtil.UNKNOWN_UUID.equals(UuidUtil.fromByteString(change.getEditor()))) {
|
||||
describeUnknownEditorMemberAdditions(change, updates);
|
||||
describeUnknownEditorMemberRemovals(change, updates);
|
||||
|
||||
describeUnknownEditorModifyMemberRoles(change, updates);
|
||||
describeUnknownEditorInvitations(change, updates);
|
||||
describeUnknownEditorRevokedInvitations(change, updates);
|
||||
@@ -91,13 +94,15 @@ final class GroupsV2UpdateMessageProducer {
|
||||
describeUnknownEditorNewAttributeAccess(change, updates);
|
||||
describeUnknownEditorNewMembershipAccess(change, updates);
|
||||
|
||||
describeUnknownEditorMemberRemovals(change, updates);
|
||||
|
||||
if (updates.isEmpty()) {
|
||||
describeUnknownEditorUnknownChange(updates);
|
||||
}
|
||||
|
||||
} else {
|
||||
describeMemberAdditions(change, updates);
|
||||
describeMemberRemovals(change, updates);
|
||||
|
||||
describeModifyMemberRoles(change, updates);
|
||||
describeInvitations(change, updates);
|
||||
describeRevokedInvitations(change, updates);
|
||||
@@ -108,6 +113,8 @@ final class GroupsV2UpdateMessageProducer {
|
||||
describeNewAttributeAccess(change, updates);
|
||||
describeNewMembershipAccess(change, updates);
|
||||
|
||||
describeMemberRemovals(change, updates);
|
||||
|
||||
if (updates.isEmpty()) {
|
||||
describeUnknownChange(change, updates);
|
||||
}
|
||||
@@ -119,21 +126,21 @@ final class GroupsV2UpdateMessageProducer {
|
||||
/**
|
||||
* Handles case of future protocol versions where we don't know what has changed.
|
||||
*/
|
||||
private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_updated_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_updated_group, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), (editor) -> context.getString(R.string.MessageRecord_s_updated_group, editor)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorUnknownChange(@NonNull List<String> updates) {
|
||||
updates.add(context.getString(R.string.MessageRecord_the_group_was_updated));
|
||||
private void describeUnknownEditorUnknownChange(@NonNull List<UpdateDescription> updates) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_was_updated)));
|
||||
}
|
||||
|
||||
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (DecryptedMember member : change.getNewMembersList()) {
|
||||
@@ -141,37 +148,37 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_added_s, describe(member.getUuid())));
|
||||
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added)));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_you, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
} else {
|
||||
if (member.getUuid().equals(change.getEditor())) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(member.getUuid())));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_s, describe(change.getEditor()), describe(member.getUuid())));
|
||||
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedMember member : change.getNewMembersList()) {
|
||||
boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(member.getUuid())));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (ByteString member : change.getDeleteMembersList()) {
|
||||
@@ -179,98 +186,98 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (editorIsYou) {
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_left_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_removed_s, describe(member)));
|
||||
updates.add(updateDescription(member, removedMember -> context.getString(R.string.MessageRecord_you_removed_s, removedMember)));
|
||||
}
|
||||
} else {
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_removed_you_from_the_group, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_removed_you_from_the_group, editor)));
|
||||
} else {
|
||||
if (member.equals(change.getEditor())) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_left_the_group, describe(member)));
|
||||
updates.add(updateDescription(member, leavingMember -> context.getString(R.string.MessageRecord_s_left_the_group, leavingMember)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_removed_s, describe(change.getEditor()), describe(member)));
|
||||
updates.add(updateDescription(change.getEditor(), member, (editor, removedMember) -> context.getString(R.string.MessageRecord_s_removed_s, editor, removedMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (ByteString member : change.getDeleteMembersList()) {
|
||||
boolean removedMemberIsYou = member.equals(selfUuidBytes);
|
||||
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, describe(member)));
|
||||
updates.add(updateDescription(member, oldMember -> context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, oldMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
|
||||
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
|
||||
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_made_s_an_admin, describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_you_made_s_an_admin, newAdmin)));
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_made_you_an_admin, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_made_you_an_admin, editor)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_made_s_an_admin, describe(change.getEditor()), describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, newAdmin) -> context.getString(R.string.MessageRecord_s_made_s_an_admin, editor, newAdmin)));
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, oldAdmin)));
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, editor)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, describe(change.getEditor()), describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, oldAdmin) -> context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, editor, oldAdmin)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
|
||||
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_are_now_an_admin));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_is_now_an_admin, describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_s_is_now_an_admin, newAdmin)));
|
||||
}
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, oldAdmin)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
int notYouInviteCount = 0;
|
||||
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
int notYouInviteCount = 0;
|
||||
|
||||
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
|
||||
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor)));
|
||||
} else {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_invited_s_to_the_group, describe(invitee.getUuid())));
|
||||
updates.add(updateDescription(invitee.getUuid(), newInvitee -> context.getString(R.string.MessageRecord_you_invited_s_to_the_group, newInvitee)));
|
||||
} else {
|
||||
notYouInviteCount++;
|
||||
}
|
||||
@@ -278,39 +285,40 @@ final class GroupsV2UpdateMessageProducer {
|
||||
}
|
||||
|
||||
if (notYouInviteCount > 0) {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCount, describe(change.getEditor()), notYouInviteCount));
|
||||
final int notYouInviteCountFinalCopy = notYouInviteCount;
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, editor, notYouInviteCountFinalCopy)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
int notYouInviteCount = 0;
|
||||
|
||||
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
|
||||
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_were_invited_to_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group)));
|
||||
} else {
|
||||
notYouInviteCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (notYouInviteCount > 0) {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
int notDeclineCount = 0;
|
||||
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
int notDeclineCount = 0;
|
||||
|
||||
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
|
||||
boolean decline = invitee.getUuid().equals(change.getEditor());
|
||||
if (decline) {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group)));
|
||||
}
|
||||
} else {
|
||||
notDeclineCount++;
|
||||
@@ -319,176 +327,201 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (notDeclineCount > 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount)));
|
||||
} else {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCount, describe(change.getEditor()), notDeclineCount));
|
||||
final int notDeclineCountFinalCopy = notDeclineCount;
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, editor, notDeclineCountFinalCopy)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
int notDeclineCount = 0;
|
||||
|
||||
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
|
||||
boolean inviteeWasYou = invitee.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (inviteeWasYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_your_invitation_to_the_group_was_revoked));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_invitation_to_the_group_was_revoked)));
|
||||
} else {
|
||||
notDeclineCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (notDeclineCount > 0) {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_accepted_invite));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_added_invited_member_s, describe(uuid)));
|
||||
updates.add(updateDescription(uuid, newPromotedMember -> context.getString(R.string.MessageRecord_you_added_invited_member_s, newPromotedMember)));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_you, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
} else {
|
||||
if (uuid.equals(change.getEditor())) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_accepted_invite, describe(uuid)));
|
||||
updates.add(updateDescription(uuid, newAcceptedMember -> context.getString(R.string.MessageRecord_s_accepted_invite, newAcceptedMember)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_invited_member_s, describe(change.getEditor()), describe(uuid)));
|
||||
updates.add(updateDescription(change.getEditor(), uuid, (editor, newAcceptedMember) -> context.getString(R.string.MessageRecord_s_added_invited_member_s, editor, newAcceptedMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(uuid)));
|
||||
updates.add(updateDescription(uuid, newMemberName -> context.getString(R.string.MessageRecord_s_joined_the_group, newMemberName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.hasNewTitle()) {
|
||||
String newTitle = StringUtil.isolateBidi(change.getNewTitle().getValue());
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, change.getNewTitle().getValue()));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, describe(change.getEditor()), change.getNewTitle().getValue()));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, editor, newTitle)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewTitle()) {
|
||||
updates.add(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, change.getNewTitle().getValue()));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.getNewTitle().getValue()))));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.hasNewAvatar()) {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_changed_the_group_avatar));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_changed_the_group_avatar, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_avatar, editor)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewAvatar()) {
|
||||
updates.add(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.hasNewTimer()) {
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, describe(change.getEditor()), time));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, editor, time)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewTimer()) {
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
|
||||
updates.add(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, describe(change.getEditor()), accessLevel));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, editor, accessLevel)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
|
||||
updates.add(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, describe(change.getEditor()), accessLevel));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, editor, accessLevel)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
|
||||
updates.add(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel)));
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String describe(@NonNull ByteString uuid) {
|
||||
return descriptionStrategy.describe(UuidUtil.fromByteString(uuid));
|
||||
}
|
||||
|
||||
interface DescribeMemberStrategy {
|
||||
|
||||
/**
|
||||
* Map a UUID to a string that describes the group member.
|
||||
*/
|
||||
@NonNull String describe(@NonNull UUID uuid);
|
||||
@NonNull
|
||||
@WorkerThread
|
||||
String describe(@NonNull UUID uuid);
|
||||
}
|
||||
|
||||
private interface StringFactory1Arg {
|
||||
String create(String arg1);
|
||||
}
|
||||
|
||||
private interface StringFactory2Args {
|
||||
String create(String arg1, String arg2);
|
||||
}
|
||||
|
||||
private static UpdateDescription updateDescription(@NonNull String string) {
|
||||
return UpdateDescription.staticDescription(string);
|
||||
}
|
||||
|
||||
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull StringFactory1Arg stringFactory) {
|
||||
UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes);
|
||||
|
||||
return UpdateDescription.mentioning(Collections.singletonList(uuid1), () -> stringFactory.create(descriptionStrategy.describe(uuid1)));
|
||||
}
|
||||
|
||||
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull ByteString uuid2Bytes, @NonNull StringFactory2Args stringFactory) {
|
||||
UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes);
|
||||
UUID uuid2 = UuidUtil.fromByteStringOrUnknown(uuid2Bytes);
|
||||
|
||||
return UpdateDescription.mentioning(Arrays.asList(uuid1, uuid2), () -> stringFactory.create(descriptionStrategy.describe(uuid1), descriptionStrategy.describe(uuid2)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class LiveUpdateMessage {
|
||||
|
||||
/**
|
||||
* Creates a live data that observes the recipients mentioned in the {@link UpdateDescription} and
|
||||
* recreates the string asynchronously when they change.
|
||||
*/
|
||||
@AnyThread
|
||||
public static LiveData<String> fromMessageDescription(@NonNull UpdateDescription updateDescription) {
|
||||
if (updateDescription.isStringStatic()) {
|
||||
return LiveDataUtil.just(updateDescription.getStaticString());
|
||||
}
|
||||
|
||||
List<LiveData<Recipient>> allMentionedRecipients = Stream.of(updateDescription.getMentioned())
|
||||
.map(uuid -> Recipient.resolved(RecipientId.from(uuid, null)).live().getLiveData())
|
||||
.toList();
|
||||
|
||||
LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object())
|
||||
: LiveDataUtil.merge(allMentionedRecipients);
|
||||
|
||||
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> updateDescription.getString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes a single recipient and recreates the string asynchronously when they change.
|
||||
*/
|
||||
public static LiveData<String> recipientToStringAsync(@NonNull RecipientId recipientId,
|
||||
@NonNull Function<Recipient, String> createStringInBackground)
|
||||
{
|
||||
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveData(), createStringInBackground);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,8 +23,8 @@ import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
@@ -39,9 +39,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -109,78 +112,87 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (isGroupUpdate() && isGroupV2()) {
|
||||
return new SpannableString(getGv2Description(context));
|
||||
} else if (isGroupUpdate() && isOutgoing()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group));
|
||||
} else if (isGroupUpdate()) {
|
||||
return new SpannableString(GroupUtil.getDescription(context, getBody(), false).toString(getIndividualRecipient()));
|
||||
} else if (isGroupQuit() && isOutgoing()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_left_group));
|
||||
} else if (isGroupQuit()) {
|
||||
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIncomingCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isOutgoingCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_you_called));
|
||||
} else if (isMissedCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_missed_call));
|
||||
} else if (isJoined()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
int seconds = (int)(getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
|
||||
: new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().getDisplayName(context)));
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
|
||||
: new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().getDisplayName(context), time));
|
||||
} else if (isIdentityUpdate()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIdentityVerified()) {
|
||||
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().getDisplayName(context)));
|
||||
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIdentityDefault()) {
|
||||
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().getDisplayName(context)));
|
||||
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isProfileChange()) {
|
||||
return new SpannableString(getProfileChangeDescription(context));
|
||||
UpdateDescription updateDisplayBody = getUpdateDisplayBody(context);
|
||||
|
||||
if (updateDisplayBody != null) {
|
||||
return new SpannableString(updateDisplayBody.getString());
|
||||
}
|
||||
|
||||
return new SpannableString(getBody());
|
||||
}
|
||||
|
||||
private @NonNull String getGv2Description(@NonNull Context context) {
|
||||
if (!isGroupUpdate() || !isGroupV2()) {
|
||||
throw new AssertionError();
|
||||
public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context) {
|
||||
if (isGroupUpdate() && isGroupV2()) {
|
||||
return getGv2ChangeDescription(context, getBody());
|
||||
} else if (isGroupUpdate() && isOutgoing()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_updated_group));
|
||||
} else if (isGroupUpdate()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> GroupUtil.getNonV2GroupDescription(context, getBody()).toString(r));
|
||||
} else if (isGroupQuit() && isOutgoing()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_left_group));
|
||||
} else if (isGroupQuit()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.ConversationItem_group_action_left, r.getDisplayName(context)));
|
||||
} else if (isIncomingCall()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you, r.getDisplayName(context)));
|
||||
} else if (isOutgoingCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_called));
|
||||
} else if (isMissedCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_call));
|
||||
} else if (isJoined()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
int seconds = (int)(getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, r.getDisplayName(context)));
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, r.getDisplayName(context), time));
|
||||
} else if (isIdentityUpdate()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)));
|
||||
} else if (isIdentityVerified()) {
|
||||
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, r.getDisplayName(context)));
|
||||
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, r.getDisplayName(context)));
|
||||
} else if (isIdentityDefault()) {
|
||||
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, r.getDisplayName(context)));
|
||||
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, r.getDisplayName(context)));
|
||||
} else if (isProfileChange()) {
|
||||
return staticUpdateDescription(getProfileChangeDescription(context));
|
||||
} else if (isEndSession()) {
|
||||
if (isOutgoing()) return staticUpdateDescription(context.getString(R.string.SmsMessageRecord_secure_session_reset));
|
||||
else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @NonNull UpdateDescription getGv2ChangeDescription(@NonNull Context context, @NonNull String body) {
|
||||
try {
|
||||
ShortStringDescriptionStrategy descriptionStrategy = new ShortStringDescriptionStrategy(context);
|
||||
byte[] decoded = Base64.decode(getBody());
|
||||
byte[] decoded = Base64.decode(body);
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
|
||||
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get());
|
||||
|
||||
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() > 0) {
|
||||
DecryptedGroupChange change = decryptedGroupV2Context.getChange();
|
||||
List<String> strings = updateMessageProducer.describeChange(change);
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < strings.size(); i++) {
|
||||
if (i > 0) result.append('\n');
|
||||
result.append(strings.get(i));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getChange()));
|
||||
} else {
|
||||
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||
return context.getString(R.string.MessageRecord_group_updated);
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated));
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient, @NonNull Function<Recipient, String> stringFunction) {
|
||||
return UpdateDescription.mentioning(Collections.singletonList(recipient.getUuid().or(UuidUtil.UNKNOWN_UUID)), () -> stringFunction.apply(recipient.resolve()));
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string) {
|
||||
return UpdateDescription.staticDescription(string);
|
||||
}
|
||||
|
||||
private @NonNull String getProfileChangeDescription(@NonNull Context context) {
|
||||
try {
|
||||
byte[] decoded = Base64.decode(getBody());
|
||||
@@ -188,8 +200,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
|
||||
if (profileChangeDetails.hasProfileNameChange()) {
|
||||
String displayName = getIndividualRecipient().getDisplayName(context);
|
||||
String newName = ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getNew()).toString();
|
||||
String previousName = ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getPrevious()).toString();
|
||||
String newName = StringUtil.isolateBidi(ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getNew()).toString());
|
||||
String previousName = StringUtil.isolateBidi(ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getPrevious()).toString());
|
||||
|
||||
if (getIndividualRecipient().isSystemContact()) {
|
||||
return context.getString(R.string.MessageRecord_changed_their_profile_name_from_to, displayName, previousName, newName);
|
||||
@@ -316,7 +328,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure());
|
||||
}
|
||||
|
||||
protected SpannableString emphasisAdded(String sequence) {
|
||||
protected static SpannableString emphasisAdded(String sequence) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Contains a list of people mentioned in an update message and a function to create the update message.
|
||||
*/
|
||||
public final class UpdateDescription {
|
||||
|
||||
public interface StringFactory {
|
||||
@WorkerThread
|
||||
String create();
|
||||
}
|
||||
|
||||
private final Collection<UUID> mentioned;
|
||||
private final StringFactory stringFactory;
|
||||
private final String staticString;
|
||||
|
||||
private UpdateDescription(@NonNull Collection<UUID> mentioned,
|
||||
@Nullable StringFactory stringFactory,
|
||||
@Nullable String staticString)
|
||||
{
|
||||
if (staticString == null && stringFactory == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
this.mentioned = mentioned;
|
||||
this.stringFactory = stringFactory;
|
||||
this.staticString = staticString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update description which has a string value created by a supplied factory method that
|
||||
* will be run on a background thread.
|
||||
*
|
||||
* @param mentioned UUIDs of recipients that are mentioned in the string.
|
||||
* @param stringFactory The background method for generating the string.
|
||||
*/
|
||||
public static UpdateDescription mentioning(@NonNull Collection<UUID> mentioned,
|
||||
@NonNull StringFactory stringFactory)
|
||||
{
|
||||
return new UpdateDescription(UuidUtil.filterKnown(mentioned),
|
||||
stringFactory,
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update description that's string value is fixed.
|
||||
*/
|
||||
public static UpdateDescription staticDescription(@NonNull String staticString) {
|
||||
return new UpdateDescription(Collections.emptyList(), null, staticString);
|
||||
}
|
||||
|
||||
public boolean isStringStatic() {
|
||||
return staticString != null;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public @NonNull String getStaticString() {
|
||||
if (staticString == null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
return staticString;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull String getString() {
|
||||
if (staticString != null) {
|
||||
return staticString;
|
||||
}
|
||||
|
||||
Util.assertNotMainThread();
|
||||
|
||||
//noinspection ConstantConditions
|
||||
return stringFactory.create();
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public Collection<UUID> getMentioned() {
|
||||
return mentioned;
|
||||
}
|
||||
|
||||
public static UpdateDescription concatWithNewLines(@NonNull List<UpdateDescription> updateDescriptions) {
|
||||
if (updateDescriptions.size() == 0) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (updateDescriptions.size() == 1) {
|
||||
return updateDescriptions.get(0);
|
||||
}
|
||||
|
||||
if (allAreStatic(updateDescriptions)) {
|
||||
return UpdateDescription.staticDescription(concatStaticLines(updateDescriptions));
|
||||
}
|
||||
|
||||
Set<UUID> allMentioned = new HashSet<>();
|
||||
|
||||
for (UpdateDescription updateDescription : updateDescriptions) {
|
||||
allMentioned.addAll(updateDescription.getMentioned());
|
||||
}
|
||||
|
||||
return UpdateDescription.mentioning(allMentioned, () -> concatLines(updateDescriptions));
|
||||
}
|
||||
|
||||
private static boolean allAreStatic(@NonNull Collection<UpdateDescription> updateDescriptions) {
|
||||
for (UpdateDescription description : updateDescriptions) {
|
||||
if (!description.isStringStatic()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static String concatLines(@NonNull List<UpdateDescription> updateDescriptions) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < updateDescriptions.size(); i++) {
|
||||
if (i > 0) result.append('\n');
|
||||
result.append(updateDescriptions.get(i).getString());
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
private static String concatStaticLines(@NonNull List<UpdateDescription> updateDescriptions) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < updateDescriptions.size(); i++) {
|
||||
if (i > 0) result.append('\n');
|
||||
result.append(updateDescriptions.get(i).getStaticString());
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,6 @@ public class GiphyStickerLoader extends GiphyLoader {
|
||||
|
||||
@Override
|
||||
protected String getSearchUrl() {
|
||||
return "https://api.giphy.com/v1/stickers/search?q=cat&api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE + "&q=%s";
|
||||
return "https://api.giphy.com/v1/stickers/search?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE + "&q=%s";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupC
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -122,12 +123,15 @@ final class GroupManagerV1 {
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
List<GroupContext.Member> uuidMembers = new LinkedList<>();
|
||||
List<String> e164Members = new LinkedList<>();
|
||||
List<GroupContext.Member> uuidMembers = new ArrayList<>(members.size());
|
||||
List<String> e164Members = new ArrayList<>(members.size());
|
||||
|
||||
for (RecipientId member : members) {
|
||||
Recipient recipient = Recipient.resolved(member);
|
||||
uuidMembers.add(GroupV1MessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient)));
|
||||
if (recipient.hasE164()) {
|
||||
e164Members.add(recipient.requireE164());
|
||||
uuidMembers.add(GroupV1MessageProcessor.createMember(recipient.requireE164()));
|
||||
}
|
||||
}
|
||||
|
||||
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
|
||||
@@ -135,7 +139,9 @@ final class GroupManagerV1 {
|
||||
.setType(GroupContext.Type.UPDATE)
|
||||
.addAllMembersE164(e164Members)
|
||||
.addAllMembers(uuidMembers);
|
||||
|
||||
if (groupName != null) groupContextBuilder.setName(groupName);
|
||||
|
||||
GroupContext groupContext = groupContextBuilder.build();
|
||||
|
||||
if (avatar != null) {
|
||||
|
||||
@@ -65,15 +65,14 @@ final class GroupManagerV2 {
|
||||
|
||||
private static final String TAG = Log.tag(GroupManagerV2.class);
|
||||
|
||||
private final Context context;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
private final GroupsV2Operations groupsV2Operations;
|
||||
private final GroupsV2Authorization authorization;
|
||||
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
||||
private final UUID selfUuid;
|
||||
private final GroupCandidateHelper groupCandidateHelper;
|
||||
private final GroupsV2CapabilityChecker capabilityChecker;
|
||||
private final Context context;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
private final GroupsV2Operations groupsV2Operations;
|
||||
private final GroupsV2Authorization authorization;
|
||||
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
||||
private final UUID selfUuid;
|
||||
private final GroupCandidateHelper groupCandidateHelper;
|
||||
|
||||
GroupManagerV2(@NonNull Context context) {
|
||||
this.context = context;
|
||||
@@ -84,7 +83,6 @@ final class GroupManagerV2 {
|
||||
this.groupsV2StateProcessor = ApplicationDependencies.getGroupsV2StateProcessor();
|
||||
this.selfUuid = Recipient.self().getUuid().get();
|
||||
this.groupCandidateHelper = new GroupCandidateHelper(context);
|
||||
this.capabilityChecker = new GroupsV2CapabilityChecker();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -116,7 +114,7 @@ final class GroupManagerV2 {
|
||||
@Nullable byte[] avatar)
|
||||
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
|
||||
{
|
||||
if (!capabilityChecker.allAndSelfSupportGroupsV2AndUuid(members)) {
|
||||
if (!GroupsV2CapabilityChecker.allAndSelfSupportGroupsV2AndUuid(members)) {
|
||||
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
|
||||
}
|
||||
|
||||
@@ -196,7 +194,7 @@ final class GroupManagerV2 {
|
||||
@NonNull GroupManager.GroupActionResult addMembers(@NonNull Collection<RecipientId> newMembers)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, MembershipNotSuitableForV2Exception
|
||||
{
|
||||
if (!capabilityChecker.allSupportGroupsV2AndUuid(newMembers)) {
|
||||
if (!GroupsV2CapabilityChecker.allSupportGroupsV2AndUuid(newMembers)) {
|
||||
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ public final class GroupV1MessageProcessor {
|
||||
|
||||
if (group.getMembers().isPresent()) {
|
||||
for (SignalServiceAddress member : group.getMembers().get()) {
|
||||
members.add(Recipient.externalPush(context, member).getId());
|
||||
members.add(Recipient.externalGV1Member(context, member).getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +131,10 @@ public final class GroupV1MessageProcessor {
|
||||
Set<RecipientId> recordMembers = new HashSet<>(groupRecord.getMembers());
|
||||
Set<RecipientId> messageMembers = new HashSet<>();
|
||||
|
||||
for (SignalServiceAddress messageMember : group.getMembers().get()) {
|
||||
messageMembers.add(Recipient.externalPush(context, messageMember).getId());
|
||||
if (group.getMembers().isPresent()) {
|
||||
for (SignalServiceAddress messageMember : group.getMembers().get()) {
|
||||
messageMembers.add(Recipient.externalGV1Member(context, messageMember).getId());
|
||||
}
|
||||
}
|
||||
|
||||
Set<RecipientId> addedMembers = new HashSet<>(messageMembers);
|
||||
@@ -150,18 +152,19 @@ public final class GroupV1MessageProcessor {
|
||||
database.updateMembers(id, new LinkedList<>(unionMembers));
|
||||
|
||||
builder.clearMembers();
|
||||
builder.clearMembersE164();
|
||||
|
||||
for (RecipientId addedMember : addedMembers) {
|
||||
Recipient recipient = Recipient.resolved(addedMember);
|
||||
|
||||
if (recipient.getE164().isPresent()) {
|
||||
builder.addMembersE164(recipient.getE164().get());
|
||||
builder.addMembersE164(recipient.requireE164());
|
||||
builder.addMembers(createMember(recipient.requireE164()));
|
||||
}
|
||||
|
||||
builder.addMembers(createMember(RecipientUtil.toSignalServiceAddress(context, recipient)));
|
||||
}
|
||||
} else {
|
||||
builder.clearMembers();
|
||||
builder.clearMembersE164();
|
||||
}
|
||||
|
||||
if (missingMembers.size() > 0) {
|
||||
@@ -287,6 +290,8 @@ public final class GroupV1MessageProcessor {
|
||||
.map(a -> a.getNumber().get())
|
||||
.toList());
|
||||
builder.addAllMembers(Stream.of(group.getMembers().get())
|
||||
.filter(address -> address.getNumber().isPresent())
|
||||
.map(address -> address.getNumber().get())
|
||||
.map(GroupV1MessageProcessor::createMember)
|
||||
.toList());
|
||||
}
|
||||
@@ -294,17 +299,9 @@ public final class GroupV1MessageProcessor {
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static GroupContext.Member createMember(SignalServiceAddress address) {
|
||||
public static GroupContext.Member createMember(@NonNull String e164) {
|
||||
GroupContext.Member.Builder member = GroupContext.Member.newBuilder();
|
||||
|
||||
if (address.getUuid().isPresent()) {
|
||||
member.setUuid(address.getUuid().get().toString());
|
||||
}
|
||||
|
||||
if (address.getNumber().isPresent()) {
|
||||
member.setE164(address.getNumber().get());
|
||||
}
|
||||
|
||||
member.setE164(e164);
|
||||
return member.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -24,17 +25,17 @@ public final class GroupsV2CapabilityChecker {
|
||||
|
||||
private static final String TAG = Log.tag(GroupsV2CapabilityChecker.class);
|
||||
|
||||
public GroupsV2CapabilityChecker() {}
|
||||
private GroupsV2CapabilityChecker() {}
|
||||
|
||||
/**
|
||||
* @param resolved A collection of resolved recipients.
|
||||
*/
|
||||
@WorkerThread
|
||||
public void refreshCapabilitiesIfNecessary(@NonNull Collection<Recipient> resolved) throws IOException {
|
||||
List<RecipientId> needsRefresh = Stream.of(resolved)
|
||||
.filter(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED)
|
||||
.map(Recipient::getId)
|
||||
.toList();
|
||||
public static void refreshCapabilitiesIfNecessary(@NonNull Collection<Recipient> resolved) throws IOException {
|
||||
Set<RecipientId> needsRefresh = Stream.of(resolved)
|
||||
.filter(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED)
|
||||
.map(Recipient::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (needsRefresh.size() > 0) {
|
||||
Log.d(TAG, "[refreshCapabilitiesIfNecessary] Need to refresh " + needsRefresh.size() + " recipients.");
|
||||
@@ -51,7 +52,7 @@ public final class GroupsV2CapabilityChecker {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
boolean allAndSelfSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
|
||||
static boolean allAndSelfSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
|
||||
throws IOException
|
||||
{
|
||||
HashSet<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
|
||||
@@ -62,7 +63,7 @@ public final class GroupsV2CapabilityChecker {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
boolean allSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
|
||||
static boolean allSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
|
||||
throws IOException
|
||||
{
|
||||
Set<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class GroupMemberEntry {
|
||||
|
||||
@@ -152,7 +153,7 @@ public abstract class GroupMemberEntry {
|
||||
|
||||
PendingMember other = (PendingMember) obj;
|
||||
return other.invitee.equals(invitee) &&
|
||||
other.inviteeCipherText.equals(inviteeCipherText) &&
|
||||
Objects.equals(other.inviteeCipherText, inviteeCipherText) &&
|
||||
other.cancellable == cancellable;
|
||||
}
|
||||
|
||||
@@ -160,7 +161,7 @@ public abstract class GroupMemberEntry {
|
||||
public int hashCode() {
|
||||
int hash = invitee.hashCode();
|
||||
hash *= 31;
|
||||
hash += inviteeCipherText.hashCode();
|
||||
hash += Objects.hashCode(inviteeCipherText);
|
||||
hash *= 31;
|
||||
return hash + (cancellable ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups.ui.addmembers;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -58,14 +59,19 @@ public class AddMembersActivity extends PushContactSelectionActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (getGroupId().isV1() && recipientId.isPresent() && !Recipient.resolved(recipientId.get()).hasE164()) {
|
||||
Toast.makeText(this, R.string.AddMembersActivity__this_person_cant_be_added_to_legacy_groups, Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() >= 1) {
|
||||
enableDone();
|
||||
}
|
||||
enableDone();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -39,13 +39,18 @@ public final class AddToGroupViewModel extends ViewModel {
|
||||
events.postValue(new Event.CloseEvent());
|
||||
} else if (groupRecipientIds.size() == 1) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientIds.get(0));
|
||||
String recipientName = Recipient.resolved(recipientId).getDisplayName(context);
|
||||
String recipientName = recipient.getDisplayName(context);
|
||||
String groupName = groupRecipient.getDisplayName(context);
|
||||
|
||||
events.postValue(new Event.AddToSingleGroupConfirmationEvent(context.getResources().getString(R.string.AddToGroupActivity_add_member),
|
||||
context.getResources().getString(R.string.AddToGroupActivity_add_s_to_s, recipientName, groupName),
|
||||
groupRecipient, recipientName, groupName));
|
||||
if (groupRecipient.getGroupId().get().isV1() && !recipient.hasE164()) {
|
||||
events.postValue(new Event.LegacyGroupDenialEvent());
|
||||
} else {
|
||||
events.postValue(new Event.AddToSingleGroupConfirmationEvent(context.getResources().getString(R.string.AddToGroupActivity_add_member),
|
||||
context.getResources().getString(R.string.AddToGroupActivity_add_s_to_s, recipientName, groupName),
|
||||
groupRecipient, recipientName, groupName));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new AssertionError("Does not support multi-select");
|
||||
@@ -107,6 +112,9 @@ public final class AddToGroupViewModel extends ViewModel {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
static class LegacyGroupDenialEvent extends Event {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
@@ -91,6 +91,8 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> viewModel.onAddToGroupsConfirmed(addEvent))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
} else if (event instanceof Event.LegacyGroupDenialEvent) {
|
||||
Toast.makeText(this, R.string.AddToGroupActivity_this_person_cant_be_added_to_legacy_groups, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
@@ -112,20 +114,23 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (contactsFragment.isMulti()) {
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() >= MINIMUM_GROUP_SELECT_SIZE) {
|
||||
enableNext();
|
||||
}
|
||||
throw new UnsupportedOperationException("Not yet built to handle multi-select.");
|
||||
// if (contactsFragment.hasQueryFilter()) {
|
||||
// getToolbar().clear();
|
||||
// }
|
||||
//
|
||||
// if (contactsFragment.getSelectedContactsCount() >= MINIMUM_GROUP_SELECT_SIZE) {
|
||||
// enableNext();
|
||||
// }
|
||||
} else {
|
||||
if (recipientId.isPresent()) {
|
||||
viewModel.onContinueWithSelection(Collections.singletonList(recipientId.get()));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
@@ -30,6 +31,8 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CreateGroupActivity extends ContactSelectionActivity {
|
||||
@@ -90,14 +93,14 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() >= MINIMUM_GROUP_SIZE) {
|
||||
enableNext();
|
||||
}
|
||||
enableNext();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -150,9 +153,9 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
||||
|
||||
stopwatch.split("registered");
|
||||
|
||||
if (FeatureFlags.groupsV2()) {
|
||||
if (FeatureFlags.groupsV2create()) {
|
||||
try {
|
||||
new GroupsV2CapabilityChecker().refreshCapabilitiesIfNecessary(resolved);
|
||||
GroupsV2CapabilityChecker.refreshCapabilitiesIfNecessary(resolved);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to refresh all recipient capabilities.", e);
|
||||
}
|
||||
@@ -160,13 +163,31 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
||||
|
||||
stopwatch.split("capabilities");
|
||||
|
||||
resolved = Recipient.resolvedList(ids);
|
||||
|
||||
if (Stream.of(resolved).anyMatch(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED) &&
|
||||
Stream.of(resolved).anyMatch(r -> !r.hasE164()))
|
||||
{
|
||||
Log.w(TAG, "Invalid GV1 group...");
|
||||
ids = Collections.emptyList();
|
||||
}
|
||||
|
||||
stopwatch.split("gv1-check");
|
||||
|
||||
return ids;
|
||||
}, ids -> {
|
||||
dismissibleDialog.dismiss();
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
startActivityForResult(AddGroupDetailsActivity.newIntent(this, ids), REQUEST_CODE_ADD_DETAILS);
|
||||
if (ids.isEmpty()) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.CreateGroupActivity_some_contacts_cannot_be_in_legacy_groups)
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss())
|
||||
.show();
|
||||
} else {
|
||||
startActivityForResult(AddGroupDetailsActivity.newIntent(this, ids), REQUEST_CODE_ADD_DETAILS);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
@@ -51,6 +53,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -297,11 +300,14 @@ public final class GroupsV2StateProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
Collection<RecipientId> updated = recipientDatabase.persistProfileKeySet(profileKeys);
|
||||
Set<RecipientId> updated = recipientDatabase.persistProfileKeySet(profileKeys);
|
||||
|
||||
if (!updated.isEmpty()) {
|
||||
Log.i(TAG, String.format(Locale.US, "Learned %d new profile keys, scheduling profile retrievals", updated.size()));
|
||||
RetrieveProfileJob.enqueue(updated);
|
||||
Log.i(TAG, String.format(Locale.US, "Learned %d new profile keys, fetching profiles", updated.size()));
|
||||
|
||||
for (Job job : RetrieveProfileJob.forRecipients(updated)) {
|
||||
jobManager.runSynchronously(job, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public final class GroupV2UpdateSelfProfileKeyJob extends BaseJob {
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
|
||||
GroupV2UpdateSelfProfileKeyJob(@NonNull GroupId.V2 groupId) {
|
||||
public GroupV2UpdateSelfProfileKeyJob(@NonNull GroupId.V2 groupId) {
|
||||
this(new Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
|
||||
@@ -65,7 +65,7 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PushGroupSendJob extends PushSendJob {
|
||||
public final class PushGroupSendJob extends PushSendJob {
|
||||
|
||||
public static final String KEY = "PushGroupSendJob";
|
||||
|
||||
@@ -74,8 +74,8 @@ public class PushGroupSendJob extends PushSendJob {
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_FILTER_RECIPIENT = "filter_recipient";
|
||||
|
||||
private long messageId;
|
||||
private RecipientId filterRecipient;
|
||||
private final long messageId;
|
||||
private final RecipientId filterRecipient;
|
||||
|
||||
public PushGroupSendJob(long messageId, @NonNull RecipientId destination, @Nullable RecipientId filterRecipient, boolean hasMedia) {
|
||||
this(new Job.Parameters.Builder()
|
||||
@@ -237,7 +237,10 @@ public class PushGroupSendJob extends PushSendJob {
|
||||
database.markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
|
||||
List<RecipientId> mismatchRecipientIds = Stream.of(identityMismatches).map(mismatch -> mismatch.getRecipientId(context)).toList();
|
||||
Set<RecipientId> mismatchRecipientIds = Stream.of(identityMismatches)
|
||||
.map(mismatch -> mismatch.getRecipientId(context))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
RetrieveProfileJob.enqueue(mismatchRecipientIds);
|
||||
}
|
||||
} catch (UntrustedIdentityException | UndeliverableMessageException e) {
|
||||
@@ -320,8 +323,8 @@ public class PushGroupSendJob extends PushSendJob {
|
||||
GroupContext groupContext = properties.getGroupContext();
|
||||
SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
|
||||
SignalServiceGroup.Type type = properties.isQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
|
||||
List<SignalServiceAddress> members = Stream.of(groupContext.getMembersList())
|
||||
.map(m -> new SignalServiceAddress(UuidUtil.parseOrNull(m.getUuid()), m.getE164()))
|
||||
List<SignalServiceAddress> members = Stream.of(groupContext.getMembersE164List())
|
||||
.map(e164 -> new SignalServiceAddress(null, e164))
|
||||
.toList();
|
||||
SignalServiceGroup group = new SignalServiceGroup(type, groupId.getDecodedId(), groupContext.getName(), members, avatar);
|
||||
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
||||
|
||||
@@ -508,7 +508,8 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_DESCRIPTION, message.getDescription())
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_OPAQUE, message.getOpaque())
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_SDP, message.getSdp())
|
||||
.putExtra(WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP, content.getServerReceivedTimestamp())
|
||||
.putExtra(WebRtcCallService.EXTRA_SERVER_DELIVERED_TIMESTAMP, content.getServerDeliveredTimestamp())
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, message.getType().getCode())
|
||||
@@ -527,11 +528,12 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
|
||||
|
||||
intent.setAction(WebRtcCallService.ACTION_RECEIVE_ANSWER)
|
||||
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
|
||||
.putExtra(WebRtcCallService.EXTRA_ANSWER_DESCRIPTION, message.getDescription())
|
||||
.putExtra(WebRtcCallService.EXTRA_MULTI_RING, content.getCallMessage().get().isMultiRing());
|
||||
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
|
||||
.putExtra(WebRtcCallService.EXTRA_ANSWER_OPAQUE, message.getOpaque())
|
||||
.putExtra(WebRtcCallService.EXTRA_ANSWER_SDP, message.getSdp())
|
||||
.putExtra(WebRtcCallService.EXTRA_MULTI_RING, content.getCallMessage().get().isMultiRing());
|
||||
|
||||
context.startService(intent);
|
||||
}
|
||||
@@ -1755,6 +1757,12 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
} else if (conversation.isGroup()) {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
|
||||
boolean isGv2Message = message.isGroupV2Message();
|
||||
|
||||
if (isGv2Message && !FeatureFlags.groupsV2() && groupDatabase.isUnknownGroup(groupId.get())) {
|
||||
Log.i(TAG, "Ignoring GV2 message for a new group by feature flag.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
||||
return false;
|
||||
@@ -1763,17 +1771,11 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
boolean isTextMessage = message.getBody().isPresent();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent();
|
||||
boolean isExpireMessage = message.isExpirationUpdate();
|
||||
boolean isGv2Message = message.isGroupV2Message();
|
||||
boolean isGv2Update = message.isGroupV2Update();
|
||||
boolean isContentMessage = !message.isGroupV1Update() && !isGv2Update && !isExpireMessage && (isTextMessage || isMediaMessage);
|
||||
boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
|
||||
boolean isLeaveMessage = message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.QUIT;
|
||||
|
||||
if (isGv2Message && !FeatureFlags.groupsV2()) {
|
||||
Log.i(TAG, "Ignoring GV2 message by feature flag.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage && !isGv2Update);
|
||||
} else {
|
||||
return sender.isBlocked();
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
@@ -72,7 +71,7 @@ public class PushTextSendJob extends PushSendJob {
|
||||
public void onPushSend() throws NoSuchMessageException, RetryLaterException {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(messageId);
|
||||
SmsMessageRecord record = database.getMessageRecord(messageId);
|
||||
|
||||
if (!record.isPending() && !record.isFailed()) {
|
||||
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
|
||||
@@ -67,7 +67,7 @@ public class ReactionSendJob extends BaseJob {
|
||||
throws NoSuchMessageException
|
||||
{
|
||||
MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId)
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
@@ -140,7 +140,7 @@ public class ReactionSendJob extends BaseJob {
|
||||
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
} else {
|
||||
db = DatabaseFactory.getSmsDatabase(context);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
}
|
||||
|
||||
Recipient targetAuthor = message.isOutgoing() ? Recipient.self() : message.getIndividualRecipient();
|
||||
|
||||
@@ -57,7 +57,7 @@ public class RemoteDeleteSendJob extends BaseJob {
|
||||
throws NoSuchMessageException
|
||||
{
|
||||
MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId)
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
@@ -119,7 +119,7 @@ public class RemoteDeleteSendJob extends BaseJob {
|
||||
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
} else {
|
||||
db = DatabaseFactory.getSmsDatabase(context);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
}
|
||||
|
||||
long targetSentTimestamp = message.getDateSent();
|
||||
|
||||
@@ -71,11 +71,6 @@ final class RequestGroupV2InfoWorkerJob extends BaseJob {
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException, GroupNotAMemberException, GroupChangeBusyException {
|
||||
if (!FeatureFlags.groupsV2()) {
|
||||
Log.w(TAG, "Group update skipped due to feature flag " + groupId);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Updating group to revision " + toRevision);
|
||||
|
||||
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(groupId);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
@@ -9,6 +8,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
@@ -50,10 +50,9 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -73,22 +72,20 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
|
||||
private final List<RecipientId> recipientIds;
|
||||
private final Set<RecipientId> recipientIds;
|
||||
|
||||
/**
|
||||
* Identical to {@link #enqueue(Collection)})}, but run on a background thread for convenience.
|
||||
* Identical to {@link #enqueue(Set)})}, but run on a background thread for convenience.
|
||||
*/
|
||||
public static void enqueueAsync(@NonNull RecipientId recipientId) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
ApplicationDependencies.getJobManager().add(forRecipient(recipientId));
|
||||
});
|
||||
SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(forRecipient(recipientId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the necessary job to refresh the profile of the requested recipient. Works for any
|
||||
* RecipientId, including individuals, groups, or yourself.
|
||||
*
|
||||
* Identical to {@link #enqueue(Collection)})}
|
||||
* Identical to {@link #enqueue(Set)})}
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull RecipientId recipientId) {
|
||||
@@ -100,7 +97,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
* RecipientIds, including individuals, groups, or yourself.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Collection<RecipientId> recipientIds) {
|
||||
public static void enqueue(@NonNull Set<RecipientId> recipientIds) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
|
||||
for (Job job : forRecipients(recipientIds)) {
|
||||
@@ -121,26 +118,28 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
return new RetrieveProfileJob(Stream.of(recipients).map(Recipient::getId).toList());
|
||||
return new RetrieveProfileJob(Stream.of(recipients).map(Recipient::getId).collect(Collectors.toSet()));
|
||||
} else {
|
||||
return new RetrieveProfileJob(Collections.singletonList(recipientId));
|
||||
return new RetrieveProfileJob(Collections.singleton(recipientId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Works for any RecipientId, whether it's an individual, group, or yourself.
|
||||
*
|
||||
* @return A list of length 2 or less. Two iff you are in the recipients.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull List<Job> forRecipients(@NonNull Collection<RecipientId> recipientIds) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
List<RecipientId> combined = new LinkedList<>();
|
||||
List<Job> jobs = new LinkedList<>();
|
||||
public static @NonNull List<Job> forRecipients(@NonNull Set<RecipientId> recipientIds) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
Set<RecipientId> combined = new HashSet<>(recipientIds.size());
|
||||
boolean includeSelf = false;
|
||||
|
||||
for (RecipientId recipientId : recipientIds) {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
jobs.add(new RefreshOwnProfileJob());
|
||||
includeSelf = true;
|
||||
} else if (recipient.isGroup()) {
|
||||
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
combined.addAll(Stream.of(recipients).map(Recipient::getId).toList());
|
||||
@@ -149,7 +148,15 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
jobs.add(new RetrieveProfileJob(combined));
|
||||
List<Job> jobs = new ArrayList<>(2);
|
||||
|
||||
if (includeSelf) {
|
||||
jobs.add(new RefreshOwnProfileJob());
|
||||
}
|
||||
|
||||
if (combined.size() > 0) {
|
||||
jobs.add(new RetrieveProfileJob(combined));
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
@@ -158,7 +165,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
* Will fetch some profiles to ensure we're decently up-to-date if we haven't done so within a
|
||||
* certain time period.
|
||||
*/
|
||||
public static void enqueueRoutineFetchIfNeccessary(Application application) {
|
||||
public static void enqueueRoutineFetchIfNecessary(Application application) {
|
||||
long timeSinceRefresh = System.currentTimeMillis() - SignalStore.misc().getLastProfileRefreshTime();
|
||||
if (timeSinceRefresh < TimeUnit.HOURS.toMillis(12)) {
|
||||
Log.i(TAG, "Too soon to refresh. Did the last refresh " + timeSinceRefresh + " ms ago.");
|
||||
@@ -175,7 +182,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
|
||||
if (ids.size() > 0) {
|
||||
Log.i(TAG, "Optimistically refreshing " + ids.size() + " eligible recipient(s).");
|
||||
enqueue(ids);
|
||||
enqueue(new HashSet<>(ids));
|
||||
} else {
|
||||
Log.i(TAG, "No recipients to refresh.");
|
||||
}
|
||||
@@ -184,7 +191,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
});
|
||||
}
|
||||
|
||||
private RetrieveProfileJob(@NonNull List<RecipientId> recipientIds) {
|
||||
private RetrieveProfileJob(@NonNull Set<RecipientId> recipientIds) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(3)
|
||||
@@ -192,7 +199,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
recipientIds);
|
||||
}
|
||||
|
||||
private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull List<RecipientId> recipientIds) {
|
||||
private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull Set<RecipientId> recipientIds) {
|
||||
super(parameters);
|
||||
this.recipientIds = recipientIds;
|
||||
}
|
||||
@@ -279,7 +286,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
@Override
|
||||
public void onFailure() {}
|
||||
|
||||
private void process(Recipient recipient, ProfileAndCredential profileAndCredential) throws IOException {
|
||||
private void process(Recipient recipient, ProfileAndCredential profileAndCredential) {
|
||||
SignalServiceProfile profile = profileAndCredential.getProfile();
|
||||
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||
|
||||
@@ -410,7 +417,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
private void setProfileAvatar(Recipient recipient, String profileAvatar) {
|
||||
private static void setProfileAvatar(Recipient recipient, String profileAvatar) {
|
||||
if (recipient.getProfileKey() == null) return;
|
||||
|
||||
if (!Util.equals(profileAvatar, recipient.getProfileAvatar())) {
|
||||
@@ -434,8 +441,8 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
|
||||
@Override
|
||||
public @NonNull RetrieveProfileJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
String[] ids = data.getStringArray(KEY_RECIPIENTS);
|
||||
List<RecipientId> recipientIds = Stream.of(ids).map(RecipientId::from).toList();
|
||||
String[] ids = data.getStringArray(KEY_RECIPIENTS);
|
||||
Set<RecipientId> recipientIds = Stream.of(ids).map(RecipientId::from).collect(Collectors.toSet());
|
||||
|
||||
return new RetrieveProfileJob(parameters, recipientIds);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class SmsSendJob extends SendJob {
|
||||
}
|
||||
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(messageId);
|
||||
SmsMessageRecord record = database.getMessageRecord(messageId);
|
||||
|
||||
if (!record.isPending() && !record.isFailed()) {
|
||||
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
|
||||
@@ -92,7 +92,7 @@ public class SmsSentJob extends BaseJob {
|
||||
private void handleSentResult(long messageId, int result) {
|
||||
try {
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(messageId);
|
||||
SmsMessageRecord record = database.getMessageRecord(messageId);
|
||||
|
||||
switch (result) {
|
||||
case Activity.RESULT_OK:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
|
||||
public class EmojiValues extends SignalStoreValues {
|
||||
|
||||
private static final String PREFIX = "emojiPref__";
|
||||
|
||||
EmojiValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onFirstEverAppLaunch() {
|
||||
|
||||
}
|
||||
|
||||
public void setPreferredVariation(@NonNull String emoji) {
|
||||
String canonical = EmojiUtil.getCanonicalRepresentation(emoji);
|
||||
|
||||
if (canonical.equals(emoji)) {
|
||||
getStore().beginWrite().remove(PREFIX + canonical).apply();
|
||||
} else {
|
||||
putString(PREFIX + canonical, emoji);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull String getPreferredVariation(@NonNull String emoji) {
|
||||
String canonical = EmojiUtil.getCanonicalRepresentation(emoji);
|
||||
|
||||
return getString(PREFIX + canonical, emoji);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public final class SignalStore {
|
||||
private final TooltipValues tooltipValues;
|
||||
private final MiscellaneousValues misc;
|
||||
private final InternalValues internalValues;
|
||||
private final EmojiValues emojiValues;
|
||||
|
||||
private SignalStore() {
|
||||
this.store = ApplicationDependencies.getKeyValueStore();
|
||||
@@ -36,6 +37,7 @@ public final class SignalStore {
|
||||
this.tooltipValues = new TooltipValues(store);
|
||||
this.misc = new MiscellaneousValues(store);
|
||||
this.internalValues = new InternalValues(store);
|
||||
this.emojiValues = new EmojiValues(store);
|
||||
}
|
||||
|
||||
public static void onFirstEverAppLaunch() {
|
||||
@@ -86,6 +88,10 @@ public final class SignalStore {
|
||||
return INSTANCE.internalValues;
|
||||
}
|
||||
|
||||
public static @NonNull EmojiValues emojiValues() {
|
||||
return INSTANCE.emojiValues;
|
||||
}
|
||||
|
||||
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
|
||||
return new GroupsV2AuthorizationSignalStoreCache(getStore());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@@ -80,7 +81,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
else if (messageRecord.isOutgoing()) conversationItem = (ConversationItem) sentStub.inflate();
|
||||
else conversationItem = (ConversationItem) receivedStub.inflate();
|
||||
}
|
||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), messageRecord.getRecipient(), null, false);
|
||||
conversationItem.bind(new ConversationMessage(messageRecord), Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), messageRecord.getRecipient(), null, false);
|
||||
}
|
||||
|
||||
private void bindErrorState(MessageRecord messageRecord) {
|
||||
|
||||
@@ -65,7 +65,7 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
||||
if (recipient.isGroup()) {
|
||||
question.setText(R.string.MessageRequestBottomView_unblock_this_group_and_share_your_name_and_photo_with_its_members);
|
||||
} else {
|
||||
String name = recipient.getProfileName().isEmpty() ? recipient.getDisplayName(getContext()) : recipient.getProfileName().getGivenName();
|
||||
String name = recipient.getShortDisplayName(getContext());
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them, HtmlUtil.bold(name)), 0));
|
||||
}
|
||||
setActiveInactiveGroups(blockedButtons, normalButtons);
|
||||
@@ -77,7 +77,7 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
||||
question.setText(R.string.MessageRequestBottomView_do_you_want_to_join_this_group_they_wont_know_youve_seen_their_messages_until_you_accept);
|
||||
}
|
||||
} else {
|
||||
String name = recipient.getProfileName().isEmpty() ? recipient.getDisplayName(getContext()) : recipient.getProfileName().getGivenName();
|
||||
String name = recipient.getShortDisplayName(getContext());
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept, HtmlUtil.bold(name)), 0));
|
||||
}
|
||||
setActiveInactiveGroups(normalButtons, blockedButtons);
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.mms;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
@@ -11,6 +13,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
|
||||
@@ -123,20 +126,15 @@ public final class MessageGroupContext {
|
||||
|
||||
@Override
|
||||
public @NonNull List<RecipientId> getMembersListExcludingSelf() {
|
||||
List<GroupContext.Member> membersList = groupContext.getMembersList();
|
||||
if (membersList.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
LinkedList<RecipientId> members = new LinkedList<>();
|
||||
RecipientId selfId = Recipient.self().getId();
|
||||
|
||||
for (GroupContext.Member member : membersList) {
|
||||
RecipientId recipient = RecipientId.from(UuidUtil.parseOrNull(member.getUuid()), member.getE164());
|
||||
if (!Recipient.self().getId().equals(recipient)) {
|
||||
members.add(recipient);
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
return Stream.of(groupContext.getMembersList())
|
||||
.map(GroupContext.Member::getE164)
|
||||
.withoutNulls()
|
||||
.map(e164 -> new SignalServiceAddress(null, e164))
|
||||
.map(RecipientId::from)
|
||||
.filterNot(selfId::equals)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
public class UnknownSenderView extends FrameLayout {
|
||||
|
||||
@@ -50,10 +51,14 @@ public class UnknownSenderView extends FrameLayout {
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient.getId(), true);
|
||||
if (threadId != -1) DatabaseFactory.getThreadDatabase(context).setHasSent(threadId, true);
|
||||
listener.onActionTaken();
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
listener.onActionTaken();
|
||||
}
|
||||
}.executeOnExecutor(SignalExecutors.BOUNDED);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
@@ -78,10 +83,14 @@ public class UnknownSenderView extends FrameLayout {
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
|
||||
if (threadId != -1) DatabaseFactory.getThreadDatabase(context).setHasSent(threadId, true);
|
||||
listener.onActionTaken();
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
listener.onActionTaken();
|
||||
}
|
||||
}.executeOnExecutor(SignalExecutors.BOUNDED);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -22,19 +22,20 @@ class EditProfileViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<String> givenName = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> familyName = new MutableLiveData<>();
|
||||
private final LiveData<ProfileName> internalProfileName = Transformations.map(new LiveDataPair<>(givenName, familyName),
|
||||
pair -> ProfileName.fromParts(pair.first(), pair.second()));
|
||||
private final LiveData<String> trimmedGivenName = Transformations.map(givenName, StringUtil::trimToVisualBounds);
|
||||
private final LiveData<String> trimmedFamilyName = Transformations.map(familyName, StringUtil::trimToVisualBounds);
|
||||
private final LiveData<ProfileName> internalProfileName = LiveDataUtil.combineLatest(trimmedGivenName, trimmedFamilyName, ProfileName::fromParts);
|
||||
private final MutableLiveData<byte[]> internalAvatar = new MutableLiveData<>();
|
||||
private final MutableLiveData<byte[]> originalAvatar = new MutableLiveData<>();
|
||||
private final MutableLiveData<Optional<String>> internalUsername = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> originalDisplayName = new MutableLiveData<>();
|
||||
private final LiveData<Boolean> isFormValid = Transformations.map(givenName, name -> !StringUtil.isVisuallyEmpty(name));
|
||||
private final LiveData<Boolean> isFormValid = Transformations.map(trimmedGivenName, s -> s.length() > 0);
|
||||
private final EditProfileRepository repository;
|
||||
private final GroupId groupId;
|
||||
|
||||
private EditProfileViewModel(@NonNull EditProfileRepository repository, boolean hasInstanceState, @Nullable GroupId groupId) {
|
||||
this.repository = repository;
|
||||
this.groupId = groupId;
|
||||
this.repository = repository;
|
||||
this.groupId = groupId;
|
||||
|
||||
repository.getCurrentUsername(internalUsername::postValue);
|
||||
|
||||
@@ -141,9 +142,8 @@ class EditProfileViewModel extends ViewModel {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new EditProfileViewModel(repository, hasInstanceState, groupId);
|
||||
}
|
||||
|
||||
@@ -2,15 +2,25 @@ package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
final class EmojiCount {
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final int count;
|
||||
import java.util.List;
|
||||
|
||||
EmojiCount(@NonNull String baseEmoji, @NonNull String emoji, int count) {
|
||||
final class EmojiCount {
|
||||
|
||||
static EmojiCount all(@NonNull List<ReactionDetails> reactions) {
|
||||
return new EmojiCount("", "", reactions);
|
||||
}
|
||||
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final List<ReactionDetails> reactions;
|
||||
|
||||
EmojiCount(@NonNull String baseEmoji,
|
||||
@NonNull String emoji,
|
||||
@NonNull List<ReactionDetails> reactions)
|
||||
{
|
||||
this.baseEmoji = baseEmoji;
|
||||
this.displayEmoji = emoji;
|
||||
this.count = count;
|
||||
this.reactions = reactions;
|
||||
}
|
||||
|
||||
public @NonNull String getBaseEmoji() {
|
||||
@@ -22,6 +32,10 @@ final class EmojiCount {
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
return reactions.size();
|
||||
}
|
||||
|
||||
public @NonNull List<ReactionDetails> getReactions() {
|
||||
return reactions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
public class ReactionDetails {
|
||||
private final Recipient sender;
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final long timestamp;
|
||||
|
||||
ReactionDetails(@NonNull Recipient sender, @NonNull String baseEmoji, @NonNull String displayEmoji, long timestamp) {
|
||||
this.sender = sender;
|
||||
this.baseEmoji = baseEmoji;
|
||||
this.displayEmoji = displayEmoji;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public @NonNull String getBaseEmoji() {
|
||||
return baseEmoji;
|
||||
}
|
||||
|
||||
public @NonNull String getDisplayEmoji() {
|
||||
return displayEmoji;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmojiCountAdapter.ViewHolder> {
|
||||
|
||||
private List<EmojiCount> emojiCountList = Collections.emptyList();
|
||||
private int totalCount = 0;
|
||||
private int selectedPosition = -1;
|
||||
|
||||
private final OnEmojiCountSelectedListener onEmojiCountSelectedListener;
|
||||
|
||||
ReactionEmojiCountAdapter(@NonNull OnEmojiCountSelectedListener onEmojiCountSelectedListener) {
|
||||
this.onEmojiCountSelectedListener = onEmojiCountSelectedListener;
|
||||
}
|
||||
|
||||
void updateData(@NonNull List<EmojiCount> newEmojiCount) {
|
||||
if (selectedPosition != -1 && selectedPosition != 0) {
|
||||
int emojiPosition = selectedPosition - 1;
|
||||
EmojiCount oldSelection = emojiCountList.get(emojiPosition);
|
||||
int newPosition = -1;
|
||||
|
||||
for (int i = 0; i < newEmojiCount.size(); i++) {
|
||||
if (newEmojiCount.get(i).getBaseEmoji().equals(oldSelection.getBaseEmoji())) {
|
||||
newPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newPosition == -1 && !newEmojiCount.isEmpty()) {
|
||||
selectedPosition = 0;
|
||||
onEmojiCountSelectedListener.onSelected(null);
|
||||
} else {
|
||||
selectedPosition = newPosition + 1;
|
||||
}
|
||||
} else if (!newEmojiCount.isEmpty()) {
|
||||
selectedPosition = 0;
|
||||
onEmojiCountSelectedListener.onSelected(null);
|
||||
}
|
||||
|
||||
this.emojiCountList = newEmojiCount;
|
||||
|
||||
this.totalCount = Stream.of(emojiCountList).reduce(0, (sum, e) -> sum + e.getCount());
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_emoji_item, parent, false), position -> {
|
||||
if (position != -1 && position != selectedPosition) {
|
||||
onEmojiCountSelectedListener.onSelected(position == 0 ? null : emojiCountList.get(position - 1).getBaseEmoji());
|
||||
|
||||
int oldPosition = selectedPosition;
|
||||
selectedPosition = position;
|
||||
|
||||
notifyItemChanged(oldPosition);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
if (position == 0) {
|
||||
holder.bind(null, totalCount, selectedPosition == position);
|
||||
} else {
|
||||
EmojiCount item = emojiCountList.get(position - 1);
|
||||
holder.bind(item.getDisplayEmoji(), item.getCount(), selectedPosition == position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 1 + emojiCountList.size();
|
||||
}
|
||||
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final Drawable selectedBackground;
|
||||
private final EmojiTextView emojiView;
|
||||
private final TextView countView;
|
||||
|
||||
ViewHolder(@NonNull View itemView, @NonNull OnViewHolderClickListener onClickListener) {
|
||||
super(itemView);
|
||||
emojiView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_emoji);
|
||||
countView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_text );
|
||||
selectedBackground = ThemeUtil.getThemedDrawable(itemView.getContext(), R.attr.reactions_bottom_dialog_fragment_emoji_selected);
|
||||
|
||||
itemView.setOnClickListener(v -> onClickListener.onClick(getAdapterPosition()));
|
||||
}
|
||||
|
||||
void bind(@Nullable String emoji, int count, boolean selected) {
|
||||
if (emoji != null) {
|
||||
emojiView.setVisibility(View.VISIBLE);
|
||||
emojiView.setText(emoji);
|
||||
countView.setText(String.valueOf(count));
|
||||
} else {
|
||||
emojiView.setVisibility(View.GONE);
|
||||
countView.setText(itemView.getContext().getString(R.string.ReactionsBottomSheetDialogFragment_all, count));
|
||||
}
|
||||
itemView.setBackground(selected ? selectedBackground : null);
|
||||
}
|
||||
}
|
||||
|
||||
interface OnViewHolderClickListener {
|
||||
void onClick(int position);
|
||||
}
|
||||
|
||||
interface OnEmojiCountSelectedListener {
|
||||
void onSelected(@Nullable String emoji);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader.Reaction;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -19,9 +18,9 @@ import java.util.List;
|
||||
|
||||
final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecipientsAdapter.ViewHolder> {
|
||||
|
||||
private List<Reaction> data = Collections.emptyList();
|
||||
private List<ReactionDetails> data = Collections.emptyList();
|
||||
|
||||
public void updateData(List<Reaction> newData) {
|
||||
public void updateData(List<ReactionDetails> newData) {
|
||||
data = newData;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
@@ -44,7 +43,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
|
||||
return data.size();
|
||||
}
|
||||
|
||||
final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView recipient;
|
||||
@@ -58,7 +57,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
|
||||
emoji = itemView.findViewById(R.id.reactions_bottom_view_recipient_emoji);
|
||||
}
|
||||
|
||||
void bind(@NonNull Reaction reaction) {
|
||||
void bind(@NonNull ReactionDetails reaction) {
|
||||
this.emoji.setText(reaction.getDisplayEmoji());
|
||||
|
||||
if (reaction.getSender().isLocalNumber()) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ReactionViewPagerAdapter provides pages to a ViewPager2 which contains the reactions on a given message.
|
||||
*/
|
||||
class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPagerAdapter.ViewHolder> {
|
||||
|
||||
private int selectedPosition = 0;
|
||||
|
||||
protected ReactionViewPagerAdapter() {
|
||||
super(new AlwaysChangedDiffUtil<>());
|
||||
}
|
||||
|
||||
@NonNull EmojiCount getEmojiCount(int position) {
|
||||
return getItem(position);
|
||||
}
|
||||
|
||||
void enableNestedScrollingForPosition(int position) {
|
||||
selectedPosition = position;
|
||||
|
||||
notifyItemRangeChanged(0, getItemCount(), new Object());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recycler, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position);
|
||||
} else {
|
||||
holder.setSelected(selectedPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.onBind(getItem(position));
|
||||
holder.setSelected(selectedPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
recyclerView.setNestedScrollingEnabled(false);
|
||||
ViewGroup.LayoutParams params = recyclerView.getLayoutParams();
|
||||
params.height = (int) (recyclerView.getResources().getDisplayMetrics().heightPixels * 0.80);
|
||||
recyclerView.setLayoutParams(params);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final RecyclerView recycler;
|
||||
private final ReactionRecipientsAdapter adapter = new ReactionRecipientsAdapter();
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
recycler = (RecyclerView) itemView;
|
||||
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
recycler.setLayoutParams(params);
|
||||
recycler.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void onBind(@NonNull EmojiCount emojiCount) {
|
||||
adapter.updateData(emojiCount.getReactions());
|
||||
}
|
||||
|
||||
public void setSelected(int position) {
|
||||
recycler.setNestedScrollingEnabled(getAdapterPosition() == position);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,32 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogFragment {
|
||||
|
||||
@@ -23,12 +34,11 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
private static final String ARGS_IS_MMS = "reactions.args.is.mms";
|
||||
|
||||
private long messageId;
|
||||
private RecyclerView recipientRecyclerView;
|
||||
private RecyclerView emojiRecyclerView;
|
||||
private ViewPager2 recipientPagerView;
|
||||
private ReactionsLoader reactionsLoader;
|
||||
private ReactionRecipientsAdapter recipientsAdapter;
|
||||
private ReactionEmojiCountAdapter emojiCountAdapter;
|
||||
private ReactionViewPagerAdapter recipientsAdapter;
|
||||
private ReactionsViewModel viewModel;
|
||||
private Callback callback;
|
||||
|
||||
public static DialogFragment create(long messageId, boolean isMms) {
|
||||
Bundle args = new Bundle();
|
||||
@@ -42,13 +52,20 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
callback = (Callback) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
||||
if (ThemeUtil.isDarkTheme(requireContext())) {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed);
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny);
|
||||
} else {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_Light_BottomSheetDialog_Fixed);
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny);
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -63,16 +80,56 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
recipientRecyclerView = view.findViewById(R.id.reactions_bottom_view_recipient_recycler);
|
||||
emojiRecyclerView = view.findViewById(R.id.reactions_bottom_view_emoji_recycler);
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
emojiRecyclerView.setNestedScrollingEnabled(false);
|
||||
messageId = getArguments().getLong(ARGS_MESSAGE_ID);
|
||||
if (savedInstanceState == null) {
|
||||
FrameLayout container = requireDialog().findViewById(R.id.container);
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(requireContext());
|
||||
View statusBarShader = layoutInflater.inflate(R.layout.react_with_any_emoji_status_fade, container, false);
|
||||
TabLayout emojiTabs = (TabLayout) layoutInflater.inflate(R.layout.reactions_bottom_sheet_dialog_fragment_tabs, container, false);
|
||||
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtil.getStatusBarHeight(container));
|
||||
|
||||
statusBarShader.setLayoutParams(params);
|
||||
|
||||
container.addView(statusBarShader, 0);
|
||||
container.addView(emojiTabs);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets());
|
||||
|
||||
new TabLayoutMediator(emojiTabs, recipientPagerView, (tab, position) -> {
|
||||
tab.setCustomView(R.layout.reactions_bottom_sheet_dialog_fragment_emoji_item);
|
||||
|
||||
View customView = Objects.requireNonNull(tab.getCustomView());
|
||||
EmojiImageView emoji = customView.findViewById(R.id.reactions_bottom_view_emoji_item_emoji);
|
||||
TextView text = customView.findViewById(R.id.reactions_bottom_view_emoji_item_text);
|
||||
EmojiCount emojiCount = recipientsAdapter.getEmojiCount(position);
|
||||
|
||||
if (position != 0) {
|
||||
emoji.setVisibility(View.VISIBLE);
|
||||
emoji.setImageEmoji(emojiCount.getDisplayEmoji());
|
||||
text.setText(String.valueOf(emojiCount.getCount()));
|
||||
} else {
|
||||
emoji.setVisibility(View.GONE);
|
||||
text.setText(requireContext().getString(R.string.ReactionsBottomSheetDialogFragment_all, emojiCount.getCount()));
|
||||
}
|
||||
}).attach();
|
||||
}
|
||||
|
||||
setUpViewModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
recipientPagerView = view.findViewById(R.id.reactions_bottom_view_recipient_pager);
|
||||
messageId = requireArguments().getLong(ARGS_MESSAGE_ID);
|
||||
|
||||
setUpRecipientsRecyclerView();
|
||||
setUpEmojiRecyclerView();
|
||||
setUpViewModel();
|
||||
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
requireArguments().getLong(ARGS_MESSAGE_ID),
|
||||
requireArguments().getBoolean(ARGS_IS_MMS));
|
||||
|
||||
LoaderManager.getInstance(requireActivity()).initLoader((int) messageId, null, reactionsLoader);
|
||||
}
|
||||
@@ -83,34 +140,48 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void setUpRecipientsRecyclerView() {
|
||||
recipientsAdapter = new ReactionRecipientsAdapter();
|
||||
recipientRecyclerView.setAdapter(recipientsAdapter);
|
||||
@Override
|
||||
public void onDismiss(@NonNull DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
|
||||
callback.onReactionsDialogDismissed();
|
||||
}
|
||||
|
||||
private void setUpEmojiRecyclerView() {
|
||||
emojiCountAdapter = new ReactionEmojiCountAdapter((emoji -> viewModel.setFilterEmoji(emoji)));
|
||||
emojiRecyclerView.setAdapter(emojiCountAdapter);
|
||||
private void setUpRecipientsRecyclerView() {
|
||||
recipientsAdapter = new ReactionViewPagerAdapter();
|
||||
|
||||
recipientPagerView.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
recipientPagerView.post(() -> {
|
||||
recipientsAdapter.enableNestedScrollingForPosition(position);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
if (state == ViewPager2.SCROLL_STATE_IDLE) {
|
||||
recipientPagerView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
recipientPagerView.setAdapter(recipientsAdapter);
|
||||
}
|
||||
|
||||
private void setUpViewModel() {
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
getArguments().getLong(ARGS_MESSAGE_ID),
|
||||
getArguments().getBoolean(ARGS_IS_MMS));
|
||||
|
||||
ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(reactionsLoader);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ReactionsViewModel.class);
|
||||
|
||||
viewModel.getRecipients().observe(getViewLifecycleOwner(), reactions -> {
|
||||
if (reactions.size() == 0) dismiss();
|
||||
|
||||
recipientsAdapter.updateData(reactions);
|
||||
});
|
||||
|
||||
viewModel.getEmojiCounts().observe(getViewLifecycleOwner(), emojiCounts -> {
|
||||
if (emojiCounts.size() == 0) dismiss();
|
||||
if (emojiCounts.size() <= 1) dismiss();
|
||||
|
||||
emojiCountAdapter.updateData(emojiCounts);
|
||||
recipientsAdapter.submitList(emojiCounts);
|
||||
});
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onReactionsDialogDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
||||
private final boolean isMms;
|
||||
private final Context appContext;
|
||||
|
||||
private MutableLiveData<List<Reaction>> internalLiveData = new MutableLiveData<>();
|
||||
private MutableLiveData<List<ReactionDetails>> internalLiveData = new MutableLiveData<>();
|
||||
|
||||
public ReactionsLoader(@NonNull Context context, long messageId, boolean isMms)
|
||||
{
|
||||
@@ -47,6 +47,8 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
data.moveToPosition(-1);
|
||||
|
||||
MessageRecord record = isMms ? DatabaseFactory.getMmsDatabase(appContext).readerFor(data).getNext()
|
||||
: DatabaseFactory.getSmsDatabase(appContext).readerFor(data).getNext();
|
||||
|
||||
@@ -54,10 +56,10 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
||||
internalLiveData.postValue(Collections.emptyList());
|
||||
} else {
|
||||
internalLiveData.postValue(Stream.of(record.getReactions())
|
||||
.map(reactionRecord -> new Reaction(Recipient.resolved(reactionRecord.getAuthor()),
|
||||
EmojiUtil.getCanonicalRepresentation(reactionRecord.getEmoji()),
|
||||
reactionRecord.getEmoji(),
|
||||
reactionRecord.getDateReceived()))
|
||||
.map(reactionRecord -> new ReactionDetails(Recipient.resolved(reactionRecord.getAuthor()),
|
||||
EmojiUtil.getCanonicalRepresentation(reactionRecord.getEmoji()),
|
||||
reactionRecord.getEmoji(),
|
||||
reactionRecord.getDateReceived()))
|
||||
.toList());
|
||||
}
|
||||
});
|
||||
@@ -69,7 +71,7 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<List<Reaction>> getReactions() {
|
||||
public LiveData<List<ReactionDetails>> getReactions() {
|
||||
return internalLiveData;
|
||||
}
|
||||
|
||||
@@ -103,33 +105,4 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
||||
}
|
||||
}
|
||||
|
||||
static class Reaction {
|
||||
private final Recipient sender;
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final long timestamp;
|
||||
|
||||
private Reaction(@NonNull Recipient sender, @NonNull String baseEmoji, @NonNull String displayEmoji, long timestamp) {
|
||||
this.sender = sender;
|
||||
this.baseEmoji = baseEmoji;
|
||||
this.displayEmoji = displayEmoji;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public @NonNull String getBaseEmoji() {
|
||||
return baseEmoji;
|
||||
}
|
||||
|
||||
public @NonNull String getDisplayEmoji() {
|
||||
return displayEmoji;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
@@ -12,39 +11,32 @@ import com.annimon.stream.Stream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.thoughtcrime.securesms.reactions.ReactionsLoader.*;
|
||||
|
||||
public class ReactionsViewModel extends ViewModel {
|
||||
|
||||
private final Repository repository;
|
||||
private final MutableLiveData<String> filterEmoji = new MutableLiveData<>();
|
||||
|
||||
public ReactionsViewModel(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<Reaction>> getRecipients() {
|
||||
return Transformations.switchMap(filterEmoji,
|
||||
emoji -> Transformations.map(repository.getReactions(),
|
||||
reactions -> Stream.of(reactions)
|
||||
.filter(reaction -> emoji == null || reaction.getBaseEmoji().equals(emoji))
|
||||
.toList()));
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<EmojiCount>> getEmojiCounts() {
|
||||
return Transformations.map(repository.getReactions(),
|
||||
reactionList -> Stream.of(reactionList)
|
||||
.groupBy(Reaction::getBaseEmoji)
|
||||
.sorted(this::compareReactions)
|
||||
.map(entry -> new EmojiCount(entry.getKey(), getCountDisplayEmoji(entry.getValue()), entry.getValue().size()))
|
||||
.toList());
|
||||
reactionList -> {
|
||||
List<EmojiCount> emojiCounts = Stream.of(reactionList)
|
||||
.groupBy(ReactionDetails::getBaseEmoji)
|
||||
.sorted(this::compareReactions)
|
||||
.map(entry -> new EmojiCount(entry.getKey(),
|
||||
getCountDisplayEmoji(entry.getValue()),
|
||||
entry.getValue()))
|
||||
.toList();
|
||||
|
||||
emojiCounts.add(0, EmojiCount.all(reactionList));
|
||||
|
||||
return emojiCounts;
|
||||
});
|
||||
}
|
||||
|
||||
public void setFilterEmoji(String filterEmoji) {
|
||||
this.filterEmoji.setValue(filterEmoji);
|
||||
}
|
||||
|
||||
private int compareReactions(@NonNull Map.Entry<String, List<Reaction>> lhs, @NonNull Map.Entry<String, List<Reaction>> rhs) {
|
||||
private int compareReactions(@NonNull Map.Entry<String, List<ReactionDetails>> lhs, @NonNull Map.Entry<String, List<ReactionDetails>> rhs) {
|
||||
int lengthComparison = -Integer.compare(lhs.getValue().size(), rhs.getValue().size());
|
||||
if (lengthComparison != 0) return lengthComparison;
|
||||
|
||||
@@ -54,15 +46,15 @@ public class ReactionsViewModel extends ViewModel {
|
||||
return -Long.compare(latestTimestampLhs, latestTimestampRhs);
|
||||
}
|
||||
|
||||
private long getLatestTimestamp(List<Reaction> reactions) {
|
||||
private long getLatestTimestamp(List<ReactionDetails> reactions) {
|
||||
return Stream.of(reactions)
|
||||
.max((a, b) -> Long.compare(a.getTimestamp(), b.getTimestamp()))
|
||||
.map(Reaction::getTimestamp)
|
||||
.map(ReactionDetails::getTimestamp)
|
||||
.orElse(-1L);
|
||||
}
|
||||
|
||||
private @NonNull String getCountDisplayEmoji(@NonNull List<Reaction> reactions) {
|
||||
for (Reaction reaction : reactions) {
|
||||
private @NonNull String getCountDisplayEmoji(@NonNull List<ReactionDetails> reactions) {
|
||||
for (ReactionDetails reaction : reactions) {
|
||||
if (reaction.getSender().isLocalNumber()) {
|
||||
return reaction.getDisplayEmoji();
|
||||
}
|
||||
@@ -72,7 +64,7 @@ public class ReactionsViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
interface Repository {
|
||||
LiveData<List<Reaction>> getReactions();
|
||||
LiveData<List<ReactionDetails>> getReactions();
|
||||
}
|
||||
|
||||
static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
@@ -1,53 +1,82 @@
|
||||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.widget.NestedScrollView;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ReactWithAnyEmojiAdapter extends RecyclerView.Adapter<ReactWithAnyEmojiAdapter.ViewHolder> {
|
||||
final class ReactWithAnyEmojiAdapter extends ListAdapter<ReactWithAnyEmojiPage, ReactWithAnyEmojiAdapter.ReactWithAnyEmojiPageViewHolder> {
|
||||
|
||||
private static final int VIEW_TYPE_SINGLE = 0;
|
||||
private static final int VIEW_TYPE_DUAL = 1;
|
||||
|
||||
private final List<EmojiPageModel> models;
|
||||
private final EmojiKeyboardProvider.EmojiEventListener emojiEventListener;
|
||||
private final EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener;
|
||||
private final Callbacks callbacks;
|
||||
|
||||
ReactWithAnyEmojiAdapter(@NonNull List<EmojiPageModel> models,
|
||||
@NonNull EmojiKeyboardProvider.EmojiEventListener emojiEventListener,
|
||||
ReactWithAnyEmojiAdapter(@NonNull EmojiKeyboardProvider.EmojiEventListener emojiEventListener,
|
||||
@NonNull EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener,
|
||||
@NonNull Callbacks callbacks)
|
||||
{
|
||||
this.models = models;
|
||||
super(new AlwaysChangedDiffUtil<>());
|
||||
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(new EmojiPageView(parent.getContext(), emojiEventListener, variationSelectorListener, true));
|
||||
public ReactWithAnyEmojiPage getItem(int position) {
|
||||
return super.getItem(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(models.get(position));
|
||||
public @NonNull ReactWithAnyEmojiPageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_SINGLE:
|
||||
return new SinglePageBlockViewHolder(createEmojiPageView(parent.getContext()));
|
||||
case VIEW_TYPE_DUAL:
|
||||
EmojiPageView block1 = createEmojiPageView(parent.getContext());
|
||||
EmojiPageView block2 = createEmojiPageView(parent.getContext());
|
||||
NestedScrollView scrollView = (NestedScrollView) LayoutInflater.from(parent.getContext()).inflate(R.layout.react_with_any_emoji_dual_block_item, parent, false);
|
||||
LinearLayout container = scrollView.findViewById(R.id.react_with_any_emoji_dual_block_item_container);
|
||||
|
||||
block1.setRecyclerNestedScrollingEnabled(false);
|
||||
block2.setRecyclerNestedScrollingEnabled(false);
|
||||
|
||||
container.addView(block1, 0);
|
||||
container.addView(block2);
|
||||
|
||||
return new DualPageBlockViewHolder(scrollView, block1, block2);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown viewType: " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return models.size();
|
||||
public void onBindViewHolder(@NonNull ReactWithAnyEmojiPageViewHolder holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
|
||||
callbacks.onViewHolderAttached(holder.getAdapterPosition(), holder.emojiPageView);
|
||||
public void onViewAttachedToWindow(@NonNull ReactWithAnyEmojiPageViewHolder holder) {
|
||||
callbacks.onViewHolderAttached(holder.getAdapterPosition(), holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,14 +88,32 @@ final class ReactWithAnyEmojiAdapter extends RecyclerView.Adapter<ReactWithAnyEm
|
||||
recyclerView.setHasFixedSize(true);
|
||||
}
|
||||
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return getItem(position).getPageBlocks().size() > 1 ? VIEW_TYPE_DUAL : VIEW_TYPE_SINGLE;
|
||||
}
|
||||
|
||||
private EmojiPageView createEmojiPageView(@NonNull Context context) {
|
||||
return new EmojiPageView(context, emojiEventListener, variationSelectorListener, true);
|
||||
}
|
||||
|
||||
static abstract class ReactWithAnyEmojiPageViewHolder extends RecyclerView.ViewHolder implements ScrollableChild {
|
||||
|
||||
public ReactWithAnyEmojiPageViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
abstract void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage);
|
||||
}
|
||||
|
||||
static final class SinglePageBlockViewHolder extends ReactWithAnyEmojiPageViewHolder {
|
||||
|
||||
private final EmojiPageView emojiPageView;
|
||||
|
||||
ViewHolder(@NonNull EmojiPageView itemView) {
|
||||
public SinglePageBlockViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
emojiPageView = itemView;
|
||||
emojiPageView = (EmojiPageView) itemView;
|
||||
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
@@ -74,12 +121,52 @@ final class ReactWithAnyEmojiAdapter extends RecyclerView.Adapter<ReactWithAnyEm
|
||||
emojiPageView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
void bind(@NonNull EmojiPageModel model) {
|
||||
emojiPageView.setModel(model);
|
||||
@Override
|
||||
void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage) {
|
||||
emojiPageView.setModel(reactWithAnyEmojiPage.getPageBlocks().get(0).getPageModel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNestedScrollingEnabled(boolean isEnabled) {
|
||||
emojiPageView.setRecyclerNestedScrollingEnabled(isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
static final class DualPageBlockViewHolder extends ReactWithAnyEmojiPageViewHolder {
|
||||
|
||||
private final EmojiPageView block1;
|
||||
private final EmojiPageView block2;
|
||||
private final TextView block2Label;
|
||||
|
||||
public DualPageBlockViewHolder(@NonNull View itemView,
|
||||
@NonNull EmojiPageView block1,
|
||||
@NonNull EmojiPageView block2)
|
||||
{
|
||||
super(itemView);
|
||||
|
||||
this.block1 = block1;
|
||||
this.block2 = block2;
|
||||
this.block2Label = itemView.findViewById(R.id.react_with_any_emoji_dual_block_item_block_2_label);
|
||||
}
|
||||
|
||||
@Override
|
||||
void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage) {
|
||||
block1.setModel(reactWithAnyEmojiPage.getPageBlocks().get(0).getPageModel());
|
||||
block2.setModel(reactWithAnyEmojiPage.getPageBlocks().get(1).getPageModel());
|
||||
block2Label.setText(reactWithAnyEmojiPage.getPageBlocks().get(1).getLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNestedScrollingEnabled(boolean isEnabled) {
|
||||
((NestedScrollView) itemView).setNestedScrollingEnabled(isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
void onViewHolderAttached(int adapterPosition, EmojiPageView pageView);
|
||||
void onViewHolderAttached(int adapterPosition, ScrollableChild pageView);
|
||||
}
|
||||
|
||||
interface ScrollableChild {
|
||||
void setNestedScrollingEnabled(boolean isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
@@ -35,6 +36,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -48,13 +50,14 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
private static final String ARG_MESSAGE_ID = "arg_message_id";
|
||||
private static final String ARG_IS_MMS = "arg_is_mms";
|
||||
|
||||
private ReactWithAnyEmojiViewModel viewModel;
|
||||
private TextSwitcher categoryLabel;
|
||||
private ViewPager2 categoryPager;
|
||||
private ReactWithAnyEmojiAdapter adapter;
|
||||
private OnPageChanged onPageChanged;
|
||||
private SparseArray<EmojiPageView> pageArray = new SparseArray<>();
|
||||
private Callback callback;
|
||||
private ReactWithAnyEmojiViewModel viewModel;
|
||||
private TextSwitcher categoryLabel;
|
||||
private ViewPager2 categoryPager;
|
||||
private ReactWithAnyEmojiAdapter adapter;
|
||||
private OnPageChanged onPageChanged;
|
||||
private SparseArray<ReactWithAnyEmojiAdapter.ScrollableChild> pageArray = new SparseArray<>();
|
||||
private Callback callback;
|
||||
private ReactionsLoader reactionsLoader;
|
||||
|
||||
public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord) {
|
||||
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
|
||||
@@ -122,12 +125,18 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
requireArguments().getLong(ARG_MESSAGE_ID),
|
||||
requireArguments().getBoolean(ARG_IS_MMS));
|
||||
|
||||
LoaderManager.getInstance(requireActivity()).initLoader((int) requireArguments().getLong(ARG_MESSAGE_ID), null, reactionsLoader);
|
||||
|
||||
initializeViewModel();
|
||||
|
||||
categoryLabel = view.findViewById(R.id.category_label);
|
||||
categoryPager = view.findViewById(R.id.category_pager);
|
||||
categoryLabel = view.findViewById(R.id.category_label);
|
||||
categoryPager = view.findViewById(R.id.category_pager);
|
||||
|
||||
adapter = new ReactWithAnyEmojiAdapter(viewModel.getEmojiPageModels(), this, this, (position, pageView) -> {
|
||||
adapter = new ReactWithAnyEmojiAdapter(this, this, (position, pageView) -> {
|
||||
pageArray.put(position, pageView);
|
||||
|
||||
if (categoryPager.getCurrentItem() == position) {
|
||||
@@ -140,10 +149,15 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
categoryPager.setAdapter(adapter);
|
||||
categoryPager.registerOnPageChangeCallback(onPageChanged);
|
||||
|
||||
int startPateIndex = viewModel.getStartIndex();
|
||||
viewModel.getEmojiPageModels().observe(getViewLifecycleOwner(), pages -> {
|
||||
int pageToSet = adapter.getItemCount() == 0 ? (pages.get(0).hasEmoji() ? 0 : 1) : -1;
|
||||
|
||||
categoryPager.setCurrentItem(startPateIndex, false);
|
||||
presentCategoryLabel(viewModel.getCategoryIconAttr(startPateIndex));
|
||||
adapter.submitList(pages);
|
||||
|
||||
if (pageToSet >= 0) {
|
||||
categoryPager.setCurrentItem(pageToSet);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,7 +180,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
|
||||
new TabLayoutMediator(categoryTabs, categoryPager, (tab, position) -> {
|
||||
tab.setCustomView(react_with_any_emoji_tab)
|
||||
.setIcon(ThemeUtil.getThemedDrawable(requireContext(), viewModel.getCategoryIconAttr(position)));
|
||||
.setIcon(ThemeUtil.getThemedDrawable(requireContext(), adapter.getItem(position).getIconAttr()));
|
||||
}).attach();
|
||||
}
|
||||
}
|
||||
@@ -174,6 +188,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
LoaderManager.getInstance(requireActivity()).destroyLoader((int) requireArguments().getLong(ARG_MESSAGE_ID));
|
||||
|
||||
categoryPager.unregisterOnPageChangeCallback(onPageChanged);
|
||||
}
|
||||
@@ -188,7 +203,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
private void initializeViewModel() {
|
||||
Bundle args = requireArguments();
|
||||
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext());
|
||||
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
|
||||
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsLoader, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class);
|
||||
}
|
||||
@@ -210,53 +225,16 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
||||
|
||||
private void updateFocusedRecycler(int position) {
|
||||
for (int i = 0; i < pageArray.size(); i++) {
|
||||
pageArray.valueAt(i).setRecyclerNestedScrollingEnabled(false);
|
||||
pageArray.valueAt(i).setNestedScrollingEnabled(false);
|
||||
}
|
||||
|
||||
EmojiPageView toFocus = pageArray.get(position);
|
||||
ReactWithAnyEmojiAdapter.ScrollableChild toFocus = pageArray.get(position);
|
||||
if (toFocus != null) {
|
||||
toFocus.setRecyclerNestedScrollingEnabled(true);
|
||||
toFocus.setNestedScrollingEnabled(true);
|
||||
categoryPager.requestLayout();
|
||||
}
|
||||
|
||||
presentCategoryLabel(viewModel.getCategoryIconAttr(position));
|
||||
}
|
||||
|
||||
private void presentCategoryLabel(@AttrRes int iconAttr) {
|
||||
switch (iconAttr) {
|
||||
case R.attr.emoji_category_recent:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used));
|
||||
break;
|
||||
case R.attr.emoji_category_people:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people));
|
||||
break;
|
||||
case R.attr.emoji_category_nature:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature));
|
||||
break;
|
||||
case R.attr.emoji_category_foods:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food));
|
||||
break;
|
||||
case R.attr.emoji_category_activity:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities));
|
||||
break;
|
||||
case R.attr.emoji_category_places:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places));
|
||||
break;
|
||||
case R.attr.emoji_category_objects:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects));
|
||||
break;
|
||||
case R.attr.emoji_category_symbols:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols));
|
||||
break;
|
||||
case R.attr.emoji_category_flags:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags));
|
||||
break;
|
||||
case R.attr.emoji_category_emoticons:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
categoryLabel.setText(getString(adapter.getItem(position).getLabel()));
|
||||
}
|
||||
|
||||
private class OnPageChanged extends ViewPager2.OnPageChangeCallback {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a swipeable page in the ReactWithAnyEmoji dialog fragment, encapsulating any
|
||||
* {@link ReactWithAnyEmojiPageBlock}s contained on that page. It is assumed that there is at least
|
||||
* one page present.
|
||||
*
|
||||
* This class also exposes several properties based off of that list, in order to allow the ReactWithAny
|
||||
* bottom sheet to properly lay out its tabs and assign labels as the user moves between pages.
|
||||
*/
|
||||
class ReactWithAnyEmojiPage {
|
||||
|
||||
private final List<ReactWithAnyEmojiPageBlock> pageBlocks;
|
||||
|
||||
ReactWithAnyEmojiPage(@NonNull List<ReactWithAnyEmojiPageBlock> pageBlocks) {
|
||||
Preconditions.checkArgument(!pageBlocks.isEmpty());
|
||||
|
||||
this.pageBlocks = pageBlocks;
|
||||
}
|
||||
|
||||
public @StringRes int getLabel() {
|
||||
return pageBlocks.get(0).getLabel();
|
||||
}
|
||||
|
||||
public boolean hasEmoji() {
|
||||
return !pageBlocks.get(0).getPageModel().getEmoji().isEmpty();
|
||||
}
|
||||
|
||||
public List<ReactWithAnyEmojiPageBlock> getPageBlocks() {
|
||||
return pageBlocks;
|
||||
}
|
||||
|
||||
public @AttrRes int getIconAttr() {
|
||||
return pageBlocks.get(0).getPageModel().getIconAttr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
|
||||
/**
|
||||
* Wraps a single "class" of Emojis, be it a predefined category, recents, etc. and provides
|
||||
* a label for that "class".
|
||||
*/
|
||||
class ReactWithAnyEmojiPageBlock {
|
||||
|
||||
private final int label;
|
||||
private final EmojiPageModel pageModel;
|
||||
|
||||
ReactWithAnyEmojiPageBlock(@StringRes int label, @NonNull EmojiPageModel pageModel) {
|
||||
this.label = label;
|
||||
this.pageModel = pageModel;
|
||||
}
|
||||
|
||||
public @StringRes int getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public EmojiPageModel getPageModel() {
|
||||
return pageModel;
|
||||
}
|
||||
}
|
||||
@@ -2,44 +2,117 @@ package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionDetails;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
final class ReactWithAnyEmojiRepository {
|
||||
|
||||
private static final String TAG = Log.tag(ReactWithAnyEmojiRepository.class);
|
||||
|
||||
private static final String RECENT_STORAGE_KEY = "reactions_recent_emoji";
|
||||
|
||||
private final Context context;
|
||||
private final RecentEmojiPageModel recentEmojiPageModel;
|
||||
private final List<EmojiPageModel> emojiPageModels;
|
||||
private final List<ReactWithAnyEmojiPage> emojiPages;
|
||||
|
||||
ReactWithAnyEmojiRepository(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.recentEmojiPageModel = new RecentEmojiPageModel(context, RECENT_STORAGE_KEY);
|
||||
this.emojiPageModels = new LinkedList<>();
|
||||
this.emojiPages = new LinkedList<>();
|
||||
|
||||
emojiPageModels.add(recentEmojiPageModel);
|
||||
emojiPageModels.addAll(EmojiUtil.getDisplayPages());
|
||||
emojiPageModels.remove(emojiPageModels.size() - 1);
|
||||
emojiPages.addAll(Stream.of(EmojiUtil.getDisplayPages())
|
||||
.map(page -> new ReactWithAnyEmojiPage(Collections.singletonList(new ReactWithAnyEmojiPageBlock(getCategoryLabel(page.getIconAttr()), page))))
|
||||
.toList());
|
||||
emojiPages.remove(emojiPages.size() - 1);
|
||||
}
|
||||
|
||||
List<EmojiPageModel> getEmojiPageModels() {
|
||||
return emojiPageModels;
|
||||
List<ReactWithAnyEmojiPage> getEmojiPageModels(@NonNull List<ReactionDetails> thisMessagesReactions) {
|
||||
List<ReactWithAnyEmojiPage> pages = new LinkedList<>();
|
||||
List<String> thisMessage = Stream.of(thisMessagesReactions)
|
||||
.map(ReactionDetails::getDisplayEmoji)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (thisMessage.isEmpty()) {
|
||||
pages.add(new ReactWithAnyEmojiPage(Collections.singletonList(new ReactWithAnyEmojiPageBlock(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used, recentEmojiPageModel))));
|
||||
} else {
|
||||
pages.add(new ReactWithAnyEmojiPage(Arrays.asList(new ReactWithAnyEmojiPageBlock(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__this_message, new ThisMessageEmojiPageModel(thisMessage)),
|
||||
new ReactWithAnyEmojiPageBlock(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used, recentEmojiPageModel))));
|
||||
}
|
||||
|
||||
pages.addAll(emojiPages);
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
void addEmojiToMessage(@NonNull String emoji, long messageId, boolean isMms) {
|
||||
recentEmojiPageModel.onCodePointSelected(emoji);
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
MessagingDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
MessageRecord messageRecord = db.getMessageRecord(messageId);
|
||||
ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
|
||||
.filter(record -> record.getAuthor().equals(Recipient.self().getId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> MessageSender.sendNewReaction(context, messageId, isMms, emoji));
|
||||
if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) {
|
||||
MessageSender.sendReactionRemoval(context, messageRecord.getId(), messageRecord.isMms(), oldRecord);
|
||||
} else {
|
||||
MessageSender.sendNewReaction(context, messageRecord.getId(), messageRecord.isMms(), emoji);
|
||||
Util.runOnMain(() -> recentEmojiPageModel.onCodePointSelected(emoji));
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Message not found! Ignoring.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @StringRes int getCategoryLabel(@AttrRes int iconAttr) {
|
||||
switch (iconAttr) {
|
||||
case R.attr.emoji_category_people:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people;
|
||||
case R.attr.emoji_category_nature:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature;
|
||||
case R.attr.emoji_category_foods:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food;
|
||||
case R.attr.emoji_category_activity:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities;
|
||||
case R.attr.emoji_category_places:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places;
|
||||
case R.attr.emoji_category_objects:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects;
|
||||
case R.attr.emoji_category_symbols:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols;
|
||||
case R.attr.emoji_category_flags:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags;
|
||||
case R.attr.emoji_category_emoticons:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,57 +2,65 @@ package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
|
||||
private final ReactionsLoader reactionsLoader;
|
||||
private final ReactWithAnyEmojiRepository repository;
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
|
||||
private ReactWithAnyEmojiViewModel(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
private final LiveData<List<ReactWithAnyEmojiPage>> pages;
|
||||
|
||||
private ReactWithAnyEmojiViewModel(@NonNull ReactionsLoader reactionsLoader,
|
||||
@NonNull ReactWithAnyEmojiRepository repository,
|
||||
long messageId,
|
||||
boolean isMms) {
|
||||
this.reactionsLoader = reactionsLoader;
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.pages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels);
|
||||
}
|
||||
|
||||
List<EmojiPageModel> getEmojiPageModels() {
|
||||
return repository.getEmojiPageModels();
|
||||
}
|
||||
|
||||
int getStartIndex() {
|
||||
return repository.getEmojiPageModels().get(0).getEmoji().size() == 0 ? 1 : 0;
|
||||
LiveData<List<ReactWithAnyEmojiPage>> getEmojiPageModels() {
|
||||
return pages;
|
||||
}
|
||||
|
||||
void onEmojiSelected(@NonNull String emoji) {
|
||||
SignalStore.emojiValues().setPreferredVariation(emoji);
|
||||
repository.addEmojiToMessage(emoji, messageId, isMms);
|
||||
}
|
||||
|
||||
@AttrRes int getCategoryIconAttr(int position) {
|
||||
return repository.getEmojiPageModels().get(position).getIconAttr();
|
||||
}
|
||||
|
||||
static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final ReactionsLoader reactionsLoader;
|
||||
private final ReactWithAnyEmojiRepository repository;
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
|
||||
Factory(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
Factory(@NonNull ReactionsLoader reactionsLoader, @NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
|
||||
this.reactionsLoader = reactionsLoader;
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms));
|
||||
return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.Emoji;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Contains the Emojis that have been used in reactions for a given message.
|
||||
*/
|
||||
class ThisMessageEmojiPageModel implements EmojiPageModel {
|
||||
|
||||
private final List<String> emoji;
|
||||
|
||||
ThisMessageEmojiPageModel(@NonNull List<String> emoji) {
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconAttr() {
|
||||
return R.attr.emoji_category_recent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<String> getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Emoji> getDisplayEmoji() {
|
||||
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSpriteMap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getSprite() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDynamic() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -26,19 +26,18 @@ import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientIdResult;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@@ -157,6 +156,20 @@ public class Recipient {
|
||||
return externalPush(context, signalServiceAddress.getUuid().orNull(), signalServiceAddress.getNumber().orNull(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-populated {@link Recipient} based off of a {@link SignalServiceAddress},
|
||||
* creating one in the database if necessary. We special-case GV1 members because we want to
|
||||
* prioritize E164 addresses and not use the UUIDs if possible.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull Recipient externalGV1Member(@NonNull Context context, @NonNull SignalServiceAddress address) {
|
||||
if (address.getNumber().isPresent()) {
|
||||
return externalPush(context, null, address.getNumber().get(), false);
|
||||
} else {
|
||||
return externalPush(context, address.getUuid().orNull(), null, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-populated {@link Recipient} based off of a {@link SignalServiceAddress},
|
||||
* creating one in the database if necessary. This should only used for high-trust sources,
|
||||
@@ -386,18 +399,22 @@ public class Recipient {
|
||||
}
|
||||
|
||||
public @NonNull String getDisplayName(@NonNull Context context) {
|
||||
return Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().toString(),
|
||||
getDisplayUsername(),
|
||||
e164,
|
||||
email,
|
||||
context.getString(R.string.Recipient_unknown));
|
||||
String name = Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().toString(),
|
||||
getDisplayUsername(),
|
||||
e164,
|
||||
email,
|
||||
context.getString(R.string.Recipient_unknown));
|
||||
|
||||
return StringUtil.isolateBidi(name);
|
||||
}
|
||||
|
||||
public @NonNull String getShortDisplayName(@NonNull Context context) {
|
||||
return Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().getGivenName(),
|
||||
getDisplayName(context));
|
||||
String name = Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().getGivenName(),
|
||||
getDisplayName(context));
|
||||
|
||||
return StringUtil.isolateBidi(name);
|
||||
}
|
||||
|
||||
public @NonNull MaterialColor getColor() {
|
||||
@@ -784,7 +801,7 @@ public class Recipient {
|
||||
return ApplicationDependencies.getRecipientCache().getLive(id);
|
||||
}
|
||||
|
||||
private @Nullable String getDisplayUsername() {
|
||||
public @Nullable String getDisplayUsername() {
|
||||
if (!TextUtils.isEmpty(username)) {
|
||||
return "@" + username;
|
||||
} else {
|
||||
|
||||
@@ -4,9 +4,11 @@ import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.ringrtc.CallId;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.IceCandidate;
|
||||
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||
|
||||
/**
|
||||
@@ -24,14 +26,11 @@ public class IceCandidateParcel implements Parcelable {
|
||||
}
|
||||
|
||||
public IceCandidateParcel(@NonNull IceUpdateMessage iceUpdateMessage) {
|
||||
this.iceCandidate = new IceCandidate(iceUpdateMessage.getSdpMid(),
|
||||
iceUpdateMessage.getSdpMLineIndex(),
|
||||
iceUpdateMessage.getSdp());
|
||||
this.iceCandidate = new IceCandidate(iceUpdateMessage.getOpaque(), iceUpdateMessage.getSdp());
|
||||
}
|
||||
|
||||
private IceCandidateParcel(@NonNull Parcel in) {
|
||||
this.iceCandidate = new IceCandidate(in.readString(),
|
||||
in.readInt(),
|
||||
this.iceCandidate = new IceCandidate(in.createByteArray(),
|
||||
in.readString());
|
||||
}
|
||||
|
||||
@@ -41,9 +40,8 @@ public class IceCandidateParcel implements Parcelable {
|
||||
|
||||
public @NonNull IceUpdateMessage getIceUpdateMessage(@NonNull CallId callId) {
|
||||
return new IceUpdateMessage(callId.longValue(),
|
||||
iceCandidate.sdpMid,
|
||||
iceCandidate.sdpMLineIndex,
|
||||
iceCandidate.sdp);
|
||||
iceCandidate.getOpaque(),
|
||||
iceCandidate.getSdp());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,9 +51,8 @@ public class IceCandidateParcel implements Parcelable {
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
dest.writeString(iceCandidate.sdpMid);
|
||||
dest.writeInt(iceCandidate.sdpMLineIndex);
|
||||
dest.writeString(iceCandidate.sdp);
|
||||
dest.writeByteArray(iceCandidate.getOpaque());
|
||||
dest.writeString(iceCandidate.getSdp());
|
||||
}
|
||||
|
||||
public static final Creator<IceCandidateParcel> CREATOR = new Creator<IceCandidateParcel>() {
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class VerificationCodeParser {
|
||||
|
||||
private static final Pattern CHALLENGE_PATTERN = Pattern.compile(".*Your (Signal|TextSecure) verification code:? ([0-9]{3,4})-([0-9]{3,4}).*", Pattern.DOTALL);
|
||||
private static final Pattern CHALLENGE_PATTERN = Pattern.compile("(.*\\D|^)([0-9]{3,4})-([0-9]{3,4}).*", Pattern.DOTALL);
|
||||
|
||||
public static Optional<String> parse(Context context, String messageBody) {
|
||||
if (messageBody == null) {
|
||||
@@ -39,6 +39,7 @@ public class VerificationCodeParser {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(challengeMatcher.group(2) + challengeMatcher.group(3));
|
||||
return Optional.of(challengeMatcher.group(challengeMatcher.groupCount() - 1) +
|
||||
challengeMatcher.group(challengeMatcher.groupCount()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,14 @@ import android.util.Pair;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.CallManager.CallEvent;
|
||||
import org.signal.ringrtc.IceCandidate;
|
||||
import org.signal.ringrtc.Remote;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
@@ -56,7 +59,6 @@ import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
@@ -105,13 +107,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
public static final String EXTRA_BLUETOOTH = "audio_bluetooth";
|
||||
public static final String EXTRA_REMOTE_PEER = "remote_peer";
|
||||
public static final String EXTRA_REMOTE_DEVICE = "remote_device";
|
||||
public static final String EXTRA_OFFER_DESCRIPTION = "offer_description";
|
||||
public static final String EXTRA_OFFER_OPAQUE = "offer_opaque";
|
||||
public static final String EXTRA_OFFER_SDP = "offer_sdp";
|
||||
public static final String EXTRA_OFFER_TYPE = "offer_type";
|
||||
public static final String EXTRA_MULTI_RING = "multi_ring";
|
||||
public static final String EXTRA_HANGUP_TYPE = "hangup_type";
|
||||
public static final String EXTRA_HANGUP_IS_LEGACY = "hangup_is_legacy";
|
||||
public static final String EXTRA_HANGUP_DEVICE_ID = "hangup_device_id";
|
||||
public static final String EXTRA_ANSWER_DESCRIPTION = "answer_description";
|
||||
public static final String EXTRA_ANSWER_OPAQUE = "answer_opaque";
|
||||
public static final String EXTRA_ANSWER_SDP = "answer_sdp";
|
||||
public static final String EXTRA_ICE_CANDIDATES = "ice_candidates";
|
||||
public static final String EXTRA_ENABLE = "enable_value";
|
||||
public static final String EXTRA_BROADCAST = "broadcast";
|
||||
@@ -389,7 +393,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
CallId callId = getCallId(intent);
|
||||
RemotePeer remotePeer = getRemotePeer(intent);
|
||||
Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
|
||||
String offer = intent.getStringExtra(EXTRA_OFFER_DESCRIPTION);
|
||||
byte[] opaque = intent.getByteArrayExtra(EXTRA_OFFER_OPAQUE);
|
||||
String sdp = intent.getStringExtra(EXTRA_OFFER_SDP);
|
||||
long serverReceivedTimestamp = intent.getLongExtra(EXTRA_SERVER_RECEIVED_TIMESTAMP, -1);
|
||||
long serverDeliveredTimestamp = intent.getLongExtra(EXTRA_SERVER_DELIVERED_TIMESTAMP, -1);
|
||||
OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE));
|
||||
@@ -422,7 +427,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.i(TAG, "handleReceivedOffer(): messageAgeSec: " + messageAgeSec + ", serverReceivedTimestamp: " + serverReceivedTimestamp + ", serverDeliveredTimestamp: " + serverDeliveredTimestamp);
|
||||
|
||||
try {
|
||||
callManager.receivedOffer(callId, remotePeer, remoteDevice, offer, messageAgeSec, callType, 1, isMultiRing, true);
|
||||
callManager.receivedOffer(callId, remotePeer, remoteDevice, opaque, sdp, messageAgeSec, callType, 1, isMultiRing, true);
|
||||
} catch (CallException e) {
|
||||
callFailure("Unable to process received offer: ", e);
|
||||
}
|
||||
@@ -620,9 +625,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
camera,
|
||||
iceServers,
|
||||
isAlwaysTurn,
|
||||
deviceList,
|
||||
enableVideoOnCreate,
|
||||
true);
|
||||
enableVideoOnCreate);
|
||||
} catch (CallException e) {
|
||||
callFailure("Unable to proceed with call: ", e);
|
||||
}
|
||||
@@ -664,9 +667,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
camera,
|
||||
iceServers,
|
||||
hideIp,
|
||||
deviceList,
|
||||
false,
|
||||
true);
|
||||
false);
|
||||
} catch (CallException e) {
|
||||
callFailure("Unable to proceed with call: ", e);
|
||||
}
|
||||
@@ -704,12 +705,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
CallId callId = getCallId(intent);
|
||||
Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
|
||||
boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false);
|
||||
String offer = intent.getStringExtra(EXTRA_OFFER_DESCRIPTION);
|
||||
byte[] opaque = intent.getByteArrayExtra(EXTRA_OFFER_OPAQUE);
|
||||
String sdp = intent.getStringExtra(EXTRA_OFFER_SDP);
|
||||
OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE));
|
||||
|
||||
Log.i(TAG, "handleSendOffer: id: " + callId.format(remoteDevice));
|
||||
|
||||
OfferMessage offerMessage = new OfferMessage(callId.longValue(), offer, offerType);
|
||||
OfferMessage offerMessage = new OfferMessage(callId.longValue(), sdp, offerType, opaque);
|
||||
Integer destinationDeviceId = broadcast ? null : remoteDevice;
|
||||
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOffer(offerMessage, true, destinationDeviceId);
|
||||
|
||||
@@ -721,11 +723,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
CallId callId = getCallId(intent);
|
||||
Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
|
||||
boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false);
|
||||
String answer = intent.getStringExtra(EXTRA_ANSWER_DESCRIPTION);
|
||||
byte[] opaque = intent.getByteArrayExtra(EXTRA_ANSWER_OPAQUE);
|
||||
String sdp = intent.getStringExtra(EXTRA_ANSWER_SDP);
|
||||
|
||||
Log.i(TAG, "handleSendAnswer: id: " + callId.format(remoteDevice));
|
||||
|
||||
AnswerMessage answerMessage = new AnswerMessage(callId.longValue(), answer);
|
||||
AnswerMessage answerMessage = new AnswerMessage(callId.longValue(), sdp, opaque);
|
||||
Integer destinationDeviceId = broadcast ? null : remoteDevice;
|
||||
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forAnswer(answerMessage, true, destinationDeviceId);
|
||||
|
||||
@@ -788,13 +791,14 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
private void handleReceivedAnswer(Intent intent) {
|
||||
CallId callId = getCallId(intent);
|
||||
Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
|
||||
String description = intent.getStringExtra(EXTRA_ANSWER_DESCRIPTION);
|
||||
byte[] opaque = intent.getByteArrayExtra(EXTRA_ANSWER_OPAQUE);
|
||||
String sdp = intent.getStringExtra(EXTRA_ANSWER_SDP);
|
||||
boolean isMultiRing = intent.getBooleanExtra(EXTRA_MULTI_RING, false);
|
||||
|
||||
Log.i(TAG, "handleReceivedAnswer(): id: " + callId.format(remoteDevice));
|
||||
|
||||
try {
|
||||
callManager.receivedAnswer(callId, remoteDevice, description , isMultiRing);
|
||||
callManager.receivedAnswer(callId, remoteDevice, opaque, sdp, isMultiRing);
|
||||
} catch (CallException e) {
|
||||
callFailure("receivedAnswer() failed: ", e);
|
||||
}
|
||||
@@ -1802,7 +1806,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendOffer(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast, String offer, CallManager.CallMediaType callMediaType) {
|
||||
public void onSendOffer(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast, byte[] opaque, String sdp, CallManager.CallMediaType callMediaType) {
|
||||
Log.i(TAG, "onSendOffer: id: " + callId.format(remoteDevice) + " type: " + callMediaType.name());
|
||||
|
||||
if (remote instanceof RemotePeer) {
|
||||
@@ -1811,12 +1815,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
|
||||
intent.setAction(ACTION_SEND_OFFER)
|
||||
.putExtra(EXTRA_CALL_ID, callId.longValue())
|
||||
.putExtra(EXTRA_REMOTE_PEER, remotePeer)
|
||||
.putExtra(EXTRA_REMOTE_DEVICE, remoteDevice)
|
||||
.putExtra(EXTRA_BROADCAST, broadcast)
|
||||
.putExtra(EXTRA_OFFER_DESCRIPTION, offer)
|
||||
.putExtra(EXTRA_OFFER_TYPE, offerType);
|
||||
.putExtra(EXTRA_CALL_ID, callId.longValue())
|
||||
.putExtra(EXTRA_REMOTE_PEER, remotePeer)
|
||||
.putExtra(EXTRA_REMOTE_DEVICE, remoteDevice)
|
||||
.putExtra(EXTRA_BROADCAST, broadcast)
|
||||
.putExtra(EXTRA_OFFER_OPAQUE, opaque)
|
||||
.putExtra(EXTRA_OFFER_SDP, sdp)
|
||||
.putExtra(EXTRA_OFFER_TYPE, offerType);
|
||||
|
||||
startService(intent);
|
||||
} else {
|
||||
@@ -1825,7 +1830,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendAnswer(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast, String answer) {
|
||||
public void onSendAnswer(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast, byte[] opaque, String sdp) {
|
||||
Log.i(TAG, "onSendAnswer: id: " + callId.format(remoteDevice));
|
||||
|
||||
if (remote instanceof RemotePeer) {
|
||||
@@ -1833,11 +1838,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
|
||||
intent.setAction(ACTION_SEND_ANSWER)
|
||||
.putExtra(EXTRA_CALL_ID, callId.longValue())
|
||||
.putExtra(EXTRA_REMOTE_PEER, remotePeer)
|
||||
.putExtra(EXTRA_REMOTE_DEVICE, remoteDevice)
|
||||
.putExtra(EXTRA_BROADCAST, broadcast)
|
||||
.putExtra(EXTRA_ANSWER_DESCRIPTION, answer);
|
||||
.putExtra(EXTRA_CALL_ID, callId.longValue())
|
||||
.putExtra(EXTRA_REMOTE_PEER, remotePeer)
|
||||
.putExtra(EXTRA_REMOTE_DEVICE, remoteDevice)
|
||||
.putExtra(EXTRA_BROADCAST, broadcast)
|
||||
.putExtra(EXTRA_ANSWER_OPAQUE, opaque)
|
||||
.putExtra(EXTRA_ANSWER_SDP, sdp);
|
||||
|
||||
startService(intent);
|
||||
} else {
|
||||
|
||||
@@ -148,8 +148,9 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
else super.onBackPressed();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
SimpleTask.run(this.getLifecycle(), () -> {
|
||||
Recipient recipient;
|
||||
if (recipientId.isPresent()) {
|
||||
@@ -162,6 +163,8 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
|
||||
return new Pair<>(existingThread, recipient);
|
||||
}, result -> onDestinationChosen(result.first(), result.second().getId()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -502,7 +502,7 @@ public class MessageSender {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
SmsMessageRecord message = smsDatabase.getMessage(messageId);
|
||||
SmsMessageRecord message = smsDatabase.getMessageRecord(messageId);
|
||||
SyncMessageId syncId = new SyncMessageId(Recipient.self().getId(), message.getDateSent());
|
||||
|
||||
smsDatabase.markAsSent(messageId, true);
|
||||
|
||||
@@ -4,7 +4,9 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
public final class DrawableUtil {
|
||||
|
||||
@@ -19,4 +21,13 @@ public final class DrawableUtil {
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Drawable} that safely wraps and tints the provided drawable.
|
||||
*/
|
||||
public static @NonNull Drawable tint(@NonNull Drawable drawable, @ColorInt int tint) {
|
||||
Drawable tinted = DrawableCompat.wrap(drawable).mutate();
|
||||
DrawableCompat.setTint(tinted, tint);
|
||||
return tinted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public final class FeatureFlags {
|
||||
|
||||
private static final String TAG = Log.tag(FeatureFlags.class);
|
||||
|
||||
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(0);
|
||||
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
|
||||
private static final String USERNAMES = "android.usernames";
|
||||
private static final String ATTACHMENTS_V3 = "android.attachmentsV3.2";
|
||||
@@ -58,6 +58,7 @@ public final class FeatureFlags {
|
||||
private static final String CDS = "android.cds";
|
||||
private static final String RECIPIENT_TRUST = "android.recipientTrust";
|
||||
private static final String INTERNAL_USER = "android.internalUser";
|
||||
private static final String MENTIONS = "android.mentions";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -71,7 +72,8 @@ public final class FeatureFlags {
|
||||
GROUPS_V2_CREATE,
|
||||
GROUPS_V2_CAPACITY,
|
||||
RECIPIENT_TRUST,
|
||||
INTERNAL_USER
|
||||
INTERNAL_USER,
|
||||
MENTIONS
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -221,6 +223,11 @@ public final class FeatureFlags {
|
||||
return getBoolean(RECIPIENT_TRUST, false);
|
||||
}
|
||||
|
||||
/** Whether or not we allow mentions send support in groups. */
|
||||
public static boolean mentions() {
|
||||
return getBoolean(MENTIONS, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
@@ -67,13 +66,13 @@ public final class GroupUtil {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup, boolean isV2) {
|
||||
public static @NonNull GroupDescription getNonV2GroupDescription(@NonNull Context context, @Nullable String encodedGroup) {
|
||||
if (encodedGroup == null) {
|
||||
return new GroupDescription(context, null);
|
||||
}
|
||||
|
||||
try {
|
||||
MessageGroupContext groupContext = new MessageGroupContext(encodedGroup, isV2);
|
||||
MessageGroupContext groupContext = new MessageGroupContext(encodedGroup, false);
|
||||
return new GroupDescription(context, groupContext);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
@@ -117,7 +116,8 @@ public final class GroupUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public String toString(Recipient sender) {
|
||||
@WorkerThread
|
||||
public String toString(@NonNull Recipient sender) {
|
||||
StringBuilder description = new StringBuilder();
|
||||
description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.getDisplayName(context)));
|
||||
|
||||
@@ -125,7 +125,7 @@ public final class GroupUtil {
|
||||
return description.toString();
|
||||
}
|
||||
|
||||
String title = groupContext.getName();
|
||||
String title = StringUtil.isolateBidi(groupContext.getName());
|
||||
|
||||
if (members != null && members.size() > 0) {
|
||||
description.append("\n");
|
||||
@@ -133,7 +133,7 @@ public final class GroupUtil {
|
||||
members.size(), toString(members)));
|
||||
}
|
||||
|
||||
if (title != null && !title.trim().isEmpty()) {
|
||||
if (!title.trim().isEmpty()) {
|
||||
if (members != null) description.append(" ");
|
||||
else description.append("\n");
|
||||
description.append(context.getString(R.string.GroupUtil_group_name_is_now, title));
|
||||
@@ -142,22 +142,6 @@ public final class GroupUtil {
|
||||
return description.toString();
|
||||
}
|
||||
|
||||
public void addObserver(RecipientForeverObserver listener) {
|
||||
if (this.members != null) {
|
||||
for (RecipientId member : this.members) {
|
||||
Recipient.live(member).observeForever(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeObserver(RecipientForeverObserver listener) {
|
||||
if (this.members != null) {
|
||||
for (RecipientId member : this.members) {
|
||||
Recipient.live(member).removeForeverObserver(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String toString(List<RecipientId> recipients) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.text.Layout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Utility functions for dealing with {@link Layout}.
|
||||
*
|
||||
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||
*/
|
||||
public class LayoutUtil {
|
||||
private static final float DEFAULT_LINE_SPACING_EXTRA = 0f;
|
||||
|
||||
private static final float DEFAULT_LINE_SPACING_MULTIPLIER = 1f;
|
||||
|
||||
public static int getLineHeight(@NonNull Layout layout, int line) {
|
||||
return layout.getLineTop(line + 1) - layout.getLineTop(line);
|
||||
}
|
||||
|
||||
public static int getLineTopWithoutPadding(@NonNull Layout layout, int line) {
|
||||
int lineTop = layout.getLineTop(line);
|
||||
if (line == 0) {
|
||||
lineTop -= layout.getTopPadding();
|
||||
}
|
||||
return lineTop;
|
||||
}
|
||||
|
||||
public static int getLineBottomWithoutPadding(@NonNull Layout layout, int line) {
|
||||
int lineBottom = getLineBottomWithoutSpacing(layout, line);
|
||||
if (line == layout.getLineCount() - 1) {
|
||||
lineBottom -= layout.getBottomPadding();
|
||||
}
|
||||
return lineBottom;
|
||||
}
|
||||
|
||||
public static int getLineBottomWithoutSpacing(@NonNull Layout layout, int line) {
|
||||
int lineBottom = layout.getLineBottom(line);
|
||||
boolean isLastLine = line == layout.getLineCount() - 1;
|
||||
float lineSpacingExtra = layout.getSpacingAdd();
|
||||
float lineSpacingMultiplier = layout.getSpacingMultiplier();
|
||||
boolean hasLineSpacing = lineSpacingExtra != DEFAULT_LINE_SPACING_EXTRA || lineSpacingMultiplier != DEFAULT_LINE_SPACING_MULTIPLIER;
|
||||
|
||||
int lineBottomWithoutSpacing;
|
||||
if (!hasLineSpacing || isLastLine) {
|
||||
lineBottomWithoutSpacing = lineBottom;
|
||||
} else {
|
||||
float extra;
|
||||
if (Float.compare(lineSpacingMultiplier, DEFAULT_LINE_SPACING_MULTIPLIER) != 0) {
|
||||
int lineHeight = getLineHeight(layout, line);
|
||||
extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier;
|
||||
} else {
|
||||
extra = lineSpacingExtra;
|
||||
}
|
||||
|
||||
lineBottomWithoutSpacing = (int) (lineBottom - extra);
|
||||
}
|
||||
|
||||
return lineBottomWithoutSpacing;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A reusable and composable {@link androidx.recyclerview.widget.RecyclerView.Adapter} built on-top of {@link ListAdapter} to
|
||||
* provide async item diffing support.
|
||||
* <p></p>
|
||||
* The adapter makes use of mapping a model class to view holder factory at runtime via one of the {@link #registerFactory(Class, Factory)}
|
||||
* methods. The factory creates a view holder specifically designed to handle the paired model type. This allows the view holder concretely
|
||||
* deal with the model type it cares about. Due to the enforcement of matching generics during factory registration we can safely ignore or
|
||||
* override compiler typing recommendations when binding and diffing.
|
||||
* <p></p>
|
||||
* General pattern for implementation:
|
||||
* <ol>
|
||||
* <li>Create {@link MappingModel}s for the items in the list. These encapsulate data massaging methods for views to use and the diff logic.</li>
|
||||
* <li>Create {@link MappingViewHolder}s for each item type in the list and their corresponding {@link Factory}.</li>
|
||||
* <li>Create an instance or subclass of {@link MappingAdapter} and register the mapping of model type to view holder factory for that model type.</li>
|
||||
* </ol>
|
||||
* Event listeners, click or otherwise, are handled at the view holder level and should be passed into the appropriate view holder factories. This
|
||||
* pattern mimics how we pass data into view models via factories.
|
||||
* <p></p>
|
||||
* NOTE: There can only be on factory registered per model type. Registering two for the same type will result in the last one being used. However, the
|
||||
* same factory can be registered multiple times for multiple model types (if the model type class hierarchy supports it).
|
||||
*/
|
||||
public class MappingAdapter extends ListAdapter<MappingModel<?>, MappingViewHolder<?>> {
|
||||
|
||||
private final Map<Integer, Factory<?>> factories;
|
||||
private final Map<Class<?>, Integer> itemTypes;
|
||||
private int typeCount;
|
||||
|
||||
public MappingAdapter() {
|
||||
super(new MappingDiffCallback());
|
||||
|
||||
factories = new HashMap<>();
|
||||
itemTypes = new HashMap<>();
|
||||
typeCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull MappingViewHolder<?> holder) {
|
||||
super.onViewAttachedToWindow(holder);
|
||||
holder.onAttachedToWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull MappingViewHolder<?> holder) {
|
||||
super.onViewDetachedFromWindow(holder);
|
||||
holder.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
public <T extends MappingModel<T>> void registerFactory(Class<T> clazz, Factory<T> factory) {
|
||||
int type = typeCount++;
|
||||
factories.put(type, factory);
|
||||
itemTypes.put(clazz, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
Integer type = itemTypes.get(getItem(position).getClass());
|
||||
if (type != null) {
|
||||
return type;
|
||||
}
|
||||
throw new AssertionError("No view holder factory for type: " + getItem(position).getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MappingViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return Objects.requireNonNull(factories.get(viewType)).createViewHolder(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull MappingViewHolder holder, int position) {
|
||||
//noinspection unchecked
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
private static class MappingDiffCallback extends DiffUtil.ItemCallback<MappingModel<?>> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) {
|
||||
if (oldItem.getClass() == newItem.getClass()) {
|
||||
//noinspection unchecked
|
||||
return oldItem.areItemsTheSame(newItem);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) {
|
||||
if (oldItem.getClass() == newItem.getClass()) {
|
||||
//noinspection unchecked
|
||||
return oldItem.areContentsTheSame(newItem);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Factory<T extends MappingModel<T>> {
|
||||
@NonNull MappingViewHolder<T> createViewHolder(ViewGroup parent);
|
||||
}
|
||||
|
||||
public static class LayoutFactory<T extends MappingModel<T>> implements Factory<T> {
|
||||
private Function<View, MappingViewHolder<T>> creator;
|
||||
private final int layout;
|
||||
|
||||
public LayoutFactory(Function<View, MappingViewHolder<T>> creator, @LayoutRes int layout) {
|
||||
this.creator = creator;
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MappingViewHolder<T> createViewHolder(ViewGroup parent) {
|
||||
return creator.apply(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface MappingModel<T> {
|
||||
boolean areItemsTheSame(@NonNull T newItem);
|
||||
boolean areContentsTheSame(@NonNull T newItem);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user