diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index eb9b4f23d5..493dd4ef21 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -442,6 +442,11 @@
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java
index 0e05503349..712ad2b0f2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java
@@ -197,7 +197,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
recipientsEditor.setHint(R.string.recipients_panel__add_members);
recipientsPanel.setPanelChangeListener(this);
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
- avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_outline_40, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this)));
+ avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this)));
avatar.setOnClickListener(view -> AvatarSelection.startAvatarSelection(this, false, false));
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
index e490bd6fde..278f3a44a0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
@@ -16,6 +16,7 @@ import androidx.appcompat.widget.AppCompatImageView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
@@ -49,10 +50,11 @@ public final class AvatarImageView extends AppCompatImageView {
DARK_THEME_OUTLINE_PAINT.setAntiAlias(true);
}
- private int size;
- private boolean inverted;
- private Paint outlinePaint;
- private OnClickListener listener;
+ private int size;
+ private boolean inverted;
+ private Paint outlinePaint;
+ private OnClickListener listener;
+ private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
private @Nullable RecipientContactPhoto recipientContactPhoto;
private @NonNull Drawable unknownRecipientDrawable;
@@ -102,6 +104,10 @@ public final class AvatarImageView extends AppCompatImageView {
super.setOnClickListener(listener);
}
+ public void setFallbackPhotoProvider(Recipient.FallbackPhotoProvider fallbackPhotoProvider) {
+ this.fallbackPhotoProvider = fallbackPhotoProvider;
+ }
+
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
if (recipient != null) {
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
@@ -111,8 +117,8 @@ public final class AvatarImageView extends AppCompatImageView {
recipientContactPhoto = photo;
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL
- ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted)
- : photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted);
+ ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
+ : photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
if (photo.contactPhoto != null) {
requestManager.load(photo.contactPhoto)
@@ -130,7 +136,13 @@ public final class AvatarImageView extends AppCompatImageView {
} else {
recipientContactPhoto = null;
requestManager.clear(this);
- setImageDrawable(unknownRecipientDrawable);
+ if (fallbackPhotoProvider != null) {
+ setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
+ .asDrawable(getContext(), MaterialColor.STEEL.toAvatarColor(getContext()), inverted));
+ } else {
+ setImageDrawable(unknownRecipientDrawable);
+ }
+
super.setOnClickListener(listener);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/MaskView.java b/app/src/main/java/org/thoughtcrime/securesms/components/MaskView.java
index ea5504bc11..7f75d4b75d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/MaskView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/MaskView.java
@@ -21,6 +21,7 @@ public class MaskView extends View {
private ViewGroup activityContentView;
private Paint maskPaint;
private Rect drawingRect = new Rect();
+ private float targetParentTranslationY;
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
@@ -63,6 +64,10 @@ public class MaskView extends View {
invalidate();
}
+ public void setTargetParentTranslationY(float targetParentTranslationY) {
+ this.targetParentTranslationY = targetParentTranslationY;
+ }
+
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
@@ -75,6 +80,8 @@ public class MaskView extends View {
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
drawingRect.bottom = Math.min(drawingRect.bottom, getBottom() - getPaddingBottom());
+ drawingRect.top += targetParentTranslationY;
+ drawingRect.bottom += targetParentTranslationY;
Bitmap mask = Bitmap.createBitmap(target.getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
Canvas maskCanvas = new Canvas(mask);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 4facf9f40a..2fdad3e8b0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -56,7 +56,6 @@ 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;
@@ -71,7 +70,9 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.text.HtmlCompat;
import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
@@ -162,8 +163,8 @@ import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult;
-import org.thoughtcrime.securesms.messagerequests.MessageRequestFragment;
-import org.thoughtcrime.securesms.messagerequests.MessageRequestFragmentViewModel;
+import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
+import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide;
@@ -215,6 +216,7 @@ 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.HtmlUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageUtil;
@@ -308,7 +310,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private TypingStatusTextWatcher typingTextWatcher;
private ConversationSearchBottomBar searchNav;
private MenuItem searchViewItem;
- private FrameLayout messageRequestOverlay;
+ private MessageRequestsBottomView messageRequestBottomView;
private ConversationReactionOverlay reactionOverlay;
private AttachmentManager attachmentManager;
@@ -319,6 +321,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected HidingLinearLayout quickAttachmentToggle;
protected HidingLinearLayout inlineAttachmentToggle;
private InputPanel inputPanel;
+ private View panelParent;
private LinkPreviewViewModel linkPreviewViewModel;
private ConversationSearchViewModel searchViewModel;
@@ -331,9 +334,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private int distributionType;
private boolean archived;
private boolean isSecureText;
- private boolean isDefaultSms = true;
- private boolean isMmsEnabled = true;
- private boolean isSecurityInitialized = false;
+ private boolean isDefaultSms = true;
+ private boolean isMmsEnabled = true;
+ private boolean isSecurityInitialized = false;
+ private boolean shouldDisplayMessageRequestUi = true;
private final IdentityRecordList identityRecords = new IdentityRecordList();
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
@@ -687,6 +691,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MenuInflater inflater = this.getMenuInflater();
menu.clear();
+ if (isInMessageRequest()) {
+ if (isActiveGroup()) {
+ inflater.inflate(R.menu.conversation_message_requests_group, menu);
+ }
+
+ inflater.inflate(R.menu.conversation_message_requests, menu);
+
+ if (recipient != null && recipient.get().isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
+ else inflater.inflate(R.menu.conversation_unmuted, menu);
+
+ super.onPrepareOptionsMenu(menu);
+ return true;
+ }
+
if (isSecureText) {
if (recipient.get().getExpireMessages() > 0) {
inflater.inflate(R.menu.conversation_expiring_on, menu);
@@ -932,6 +950,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleConversationSettings() {
+ if (isInMessageRequest()) return;
+
Intent intent = new Intent(ConversationActivity.this, RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId());
intent.putExtra(RecipientPreferenceActivity.CAN_HAVE_SAFETY_NUMBER_EXTRA,
@@ -967,13 +987,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.setMessage(bodyRes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.ConversationActivity_unblock, (dialog, which) -> {
- SimpleTask.run(() -> {
+ SignalExecutors.BOUNDED.execute(() -> {
RecipientUtil.unblock(ConversationActivity.this, recipient.get());
- return RecipientUtil.isRecipientMessageRequestAccepted(ConversationActivity.this, recipient.get());
- }, messageRequestAccepted -> {
- if (!messageRequestAccepted) {
- onMessageRequest();
- }
});
}).show();
}
@@ -1200,7 +1215,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private boolean handleDisplayQuickContact() {
- if (recipient.get().isGroup()) return false;
+ if (isInMessageRequest() || recipient.get().isGroup()) return false;
if (recipient.get().getContactUri() != null) {
ContactsContract.QuickContact.showQuickContact(ConversationActivity.this, titleView, recipient.get().getContactUri(), ContactsContract.QuickContact.MODE_LARGE, null);
@@ -1612,28 +1627,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void initializeViews() {
- titleView = findViewById(R.id.conversation_title_view);
- buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
- sendButton = ViewUtil.findById(this, R.id.send_button);
- attachButton = ViewUtil.findById(this, R.id.attach_button);
- composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
- charactersLeft = ViewUtil.findById(this, R.id.space_left);
- emojiDrawerStub = ViewUtil.findStubById(this, R.id.emoji_drawer_stub);
- attachmentKeyboardStub = ViewUtil.findStubById(this, R.id.attachment_keyboard_stub);
- unblockButton = ViewUtil.findById(this, R.id.unblock_button);
- makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button);
- registerButton = ViewUtil.findById(this, R.id.register_button);
- composePanel = ViewUtil.findById(this, R.id.bottom_panel);
- container = ViewUtil.findById(this, R.id.layout_container);
- reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
- unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
- groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub);
- quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
- 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);
- reactionOverlay = ViewUtil.findById(this, R.id.conversation_reaction_scrubber);
+ titleView = findViewById(R.id.conversation_title_view);
+ buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
+ sendButton = ViewUtil.findById(this, R.id.send_button);
+ attachButton = ViewUtil.findById(this, R.id.attach_button);
+ composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
+ charactersLeft = ViewUtil.findById(this, R.id.space_left);
+ emojiDrawerStub = ViewUtil.findStubById(this, R.id.emoji_drawer_stub);
+ attachmentKeyboardStub = ViewUtil.findStubById(this, R.id.attachment_keyboard_stub);
+ unblockButton = ViewUtil.findById(this, R.id.unblock_button);
+ makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button);
+ registerButton = ViewUtil.findById(this, R.id.register_button);
+ composePanel = ViewUtil.findById(this, R.id.bottom_panel);
+ container = ViewUtil.findById(this, R.id.layout_container);
+ reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
+ unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
+ groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub);
+ quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
+ inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
+ inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
+ panelParent = ViewUtil.findById(this, R.id.conversation_activity_panel_parent);
+ 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);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
@@ -2051,7 +2067,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void setGroupShareProfileReminder(@NonNull Recipient recipient) {
- if (!FeatureFlags.messageRequests() && recipient.isPushGroup() && !recipient.isProfileSharing()) {
+ if (!shouldDisplayMessageRequestUi && recipient.isPushGroup() && !recipient.isProfileSharing()) {
groupShareProfileView.get().setRecipient(recipient);
groupShareProfileView.get().setVisibility(View.VISIBLE);
} else if (groupShareProfileView.resolved()) {
@@ -2095,6 +2111,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
+ private boolean isInMessageRequest() {
+ return messageRequestBottomView.getVisibility() == View.VISIBLE;
+ }
private boolean isSingleConversation() {
return getRecipient() != null && !getRecipient().isGroup();
@@ -2265,7 +2284,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
long id = fragment.stageOutgoingMessage(message);
SimpleTask.run(() -> {
- if (initiating) {
+ if (!FeatureFlags.messageRequests() && initiating) {
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(recipient.getId(), true);
}
@@ -2278,7 +2297,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}, this::sendComplete);
}
- private void sendMediaMessage(final boolean forceSms, final long expiresIn, final boolean viewOnce, final int subscriptionId, boolean initiating)
+ private void sendMediaMessage(final boolean forceSms, final long expiresIn, final boolean viewOnce, final int subscriptionId, final boolean initiating)
throws InvalidMessageException
{
Log.i(TAG, "Sending media message...");
@@ -2339,8 +2358,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final long id = fragment.stageOutgoingMessage(outgoingMessage);
SimpleTask.run(() -> {
- if (initiating) {
- DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
+ if (!FeatureFlags.messageRequests() && initiating) {
+ DatabaseFactory.getRecipientDatabase(this).setProfileSharing(recipient.getId(), true);
}
return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
@@ -2355,7 +2374,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return future;
}
- private void sendTextMessage(final boolean forceSms, final long expiresIn, final int subscriptionId, final boolean initiatingConversation)
+ private void sendTextMessage(final boolean forceSms, final long expiresIn, final int subscriptionId, final boolean initiating)
throws InvalidMessageException
{
if (!isDefaultSms && (!isSecureText || forceSms)) {
@@ -2386,7 +2405,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new AsyncTask() {
@Override
protected Long doInBackground(OutgoingTextMessage... messages) {
- if (initiatingConversation) {
+ if (!FeatureFlags.messageRequests() && initiating) {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
}
@@ -2499,9 +2518,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onSuccess(final @NonNull Pair result) {
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
+ boolean initiating = threadId == -1;
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
long expiresIn = recipient.get().getExpireMessages() * 1000L;
- boolean initiating = threadId == -1;
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first(), result.second(), MediaUtil.AUDIO_AAC, true);
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
@@ -2757,37 +2776,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
@Override
- public void onMessageRequest() {
- long threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
- RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
+ public void onMessageRequest(@NonNull MessageRequestViewModel viewModel) {
- if (threadId == -1) {
- throw new IllegalStateException("MessageRequest is not supported here");
- }
+ messageRequestBottomView.setAcceptOnClickListener(v -> viewModel.accept());
+ messageRequestBottomView.setDeleteOnClickListener(v -> viewModel.delete());
+ messageRequestBottomView.setBlockOnClickListener(v -> viewModel.block());
- 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) {
+ viewModel.getRecipient().observe(this, this::presentMessageRequestBottomViewTo);
+ viewModel.getShouldDisplayMessageRequest().observe(this, this::handleShouldDisplayMessageRequest);
+ viewModel.getMesasgeRequestStatus().observe(this, status -> {
+ switch (status) {
case ACCEPTED:
- getSupportFragmentManager().popBackStack();
- messageRequestOverlay.setVisibility(View.GONE);
- container.setVisibility(View.VISIBLE);
+ messageRequestBottomView.setVisibility(View.GONE);
return;
case DELETED:
case BLOCKED:
@@ -2796,6 +2796,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
});
}
+ @Override
public void handleReaction(@NonNull View maskTarget,
@NonNull MessageRecord messageRecord,
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
@@ -2803,7 +2804,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
{
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
reactionOverlay.setOnHideListener(onHideListener);
- reactionOverlay.show(this, maskTarget, messageRecord, inputPanel.getMeasuredHeight());
+ reactionOverlay.show(this, maskTarget, messageRecord, panelParent.getMeasuredHeight());
+ }
+
+ @Override
+ public void onListVerticalTranslationChanged(float translationY) {
+ reactionOverlay.setListVerticalTranslation(translationY);
}
@Override
@@ -2892,7 +2898,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
@Override
- public void onForwardClicked() {
+ public void onForwardClicked() {
inputPanel.clearQuote();
}
@@ -2903,6 +2909,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
updateLinkPreviewState();
}
+ private void handleShouldDisplayMessageRequest(boolean shouldDisplayMessageRequest) {
+ shouldDisplayMessageRequestUi = shouldDisplayMessageRequest;
+ setGroupShareProfileReminder(recipient.get());
+
+ if (getIntent().hasExtra(TEXT_EXTRA) || getIntent().hasExtra(MEDIA_EXTRA) || getIntent().hasExtra(STICKER_EXTRA) || (isPushGroupConversation() && !isActiveGroup())) {
+ messageRequestBottomView.setVisibility(View.GONE);
+ } else {
+ messageRequestBottomView.setVisibility(shouldDisplayMessageRequest ? View.VISIBLE : View.GONE);
+ }
+
+ invalidateOptionsMenu();
+ }
+
private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
@Override
public void onDismissed(final List unverifiedIdentities) {
@@ -2996,4 +3015,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
}
+
+ private void presentMessageRequestBottomViewTo(@Nullable Recipient recipient) {
+ if (recipient == null) return;
+
+ messageRequestBottomView.setQuestionText(HtmlCompat.fromHtml(getString(R.string.MessageRequestBottomView_do_you_want_to_let, HtmlUtil.bold(recipient.getDisplayName(this))), 0));
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java
new file mode 100644
index 0000000000..328a5d08ba
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java
@@ -0,0 +1,91 @@
+package org.thoughtcrime.securesms.conversation;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.AvatarImageView;
+import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
+import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
+import org.thoughtcrime.securesms.mms.GlideRequests;
+import org.thoughtcrime.securesms.recipients.Recipient;
+
+public class ConversationBannerView extends ConstraintLayout {
+
+ private AvatarImageView contactAvatar;
+ private TextView contactTitle;
+ private TextView contactSubtitle;
+ private TextView contactDescription;
+
+ public ConversationBannerView(Context context) {
+ this(context, null);
+ }
+
+ public ConversationBannerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ConversationBannerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ inflate(getContext(), R.layout.conversation_banner_view, this);
+
+ contactAvatar = findViewById(R.id.message_request_avatar);
+ contactTitle = findViewById(R.id.message_request_title);
+ contactSubtitle = findViewById(R.id.message_request_subtitle);
+ contactDescription = findViewById(R.id.message_request_description);
+
+ contactAvatar.setFallbackPhotoProvider(new FallbackPhotoProvider());
+ }
+
+ public void setAvatar(@NonNull GlideRequests requests, @Nullable Recipient recipient) {
+ contactAvatar.setAvatar(requests, recipient, false);
+ }
+
+ public void setTitle(@Nullable CharSequence title) {
+ contactTitle.setText(title);
+ }
+
+ public void setSubtitle(@Nullable CharSequence subtitle) {
+ contactSubtitle.setText(subtitle);
+ }
+
+ public void setDescription(@Nullable CharSequence description) {
+ contactDescription.setText(description);
+ }
+
+ public void hideSubtitle() {
+ contactSubtitle.setVisibility(View.GONE);
+ }
+
+ public void showDescription() {
+ contactDescription.setVisibility(View.VISIBLE);
+ }
+
+ public void hideDescription() {
+ contactDescription.setVisibility(View.GONE);
+ }
+
+ private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
+ @Override
+ public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
+ return new ResourceContactPhoto(R.drawable.ic_profile_80);
+ }
+
+ @Override
+ public @NonNull FallbackContactPhoto getPhotoForGroup() {
+ return new ResourceContactPhoto(R.drawable.ic_group_80);
+ }
+
+ @Override
+ public @NonNull FallbackContactPhoto getPhotoForLocalNumber() {
+ return new ResourceContactPhoto(R.drawable.ic_note_80);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index cf17354b7a..95670e6c0c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -49,7 +49,9 @@ import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
+import androidx.core.text.HtmlCompat;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -63,7 +65,6 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.TooltipPopup;
@@ -88,6 +89,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.mediasend.Media;
+import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
@@ -100,12 +102,14 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity;
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
+import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.sms.MessageSender;
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.HtmlUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -151,6 +155,7 @@ public class ConversationFragment extends Fragment
private int activeOffset;
private boolean firstLoad;
private boolean isReacting;
+ private boolean shouldDisplayMessageRequest;
private ActionMode actionMode;
private Locale locale;
private RecyclerView list;
@@ -162,6 +167,9 @@ public class ConversationFragment extends Fragment
private View composeDivider;
private View scrollToBottomButton;
private TextView scrollDateHeader;
+ private ConversationBannerView conversationBanner;
+ private ConversationBannerView emptyConversationBanner;
+ private MessageRequestViewModel messageRequestViewModel;
@Override
public void onCreate(Bundle icicle) {
@@ -172,10 +180,11 @@ public class ConversationFragment extends Fragment
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
- list = ViewUtil.findById(view, android.R.id.list);
- composeDivider = ViewUtil.findById(view, R.id.compose_divider);
- scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button);
- scrollDateHeader = ViewUtil.findById(view, R.id.scroll_date_header);
+ list = ViewUtil.findById(view, android.R.id.list);
+ composeDivider = ViewUtil.findById(view, R.id.compose_divider);
+ scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button);
+ scrollDateHeader = ViewUtil.findById(view, R.id.scroll_date_header);
+ emptyConversationBanner = ViewUtil.findById(view, R.id.empty_conversation_banner);
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
@@ -184,6 +193,10 @@ public class ConversationFragment extends Fragment
list.setLayoutManager(layoutManager);
list.setItemAnimator(null);
+ if (FeatureFlags.messageRequests()) {
+ conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false);
+ }
+
topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
initializeLoadMoreView(topLoadMoreView);
@@ -193,18 +206,59 @@ public class ConversationFragment extends Fragment
new ConversationItemSwipeCallback(
messageRecord -> actionMode == null &&
- canReplyToMessage(isActionMessage(messageRecord), messageRecord),
+ canReplyToMessage(isActionMessage(messageRecord), messageRecord, shouldDisplayMessageRequest),
this::handleReplyMessage
).attachToRecyclerView(list);
+ setupListLayoutListeners();
+
return view;
}
+ private void setupListLayoutListeners() {
+ if (!FeatureFlags.messageRequests()) {
+ return;
+ }
+
+ list.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> setListVerticalTranslation());
+
+ list.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
+ @Override
+ public void onChildViewAttachedToWindow(@NonNull View view) {
+ setListVerticalTranslation();
+ }
+
+ @Override
+ public void onChildViewDetachedFromWindow(@NonNull View view) {
+ setListVerticalTranslation();
+ }
+ });
+ }
+
+ private void setListVerticalTranslation() {
+ int heightOfChildren = 0;
+ for (int i = 0; i < list.getChildCount(); i++) {
+ heightOfChildren += list.getChildAt(i).getMeasuredHeight();
+ }
+
+ Log.i(TAG, "Height of children: " + heightOfChildren + " my height: " + list.getMeasuredHeight());
+ if (heightOfChildren > list.getMeasuredHeight()) {
+ list.setTranslationY(0);
+ list.setOverScrollMode(RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS);
+ } else {
+ list.setTranslationY(heightOfChildren - list.getMeasuredHeight());
+ list.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER);
+ }
+
+ listener.onListVerticalTranslationChanged(list.getTranslationY());
+ }
+
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
initializeResources();
+ initializeMessageRequestViewModel();
initializeListAdapter();
}
@@ -241,6 +295,7 @@ public class ConversationFragment extends Fragment
}
initializeResources();
+ messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
initializeListAdapter();
if (threadId == -1) {
@@ -267,6 +322,86 @@ public class ConversationFragment extends Fragment
scrollToLastSeenPosition(position);
}
+ private void initializeMessageRequestViewModel() {
+ MessageRequestViewModel.Factory factory = new MessageRequestViewModel.Factory(requireContext());
+
+ messageRequestViewModel = ViewModelProviders.of(requireActivity(), factory).get(MessageRequestViewModel.class);
+ messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
+
+ listener.onMessageRequest(messageRequestViewModel);
+
+ messageRequestViewModel.getRecipientInfo().observe(getViewLifecycleOwner(), recipientInfo -> {
+ presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
+ presentMessageRequestProfileView(requireContext(), recipientInfo, emptyConversationBanner);
+ });
+
+ messageRequestViewModel.getShouldDisplayMessageRequest().observe(getViewLifecycleOwner(), this::handleShouldDisplayMessageRequest);
+ }
+
+ private void handleShouldDisplayMessageRequest(boolean shouldDisplayMessageRequest) {
+ this.shouldDisplayMessageRequest = shouldDisplayMessageRequest;
+ }
+
+ private static void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) {
+
+ if (conversationBanner == null) {
+ return;
+ }
+
+ Recipient recipient = recipientInfo.getRecipient();
+ boolean isSelf = Recipient.self().equals(recipient);
+ int memberCount = recipientInfo.getGroupMemberCount();
+ List groups = recipientInfo.getSharedGroups();
+
+ if (recipient != null) {
+ conversationBanner.setAvatar(GlideApp.with(context), recipient);
+
+ String title = isSelf ? context.getString(R.string.note_to_self) : recipient.getDisplayName(context);
+ conversationBanner.setTitle(title);
+
+ if (recipient.isGroup()) {
+ conversationBanner.setSubtitle(context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_members, memberCount, memberCount));
+ } else if (isSelf) {
+ conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation));
+ } else {
+ String subtitle = recipient.getUsername().or(recipient.getE164()).orNull();
+
+ if (subtitle == null || subtitle.equals(title)) {
+ conversationBanner.hideSubtitle();
+ } else {
+ conversationBanner.setSubtitle(subtitle);
+ }
+ }
+ }
+
+ if (groups.isEmpty() || isSelf) {
+ conversationBanner.hideDescription();
+ } else {
+ final String description;
+
+ switch (groups.size()) {
+ case 1:
+ description = context.getString(R.string.MessageRequestProfileView_member_of_one_group, HtmlUtil.bold(groups.get(0)));
+ break;
+ case 2:
+ description = context.getString(R.string.MessageRequestProfileView_member_of_two_groups, HtmlUtil.bold(groups.get(0)), HtmlUtil.bold(groups.get(1)));
+ break;
+ case 3:
+ description = context.getString(R.string.MessageRequestProfileView_member_of_many_groups, HtmlUtil.bold(groups.get(0)), HtmlUtil.bold(groups.get(1)), HtmlUtil.bold(groups.get(2)));
+ break;
+ default:
+ int others = groups.size() - 2;
+ description = context.getString(R.string.MessageRequestProfileView_member_of_many_groups,
+ HtmlUtil.bold(groups.get(0)),
+ HtmlUtil.bold(groups.get(1)),
+ context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_member_of_others, others, others));
+ }
+
+ conversationBanner.setDescription(HtmlCompat.fromHtml(description, 0));
+ conversationBanner.showDescription();
+ }
+ }
+
private void initializeResources() {
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
@@ -288,6 +423,10 @@ public class ConversationFragment extends Fragment
setLastSeen(lastSeen);
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
+
+ emptyConversationBanner.setVisibility(View.GONE);
+ } else if (FeatureFlags.messageRequests() && threadId == -1) {
+ emptyConversationBanner.setVisibility(View.VISIBLE);
}
}
@@ -419,15 +558,16 @@ public class ConversationFragment extends Fragment
menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage && !sharedContact && !viewOnce);
menu.findItem(R.id.menu_context_details).setVisible(!actionMessage);
- menu.findItem(R.id.menu_context_reply).setVisible(canReplyToMessage(actionMessage, messageRecord));
+ menu.findItem(R.id.menu_context_reply).setVisible(canReplyToMessage(actionMessage, messageRecord, shouldDisplayMessageRequest));
}
menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage && hasText);
}
- private static boolean canReplyToMessage(boolean actionMessage, MessageRecord messageRecord) {
- return !actionMessage &&
- !messageRecord.isPending() &&
- !messageRecord.isFailed() &&
+ private static boolean canReplyToMessage(boolean actionMessage, MessageRecord messageRecord, boolean isDisplayingMessageRequest) {
+ return !actionMessage &&
+ !messageRecord.isPending() &&
+ !messageRecord.isFailed() &&
+ !isDisplayingMessageRequest &&
messageRecord.isSecure();
}
@@ -462,6 +602,7 @@ public class ConversationFragment extends Fragment
if (this.threadId != threadId) {
this.threadId = threadId;
+ messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
initializeListAdapter();
}
}
@@ -708,6 +849,8 @@ public class ConversationFragment extends Fragment
if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && loader.hasLimit()) {
adapter.setFooterView(topLoadMoreView);
+ } else if (FeatureFlags.messageRequests()) {
+ adapter.setFooterView(conversationBanner);
} else {
adapter.setFooterView(null);
}
@@ -717,11 +860,7 @@ public class ConversationFragment extends Fragment
}
if (FeatureFlags.messageRequests()) {
- if (!loader.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isProfileSharing() && !recipient.get().isBlocked() && recipient.get().isRegistered()) {
- listener.onMessageRequest();
- } else {
- clearHeaderIfNotTyping(adapter);
- }
+ clearHeaderIfNotTyping(adapter);
} else {
if (!loader.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
adapter.setHeaderView(unknownSenderView);
@@ -751,8 +890,10 @@ public class ConversationFragment extends Fragment
if (firstLoad) {
if (startingPosition >= 0) {
scrollToStartingPosition(startingPosition);
- } else {
+ } else if (loader.isMessageRequestAccepted()) {
scrollToLastSeenPosition(lastSeenPosition);
+ } else if (FeatureFlags.messageRequests()) {
+ list.post(() -> getListLayoutManager().scrollToPosition(adapter.getItemCount() - 1));
}
firstLoad = false;
} else if (previousOffset > 0) {
@@ -898,12 +1039,13 @@ public class ConversationFragment extends Fragment
void handleReplyMessage(MessageRecord messageRecord);
void onMessageActionToolbarOpened();
void onForwardClicked();
- void onMessageRequest();
+ void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
void handleReaction(@NonNull View maskTarget,
@NonNull MessageRecord messageRecord,
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
void onCursorChanged();
+ void onListVerticalTranslationChanged(float translationY);
}
private class ConversationScrollListener extends OnScrollListener {
@@ -1000,6 +1142,7 @@ public class ConversationFragment extends Fragment
if (messageRecord.isSecure() &&
!messageRecord.isUpdate() &&
!recipient.get().isBlocked() &&
+ !shouldDisplayMessageRequest &&
((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
{
isReacting = true;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
index 83712dfd70..e0a14e5b68 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
@@ -129,6 +129,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
initAnimators();
}
+ public void setListVerticalTranslation(float translationY) {
+ maskView.setTargetParentTranslationY(translationY);
+ }
+
public void show(@NonNull Activity activity, @NonNull View maskTarget, @NonNull MessageRecord messageRecord, int maskPaddingBottom) {
if (overlayState != OverlayState.HIDDEN) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
index b5c883e098..15894f1f26 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -40,6 +40,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@@ -115,9 +116,11 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.permissions.Permissions;
+import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AvatarUtil;
+import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -133,6 +136,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Set;
+import static android.app.Activity.RESULT_OK;
+
public class ConversationListFragment extends MainFragment implements LoaderManager.LoaderCallbacks,
ActionMode.Callback,
@@ -141,6 +146,10 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
MainNavigator.BackHandler,
MegaphoneActionController
{
+ public static final short MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME = 32562;
+ public static final short PROFILE_NAMES_REQUEST_CODE_CREATE_NAME = 18473;
+ public static final short PROFILE_NAMES_REQUEST_CODE_CONFIRM_NAME = 19563;
+
private static final String TAG = Log.tag(ConversationListFragment.class);
private static final int[] EMPTY_IMAGES = new int[] { R.drawable.empty_inbox_1,
@@ -310,14 +319,30 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
+ if (resultCode != RESULT_OK) {
+ return;
+ }
+
+ boolean isProfileCreatedRequestCode = requestCode == MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME ||
+ requestCode ==PROFILE_NAMES_REQUEST_CODE_CREATE_NAME;
+
+ if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN) {
Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL);
+ } else if (isProfileCreatedRequestCode) {
+ Snackbar.make(fab, R.string.ConversationListFragment__your_profile_name_has_been_created, Snackbar.LENGTH_LONG).show();
+
+ if (requestCode == MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME) {
+ viewModel.onMegaphoneCompleted(Megaphones.Event.MESSAGE_REQUESTS);
+ }
+ } else if (requestCode == PROFILE_NAMES_REQUEST_CODE_CONFIRM_NAME) {
+ Snackbar.make(fab, R.string.ConversationListFragment__your_profile_name_has_been_saved, Snackbar.LENGTH_LONG).show();
}
}
@Override
public void onConversationClicked(@NonNull ThreadRecord threadRecord) {
+ hideKeyboard();
getNavigator().goToConversation(threadRecord.getRecipient().getId(),
threadRecord.getThreadId(),
threadRecord.getDistributionType(),
@@ -330,6 +355,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
}, threadId -> {
+ hideKeyboard();
getNavigator().goToConversation(contact.getId(),
threadId,
ThreadDatabase.DistributionTypes.DEFAULT,
@@ -344,6 +370,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
int startingPosition = DatabaseFactory.getMmsSmsDatabase(getContext()).getMessagePositionInConversation(message.threadId, message.receivedTimestampMs);
return Math.max(0, startingPosition);
}, startingPosition -> {
+ hideKeyboard();
getNavigator().goToConversation(message.conversationRecipient.getId(),
message.threadId,
ThreadDatabase.DistributionTypes.DEFAULT,
@@ -382,6 +409,11 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
viewModel.onMegaphoneCompleted(event);
}
+ private void hideKeyboard() {
+ InputMethodManager imm = ServiceUtil.getInputMethodManager(requireContext());
+ imm.hideSoftInputFromWindow(requireView().getWindowToken(), 0);
+ }
+
private void initializeProfileIcon(@NonNull Recipient recipient) {
ImageView icon = requireView().findViewById(R.id.toolbar_icon);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
index 87ccb53d5f..bcbca1a918 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
@@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SearchUtil;
+import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -70,6 +71,7 @@ public class ConversationListItem extends RelativeLayout
private Set selectedThreads;
private LiveRecipient recipient;
+ private LiveRecipient groupAddedBy;
private long threadId;
private GlideRequests glideRequests;
private View subjectContainer;
@@ -82,6 +84,7 @@ public class ConversationListItem extends RelativeLayout
private AlertView alertView;
private TextView unreadIndicator;
private long lastSeen;
+ private ThreadRecord thread;
private int unreadCount;
private AvatarImageView contactPhotoImage;
@@ -89,6 +92,12 @@ public class ConversationListItem extends RelativeLayout
private int distributionType;
+ private final RecipientForeverObserver groupAddedByObserver = adder -> {
+ if (isAttachedToWindow() && subjectView != null && thread != null) {
+ subjectView.setText(thread.getDisplayBody(getContext()));
+ }
+ };
+
public ConversationListItem(Context context) {
this(context, null);
}
@@ -137,6 +146,7 @@ public class ConversationListItem extends RelativeLayout
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
+ if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
this.selectedThreads = selectedThreads;
this.recipient = thread.getRecipient().live();
@@ -145,6 +155,7 @@ public class ConversationListItem extends RelativeLayout
this.unreadCount = thread.getUnreadCount();
this.distributionType = thread.getDistributionType();
this.lastSeen = thread.getLastSeen();
+ this.thread = thread;
this.recipient.observeForever(this);
if (highlightSubstring != null) {
@@ -166,6 +177,12 @@ public class ConversationListItem extends RelativeLayout
this.subjectView.setVisibility(VISIBLE);
this.subjectView.setText(getTrimmedSnippet(thread.getDisplayBody(getContext())));
+
+ if (thread.getGroupAddedBy() != null) {
+ groupAddedBy = Recipient.live(thread.getGroupAddedBy());
+ groupAddedBy.observeForever(groupAddedByObserver);
+ }
+
this.subjectView.setTypeface(unreadCount == 0 ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
this.subjectView.setTextColor(unreadCount == 0 ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color));
@@ -199,6 +216,7 @@ public class ConversationListItem extends RelativeLayout
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
+ if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
this.selectedThreads = Collections.emptySet();
this.recipient = contact.live();
@@ -227,6 +245,7 @@ public class ConversationListItem extends RelativeLayout
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
+ if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
this.selectedThreads = Collections.emptySet();
this.recipient = messageResult.conversationRecipient.live();
@@ -255,6 +274,11 @@ public class ConversationListItem extends RelativeLayout
this.recipient = null;
contactPhotoImage.setAvatar(glideRequests, null, true);
}
+
+ if (this.groupAddedBy != null) {
+ this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
+ this.groupAddedBy = null;
+ }
}
private void setBatchState(boolean batch) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java
index 0b637d8fb0..d3f5218c56 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java
@@ -19,16 +19,18 @@ package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import org.thoughtcrime.securesms.R;
+
/**
* RecyclerView.Adapter that manages a Cursor, comparable to the CursorAdapter usable in ListView/GridView.
*/
@@ -47,8 +49,24 @@ public abstract class CursorRecyclerViewAdapter getGroupNamesContainingMember(RecipientId recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
+ String table = TABLE_NAME + " INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + RECIPIENT_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
List groupNames = new LinkedList<>();
String[] projection = new String[]{TITLE, MEMBERS};
String query = MEMBERS + " LIKE ?";
String[] args = new String[]{"%" + recipientId.serialize() + "%"};
+ String orderBy = ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.DATE + " DESC";
- try (Cursor cursor = database.query(TABLE_NAME, projection, query, args, null, null, null)) {
+ try (Cursor cursor = database.query(table, projection, query, args, null, null, orderBy)) {
while (cursor != null && cursor.moveToNext()) {
List members = Util.split(cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), ",");
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java
index e8ebf9d964..5c0b00d65e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java
@@ -30,6 +30,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
public abstract class MessagingDatabase extends Database implements MmsSmsColumns {
@@ -72,6 +73,36 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
return getMessageCountForRecipientsAndType(getOutgoingSecureMessageClause());
}
+ final int getSecureMessageCount(long threadId) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ String[] projection = new String[] {"COUNT(*)"};
+ String query = getSecureMessageClause() + "AND " + MmsSmsColumns.THREAD_ID + " = ?";
+ String[] args = new String[]{String.valueOf(threadId)};
+
+ try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ final int getOutgoingSecureMessageCount(long threadId) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ String[] projection = new String[] {"COUNT(*)"};
+ String query = getOutgoingSecureMessageClause() + "AND " + MmsSmsColumns.THREAD_ID + " = ? AND" + "(" + getTypeField() + " & " + Types.GROUP_QUIT_BIT + " = 0)";
+ String[] args = new String[]{String.valueOf(threadId)};
+
+ try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ } else {
+ return 0;
+ }
+ }
+ }
+
private int getMessageCountForRecipientsAndType(String typeClause) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
@@ -96,6 +127,14 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
return "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND (" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
}
+ private String getSecureMessageClause() {
+ String isSent = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE;
+ String isReceived = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE;
+ String isSecure = "(" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
+
+ return String.format(Locale.ENGLISH, "(%s OR %s) AND %s", isSent, isReceived, isSecure);
+ }
+
public void setReactionsSeen(long threadId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
@@ -432,14 +471,20 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public static class MarkedMessageInfo {
+ private final long threadId;
private final SyncMessageId syncMessageId;
private final ExpirationInfo expirationInfo;
- public MarkedMessageInfo(SyncMessageId syncMessageId, ExpirationInfo expirationInfo) {
+ public MarkedMessageInfo(long threadId, SyncMessageId syncMessageId, ExpirationInfo expirationInfo) {
+ this.threadId = threadId;
this.syncMessageId = syncMessageId;
this.expirationInfo = expirationInfo;
}
+ public long getThreadId() {
+ return threadId;
+ }
+
public SyncMessageId getSyncMessageId() {
return syncMessageId;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
index 889646d817..ae76b8e124 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -20,11 +20,12 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.mms.pdu_alt.NotificationInd;
@@ -81,7 +82,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
import static org.thoughtcrime.securesms.contactshare.Contact.Avatar;
@@ -244,6 +244,42 @@ public class MmsDatabase extends MessagingDatabase {
return MESSAGE_BOX;
}
+ public boolean isGroupQuitMessage(long messageId) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+
+ String[] columns = new String[]{ID};
+ String query = ID + " = ? AND " + MESSAGE_BOX + " & ?";
+ long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_QUIT_BIT;
+ String[] args = new String[]{String.valueOf(messageId), String.valueOf(type)};
+
+ try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null, null)) {
+ if (cursor.getCount() == 1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public long getLatestGroupQuitTimestamp(long threadId, long quitTimeBarrier) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+
+ String[] columns = new String[]{DATE_SENT};
+ String query = THREAD_ID + " = ? AND " + MESSAGE_BOX + " & ? AND " + DATE_SENT + " < ?";
+ long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_QUIT_BIT;
+ String[] args = new String[]{String.valueOf(threadId), String.valueOf(type), String.valueOf(quitTimeBarrier)};
+ String orderBy = DATE_SENT + " DESC";
+ String limit = "1";
+
+ try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, orderBy, limit)) {
+ if (cursor.moveToFirst()) {
+ return cursor.getLong(cursor.getColumnIndex(DATE_SENT));
+ }
+ }
+
+ return -1;
+ }
+
public int getMessageCountForThread(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
@@ -533,14 +569,20 @@ public class MmsDatabase extends MessagingDatabase {
database.beginTransaction();
try {
- cursor = database.query(TABLE_NAME, new String[] {ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED}, where, arguments, null, null, null);
+ cursor = database.query(TABLE_NAME, new String[] {ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED, THREAD_ID}, where, arguments, null, null, null);
while(cursor != null && cursor.moveToNext()) {
- if (Types.isSecureType(cursor.getLong(3))) {
- SyncMessageId syncMessageId = new SyncMessageId(RecipientId.from(cursor.getLong(1)), cursor.getLong(2));
- ExpirationInfo expirationInfo = new ExpirationInfo(cursor.getLong(0), cursor.getLong(4), cursor.getLong(5), true);
+ if (Types.isSecureType(cursor.getLong(cursor.getColumnIndex(MESSAGE_BOX)))) {
+ long threadId = cursor.getLong(cursor.getColumnIndex(THREAD_ID));
+ RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndex(RECIPIENT_ID)));
+ long dateSent = cursor.getLong(cursor.getColumnIndex(DATE_SENT));
+ long messageId = cursor.getLong(cursor.getColumnIndex(ID));
+ long expiresIn = cursor.getLong(cursor.getColumnIndex(EXPIRES_IN));
+ long expireStarted = cursor.getLong(cursor.getColumnIndex(EXPIRE_STARTED));
+ SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent);
+ ExpirationInfo expirationInfo = new ExpirationInfo(messageId, expiresIn, expireStarted, true);
- result.add(new MarkedMessageInfo(syncMessageId, expirationInfo));
+ result.add(new MarkedMessageInfo(threadId, syncMessageId, expirationInfo));
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index 438de54343..95e8e0df89 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -22,6 +22,8 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.annimon.stream.Stream;
+
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteQueryBuilder;
@@ -30,8 +32,10 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.whispersystems.libsignal.util.Pair;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
public class MmsSmsDatabase extends Database {
@@ -84,6 +88,32 @@ public class MmsSmsDatabase extends Database {
super(context, databaseHelper);
}
+ public @Nullable RecipientId getRecipientIdForLatestAdd(long threadId) {
+ long lastQuitChecked = System.currentTimeMillis();
+ Pair pair;
+
+ do {
+ pair = getRecipientIdForLatestAdd(threadId, lastQuitChecked);
+ if (pair.first() != null) {
+ return pair.first();
+ } else {
+ lastQuitChecked = pair.second();
+ }
+
+ } while (pair.second() != -1L);
+
+ return null;
+ }
+
+ private @NonNull Pair getRecipientIdForLatestAdd(long threadId, long lastQuitChecked) {
+ MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
+ SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
+ long latestQuit = mmsDatabase.getLatestGroupQuitTimestamp(threadId, lastQuitChecked);
+ RecipientId id = smsDatabase.getOldestGroupUpdateSender(threadId, latestQuit);
+
+ return new Pair<>(id, latestQuit);
+ }
+
public @Nullable MessageRecord getMessageFor(long timestamp, RecipientId author) {
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
@@ -166,6 +196,28 @@ public class MmsSmsDatabase extends Database {
}
}
+ public int getSecureConversationCount(long threadId) {
+ if (threadId == -1) {
+ return 0;
+ }
+
+ int count = DatabaseFactory.getSmsDatabase(context).getSecureMessageCount(threadId);
+ count += DatabaseFactory.getMmsDatabase(context).getSecureMessageCount(threadId);
+
+ return count;
+ }
+
+ public int getOutgoingSecureConversationCount(long threadId) {
+ if (threadId == -1L) {
+ return 0;
+ }
+
+ int count = DatabaseFactory.getSmsDatabase(context).getOutgoingSecureMessageCount(threadId);
+ count += DatabaseFactory.getMmsDatabase(context).getOutgoingSecureMessageCount(threadId);
+
+ return count;
+ }
+
public int getConversationCount(long threadId) {
int count = DatabaseFactory.getSmsDatabase(context).getMessageCountForThread(threadId);
count += DatabaseFactory.getMmsDatabase(context).getMessageCountForThread(threadId);
@@ -194,6 +246,13 @@ public class MmsSmsDatabase extends Database {
return count;
}
+ public long getThreadForMessageId(long messageId) {
+ long id = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
+
+ if (id == -1) return DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
+ else return id;
+ }
+
public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId, long timestamp) {
DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, true, false);
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true, false);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
index fa31ad9550..55b3d26bdd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -20,10 +20,12 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
-import androidx.annotation.NonNull;
import android.text.TextUtils;
import android.util.Pair;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.annimon.stream.Stream;
import net.sqlcipher.database.SQLiteDatabase;
@@ -45,7 +47,6 @@ import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
@@ -55,7 +56,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
/**
* Database for storage of SMS messages.
@@ -162,6 +162,24 @@ public class SmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
}
+ public @Nullable RecipientId getOldestGroupUpdateSender(long threadId, long minimumDateReceived) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+
+ String[] columns = new String[]{RECIPIENT_ID};
+ String query = THREAD_ID + " = ? AND " + TYPE + " & ? AND " + DATE_RECEIVED + " >= ?";
+ long type = Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT | Types.GROUP_UPDATE_BIT | Types.BASE_INBOX_TYPE;
+ String[] args = new String[]{String.valueOf(threadId), String.valueOf(type), String.valueOf(minimumDateReceived)};
+ String limit = "1";
+
+ try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, limit)) {
+ if (cursor.moveToFirst()) {
+ return RecipientId.from(cursor.getLong(cursor.getColumnIndex(RECIPIENT_ID)));
+ }
+ }
+
+ return null;
+ }
+
public long getThreadIdForMessage(long id) {
String sql = "SELECT " + THREAD_ID + " FROM " + TABLE_NAME + " WHERE " + ID + " = ?";
String[] sqlArgs = new String[] {id+""};
@@ -446,14 +464,20 @@ public class SmsDatabase extends MessagingDatabase {
database.beginTransaction();
try {
- cursor = database.query(TABLE_NAME, new String[] {ID, RECIPIENT_ID, DATE_SENT, TYPE, EXPIRES_IN, EXPIRE_STARTED}, where, arguments, null, null, null);
+ cursor = database.query(TABLE_NAME, new String[] {ID, RECIPIENT_ID, DATE_SENT, TYPE, EXPIRES_IN, EXPIRE_STARTED, THREAD_ID}, where, arguments, null, null, null);
while (cursor != null && cursor.moveToNext()) {
- if (Types.isSecureType(cursor.getLong(3))) {
- SyncMessageId syncMessageId = new SyncMessageId(RecipientId.from(cursor.getLong(1)), cursor.getLong(2));
- ExpirationInfo expirationInfo = new ExpirationInfo(cursor.getLong(0), cursor.getLong(4), cursor.getLong(5), false);
+ if (Types.isSecureType(cursor.getLong(cursor.getColumnIndex(TYPE)))) {
+ long threadId = cursor.getLong(cursor.getColumnIndex(THREAD_ID));
+ RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndex(RECIPIENT_ID)));
+ long dateSent = cursor.getLong(cursor.getColumnIndex(DATE_SENT));
+ long messageId = cursor.getLong(cursor.getColumnIndex(ID));
+ long expiresIn = cursor.getLong(cursor.getColumnIndex(EXPIRES_IN));
+ long expireStarted = cursor.getLong(cursor.getColumnIndex(EXPIRE_STARTED));
+ SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent);
+ ExpirationInfo expirationInfo = new ExpirationInfo(messageId, expiresIn, expireStarted, false);
- results.add(new MarkedMessageInfo(syncMessageId, expirationInfo));
+ results.add(new MarkedMessageInfo(threadId, syncMessageId, expirationInfo));
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
index 336cd327e4..586a675537 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -704,13 +705,30 @@ public class ThreadDatabase extends Database {
}
private @Nullable Extra getExtrasFor(MessageRecord record) {
+ boolean messageRequestAccepted = RecipientUtil.isThreadMessageRequestAccepted(context, record.getThreadId());
+ RecipientId threadRecipientId = getRecipientIdForThreadId(record.getThreadId());
+
+ if (!messageRequestAccepted && threadRecipientId != null) {
+ boolean isPushGroup = Recipient.resolved(threadRecipientId).isPushGroup();
+ if (isPushGroup) {
+ RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getRecipientIdForLatestAdd(record.getThreadId());
+
+ if (recipientId != null) {
+ return Extra.forGroupMessageRequest(recipientId);
+ }
+ }
+
+ return Extra.forMessageRequest();
+ }
+
if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
- return Extra.forRevealableMessage();
+ return Extra.forRevealable();
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
return Extra.forSticker();
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getSlides().size() > 1) {
return Extra.forAlbum();
}
+
return null;
}
@@ -829,28 +847,41 @@ public class ThreadDatabase extends Database {
@JsonProperty private final boolean isRevealable;
@JsonProperty private final boolean isSticker;
@JsonProperty private final boolean isAlbum;
+ @JsonProperty private final boolean isMessageRequestAccepted;
+ @JsonProperty private final String groupAddedBy;
public Extra(@JsonProperty("isRevealable") boolean isRevealable,
@JsonProperty("isSticker") boolean isSticker,
- @JsonProperty("isAlbum") boolean isAlbum)
+ @JsonProperty("isAlbum") boolean isAlbum,
+ @JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted,
+ @JsonProperty("groupAddedBy") String groupAddedBy)
{
- this.isRevealable = isRevealable;
- this.isSticker = isSticker;
- this.isAlbum = isAlbum;
+ this.isRevealable = isRevealable;
+ this.isSticker = isSticker;
+ this.isAlbum = isAlbum;
+ this.isMessageRequestAccepted = isMessageRequestAccepted;
+ this.groupAddedBy = groupAddedBy;
}
- public static @NonNull Extra forRevealableMessage() {
- return new Extra(true, false, false);
+ public static @NonNull Extra forRevealable() {
+ return new Extra(true, false, false, true, null);
}
public static @NonNull Extra forSticker() {
- return new Extra(false, true, false);
+ return new Extra(false, true, false, true, null);
}
public static @NonNull Extra forAlbum() {
- return new Extra(false, false, true);
+ return new Extra(false, false, true, true, null);
}
+ public static @NonNull Extra forMessageRequest() {
+ return new Extra(false, false, false, false, null);
+ }
+
+ public static @NonNull Extra forGroupMessageRequest(RecipientId recipientId) {
+ return new Extra(false, false, false, false, recipientId.serialize());
+ }
public boolean isRevealable() {
return isRevealable;
@@ -863,5 +894,13 @@ public class ThreadDatabase extends Database {
public boolean isAlbum() {
return isAlbum;
}
+
+ public boolean isMessageRequestAccepted() {
+ return isMessageRequestAccepted;
+ }
+
+ public @Nullable String getGroupAddedBy() {
+ return groupAddedBy;
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java
index fe6a2af0e7..2806e9c459 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java
@@ -4,6 +4,7 @@ import android.content.Context;
import android.database.Cursor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
import org.whispersystems.libsignal.util.Pair;
@@ -13,6 +14,7 @@ public class ConversationLoader extends AbstractCursorLoader {
private int limit;
private long lastSeen;
private boolean hasSent;
+ private boolean isMessageRequestAccepted;
public ConversationLoader(Context context, long threadId, int offset, int limit, long lastSeen) {
super(context);
@@ -43,6 +45,10 @@ public class ConversationLoader extends AbstractCursorLoader {
return hasSent;
}
+ public boolean isMessageRequestAccepted() {
+ return isMessageRequestAccepted;
+ }
+
@Override
public Cursor getCursor() {
Pair lastSeenAndHasSent = DatabaseFactory.getThreadDatabase(context).getLastSeenAndHasSent(threadId);
@@ -53,6 +59,8 @@ public class ConversationLoader extends AbstractCursorLoader {
this.lastSeen = lastSeenAndHasSent.first();
}
+ this.isMessageRequestAccepted = RecipientUtil.isThreadMessageRequestAccepted(context, threadId);
+
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId, offset, limit);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
index 905b51becb..cecb13750f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase.Extra;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -77,7 +78,11 @@ public class ThreadRecord extends DisplayRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
- if (isGroupUpdate()) {
+ if (getGroupAddedBy() != null) {
+ return emphasisAdded(context.getString(R.string.ThreadRecord_s_added_you_to_the_group, Recipient.live(getGroupAddedBy()).get().getDisplayName(context)));
+ } else if (!isMessageRequestAccepted()) {
+ return emphasisAdded(context.getString(R.string.ThreadRecord_message_request));
+ } else if (isGroupUpdate()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
} else if (isGroupQuit()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
@@ -181,4 +186,14 @@ public class ThreadRecord extends DisplayRecord {
public long getLastSeen() {
return lastSeen;
}
+
+ public @Nullable RecipientId getGroupAddedBy() {
+ if (extra != null && extra.getGroupAddedBy() != null) return RecipientId.from(extra.getGroupAddedBy());
+ else return null;
+ }
+
+ public boolean isMessageRequestAccepted() {
+ if (extra != null) return extra.isMessageRequestAccepted();
+ else return true;
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java
index acb573c4ca..c7923ed6d4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java
@@ -208,6 +208,10 @@ public class Data {
}
}
+ public Builder buildUpon() {
+ return new Builder(this);
+ }
+
public static class Builder {
@@ -224,6 +228,23 @@ public class Data {
private final Map booleans = new HashMap<>();
private final Map booleanArrays = new HashMap<>();
+ public Builder() { }
+
+ private Builder(@NonNull Data oldData) {
+ strings.putAll(oldData.strings);
+ stringArrays.putAll(oldData.stringArrays);
+ integers.putAll(oldData.integers);
+ integerArrays.putAll(oldData.integerArrays);
+ longs.putAll(oldData.longs);
+ longArrays.putAll(oldData.longArrays);
+ floats.putAll(oldData.floats);
+ floatArrays.putAll(oldData.floatArrays);
+ doubles.putAll(oldData.doubles);
+ doubleArrays.putAll(oldData.doubleArrays);
+ booleans.putAll(oldData.booleans);
+ booleanArrays.putAll(oldData.booleanArrays);
+ }
+
public Builder putString(@NonNull String key, @Nullable String value) {
strings.put(key, value);
return this;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java
index 9155df7822..8425e635fc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java
@@ -34,7 +34,7 @@ public class JobManager implements ConstraintObserver.Notifier {
private static final String TAG = JobManager.class.getSimpleName();
- public static final int CURRENT_VERSION = 4;
+ public static final int CURRENT_VERSION = 5;
private final Application application;
private final Configuration configuration;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigration.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigration.java
new file mode 100644
index 0000000000..6938af64a8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigration.java
@@ -0,0 +1,55 @@
+package org.thoughtcrime.securesms.jobmanager.migrations;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.MmsSmsDatabase;
+import org.thoughtcrime.securesms.jobmanager.Data;
+import org.thoughtcrime.securesms.jobmanager.JobMigration;
+
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class SendReadReceiptsJobMigration extends JobMigration {
+
+ private final MmsSmsDatabase mmsSmsDatabase;
+
+ public SendReadReceiptsJobMigration(@NonNull MmsSmsDatabase mmsSmsDatabase) {
+ super(5);
+ this.mmsSmsDatabase = mmsSmsDatabase;
+ }
+
+ @Override
+ protected @NonNull JobData migrate(@NonNull JobData jobData) {
+ if ("SendReadReceiptJob".equals(jobData.getFactoryKey())) {
+ return migrateSendReadReceiptJob(mmsSmsDatabase, jobData);
+ }
+ return jobData;
+ }
+
+ private static @NonNull JobData migrateSendReadReceiptJob(@NonNull MmsSmsDatabase mmsSmsDatabase, @NonNull JobData jobData) {
+ Data data = jobData.getData();
+
+ if (!data.hasLong("thread")) {
+ long[] messageIds = jobData.getData().getLongArray("message_ids");
+ SortedSet threadIds = new TreeSet<>();
+
+ for (long id : messageIds) {
+ long threadForMessageId = mmsSmsDatabase.getThreadForMessageId(id);
+ if (id != -1) {
+ threadIds.add(threadForMessageId);
+ }
+ }
+
+ if (threadIds.size() != 1) {
+ return new JobData("FailingJob", null, new Data.Builder().build());
+ } else {
+ return jobData.withData(data.buildUpon().putLong("thread", threadIds.first()).build());
+ }
+
+ } else {
+ return jobData;
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index ae289774f4..abc368f1b1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -4,6 +4,7 @@ import android.app.Application;
import androidx.annotation.NonNull;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobmanager.Constraint;
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.Job;
@@ -18,6 +19,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintOb
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration2;
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
+import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
import org.thoughtcrime.securesms.migrations.Argon2TestMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
@@ -140,6 +142,7 @@ public final class JobManagerFactories {
public static List getJobMigrations(@NonNull Application application) {
return Arrays.asList(new RecipientIdJobMigration(application),
new RecipientIdFollowUpJobMigration(),
- new RecipientIdFollowUpJobMigration2());
+ new RecipientIdFollowUpJobMigration2(),
+ new SendReadReceiptsJobMigration(DatabaseFactory.getMmsSmsDatabase(application)));
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
index 69ba9601ac..5c48f9badd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
@@ -28,8 +28,10 @@ import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
+import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
@@ -151,6 +153,10 @@ public class PushGroupSendJob extends PushSendJob {
try {
log(TAG, "Sending message: " + messageId);
+ if (FeatureFlags.messageRequests() && !message.getRecipient().resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) {
+ RecipientUtil.shareProfileIfFirstSecureMessage(context, message.getRecipient());
+ }
+
List target;
if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient).getId());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
index 8e86e9d469..03773b96bb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
@@ -117,6 +118,8 @@ public class PushMediaSendJob extends PushSendJob {
try {
log(TAG, "Sending message: " + messageId);
+ RecipientUtil.shareProfileIfFirstSecureMessage(context, message.getRecipient());
+
Recipient recipient = message.getRecipient().resolve();
byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
index b604d5021b..8a4018ddc4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
@@ -80,6 +81,8 @@ public class PushTextSendJob extends PushSendJob {
try {
log(TAG, "Sending message: " + messageId);
+ RecipientUtil.shareProfileIfFirstSecureMessage(context, record.getRecipient());
+
Recipient recipient = record.getRecipient().resolve();
byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java
index 384a117f8e..55f44e11c3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java
@@ -32,33 +32,38 @@ public class SendReadReceiptJob extends BaseJob {
private static final String TAG = SendReadReceiptJob.class.getSimpleName();
+ private static final String KEY_THREAD = "thread";
private static final String KEY_ADDRESS = "address";
private static final String KEY_RECIPIENT = "recipient";
private static final String KEY_MESSAGE_IDS = "message_ids";
private static final String KEY_TIMESTAMP = "timestamp";
+ private long threadId;
private RecipientId recipientId;
private List messageIds;
private long timestamp;
- public SendReadReceiptJob(@NonNull RecipientId recipientId, List messageIds) {
+ public SendReadReceiptJob(long threadId, @NonNull RecipientId recipientId, List messageIds) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
+ threadId,
recipientId,
messageIds,
System.currentTimeMillis());
}
private SendReadReceiptJob(@NonNull Job.Parameters parameters,
+ long threadId,
@NonNull RecipientId recipientId,
@NonNull List messageIds,
long timestamp)
{
super(parameters);
+ this.threadId = threadId;
this.recipientId = recipientId;
this.messageIds = messageIds;
this.timestamp = timestamp;
@@ -74,6 +79,7 @@ public class SendReadReceiptJob extends BaseJob {
return new Data.Builder().putString(KEY_RECIPIENT, recipientId.serialize())
.putLongArray(KEY_MESSAGE_IDS, ids)
.putLong(KEY_TIMESTAMP, timestamp)
+ .putLong(KEY_THREAD, threadId)
.build();
}
@@ -86,12 +92,12 @@ public class SendReadReceiptJob extends BaseJob {
public void onRun() throws IOException, UntrustedIdentityException {
if (!TextSecurePreferences.isReadReceiptsEnabled(context) || messageIds.isEmpty()) return;
- Recipient recipient = Recipient.resolved(recipientId);
- if (!RecipientUtil.isRecipientMessageRequestAccepted(context, recipient)) {
+ if (!RecipientUtil.isThreadMessageRequestAccepted(context, threadId)) {
Log.w(TAG, "Refusing to send receipts to untrusted recipient");
return;
}
+ Recipient recipient = Recipient.resolved(recipientId);
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
SignalServiceAddress remoteAddress = RecipientUtil.toSignalServiceAddress(context, recipient);
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, timestamp);
@@ -127,12 +133,13 @@ public class SendReadReceiptJob extends BaseJob {
List messageIds = new ArrayList<>(ids.length);
RecipientId recipientId = data.hasString(KEY_RECIPIENT) ? RecipientId.from(data.getString(KEY_RECIPIENT))
: Recipient.external(application, data.getString(KEY_ADDRESS)).getId();
+ long threadId = data.getLong(KEY_THREAD);
for (long id : ids) {
messageIds.add(id);
}
- return new SendReadReceiptJob(parameters, recipientId, messageIds, timestamp);
+ return new SendReadReceiptJob(parameters, threadId, recipientId, messageIds, timestamp);
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
index 9c1ec7585c..4d8044fc8d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
import org.thoughtcrime.securesms.util.DiffHelper;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java
index 33503042c6..86927f3822 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java
@@ -156,7 +156,7 @@ public class Megaphone {
}
enum Style {
- REACTIONS, BASIC, FULLSCREEN
+ REACTIONS, BASIC, FULLSCREEN, POPUP
}
public interface EventListener {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java
index 9897f84616..7bb277daa9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java
@@ -50,6 +50,7 @@ public class MegaphoneRepository {
public void onFirstEverAppLaunch() {
executor.execute(() -> {
database.markFinished(Event.REACTIONS);
+ database.markFinished(Event.MESSAGE_REQUESTS);
resetDatabaseCache();
});
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneViewBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneViewBuilder.java
index fa0c65a843..77d0fb9d5d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneViewBuilder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneViewBuilder.java
@@ -21,6 +21,8 @@ public class MegaphoneViewBuilder {
return null;
case REACTIONS:
return buildReactionsMegaphone(context, megaphone, listener);
+ case POPUP:
+ return buildPopupMegaphone(context, megaphone, listener);
default:
throw new IllegalArgumentException("No view implemented for style!");
}
@@ -43,4 +45,13 @@ public class MegaphoneViewBuilder {
view.present(megaphone, listener);
return view;
}
+
+ private static @NonNull View buildPopupMegaphone(@NonNull Context context,
+ @NonNull Megaphone megaphone,
+ @NonNull MegaphoneActionController listener)
+ {
+ PopupMegaphoneView view = new PopupMegaphoneView(context);
+ view.present(megaphone, listener);
+ return view;
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
index 6d23c49cf4..8b2e5c5ebf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
@@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
@@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity;
import org.thoughtcrime.securesms.lock.v2.PinUtil;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.messagerequests.MessageRequestMegaphoneActivity;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.util.FeatureFlags;
@@ -49,7 +51,8 @@ public final class Megaphones {
private static final MegaphoneSchedule ALWAYS = new ForeverSchedule(true);
private static final MegaphoneSchedule NEVER = new ForeverSchedule(false);
- private static final MegaphoneSchedule EVERY_TWO_DAYS = new RecurringSchedule(TimeUnit.DAYS.toMillis(2));
+
+ static final MegaphoneSchedule EVERY_TWO_DAYS = new RecurringSchedule(TimeUnit.DAYS.toMillis(2));
private Megaphones() {}
@@ -90,8 +93,9 @@ public final class Megaphones {
return new LinkedHashMap() {{
put(Event.REACTIONS, ALWAYS);
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
- put(Event.PROFILE_NAMES_FOR_ALL, FeatureFlags.profileNamesMegaphoneEnabled() ? EVERY_TWO_DAYS : NEVER);
+ put(Event.PROFILE_NAMES_FOR_ALL, FeatureFlags.profileNamesMegaphone() ? EVERY_TWO_DAYS : NEVER);
put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
+ put(Event.MESSAGE_REQUESTS, shouldShowMessageRequestsMegaphone() ? ALWAYS : NEVER);
}};
}
@@ -105,6 +109,8 @@ public final class Megaphones {
return buildPinReminderMegaphone(context);
case PROFILE_NAMES_FOR_ALL:
return buildProfileNamesMegaphone(context);
+ case MESSAGE_REQUESTS:
+ return buildMessageRequestsMegaphone(context);
default:
throw new IllegalArgumentException("Event not handled!");
}
@@ -195,6 +201,10 @@ public final class Megaphones {
}
private static @NonNull Megaphone buildProfileNamesMegaphone(@NonNull Context context) {
+ short requestCode = TextSecurePreferences.getProfileName(context) != ProfileName.EMPTY
+ ? ConversationListFragment.PROFILE_NAMES_REQUEST_CODE_CONFIRM_NAME
+ : ConversationListFragment.PROFILE_NAMES_REQUEST_CODE_CREATE_NAME;
+
Megaphone.Builder builder = new Megaphone.Builder(Event.PROFILE_NAMES_FOR_ALL, Megaphone.Style.BASIC)
.enableSnooze(null)
.setImage(R.drawable.profile_megaphone);
@@ -204,7 +214,7 @@ public final class Megaphones {
.setBody(R.string.ProfileNamesMegaphone__this_will_be_displayed_when_you_start)
.setActionButton(R.string.ProfileNamesMegaphone__add_profile_name, (megaphone, listener) -> {
listener.onMegaphoneSnooze(Event.PROFILE_NAMES_FOR_ALL);
- listener.onMegaphoneNavigationRequested(new Intent(context, EditProfileActivity.class));
+ listener.onMegaphoneNavigationRequested(new Intent(context, EditProfileActivity.class), requestCode);
})
.build();
} else {
@@ -212,17 +222,34 @@ public final class Megaphones {
.setBody(R.string.ProfileNamesMegaphone__your_profile_can_now_include)
.setActionButton(R.string.ProfileNamesMegaphone__confirm_name, (megaphone, listener) -> {
listener.onMegaphoneCompleted(Event.PROFILE_NAMES_FOR_ALL);
- listener.onMegaphoneNavigationRequested(new Intent(context, EditProfileActivity.class));
+ listener.onMegaphoneNavigationRequested(new Intent(context, EditProfileActivity.class), requestCode);
})
.build();
}
}
+ private static @NonNull Megaphone buildMessageRequestsMegaphone(@NonNull Context context) {
+ return new Megaphone.Builder(Event.MESSAGE_REQUESTS, Megaphone.Style.FULLSCREEN)
+ .disableSnooze()
+ .setMandatory(true)
+ .setOnVisibleListener(((megaphone, listener) -> {
+ listener.onMegaphoneNavigationRequested(new Intent(context, MessageRequestMegaphoneActivity.class),
+ ConversationListFragment.MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME);
+ }))
+ .build();
+ }
+
+ private static boolean shouldShowMessageRequestsMegaphone() {
+ boolean userHasAProfileName = TextSecurePreferences.getProfileName(ApplicationDependencies.getApplication()) != ProfileName.EMPTY;
+ return FeatureFlags.messageRequests() && !userHasAProfileName;
+ }
+
public enum Event {
REACTIONS("reactions"),
PINS_FOR_ALL("pins_for_all"),
PIN_REMINDER("pin_reminder"),
- PROFILE_NAMES_FOR_ALL("profile_names");
+ PROFILE_NAMES_FOR_ALL("profile_names"),
+ MESSAGE_REQUESTS("message_requests");
private final String key;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/PopupMegaphoneView.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PopupMegaphoneView.java
new file mode 100644
index 0000000000..9ad9febfb1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PopupMegaphoneView.java
@@ -0,0 +1,86 @@
+package org.thoughtcrime.securesms.megaphone;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.R;
+
+public class PopupMegaphoneView extends FrameLayout {
+
+ private ImageView image;
+ private TextView titleText;
+ private TextView bodyText;
+ private View xButton;
+
+ private Megaphone megaphone;
+ private MegaphoneActionController megaphoneListener;
+
+ public PopupMegaphoneView(@NonNull Context context) {
+ super(context);
+ init(context);
+ }
+
+ public PopupMegaphoneView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(@NonNull Context context) {
+ inflate(context, R.layout.popup_megaphone_view, this);
+
+ this.image = findViewById(R.id.popup_megaphone_image);
+ this.titleText = findViewById(R.id.popup_megaphone_title);
+ this.bodyText = findViewById(R.id.popup_megaphone_body);
+ this.xButton = findViewById(R.id.popup_x);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (megaphone != null && megaphoneListener != null && megaphone.getOnVisibleListener() != null) {
+ megaphone.getOnVisibleListener().onEvent(megaphone, megaphoneListener);
+ }
+ }
+
+ public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController megaphoneListener) {
+ this.megaphone = megaphone;
+ this.megaphoneListener = megaphoneListener;
+
+ if (megaphone.getImage() != 0) {
+ image.setVisibility(VISIBLE);
+ image.setImageResource(megaphone.getImage());
+ } else {
+ image.setVisibility(GONE);
+ }
+
+ if (megaphone.getTitle() != 0) {
+ titleText.setVisibility(VISIBLE);
+ titleText.setText(megaphone.getTitle());
+ } else {
+ titleText.setVisibility(GONE);
+ }
+
+ if (megaphone.getBody() != 0) {
+ bodyText.setVisibility(VISIBLE);
+ bodyText.setText(megaphone.getBody());
+ } else {
+ bodyText.setVisibility(GONE);
+ }
+
+ if (megaphone.hasButton()) {
+ xButton.setOnClickListener(v -> megaphone.getButtonClickListener().onEvent(megaphone, megaphoneListener));
+ } else {
+ xButton.setOnClickListener(v -> megaphoneListener.onMegaphoneCompleted(megaphone.getEvent()));
+ }
+
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragment.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragment.java
deleted file mode 100644
index 45fa1b179f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragment.java
+++ /dev/null
@@ -1,172 +0,0 @@
-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/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentState.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentState.java
deleted file mode 100644
index 7ec1e72b3a..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentState.java
+++ /dev/null
@@ -1,90 +0,0 @@
-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/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentViewModel.java
deleted file mode 100644
index 67c59cd580..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentViewModel.java
+++ /dev/null
@@ -1,139 +0,0 @@
-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/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestMegaphoneActivity.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestMegaphoneActivity.java
new file mode 100644
index 0000000000..aeed83f0b1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestMegaphoneActivity.java
@@ -0,0 +1,73 @@
+package org.thoughtcrime.securesms.messagerequests;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.airbnb.lottie.LottieAnimationView;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.megaphone.Megaphones;
+import org.thoughtcrime.securesms.profiles.ProfileName;
+import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+
+public class MessageRequestMegaphoneActivity extends PassphraseRequiredActionBarActivity {
+
+ public static final short EDIT_PROFILE_REQUEST_CODE = 24563;
+
+ private DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState, boolean isReady) {
+ dynamicTheme.onCreate(this);
+
+ setContentView(R.layout.message_requests_megaphone_activity);
+
+
+ LottieAnimationView lottie = findViewById(R.id.message_requests_lottie);
+ TextView profileNameButton = findViewById(R.id.message_requests_confirm_profile_name);
+
+ lottie.setAnimation(R.raw.lottie_message_requests_splash);
+ lottie.playAnimation();
+
+ profileNameButton.setOnClickListener(v -> {
+ final Intent profile = new Intent(this, EditProfileActivity.class);
+
+ profile.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
+ profile.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
+
+ startActivityForResult(profile, EDIT_PROFILE_REQUEST_CODE);
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == EDIT_PROFILE_REQUEST_CODE &&
+ resultCode == RESULT_OK &&
+ TextSecurePreferences.getProfileName(this) != ProfileName.EMPTY) {
+ ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.MESSAGE_REQUESTS);
+ setResult(RESULT_OK);
+ finish();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ dynamicTheme.onResume(this);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java
similarity index 65%
rename from app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentRepository.java
rename to app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java
index 8a0cad9f43..b20ca3dd37 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestFragmentRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java
@@ -25,46 +25,26 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
-public class MessageRequestFragmentRepository {
+public class MessageRequestRepository {
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) {
+ public MessageRequestRepository(@NonNull Context context) {
this.context = context.getApplicationContext();
- this.recipientId = recipientId;
- this.threadId = threadId;
- this.liveRecipient = Recipient.live(recipientId);
}
- public LiveRecipient getLiveRecipient() {
- return liveRecipient;
+ public LiveRecipient getLiveRecipient(@NonNull RecipientId recipientId) {
+ return Recipient.live(recipientId);
}
- 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) {
+ public void getGroups(@NonNull RecipientId recipientId, @NonNull Consumer> onGroupsLoaded) {
SimpleTask.run(() -> {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
return groupDatabase.getGroupNamesContainingMember(recipientId);
}, onGroupsLoaded::accept);
}
- public void getMemberCount(@NonNull Consumer onMemberCountLoaded) {
+ public void getMemberCount(@NonNull RecipientId recipientId, @NonNull Consumer onMemberCountLoaded) {
SimpleTask.run(() -> {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Optional groupRecord = groupDatabase.getGroup(recipientId);
@@ -72,10 +52,15 @@ public class MessageRequestFragmentRepository {
}, onMemberCountLoaded::accept);
}
- public void acceptMessageRequest(@NonNull Runnable onMessageRequestAccepted) {
+ public void getMessageRequestAccepted(long threadId, @NonNull Consumer recipientRequestAccepted) {
+ SimpleTask.run(() -> RecipientUtil.isThreadMessageRequestAccepted(context, threadId),
+ recipientRequestAccepted::accept);
+ }
+
+ public void acceptMessageRequest(@NonNull LiveRecipient liveRecipient, long threadId, @NonNull Runnable onMessageRequestAccepted) {
SimpleTask.run(() -> {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
- recipientDatabase.setProfileSharing(recipientId, true);
+ recipientDatabase.setProfileSharing(liveRecipient.getId(), true);
liveRecipient.refresh();
List messageIds = DatabaseFactory.getThreadDatabase(context)
@@ -87,7 +72,7 @@ public class MessageRequestFragmentRepository {
}, v -> onMessageRequestAccepted.run());
}
- public void deleteMessageRequest(@NonNull Runnable onMessageRequestDeleted) {
+ public void deleteMessageRequest(long threadId, @NonNull Runnable onMessageRequestDeleted) {
SimpleTask.run(() -> {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
threadDatabase.deleteConversation(threadId);
@@ -95,7 +80,7 @@ public class MessageRequestFragmentRepository {
}, v -> onMessageRequestDeleted.run());
}
- public void blockMessageRequest(@NonNull Runnable onMessageRequestBlocked) {
+ public void blockMessageRequest(@NonNull LiveRecipient liveRecipient, @NonNull Runnable onMessageRequestBlocked) {
SimpleTask.run(() -> {
Recipient recipient = liveRecipient.resolve();
RecipientUtil.block(context, recipient);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java
new file mode 100644
index 0000000000..9f08118e94
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java
@@ -0,0 +1,177 @@
+package org.thoughtcrime.securesms.messagerequests;
+
+import android.content.Context;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.thoughtcrime.securesms.recipients.LiveRecipient;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.SingleLiveEvent;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+import org.thoughtcrime.securesms.util.livedata.LiveDataTriple;
+
+import java.util.Collections;
+import java.util.List;
+
+public class MessageRequestViewModel extends ViewModel {
+
+ private final SingleLiveEvent status = new SingleLiveEvent<>();
+ private final MutableLiveData recipient = new MutableLiveData<>();
+ private final MutableLiveData> groups = new MutableLiveData<>(Collections.emptyList());
+ private final MutableLiveData memberCount = new MutableLiveData<>(0);
+ private final MutableLiveData shouldDisplayMessageRequest = new MutableLiveData<>();
+ private final LiveData recipientInfo = Transformations.map(new LiveDataTriple<>(recipient, memberCount, groups),
+ triple -> new RecipientInfo(triple.first(), triple.second(), triple.third()));
+
+ private final MessageRequestRepository repository;
+
+ private LiveRecipient liveRecipient;
+ private long threadId;
+
+ @SuppressWarnings("CodeBlock2Expr")
+ private final RecipientForeverObserver recipientObserver = recipient -> {
+ if (Recipient.self().equals(recipient) || recipient.isBlocked() || recipient.isForceSmsSelection() || !recipient.isRegistered()) {
+ shouldDisplayMessageRequest.setValue(false);
+ } else {
+ loadMessageRequestAccepted();
+ }
+ this.recipient.setValue(recipient);
+ };
+
+ private MessageRequestViewModel(MessageRequestRepository repository) {
+ this.repository = repository;
+ }
+
+ public void setConversationInfo(@NonNull RecipientId recipientId, long threadId) {
+ if (liveRecipient != null) {
+ liveRecipient.removeForeverObserver(recipientObserver);
+ }
+
+ liveRecipient = Recipient.live(recipientId);
+ this.threadId = threadId;
+
+ loadRecipient();
+ loadGroups();
+ loadMemberCount();
+ }
+
+ @Override
+ protected void onCleared() {
+ if (liveRecipient != null) {
+ liveRecipient.removeForeverObserver(recipientObserver);
+ }
+ }
+
+ public LiveData getShouldDisplayMessageRequest() {
+ return shouldDisplayMessageRequest;
+ }
+
+ public LiveData getRecipient() {
+ return recipient;
+ }
+
+ public LiveData getRecipientInfo() {
+ return recipientInfo;
+ }
+
+ public LiveData getMesasgeRequestStatus() {
+ return status;
+ }
+
+ @MainThread
+ public void accept() {
+ repository.acceptMessageRequest(liveRecipient, threadId, () -> {
+ status.setValue(Status.ACCEPTED);
+ });
+ }
+
+ @MainThread
+ public void delete() {
+ repository.deleteMessageRequest(threadId, () -> {
+ status.setValue(Status.DELETED);
+ });
+ }
+
+ @MainThread
+ public void block() {
+ repository.blockMessageRequest(liveRecipient, () -> {
+ status.setValue(Status.BLOCKED);
+ });
+ }
+
+ private void loadRecipient() {
+ liveRecipient.observeForever(recipientObserver);
+ SignalExecutors.BOUNDED.execute(liveRecipient::refresh);
+ }
+
+ private void loadGroups() {
+ repository.getGroups(liveRecipient.getId(), this.groups::setValue);
+ }
+
+ private void loadMemberCount() {
+ repository.getMemberCount(liveRecipient.getId(), memberCount -> {
+ this.memberCount.setValue(memberCount == null ? 0 : memberCount);
+ });
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ private void loadMessageRequestAccepted() {
+ repository.getMessageRequestAccepted(threadId, accepted -> shouldDisplayMessageRequest.setValue(!accepted));
+ }
+
+ public static class Factory implements ViewModelProvider.Factory {
+
+ private final Context context;
+
+ public Factory(Context context) {
+ this.context = context;
+ }
+
+ @NonNull
+ @Override
+ public T create(@NonNull Class modelClass) {
+ return (T) new MessageRequestViewModel(new MessageRequestRepository(context.getApplicationContext()));
+ }
+ }
+
+ public static class RecipientInfo {
+ private final @Nullable Recipient recipient;
+ private final int groupMemberCount;
+ private final @NonNull List sharedGroups;
+
+ private RecipientInfo(@Nullable Recipient recipient, @Nullable Integer groupMemberCount, @Nullable List sharedGroups) {
+ this.recipient = recipient;
+ this.groupMemberCount = groupMemberCount == null ? 0 : groupMemberCount;
+ this.sharedGroups = sharedGroups == null ? Collections.emptyList() : sharedGroups;
+ }
+
+ @Nullable
+ public Recipient getRecipient() {
+ return recipient;
+ }
+
+ public int getGroupMemberCount() {
+ return groupMemberCount;
+ }
+
+ @NonNull
+ public List getSharedGroups() {
+ return sharedGroups;
+ }
+ }
+
+ public enum Status {
+ BLOCKED,
+ DELETED,
+ ACCEPTED
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.java
new file mode 100644
index 0000000000..54c7909e0b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.java
@@ -0,0 +1,58 @@
+package org.thoughtcrime.securesms.messagerequests;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import org.thoughtcrime.securesms.R;
+
+public class MessageRequestsBottomView extends ConstraintLayout {
+
+ private TextView question;
+ private View accept;
+ private View block;
+ private View delete;
+
+ public MessageRequestsBottomView(Context context) {
+ super(context);
+ }
+
+ public MessageRequestsBottomView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MessageRequestsBottomView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ inflate(getContext(), R.layout.message_request_bottom_bar, this);
+
+ question = findViewById(R.id.message_request_question);
+ accept = findViewById(R.id.message_request_accept);
+ block = findViewById(R.id.message_request_block);
+ delete = findViewById(R.id.message_request_delete);
+ }
+
+ public void setQuestionText(CharSequence questionText) {
+ question.setText(questionText);
+ }
+
+ public void setAcceptOnClickListener(OnClickListener acceptOnClickListener) {
+ accept.setOnClickListener(acceptOnClickListener);
+ }
+
+ public void setDeleteOnClickListener(OnClickListener deleteOnClickListener) {
+ delete.setOnClickListener(deleteOnClickListener);
+ }
+
+ public void setBlockOnClickListener(OnClickListener blockOnClickListener) {
+ block.setOnClickListener(blockOnClickListener);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java
index 12e32420a7..4503effff5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java
@@ -5,6 +5,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
+
import androidx.annotation.NonNull;
import androidx.core.app.NotificationManagerCompat;
@@ -78,15 +79,20 @@ public class MarkReadReceiver extends BroadcastReceiver {
ApplicationDependencies.getJobManager().add(new MultiDeviceReadUpdateJob(syncMessageIds));
- Map> recipientIdMap = Stream.of(markedReadMessages)
- .map(MarkedMessageInfo::getSyncMessageId)
- .collect(Collectors.groupingBy(SyncMessageId::getRecipientId));
+ Map> threadToInfo = Stream.of(markedReadMessages)
+ .collect(Collectors.groupingBy(MarkedMessageInfo::getThreadId));
- for (Map.Entry> entry : recipientIdMap.entrySet()) {
- List timestamps = Stream.of(entry.getValue()).map(SyncMessageId::getTimetamp).toList();
+ Stream.of(threadToInfo).forEach(threadToInfoEntry -> {
+ Map> idMapForThread = Stream.of(threadToInfoEntry.getValue())
+ .map(MarkedMessageInfo::getSyncMessageId)
+ .collect(Collectors.groupingBy(SyncMessageId::getRecipientId));
- ApplicationDependencies.getJobManager().add(new SendReadReceiptJob(entry.getKey(), timestamps));
- }
+ Stream.of(idMapForThread).forEach(entry -> {
+ List timestamps = Stream.of(entry.getValue()).map(SyncMessageId::getTimetamp).toList();
+
+ ApplicationDependencies.getJobManager().add(new SendReadReceiptJob(threadToInfoEntry.getKey(), entry.getKey(), timestamps));
+ });
+ });
}
private static void scheduleDeletion(Context context, ExpirationInfo expirationInfo) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java
index 5dc2e39d46..280f1b0632 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java
@@ -74,7 +74,7 @@ public final class ProfileName implements Parcelable {
* Deserializes a profile name, trims if exceeds the limits.
*/
public static @NonNull ProfileName fromSerialized(@Nullable String profileName) {
- if (profileName == null) {
+ if (profileName == null || profileName.isEmpty()) {
return EMPTY;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java
index b2398fd3c8..610814c71c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java
@@ -47,6 +47,7 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
@Override
public void onProfileNameUploadCompleted() {
+ setResult(RESULT_OK);
finish();
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
index 7acc15ee55..83e2b4b6c4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
@@ -55,7 +55,8 @@ public class Recipient {
public static final Recipient UNKNOWN = new Recipient(RecipientId.UNKNOWN, new RecipientDetails());
- private static final String TAG = Log.tag(Recipient.class);
+ private static final FallbackPhotoProvider DEFAULT_FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
+ private static final String TAG = Log.tag(Recipient.class);
private final RecipientId id;
private final boolean resolving;
@@ -567,20 +568,28 @@ public class Recipient {
}
public @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted) {
- return getFallbackContactPhoto().asDrawable(context, getColor().toAvatarColor(context), inverted);
+ return getFallbackContactPhotoDrawable(context, inverted, DEFAULT_FALLBACK_PHOTO_PROVIDER);
}
- public @NonNull Drawable getSmallFallbackContactPhotoDrawable(Context context, boolean inverted) {
- return getFallbackContactPhoto().asSmallDrawable(context, getColor().toAvatarColor(context), inverted);
+ public @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted, @Nullable FallbackPhotoProvider fallbackPhotoProvider) {
+ return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asDrawable(context, getColor().toAvatarColor(context), inverted);
+ }
+
+ public @NonNull Drawable getSmallFallbackContactPhotoDrawable(Context context, boolean inverted, @Nullable FallbackPhotoProvider fallbackPhotoProvider) {
+ return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asSmallDrawable(context, getColor().toAvatarColor(context), inverted);
}
public @NonNull FallbackContactPhoto getFallbackContactPhoto() {
- if (localNumber) return new ResourceContactPhoto(R.drawable.ic_note_to_self);
- if (isResolving()) return new TransparentContactPhoto();
- else if (isGroupInternal()) return new ResourceContactPhoto(R.drawable.ic_group_outline_40, R.drawable.ic_group_outline_20, R.drawable.ic_group_large);
- else if (isGroup()) return new ResourceContactPhoto(R.drawable.ic_group_outline_40, R.drawable.ic_group_outline_20, R.drawable.ic_group_large);
- else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40);
- else return new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large);
+ return getFallbackContactPhoto(DEFAULT_FALLBACK_PHOTO_PROVIDER);
+ }
+
+ public @NonNull FallbackContactPhoto getFallbackContactPhoto(@NonNull FallbackPhotoProvider fallbackPhotoProvider) {
+ if (localNumber) return fallbackPhotoProvider.getPhotoForLocalNumber();
+ if (isResolving()) return fallbackPhotoProvider.getPhotoForResolvingRecipient();
+ else if (isGroupInternal()) return fallbackPhotoProvider.getPhotoForGroup();
+ else if (isGroup()) return fallbackPhotoProvider.getPhotoForGroup();
+ else if (!TextUtils.isEmpty(name)) return fallbackPhotoProvider.getPhotoForRecipientWithName(name);
+ else return fallbackPhotoProvider.getPhotoForRecipientWithoutName();
}
public @Nullable ContactPhoto getContactPhoto() {
@@ -734,6 +743,29 @@ public class Recipient {
return Objects.hash(id);
}
+ public static class FallbackPhotoProvider {
+ public @NonNull FallbackContactPhoto getPhotoForLocalNumber() {
+ return new ResourceContactPhoto(R.drawable.ic_note_34, R.drawable.ic_note_24);
+ }
+
+ public @NonNull FallbackContactPhoto getPhotoForResolvingRecipient() {
+ return new TransparentContactPhoto();
+ }
+
+ public @NonNull FallbackContactPhoto getPhotoForGroup() {
+ return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20, R.drawable.ic_group_outline_48);
+ }
+
+ public @NonNull FallbackContactPhoto getPhotoForRecipientWithName(String name) {
+ return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40);
+ }
+
+ public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
+ return new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_profile_outline_48);
+ }
+
+ }
+
private static class MissingAddressError extends AssertionError {
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
index b2897aae25..b9782beb68 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
@@ -11,6 +11,8 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
+import org.thoughtcrime.securesms.database.MmsSmsDatabase;
+import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.database.ThreadDatabase;
@@ -108,16 +110,55 @@ public class RecipientUtil {
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
}
+ @WorkerThread
+ public static boolean isThreadMessageRequestAccepted(@NonNull Context context, long threadId) {
+ if (!FeatureFlags.messageRequests()) {
+ return true;
+ }
+
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
+ boolean hasSentSecureMessage = DatabaseFactory.getMmsSmsDatabase(context)
+ .getOutgoingSecureConversationCount(threadId) != 0;
+ boolean noSecureMessagesInThread = DatabaseFactory.getMmsSmsDatabase(context)
+ .getSecureConversationCount(threadId) == 0;
+
+ if (recipient == null || hasSentSecureMessage || noSecureMessagesInThread) {
+ return true;
+ }
+
+ Recipient resolved = recipient.resolve();
+
+ return resolved.isProfileSharing() || resolved.isSystemContact();
+ }
+
@WorkerThread
public static boolean isRecipientMessageRequestAccepted(@NonNull Context context, @Nullable Recipient recipient) {
if (recipient == null || !FeatureFlags.messageRequests()) return true;
Recipient resolved = recipient.resolve();
- ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
- long threadId = threadDatabase.getThreadIdFor(resolved);
- boolean hasSentMessage = threadDatabase.getLastSeenAndHasSent(threadId).second() == Boolean.TRUE;
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(resolved);
+ boolean hasSentMessage = DatabaseFactory.getMmsSmsDatabase(context)
+ .getOutgoingSecureConversationCount(threadId) != 0;
+ boolean noSecureMessagesInThread = DatabaseFactory.getMmsSmsDatabase(context)
+ .getSecureConversationCount(threadId) == 0;
- return hasSentMessage || resolved.isProfileSharing() || resolved.isSystemContact();
+ return noSecureMessagesInThread || hasSentMessage || resolved.isProfileSharing() || resolved.isSystemContact();
+ }
+
+ @WorkerThread
+ public static void shareProfileIfFirstSecureMessage(@NonNull Context context, @NonNull Recipient recipient) {
+ if (!FeatureFlags.messageRequests()) {
+ return;
+ }
+
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient);
+ boolean firstMessage = DatabaseFactory.getMmsSmsDatabase(context)
+ .getOutgoingSecureConversationCount(threadId) == 0;
+
+ if (firstMessage) {
+ DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
index e64a3caa57..ccbe3fd389 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
@@ -47,7 +47,6 @@ public final class FeatureFlags {
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
private static final String UUIDS = generateKey("uuids");
- private static final String PROFILE_DISPLAY = generateKey("profileDisplay");
private static final String MESSAGE_REQUESTS = generateKey("messageRequests");
private static final String USERNAMES = generateKey("usernames");
private static final String STORAGE_SERVICE = generateKey("storageService");
@@ -65,7 +64,8 @@ public final class FeatureFlags {
VIDEO_TRIMMING,
PINS_FOR_ALL,
PINS_MEGAPHONE_KILL_SWITCH,
- PROFILE_NAMES_MEGAPHONE
+ PROFILE_NAMES_MEGAPHONE,
+ MESSAGE_REQUESTS
);
/**
@@ -139,7 +139,7 @@ public final class FeatureFlags {
/** Favoring profile names when displaying contacts. */
public static synchronized boolean profileDisplay() {
- return getValue(PROFILE_DISPLAY, false);
+ return messageRequests();
}
/** MessageRequest stuff */
@@ -172,7 +172,7 @@ public final class FeatureFlags {
}
/** Safety switch for disabling profile names megaphone */
- public static boolean profileNamesMegaphoneEnabled() {
+ public static boolean profileNamesMegaphone() {
return getValue(PROFILE_NAMES_MEGAPHONE, false) &&
TextSecurePreferences.getFirstInstallVersion(ApplicationDependencies.getApplication()) < 600;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/HtmlUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/HtmlUtil.java
new file mode 100644
index 0000000000..186c69bd50
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/HtmlUtil.java
@@ -0,0 +1,9 @@
+package org.thoughtcrime.securesms.util;
+
+import androidx.annotation.NonNull;
+
+public class HtmlUtil {
+ public static @NonNull String bold(@NonNull String target) {
+ return "" + target + "";
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Triple.java b/app/src/main/java/org/thoughtcrime/securesms/util/Triple.java
new file mode 100644
index 0000000000..86d9db9c8a
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/Triple.java
@@ -0,0 +1,43 @@
+package org.thoughtcrime.securesms.util;
+
+import androidx.annotation.Nullable;
+import androidx.core.util.ObjectsCompat;
+
+public class Triple {
+
+ private final A a;
+ private final B b;
+ private final C c;
+
+ public Triple(@Nullable A a, @Nullable B b, @Nullable C c) {
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ }
+
+ public @Nullable A first() {
+ return a;
+ }
+
+ public @Nullable B second() {
+ return b;
+ }
+
+ public @Nullable C third() {
+ return c;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Triple)) {
+ return false;
+ }
+ Triple, ?, ?> t = (Triple, ?, ?>) o;
+ return ObjectsCompat.equals(t.a, a) && ObjectsCompat.equals(t.b, b) && ObjectsCompat.equals(t.c, c);
+ }
+
+ @Override
+ public int hashCode() {
+ return (a == null ? 0 : a.hashCode()) ^ (b == null ? 0 : b.hashCode()) ^ (c == null ? 0 : c.hashCode());
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java
index 69f80a9c26..06c3fd39af 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java
@@ -320,6 +320,17 @@ public class Util {
return Optional.fromNullable(simCountryIso != null ? simCountryIso.toUpperCase() : null);
}
+ @SafeVarargs
+ public static @NonNull T firstNonNull(T ... ts) {
+ for (T t : ts) {
+ if (t != null) {
+ return t;
+ }
+ }
+
+ throw new IllegalStateException("All choices were null.");
+ }
+
public static List> partition(List list, int partitionSize) {
List> results = new LinkedList<>();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataTriple.java b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataTriple.java
new file mode 100644
index 0000000000..4eed226775
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataTriple.java
@@ -0,0 +1,136 @@
+package org.thoughtcrime.securesms.util.livedata;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+
+import org.thoughtcrime.securesms.util.Triple;
+import org.whispersystems.libsignal.util.Pair;
+
+public final class LiveDataTriple extends MediatorLiveData> {
+ private A a;
+ private B b;
+ private C c;
+
+ public LiveDataTriple(@NonNull LiveData liveDataA,
+ @NonNull LiveData liveDataB,
+ @NonNull LiveData liveDataC)
+ {
+ this(liveDataA, liveDataB, liveDataC, null, null, null);
+ }
+
+ public LiveDataTriple(@NonNull LiveData liveDataA,
+ @NonNull LiveData liveDataB,
+ @NonNull LiveData liveDataC,
+ @Nullable A initialA,
+ @Nullable B initialB,
+ @Nullable C initialC)
+ {
+ a = initialA;
+ b = initialB;
+ c = initialC;
+ setValue(new Triple<>(a, b, c));
+
+ if (liveDataA == liveDataB && liveDataA == liveDataC) {
+
+ addSource(liveDataA, a -> {
+ if (a != null) {
+ this.a = a;
+
+ //noinspection unchecked: A is B if live datas are same instance
+ this.b = (B) a;
+
+ //noinspection unchecked: A is C if live datas are same instance
+ this.c = (C) a;
+ }
+
+ setValue(new Triple<>(a, b, c));
+ });
+
+ } else if (liveDataA == liveDataB) {
+
+ addSource(liveDataA, a -> {
+ if (a != null) {
+ this.a = a;
+
+ //noinspection unchecked: A is B if live datas are same instance
+ this.b = (B) a;
+ }
+
+ setValue(new Triple<>(a, b, c));
+ });
+
+ addSource(liveDataC, c -> {
+ if (c != null) {
+ this.c = c;
+ }
+ setValue(new Triple<>(a, b, c));
+ });
+
+ } else if (liveDataA == liveDataC) {
+
+ addSource(liveDataA, a -> {
+ if (a != null) {
+ this.a = a;
+
+ //noinspection unchecked: A is C if live datas are same instance
+ this.c = (C) a;
+ }
+
+ setValue(new Triple<>(a, b, c));
+ });
+
+ addSource(liveDataB, b -> {
+ if (b != null) {
+ this.b = b;
+ }
+ setValue(new Triple<>(a, b, c));
+ });
+
+ } else if (liveDataB == liveDataC) {
+
+ addSource(liveDataB, b -> {
+ if (b != null) {
+ this.b = b;
+
+ //noinspection unchecked: A is C if live datas are same instance
+ this.c = (C) b;
+ }
+
+ setValue(new Triple<>(a, b, c));
+ });
+
+ addSource(liveDataA, a -> {
+ if (a != null) {
+ this.a = a;
+ }
+ setValue(new Triple<>(a, b, c));
+ });
+
+ } else {
+
+ addSource(liveDataA, a -> {
+ if (a != null) {
+ this.a = a;
+ }
+ setValue(new Triple<>(a, b, c));
+ });
+
+ addSource(liveDataB, b -> {
+ if (b != null) {
+ this.b = b;
+ }
+ setValue(new Triple<>(a, b, c));
+ });
+
+ addSource(liveDataC, c -> {
+ if (c != null) {
+ this.c = c;
+ }
+ setValue(new Triple<>(a, b, c));
+ });
+
+ }
+ }
+}
diff --git a/app/src/main/res/drawable-hdpi/ic_note_to_self.webp b/app/src/main/res/drawable-hdpi/ic_note_to_self.webp
deleted file mode 100644
index 37fd3ee077..0000000000
Binary files a/app/src/main/res/drawable-hdpi/ic_note_to_self.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/ic_note_to_self.webp b/app/src/main/res/drawable-mdpi/ic_note_to_self.webp
deleted file mode 100644
index 2b9a32c670..0000000000
Binary files a/app/src/main/res/drawable-mdpi/ic_note_to_self.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/message_requests_megaphone.png b/app/src/main/res/drawable-mdpi/message_requests_megaphone.png
new file mode 100644
index 0000000000..e741ffe5f3
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/message_requests_megaphone.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_note_to_self.webp b/app/src/main/res/drawable-xhdpi/ic_note_to_self.webp
deleted file mode 100644
index b0d2288b01..0000000000
Binary files a/app/src/main/res/drawable-xhdpi/ic_note_to_self.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-xhdpi/message_requests_megaphone.png b/app/src/main/res/drawable-xhdpi/message_requests_megaphone.png
new file mode 100644
index 0000000000..85d40a093e
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/message_requests_megaphone.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_note_to_self.webp b/app/src/main/res/drawable-xxhdpi/ic_note_to_self.webp
deleted file mode 100644
index 7e8cba641b..0000000000
Binary files a/app/src/main/res/drawable-xxhdpi/ic_note_to_self.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-xxhdpi/message_requests_megaphone.png b/app/src/main/res/drawable-xxhdpi/message_requests_megaphone.png
new file mode 100644
index 0000000000..73897db4ee
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/message_requests_megaphone.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_note_to_self.webp b/app/src/main/res/drawable-xxxhdpi/ic_note_to_self.webp
deleted file mode 100644
index 7046e0ea8a..0000000000
Binary files a/app/src/main/res/drawable-xxxhdpi/ic_note_to_self.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-xxxhdpi/message_requests_megaphone.png b/app/src/main/res/drawable-xxxhdpi/message_requests_megaphone.png
new file mode 100644
index 0000000000..af5bdc91d2
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/message_requests_megaphone.png differ
diff --git a/app/src/main/res/drawable/ic_group_80.xml b/app/src/main/res/drawable/ic_group_80.xml
new file mode 100644
index 0000000000..700ca216ea
--- /dev/null
+++ b/app/src/main/res/drawable/ic_group_80.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_group_outline_40.xml b/app/src/main/res/drawable/ic_group_outline_34.xml
similarity index 92%
rename from app/src/main/res/drawable/ic_group_outline_40.xml
rename to app/src/main/res/drawable/ic_group_outline_34.xml
index ea3278892c..02758fb42f 100644
--- a/app/src/main/res/drawable/ic_group_outline_40.xml
+++ b/app/src/main/res/drawable/ic_group_outline_34.xml
@@ -1,6 +1,6 @@
+
+
diff --git a/app/src/main/res/drawable/ic_note_24.xml b/app/src/main/res/drawable/ic_note_24.xml
new file mode 100644
index 0000000000..c4ed135726
--- /dev/null
+++ b/app/src/main/res/drawable/ic_note_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_note_34.xml b/app/src/main/res/drawable/ic_note_34.xml
new file mode 100644
index 0000000000..18696ac8f8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_note_34.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_note_80.xml b/app/src/main/res/drawable/ic_note_80.xml
new file mode 100644
index 0000000000..b230d7e0fc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_note_80.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_80.xml b/app/src/main/res/drawable/ic_profile_80.xml
new file mode 100644
index 0000000000..da93d76201
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_80.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_outline_48.xml b/app/src/main/res/drawable/ic_profile_outline_48.xml
new file mode 100644
index 0000000000..b716cf4fe3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_outline_48.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/conversation_activity.xml b/app/src/main/res/layout/conversation_activity.xml
index 3acb531eca..9f7e653191 100644
--- a/app/src/main/res/layout/conversation_activity.xml
+++ b/app/src/main/res/layout/conversation_activity.xml
@@ -77,6 +77,7 @@
android:layout="@layout/conversation_activity_attachment_editor_stub" />
+
+