diff --git a/res/drawable/message_request_button_background_dark.xml b/res/drawable/message_request_button_background_dark.xml
new file mode 100644
index 0000000000..03ad8b312b
--- /dev/null
+++ b/res/drawable/message_request_button_background_dark.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/message_request_button_background_light.xml b/res/drawable/message_request_button_background_light.xml
new file mode 100644
index 0000000000..19aee4649a
--- /dev/null
+++ b/res/drawable/message_request_button_background_light.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml
index 40f2c17573..a9064a4be6 100644
--- a/res/layout/conversation_activity.xml
+++ b/res/layout/conversation_activity.xml
@@ -133,4 +133,13 @@
+
+
diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml
index 172aee6941..1d5cb37021 100644
--- a/res/layout/conversation_item_received.xml
+++ b/res/layout/conversation_item_received.xml
@@ -37,10 +37,10 @@
+ android:layout_alignBottom="@id/body_bubble">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 9996d1f590..2ff55fa0b5 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -181,6 +181,10 @@
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2363d388ff..6758e9e9b6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1661,6 +1661,18 @@
Reminder:
About
Unknown
+ Accept
+ Delete
+ Block
+ Do you want to receive messages from %1$s?
+ Member of %1$s
+ Member of %1$s and %2$s
+ Member of %1$s, %2$s, and %3$s
+ %1$d members
+
+ - %d other
+ - %d others
+
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 6ef51acb82..71a1d25891 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -383,4 +383,19 @@
+
+
+
+
+
+
diff --git a/res/values/text_styles.xml b/res/values/text_styles.xml
index d8d5b77eee..8958af1663 100644
--- a/res/values/text_styles.xml
+++ b/res/values/text_styles.xml
@@ -87,4 +87,19 @@
- bold
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 15e71c2d96..d671bc697a 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -326,6 +326,9 @@
- @drawable/ic_linked_devices_24
- @drawable/ic_advanced_24
- @drawable/ic_safety_number_outline_24
+ - @drawable/message_request_button_background_light
+ - @color/core_grey_90
+ - @color/core_grey_60
- @drawable/ic_audio_light
- @drawable/ic_video_light
@@ -549,6 +552,9 @@
- @drawable/ic_linked_devices_24
- @drawable/ic_advanced_24
- @drawable/ic_safety_number_solid_24
+ - @drawable/message_request_button_background_dark
+ - @color/core_grey_05
+ - @color/core_grey_25
- @drawable/ic_audio_dark
- @drawable/ic_video_dark
diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
index 08a40ce076..3f8cda0d90 100644
--- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
+++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
@@ -34,9 +34,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
-import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
import org.thoughtcrime.securesms.logging.Log;
import android.telephony.PhoneNumberUtils;
@@ -48,7 +46,6 @@ import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
-import android.widget.Toast;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
@@ -62,11 +59,9 @@ import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
-import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
-import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
@@ -83,12 +78,12 @@ 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.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.ExecutionException;
@@ -750,38 +745,13 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
}
private void setBlocked(@NonNull final Context context, final Recipient recipient, final boolean blocked) {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- DatabaseFactory.getRecipientDatabase(context)
- .setBlocked(recipient.getId(), blocked);
-
- if (recipient.isGroup() && DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
- long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
- Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, recipient);
-
- if (threadId != -1 && leaveMessage.isPresent()) {
- MessageSender.send(context, leaveMessage.get(), threadId, false, null);
-
- GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
- String groupId = recipient.requireGroupId();
- groupDatabase.setActive(groupId, false);
- groupDatabase.remove(groupId, Recipient.self().getId());
- } else {
- Log.w(TAG, "Failed to leave group. Can't block.");
- Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show();
- }
- }
-
- if (blocked && (recipient.resolve().isSystemContact() || recipient.resolve().isProfileSharing())) {
- ApplicationDependencies.getJobManager().add(new RotateProfileKeyJob());
- }
-
- ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
-
- return null;
+ SignalExecutors.BOUNDED.execute(() -> {
+ if (blocked) {
+ RecipientUtil.block(context, recipient);
+ } else {
+ RecipientUtil.unblock(context, recipient);
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ });
}
}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index e4dd33179e..456442b96b 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -56,6 +56,7 @@ import android.view.View.OnKeyListener;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
+import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
@@ -152,7 +153,6 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.invites.InviteReminderModel;
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
-import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@@ -162,6 +162,8 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
+import org.thoughtcrime.securesms.messagerequests.MessageRequestFragment;
+import org.thoughtcrime.securesms.messagerequests.MessageRequestFragmentViewModel;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide;
@@ -193,6 +195,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.service.KeyCachingService;
@@ -213,6 +216,7 @@ import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
+import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -225,6 +229,7 @@ import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -307,6 +312,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private TypingStatusTextWatcher typingTextWatcher;
private ConversationSearchBottomBar searchNav;
private MenuItem searchViewItem;
+ private FrameLayout messageRequestOverlay;
private AttachmentTypeSelector attachmentTypeSelector;
private AttachmentManager attachmentManager;
@@ -909,16 +915,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.setMessage(bodyRes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.ConversationActivity_unblock, (dialog, which) -> {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- DatabaseFactory.getRecipientDatabase(ConversationActivity.this)
- .setBlocked(recipient.getId(), false);
- ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
-
- return null;
+ SimpleTask.run(() -> {
+ RecipientUtil.unblock(ConversationActivity.this, recipient.get());
+ return RecipientUtil.isRecipientMessageRequestAccepted(ConversationActivity.this, recipient.get());
+ }, messageRequestAccepted -> {
+ if (!messageRequestAccepted) {
+ onMessageRequest();
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ });
}).show();
}
@@ -1564,6 +1568,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
+ messageRequestOverlay = ViewUtil.findById(this, R.id.fragment_overlay_container);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
@@ -1977,7 +1982,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void setGroupShareProfileReminder(@NonNull Recipient recipient) {
- if (recipient.isPushGroup() && !recipient.isProfileSharing()) {
+ if (!FeatureFlags.MESSAGE_REQUESTS && recipient.isPushGroup() && !recipient.isProfileSharing()) {
groupShareProfileView.get().setRecipient(recipient);
groupShareProfileView.get().setVisibility(View.VISIBLE);
} else if (groupShareProfileView.resolved()) {
@@ -2696,6 +2701,46 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
+ @Override
+ public void onMessageRequest() {
+ long threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
+ RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
+
+ if (threadId == -1) {
+ throw new IllegalStateException("MessageRequest is not supported here");
+ }
+
+ if (recipientId == null) {
+ Log.w(TAG, "onMessageRequest: " + threadId + ": null recipient. finishing...");
+ finish();
+ }
+
+ Log.i(TAG, "onMessageRequest: " + threadId + ", " + recipientId.serialize());
+
+ MessageRequestFragmentViewModel.Factory factory = new MessageRequestFragmentViewModel.Factory(this, threadId, recipientId);
+ MessageRequestFragmentViewModel viewModel = ViewModelProviders.of(this, factory).get(MessageRequestFragmentViewModel.class);
+ MessageRequestFragment fragment = new MessageRequestFragment();
+
+ messageRequestOverlay.setVisibility(View.VISIBLE);
+ container.setVisibility(View.GONE);
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.fragment_overlay_container, fragment)
+ .commit();
+
+ viewModel.getState().observe(this, state -> {
+ switch (state.messageRequestState) {
+ case ACCEPTED:
+ getSupportFragmentManager().popBackStack();
+ messageRequestOverlay.setVisibility(View.GONE);
+ container.setVisibility(View.VISIBLE);
+ return;
+ case DELETED:
+ case BLOCKED:
+ finish();
+ }
+ });
+ }
+
@Override
public void setThreadId(long threadId) {
this.threadId = threadId;
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index 8cbafa1d16..dc2b03c622 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -94,6 +94,7 @@ import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity;
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.sms.MessageSender;
@@ -101,6 +102,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.util.CommunicationActions;
+import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -686,10 +688,18 @@ public class ConversationFragment extends Fragment
setLastSeen(loader.getLastSeen());
}
- if (!loader.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
- adapter.setHeaderView(unknownSenderView);
+ if (FeatureFlags.MESSAGE_REQUESTS) {
+ if (!loader.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isProfileSharing() && !recipient.get().isBlocked() && recipient.get().isRegistered()) {
+ listener.onMessageRequest();
+ } else {
+ clearHeaderIfNotTyping(adapter);
+ }
} else {
- clearHeaderIfNotTyping(adapter);
+ if (!loader.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
+ adapter.setHeaderView(unknownSenderView);
+ } else {
+ clearHeaderIfNotTyping(adapter);
+ }
}
if (loader.hasOffset()) {
@@ -859,6 +869,7 @@ public class ConversationFragment extends Fragment
void handleReplyMessage(MessageRecord messageRecord);
void onMessageActionToolbarOpened();
void onForwardClicked();
+ void onMessageRequest();
}
private class ConversationScrollListener extends OnScrollListener {
diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java
index ccc468af3d..239a895523 100644
--- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java
@@ -154,6 +154,26 @@ public class GroupDatabase extends Database {
}
}
+ public List getGroupNamesContainingMember(RecipientId recipientId) {
+ SQLiteDatabase database = databaseHelper.getReadableDatabase();
+ List groupNames = new LinkedList<>();
+ String[] projection = new String[]{TITLE, MEMBERS};
+ String query = MEMBERS + " LIKE ?";
+ String[] args = new String[]{"%" + recipientId.serialize() + "%"};
+
+ try (Cursor cursor = database.query(TABLE_NAME, projection, query, args, null, null, null)) {
+ while (cursor != null && cursor.moveToNext()) {
+ List members = Util.split(cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), ",");
+
+ if (members.contains(recipientId.serialize())) {
+ groupNames.add(cursor.getString(cursor.getColumnIndexOrThrow(TITLE)));
+ }
+ }
+ }
+
+ return groupNames;
+ }
+
public Reader getGroups() {
@SuppressLint("Recycle")
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null);
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index dabe8440a3..df29a57d69 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -467,6 +467,11 @@ public class MmsDatabase extends MessagingDatabase {
return setMessagesRead(THREAD_ID + " = ? AND " + READ + " = 0", new String[] {String.valueOf(threadId)});
}
+
+ public List setEntireThreadRead(long threadId) {
+ return setMessagesRead(THREAD_ID + " = ?", new String[] {String.valueOf(threadId)});
+ }
+
public List setAllMessagesRead() {
return setMessagesRead(READ + " = 0", null);
}
diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
index 322dd91435..6412b5d21d 100644
--- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -406,6 +406,10 @@ public class SmsDatabase extends MessagingDatabase {
return expiring;
}
+ public List setEntireThreadRead(long threadId) {
+ return setMessagesRead(THREAD_ID + " = ?", new String[] {String.valueOf(threadId)});
+ }
+
public List setMessagesRead(long threadId) {
return setMessagesRead(THREAD_ID + " = ? AND " + READ + " = 0", new String[] {String.valueOf(threadId)});
}
diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
index 60b7f1bcb3..4fa925e41b 100644
--- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.database.Cursor;
import android.database.MergeCursor;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -279,16 +280,22 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners();
- return new LinkedList() {{
- addAll(smsRecords);
- addAll(mmsRecords);
- }};
+ return Util.concatenatedList(smsRecords, mmsRecords);
}
public boolean hasCalledSince(@NonNull Recipient recipient, long timestamp) {
return DatabaseFactory.getMmsSmsDatabase(context).hasReceivedAnyCallsSince(getThreadIdFor(recipient), timestamp);
}
+ public List setEntireThreadRead(long threadId) {
+ setRead(threadId, false);
+
+ final List smsRecords = DatabaseFactory.getSmsDatabase(context).setEntireThreadRead(threadId);
+ final List mmsRecords = DatabaseFactory.getMmsDatabase(context).setEntireThreadRead(threadId);
+
+ return Util.concatenatedList(smsRecords, mmsRecords);
+ }
+
public List setRead(long threadId, boolean lastSeen) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(READ, 1);
@@ -299,6 +306,7 @@ public class ThreadDatabase extends Database {
}
SQLiteDatabase db = databaseHelper.getWritableDatabase();
+
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
final List smsRecords = DatabaseFactory.getSmsDatabase(context).setMessagesRead(threadId);
@@ -306,10 +314,7 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners();
- return new LinkedList() {{
- addAll(smsRecords);
- addAll(mmsRecords);
- }};
+ return Util.concatenatedList(smsRecords, mmsRecords);
}
public void incrementUnread(long threadId, int amount) {
diff --git a/src/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/src/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java
index aa0647e389..f1dc5e7745 100644
--- a/src/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java
@@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
@@ -87,7 +86,12 @@ public class SendReadReceiptJob extends BaseJob {
public void onRun() throws IOException, UntrustedIdentityException {
if (!TextSecurePreferences.isReadReceiptsEnabled(context) || messageIds.isEmpty()) return;
- Recipient recipient = Recipient.resolved(recipientId);
+ Recipient recipient = Recipient.resolved(recipientId);
+ if (!RecipientUtil.isRecipientMessageRequestAccepted(context, recipient)) {
+ Log.w(TAG, "Refusing to send receipts to untrusted recipient");
+ return;
+ }
+
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
SignalServiceAddress remoteAddress = RecipientUtil.toSignalServiceAddress(context, recipient);
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, timestamp);
diff --git a/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragment.java b/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragment.java
new file mode 100644
index 0000000000..45fa1b179f
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragment.java
@@ -0,0 +1,172 @@
+package org.thoughtcrime.securesms.messagerequests;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.text.HtmlCompat;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.AvatarImageView;
+import org.thoughtcrime.securesms.conversation.ConversationItem;
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+public class MessageRequestFragment extends Fragment {
+
+ private AvatarImageView contactAvatar;
+ private TextView contactTitle;
+ private TextView contactSubtitle;
+ private TextView contactDescription;
+ private FrameLayout messageView;
+ private TextView question;
+ private Button accept;
+ private Button block;
+ private Button delete;
+ private ConversationItem conversationItem;
+
+ private MessageRequestFragmentViewModel viewModel;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState)
+ {
+ return inflater.inflate(R.layout.message_request_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ contactAvatar = view.findViewById(R.id.message_request_avatar);
+ contactTitle = view.findViewById(R.id.message_request_title);
+ contactSubtitle = view.findViewById(R.id.message_request_subtitle);
+ contactDescription = view.findViewById(R.id.message_request_description);
+ messageView = view.findViewById(R.id.message_request_message);
+ question = view.findViewById(R.id.message_request_question);
+ accept = view.findViewById(R.id.message_request_accept);
+ block = view.findViewById(R.id.message_request_block);
+ delete = view.findViewById(R.id.message_request_delete);
+
+ initializeViewModel();
+ initializeBottomViewListeners();
+ }
+
+ private void initializeViewModel() {
+ viewModel = ViewModelProviders.of(requireActivity()).get(MessageRequestFragmentViewModel.class);
+ viewModel.getState().observe(getViewLifecycleOwner(), state -> {
+ if (state.messageRecord == null || state.recipient == null) return;
+
+ presentConversationItemTo(state.messageRecord, state.recipient);
+ presentMessageRequestBottomViewTo(state.recipient);
+ presentMessageRequestProfileViewTo(state.recipient, state.groups, state.memberCount);
+ });
+ }
+
+ private void presentConversationItemTo(@NonNull MessageRecord messageRecord, @NonNull Recipient recipient) {
+ if (messageRecord.isGroupAction()) {
+ if (conversationItem != null) {
+ messageView.removeAllViews();
+ }
+ return;
+ }
+
+ if (conversationItem == null) {
+ conversationItem = (ConversationItem) LayoutInflater.from(requireActivity()).inflate(R.layout.conversation_item_received, messageView, false);
+ }
+
+ conversationItem.bind(messageRecord,
+ Optional.absent(),
+ Optional.absent(),
+ GlideApp.with(this),
+ Locale.getDefault(),
+ Collections.emptySet(),
+ recipient,
+ null,
+ false);
+
+ if (messageView.getChildCount() == 0 || messageView.getChildAt(0) != conversationItem) {
+ messageView.removeAllViews();
+ messageView.addView(conversationItem);
+ }
+ }
+
+ private void presentMessageRequestProfileViewTo(@Nullable Recipient recipient, @Nullable List groups, int memberCount) {
+ if (recipient != null) {
+ contactAvatar.setAvatar(GlideApp.with(this), recipient, false);
+
+ String title = recipient.getDisplayName(requireContext());
+ contactTitle.setText(title);
+
+ if (recipient.isGroup()) {
+ contactSubtitle.setText(getString(R.string.MessageRequestProfileView_members, memberCount));
+ } else {
+ String subtitle = recipient.getUsername().or(recipient.getE164()).orNull();
+
+ if (subtitle == null || subtitle.equals(title)) {
+ contactSubtitle.setVisibility(View.GONE);
+ } else {
+ contactSubtitle.setText(subtitle);
+ }
+ }
+ }
+
+ if (groups == null || groups.isEmpty()) {
+ contactDescription.setVisibility(View.GONE);
+ } else {
+ final String description;
+
+ switch (groups.size()) {
+ case 1:
+ description = getString(R.string.MessageRequestProfileView_member_of_one_group, bold(groups.get(0)));
+ break;
+ case 2:
+ description = getString(R.string.MessageRequestProfileView_member_of_two_groups, bold(groups.get(0)), bold(groups.get(1)));
+ break;
+ case 3:
+ description = getString(R.string.MessageRequestProfileView_member_of_many_groups, bold(groups.get(0)), bold(groups.get(1)), bold(groups.get(2)));
+ break;
+ default:
+ int others = groups.size() - 2;
+ description = getString(R.string.MessageRequestProfileView_member_of_many_groups,
+ bold(groups.get(0)),
+ bold(groups.get(1)),
+ getResources().getQuantityString(R.plurals.MessageRequestProfileView_member_of_others, others, others));
+ }
+
+ contactDescription.setText(HtmlCompat.fromHtml(description, 0));
+ contactDescription.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private @NonNull String bold(@NonNull String target) {
+ return "" + target + "";
+ }
+
+ private void presentMessageRequestBottomViewTo(@Nullable Recipient recipient) {
+ if (recipient == null) return;
+
+ question.setText(HtmlCompat.fromHtml(getString(R.string.MessageRequestBottomView_do_you_want_to_let, bold(recipient.getDisplayName(requireContext()))), 0));
+ }
+
+ private void initializeBottomViewListeners() {
+ accept.setOnClickListener(v -> viewModel.accept());
+ delete.setOnClickListener(v -> viewModel.delete());
+ block.setOnClickListener(v -> viewModel.block());
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentRepository.java b/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentRepository.java
new file mode 100644
index 0000000000..8a0cad9f43
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentRepository.java
@@ -0,0 +1,106 @@
+package org.thoughtcrime.securesms.messagerequests;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.GroupDatabase;
+import org.thoughtcrime.securesms.database.MessagingDatabase;
+import org.thoughtcrime.securesms.database.MmsSmsDatabase;
+import org.thoughtcrime.securesms.database.RecipientDatabase;
+import org.thoughtcrime.securesms.database.ThreadDatabase;
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
+import org.thoughtcrime.securesms.notifications.MessageNotifier;
+import org.thoughtcrime.securesms.recipients.LiveRecipient;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.util.List;
+
+public class MessageRequestFragmentRepository {
+
+ private final Context context;
+ private final RecipientId recipientId;
+ private final long threadId;
+ private final LiveRecipient liveRecipient;
+
+ public MessageRequestFragmentRepository(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
+ this.context = context.getApplicationContext();
+ this.recipientId = recipientId;
+ this.threadId = threadId;
+ this.liveRecipient = Recipient.live(recipientId);
+ }
+
+ public LiveRecipient getLiveRecipient() {
+ return liveRecipient;
+ }
+
+ public void refreshRecipient() {
+ SignalExecutors.BOUNDED.execute(liveRecipient::refresh);
+ }
+
+ public void getMessageRecord(@NonNull Consumer onMessageRecordLoaded) {
+ SimpleTask.run(() -> {
+ MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
+ try (Cursor cursor = mmsSmsDatabase.getConversation(threadId, 0, 1)) {
+ if (!cursor.moveToFirst()) return null;
+ return mmsSmsDatabase.readerFor(cursor).getCurrent();
+ }
+ }, onMessageRecordLoaded::accept);
+ }
+
+ public void getGroups(@NonNull Consumer> onGroupsLoaded) {
+ SimpleTask.run(() -> {
+ GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
+ return groupDatabase.getGroupNamesContainingMember(recipientId);
+ }, onGroupsLoaded::accept);
+ }
+
+ public void getMemberCount(@NonNull Consumer onMemberCountLoaded) {
+ SimpleTask.run(() -> {
+ GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
+ Optional groupRecord = groupDatabase.getGroup(recipientId);
+ return groupRecord.transform(record -> record.getMembers().size()).or(0);
+ }, onMemberCountLoaded::accept);
+ }
+
+ public void acceptMessageRequest(@NonNull Runnable onMessageRequestAccepted) {
+ SimpleTask.run(() -> {
+ RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
+ recipientDatabase.setProfileSharing(recipientId, true);
+ liveRecipient.refresh();
+
+ List messageIds = DatabaseFactory.getThreadDatabase(context)
+ .setEntireThreadRead(threadId);
+ MessageNotifier.updateNotification(context);
+ MarkReadReceiver.process(context, messageIds);
+
+ return null;
+ }, v -> onMessageRequestAccepted.run());
+ }
+
+ public void deleteMessageRequest(@NonNull Runnable onMessageRequestDeleted) {
+ SimpleTask.run(() -> {
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ threadDatabase.deleteConversation(threadId);
+ return null;
+ }, v -> onMessageRequestDeleted.run());
+ }
+
+ public void blockMessageRequest(@NonNull Runnable onMessageRequestBlocked) {
+ SimpleTask.run(() -> {
+ Recipient recipient = liveRecipient.resolve();
+ RecipientUtil.block(context, recipient);
+ liveRecipient.refresh();
+ return null;
+ }, v -> onMessageRequestBlocked.run());
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentState.java b/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentState.java
new file mode 100644
index 0000000000..7ec1e72b3a
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentState.java
@@ -0,0 +1,90 @@
+package org.thoughtcrime.securesms.messagerequests;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.recipients.Recipient;
+
+import java.util.List;
+
+public class MessageRequestFragmentState {
+
+ public enum MessageRequestState {
+ LOADING,
+ PENDING,
+ BLOCKED,
+ DELETED,
+ ACCEPTED
+ }
+
+ public final @NonNull MessageRequestState messageRequestState;
+ public final @Nullable MessageRecord messageRecord;
+ public final @Nullable Recipient recipient;
+ public final @Nullable List groups;
+ public final int memberCount;
+
+
+ public MessageRequestFragmentState(@NonNull MessageRequestState messageRequestState,
+ @Nullable MessageRecord messageRecord,
+ @Nullable Recipient recipient,
+ @Nullable List groups,
+ int memberCount)
+ {
+ this.messageRequestState = messageRequestState;
+ this.messageRecord = messageRecord;
+ this.recipient = recipient;
+ this.groups = groups;
+ this.memberCount = memberCount;
+ }
+
+ public @NonNull MessageRequestFragmentState updateMessageRequestState(@NonNull MessageRequestState messageRequestState) {
+ return new MessageRequestFragmentState(messageRequestState,
+ this.messageRecord,
+ this.recipient,
+ this.groups,
+ this.memberCount);
+ }
+
+ public @NonNull MessageRequestFragmentState updateMessageRecord(@NonNull MessageRecord messageRecord) {
+ return new MessageRequestFragmentState(this.messageRequestState,
+ messageRecord,
+ this.recipient,
+ this.groups,
+ this.memberCount);
+ }
+
+ public @NonNull MessageRequestFragmentState updateRecipient(@NonNull Recipient recipient) {
+ return new MessageRequestFragmentState(this.messageRequestState,
+ this.messageRecord,
+ recipient,
+ this.groups,
+ this.memberCount);
+ }
+
+ public @NonNull MessageRequestFragmentState updateGroups(@NonNull List groups) {
+ return new MessageRequestFragmentState(this.messageRequestState,
+ this.messageRecord,
+ this.recipient,
+ groups,
+ this.memberCount);
+ }
+
+ public @NonNull MessageRequestFragmentState updateMemberCount(int memberCount) {
+ return new MessageRequestFragmentState(this.messageRequestState,
+ this.messageRecord,
+ this.recipient,
+ this.groups,
+ memberCount);
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "MessageRequestFragmentState: [" +
+ messageRequestState.name() + "] [" +
+ messageRecord + "] [" +
+ recipient + "] [" +
+ groups + "] [" +
+ memberCount + "]";
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentViewModel.java b/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentViewModel.java
new file mode 100644
index 0000000000..67c59cd580
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentViewModel.java
@@ -0,0 +1,139 @@
+package org.thoughtcrime.securesms.messagerequests;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+
+public class MessageRequestFragmentViewModel extends ViewModel {
+
+ private static final String TAG = MessageRequestFragmentViewModel.class.getSimpleName();
+
+ private final MutableLiveData internalState = new MutableLiveData<>();
+
+ private final MessageRequestFragmentRepository repository;
+
+ @SuppressWarnings("CodeBlock2Expr")
+ private final RecipientForeverObserver recipientObserver = recipient -> {
+ updateState(getNewState(s -> s.updateRecipient(recipient)));
+ };
+
+ private MessageRequestFragmentViewModel(@NonNull MessageRequestFragmentRepository repository) {
+ internalState.setValue(new MessageRequestFragmentState(MessageRequestFragmentState.MessageRequestState.LOADING, null, null, null, 0));
+ this.repository = repository;
+
+ loadRecipient();
+ loadMessageRecord();
+ loadGroups();
+ loadMemberCount();
+ }
+
+ @Override
+ protected void onCleared() {
+ repository.getLiveRecipient().removeForeverObserver(recipientObserver);
+ }
+
+ public @NonNull LiveData getState() {
+ return internalState;
+ }
+
+ @MainThread
+ public void accept() {
+ repository.acceptMessageRequest(() -> {
+ MessageRequestFragmentState state = internalState.getValue();
+ updateState(state.updateMessageRequestState(MessageRequestFragmentState.MessageRequestState.ACCEPTED));
+ });
+ }
+
+ @MainThread
+ public void delete() {
+ repository.deleteMessageRequest(() -> {
+ MessageRequestFragmentState state = internalState.getValue();
+ updateState(state.updateMessageRequestState(MessageRequestFragmentState.MessageRequestState.DELETED));
+ });
+ }
+
+ @MainThread
+ public void block() {
+ repository.blockMessageRequest(() -> {
+ MessageRequestFragmentState state = internalState.getValue();
+ updateState(state.updateMessageRequestState(MessageRequestFragmentState.MessageRequestState.BLOCKED));
+ });
+ }
+
+ private void updateState(@NonNull MessageRequestFragmentState newState) {
+ Log.i(TAG, "updateState: " + newState);
+ internalState.setValue(newState);
+ }
+
+ private void loadRecipient() {
+ repository.getLiveRecipient().observeForever(recipientObserver);
+ repository.refreshRecipient();
+ }
+
+ private void loadMessageRecord() {
+ repository.getMessageRecord(messageRecord -> {
+ MessageRequestFragmentState newState = getNewState(s -> s.updateMessageRecord(messageRecord));
+ updateState(newState);
+ });
+ }
+
+ private void loadGroups() {
+ repository.getGroups(groups -> {
+ MessageRequestFragmentState newState = getNewState(s -> s.updateGroups(groups));
+ updateState(newState);
+ });
+ }
+
+ private void loadMemberCount() {
+ repository.getMemberCount(memberCount -> {
+ MessageRequestFragmentState newState = getNewState(s -> s.updateMemberCount(memberCount == null ? 0 : memberCount));
+ updateState(newState);
+ });
+ }
+
+ private @NonNull MessageRequestFragmentState getNewState(@NonNull Function stateTransformer) {
+ MessageRequestFragmentState oldState = internalState.getValue();
+ MessageRequestFragmentState newState = stateTransformer.apply(oldState);
+ return newState.updateMessageRequestState(getUpdatedRequestState(newState));
+ }
+
+ private static @NonNull MessageRequestFragmentState.MessageRequestState getUpdatedRequestState(@NonNull MessageRequestFragmentState state) {
+ if (state.messageRequestState != MessageRequestFragmentState.MessageRequestState.LOADING) {
+ return state.messageRequestState;
+ }
+
+ if (state.messageRecord != null && state.recipient != null && state.groups != null) {
+ return MessageRequestFragmentState.MessageRequestState.PENDING;
+ }
+
+ return MessageRequestFragmentState.MessageRequestState.LOADING;
+ }
+
+ public static class Factory implements ViewModelProvider.Factory {
+ private final Context context;
+ private final long threadId;
+ private final RecipientId recipientId;
+
+ public Factory(@NonNull Context context, long threadId, @NonNull RecipientId recipientId) {
+ this.context = context;
+ this.threadId = threadId;
+ this.recipientId = recipientId;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ return (T) new MessageRequestFragmentViewModel(new MessageRequestFragmentRepository(context, recipientId, threadId));
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
index 48acf60797..9c7dc3105b 100644
--- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
+++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
@@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -323,7 +324,7 @@ public class MessageNotifier {
long timestamp = notifications.get(0).getTimestamp();
if (timestamp != 0) builder.setWhen(timestamp);
- if (!KeyCachingService.isLocked(context)) {
+ if (!KeyCachingService.isLocked(context) && RecipientUtil.isRecipientMessageRequestAccepted(context, recipient.resolve())) {
ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient);
builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId),
diff --git a/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java b/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java
index 7cf7c4caa3..0d76a42b66 100644
--- a/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java
+++ b/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java
@@ -19,12 +19,13 @@ import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.thoughtcrime.securesms.util.Util;
public class ProfilePreference extends Preference {
private ImageView avatarView;
private TextView profileNameView;
- private TextView profileNumberView;
+ private TextView profileSubtextView;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ProfilePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@@ -54,15 +55,15 @@ public class ProfilePreference extends Preference {
@Override
public void onBindViewHolder(PreferenceViewHolder viewHolder) {
super.onBindViewHolder(viewHolder);
- avatarView = (ImageView)viewHolder.findViewById(R.id.avatar);
- profileNameView = (TextView)viewHolder.findViewById(R.id.profile_name);
- profileNumberView = (TextView)viewHolder.findViewById(R.id.number);
+ avatarView = (ImageView)viewHolder.findViewById(R.id.avatar);
+ profileNameView = (TextView)viewHolder.findViewById(R.id.profile_name);
+ profileSubtextView = (TextView)viewHolder.findViewById(R.id.number);
refresh();
}
public void refresh() {
- if (profileNumberView == null) return;
+ if (profileSubtextView == null) return;
final Recipient self = Recipient.self();
final String profileName = TextSecurePreferences.getProfileName(getContext());
@@ -78,6 +79,6 @@ public class ProfilePreference extends Preference {
profileNameView.setText(profileName);
}
- profileNumberView.setText(self.requireE164());
+ profileSubtextView.setText(self.getUsername().or(self.getE164()).orNull());
}
}
diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java
index 79e8f73ae4..e5cf69dbed 100644
--- a/src/org/thoughtcrime/securesms/recipients/Recipient.java
+++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java
@@ -356,18 +356,18 @@ public class Recipient {
public @NonNull String getDisplayName(@NonNull Context context) {
return Util.getFirstNonEmpty(getName(context),
getProfileName(),
- getUsername(),
+ getUsername().orNull(),
e164,
email,
context.getString(R.string.Recipient_unknown));
}
- private @NonNull String getUsername() {
+ public @NonNull Optional getUsername() {
if (FeatureFlags.USERNAMES) {
// TODO [greyson] Replace with actual username
- return "@caycepollard";
+ return Optional.of("@caycepollard");
}
- return "";
+ return Optional.absent();
}
public @NonNull MaterialColor getColor() {
diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/src/org/thoughtcrime/securesms/recipients/RecipientUtil.java
index e2b7b2aa1f..86c1c69639 100644
--- a/src/org/thoughtcrime/securesms/recipients/RecipientUtil.java
+++ b/src/org/thoughtcrime/securesms/recipients/RecipientUtil.java
@@ -1,18 +1,27 @@
package org.thoughtcrime.securesms.recipients;
import android.content.Context;
+import android.widget.Toast;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
-import org.thoughtcrime.securesms.ApplicationContext;
+import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
-import org.thoughtcrime.securesms.database.RecipientDatabase;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
+import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
+import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.FeatureFlags;
+import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
+import org.thoughtcrime.securesms.sms.MessageSender;
+import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -54,4 +63,61 @@ public class RecipientUtil {
Recipient resolved = recipient.resolve();
return resolved.isPushGroup() || resolved.hasServiceIdentifier();
}
+
+ @WorkerThread
+ public static void block(@NonNull Context context, @NonNull Recipient recipient) {
+ if (!isBlockable(recipient)) {
+ throw new AssertionError("Recipient is not blockable!");
+ }
+
+ Recipient resolved = recipient.resolve();
+
+ DatabaseFactory.getRecipientDatabase(context).setBlocked(resolved.getId(), true);
+
+ if (resolved.isGroup() && DatabaseFactory.getGroupDatabase(context).isActive(resolved.requireGroupId())) {
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(resolved);
+ Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, resolved);
+
+ if (threadId != -1 && leaveMessage.isPresent()) {
+ MessageSender.send(context, leaveMessage.get(), threadId, false, null);
+
+ GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
+ String groupId = resolved.requireGroupId();
+ groupDatabase.setActive(groupId, false);
+ groupDatabase.remove(groupId, Recipient.self().getId());
+ } else {
+ Log.w(TAG, "Failed to leave group. Can't block.");
+ Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ if (resolved.isSystemContact() || resolved.isProfileSharing()) {
+ ApplicationDependencies.getJobManager().add(new RotateProfileKeyJob());
+ }
+
+ ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
+ }
+
+ @WorkerThread
+ public static void unblock(@NonNull Context context, @NonNull Recipient recipient) {
+ if (!isBlockable(recipient)) {
+ throw new AssertionError("Recipient is not blockable!");
+ }
+
+ DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient.getId(), false);
+ ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
+ }
+
+ @WorkerThread
+ public static boolean isRecipientMessageRequestAccepted(@NonNull Context context, @Nullable Recipient recipient) {
+ if (recipient == null || !FeatureFlags.MESSAGE_REQUESTS) return true;
+
+ Recipient resolved = recipient.resolve();
+
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ long threadId = threadDatabase.getThreadIdFor(resolved);
+ boolean hasSentMessage = threadDatabase.getLastSeenAndHasSent(threadId).second() == Boolean.TRUE;
+
+ return hasSentMessage || resolved.isProfileSharing() || resolved.isSystemContact();
+ }
}
diff --git a/src/org/thoughtcrime/securesms/util/FeatureFlags.java b/src/org/thoughtcrime/securesms/util/FeatureFlags.java
index c272ad933e..580db5af75 100644
--- a/src/org/thoughtcrime/securesms/util/FeatureFlags.java
+++ b/src/org/thoughtcrime/securesms/util/FeatureFlags.java
@@ -16,4 +16,7 @@ public class FeatureFlags {
/** New Profile Display */
public static final boolean PROFILE_DISPLAY = UUIDS;
+
+ /** MessageRequest stuff */
+ public static final boolean MESSAGE_REQUESTS = UUIDS;
}
diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java
index 28a9ee39a1..9810f064be 100644
--- a/src/org/thoughtcrime/securesms/util/Util.java
+++ b/src/org/thoughtcrime/securesms/util/Util.java
@@ -562,4 +562,14 @@ public class Util {
}
return handler;
}
+
+ public static List concatenatedList(List first, List second) {
+ final List concat = new ArrayList<>(first.size() + second.size());
+
+ concat.addAll(first);
+ concat.addAll(second);
+
+ return concat;
+ }
+
}