Improve message requests, add megaphone.
|
@ -442,6 +442,11 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".contactshare.ContactShareEditActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<OutgoingTextMessage, Void, Long>() {
|
||||
@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<Uri, Long> 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<IdentityRecord> 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Cursor>,
|
||||
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);
|
||||
|
||||
|
|
|
@ -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<Long> 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) {
|
||||
|
|
|
@ -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<VH extends RecyclerView.ViewHold
|
|||
private @Nullable View footer;
|
||||
|
||||
private static class HeaderFooterViewHolder extends RecyclerView.ViewHolder {
|
||||
public HeaderFooterViewHolder(View itemView) {
|
||||
|
||||
private ViewGroup container;
|
||||
|
||||
HeaderFooterViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.container = (ViewGroup) itemView;
|
||||
}
|
||||
|
||||
void bind(@Nullable View view) {
|
||||
unbind();
|
||||
|
||||
if (view != null) {
|
||||
container.addView(view);
|
||||
}
|
||||
}
|
||||
|
||||
void unbind() {
|
||||
container.removeAllViews();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,6 +153,8 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
|||
public final void onViewRecycled(@NonNull ViewHolder holder) {
|
||||
if (!(holder instanceof HeaderFooterViewHolder)) {
|
||||
onItemViewRecycled((VH)holder);
|
||||
} else {
|
||||
((HeaderFooterViewHolder) holder).unbind();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,9 +163,11 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
|||
@Override
|
||||
public @NonNull final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case HEADER_TYPE: return new HeaderFooterViewHolder(header);
|
||||
case FOOTER_TYPE: return new HeaderFooterViewHolder(footer);
|
||||
default: return onCreateItemViewHolder(parent, viewType);
|
||||
case HEADER_TYPE:
|
||||
case FOOTER_TYPE:
|
||||
return new HeaderFooterViewHolder(LayoutInflater.from(context).inflate(R.layout.cursor_adapter_header_footer_view, parent, false));
|
||||
default:
|
||||
return onCreateItemViewHolder(parent, viewType);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,7 +176,11 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
|||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (!isHeaderPosition(position) && !isFooterPosition(position)) {
|
||||
if (isHeaderPosition(position)) {
|
||||
((HeaderFooterViewHolder) viewHolder).bind(header);
|
||||
} else if (isFooterPosition(position)) {
|
||||
((HeaderFooterViewHolder) viewHolder).bind(footer);
|
||||
} else {
|
||||
if (isFastAccessPosition(position)) onBindFastAccessItemViewHolder((VH)viewHolder, position);
|
||||
else onBindItemViewHolder((VH)viewHolder, getCursorAtPositionOrThrow(position));
|
||||
}
|
||||
|
|
|
@ -165,12 +165,14 @@ public class GroupDatabase extends Database {
|
|||
|
||||
public List<String> 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<String> 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<String> members = Util.split(cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), ",");
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<RecipientId, Long> 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<RecipientId, Long> 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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Long, Boolean> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Boolean> booleans = new HashMap<>();
|
||||
private final Map<String, boolean[]> 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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Long> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<JobMigration> getJobMigrations(@NonNull Application application) {
|
||||
return Arrays.asList(new RecipientIdJobMigration(application),
|
||||
new RecipientIdFollowUpJobMigration(),
|
||||
new RecipientIdFollowUpJobMigration2());
|
||||
new RecipientIdFollowUpJobMigration2(),
|
||||
new SendReadReceiptsJobMigration(DatabaseFactory.getMmsSmsDatabase(application)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<RecipientId> target;
|
||||
|
||||
if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient).getId());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<Long> messageIds;
|
||||
private long timestamp;
|
||||
|
||||
public SendReadReceiptJob(@NonNull RecipientId recipientId, List<Long> messageIds) {
|
||||
public SendReadReceiptJob(long threadId, @NonNull RecipientId recipientId, List<Long> 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<Long> 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<Long> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -156,7 +156,7 @@ public class Megaphone {
|
|||
}
|
||||
|
||||
enum Style {
|
||||
REACTIONS, BASIC, FULLSCREEN
|
||||
REACTIONS, BASIC, FULLSCREEN, POPUP
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
|
|
|
@ -50,6 +50,7 @@ public class MegaphoneRepository {
|
|||
public void onFirstEverAppLaunch() {
|
||||
executor.execute(() -> {
|
||||
database.markFinished(Event.REACTIONS);
|
||||
database.markFinished(Event.MESSAGE_REQUESTS);
|
||||
resetDatabaseCache();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Event, MegaphoneSchedule>() {{
|
||||
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;
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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<String> 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 "<b>" + target + "</b>";
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> groups;
|
||||
public final int memberCount;
|
||||
|
||||
|
||||
public MessageRequestFragmentState(@NonNull MessageRequestState messageRequestState,
|
||||
@Nullable MessageRecord messageRecord,
|
||||
@Nullable Recipient recipient,
|
||||
@Nullable List<String> 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<String> 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 + "]";
|
||||
}
|
||||
}
|
|
@ -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<MessageRequestFragmentState> 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<MessageRequestFragmentState> 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<MessageRequestFragmentState, MessageRequestFragmentState> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return (T) new MessageRequestFragmentViewModel(new MessageRequestFragmentRepository(context, recipientId, threadId));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<MessageRecord> 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<List<String>> onGroupsLoaded) {
|
||||
public void getGroups(@NonNull RecipientId recipientId, @NonNull Consumer<List<String>> onGroupsLoaded) {
|
||||
SimpleTask.run(() -> {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
return groupDatabase.getGroupNamesContainingMember(recipientId);
|
||||
}, onGroupsLoaded::accept);
|
||||
}
|
||||
|
||||
public void getMemberCount(@NonNull Consumer<Integer> onMemberCountLoaded) {
|
||||
public void getMemberCount(@NonNull RecipientId recipientId, @NonNull Consumer<Integer> onMemberCountLoaded) {
|
||||
SimpleTask.run(() -> {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupDatabase.GroupRecord> 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<Boolean> 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<MessagingDatabase.MarkedMessageInfo> 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);
|
|
@ -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> status = new SingleLiveEvent<>();
|
||||
private final MutableLiveData<Recipient> recipient = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<String>> groups = new MutableLiveData<>(Collections.emptyList());
|
||||
private final MutableLiveData<Integer> memberCount = new MutableLiveData<>(0);
|
||||
private final MutableLiveData<Boolean> shouldDisplayMessageRequest = new MutableLiveData<>();
|
||||
private final LiveData<RecipientInfo> 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<Boolean> getShouldDisplayMessageRequest() {
|
||||
return shouldDisplayMessageRequest;
|
||||
}
|
||||
|
||||
public LiveData<Recipient> getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public LiveData<RecipientInfo> getRecipientInfo() {
|
||||
return recipientInfo;
|
||||
}
|
||||
|
||||
public LiveData<Status> 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 extends ViewModel> T create(@NonNull Class<T> 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<String> sharedGroups;
|
||||
|
||||
private RecipientInfo(@Nullable Recipient recipient, @Nullable Integer groupMemberCount, @Nullable List<String> 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<String> getSharedGroups() {
|
||||
return sharedGroups;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
BLOCKED,
|
||||
DELETED,
|
||||
ACCEPTED
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<RecipientId, List<SyncMessageId>> recipientIdMap = Stream.of(markedReadMessages)
|
||||
.map(MarkedMessageInfo::getSyncMessageId)
|
||||
.collect(Collectors.groupingBy(SyncMessageId::getRecipientId));
|
||||
Map<Long, List<MarkedMessageInfo>> threadToInfo = Stream.of(markedReadMessages)
|
||||
.collect(Collectors.groupingBy(MarkedMessageInfo::getThreadId));
|
||||
|
||||
for (Map.Entry<RecipientId, List<SyncMessageId>> entry : recipientIdMap.entrySet()) {
|
||||
List<Long> timestamps = Stream.of(entry.getValue()).map(SyncMessageId::getTimetamp).toList();
|
||||
Stream.of(threadToInfo).forEach(threadToInfoEntry -> {
|
||||
Map<RecipientId, List<SyncMessageId>> 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<Long> 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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
|
|||
|
||||
@Override
|
||||
public void onProfileNameUploadCompleted() {
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 "<b>" + target + "</b>";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
|
||||
public class Triple<A, B, C> {
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -320,6 +320,17 @@ public class Util {
|
|||
return Optional.fromNullable(simCountryIso != null ? simCountryIso.toUpperCase() : null);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static @NonNull <T> T firstNonNull(T ... ts) {
|
||||
for (T t : ts) {
|
||||
if (t != null) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("All choices were null.");
|
||||
}
|
||||
|
||||
public static <T> List<List<T>> partition(List<T> list, int partitionSize) {
|
||||
List<List<T>> results = new LinkedList<>();
|
||||
|
||||
|
|
|
@ -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<A, B, C> extends MediatorLiveData<Triple<A, B, C>> {
|
||||
private A a;
|
||||
private B b;
|
||||
private C c;
|
||||
|
||||
public LiveDataTriple(@NonNull LiveData<A> liveDataA,
|
||||
@NonNull LiveData<B> liveDataB,
|
||||
@NonNull LiveData<C> liveDataC)
|
||||
{
|
||||
this(liveDataA, liveDataB, liveDataC, null, null, null);
|
||||
}
|
||||
|
||||
public LiveDataTriple(@NonNull LiveData<A> liveDataA,
|
||||
@NonNull LiveData<B> liveDataB,
|
||||
@NonNull LiveData<C> 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));
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 128 B |
Before Width: | Height: | Size: 92 B |
After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 114 B |
After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 154 B |
After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 152 B |
After Width: | Height: | Size: 26 KiB |
|
@ -1,6 +1,6 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:width="34dp"
|
||||
android:height="34dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:fillColor="?conversation_subtitle_color"
|
||||
android:pathData="M29,16.75a6.508,6.508 0,0 1,6.5 6.5L35.5,24L37,24v-0.75a8,8 0,0 0,-6.7 -7.885,6.5 6.5,0 1,0 -8.6,0 7.941,7.941 0,0 0,-2.711 0.971A6.5,6.5 0,1 0,9.7 25.365,8 8,0 0,0 3,33.25L3,34L4.5,34v-0.75a6.508,6.508 0,0 1,6.5 -6.5h6a6.508,6.508 0,0 1,6.5 6.5L23.5,34L25,34v-0.75a8,8 0,0 0,-6.7 -7.885,6.468 6.468,0 0,0 1.508,-7.771A6.453,6.453 0,0 1,23 16.75ZM14,25.5a5,5 0,1 1,5 -5A5,5 0,0 1,14 25.5ZM21,10.5a5,5 0,1 1,5 5A5,5 0,0 1,21 10.5Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@color/core_white" android:pathData="M18,3.5a0.55,0.55 0,0 1,0.55 0.55L18.55,20a0.55,0.55 0,0 1,-0.55 0.55L6.05,20.55A0.55,0.55 0,0 1,5.5 20L5.5,4.05a0.55,0.55 0,0 1,0.55 -0.55L18,3.5M18,2L6.05,2A2.05,2.05 0,0 0,4 4.05L4,20A2.05,2.05 0,0 0,6.05 22L18,22A2.05,2.05 0,0 0,20 20L20,4.05A2.05,2.05 0,0 0,18 2ZM17,6.5L7,6.5L7,8L17,8ZM17,9.5L7,9.5L7,11L17,11ZM17,12.5L7,12.5L7,14L17,14ZM15,15.5L7,15.5L7,17h8Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="34dp"
|
||||
android:height="34dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path android:fillColor="@color/core_white" android:pathData="M29.25,4.5A2.25,2.25 0,0 1,31.5 6.75v26.5a2.25,2.25 0,0 1,-2.25 2.25L10.75,35.5A2.25,2.25 0,0 1,8.5 33.25L8.5,6.75A2.25,2.25 0,0 1,10.75 4.5h18.5m0,-1.5L10.75,3A3.76,3.76 0,0 0,7 6.75v26.5A3.76,3.76 0,0 0,10.75 37h18.5A3.76,3.76 0,0 0,33 33.25L33,6.75A3.76,3.76 0,0 0,29.25 3ZM29,11.5L11,11.5L11,13L29,13ZM29,16.5L11,16.5L11,18L29,18ZM29,21.5L11,21.5L11,23L29,23ZM25,26.5L11,26.5L11,28L25,28Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="80dp"
|
||||
android:viewportHeight="80" android:viewportWidth="80"
|
||||
android:width="80dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@color/core_white" android:pathData="M58,7.5A6.51,6.51 0,0 1,64.5 14L64.5,66A6.51,6.51 0,0 1,58 72.5L22,72.5A6.51,6.51 0,0 1,15.5 66L15.5,14A6.51,6.51 0,0 1,22 7.5L58,7.5M58,6L22,6a8,8 0,0 0,-8 8L14,66a8,8 0,0 0,8 8L58,74a8,8 0,0 0,8 -8L66,14a8,8 0,0 0,-8 -8ZM60,24L20,24v1.5L60,25.5ZM60,34L20,34v1.5L60,35.5ZM60,44L20,44v1.5L60,45.5ZM50,54L20,54v1.5L50,55.5Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="80dp"
|
||||
android:viewportHeight="80" android:viewportWidth="80"
|
||||
android:width="80dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@color/core_white" android:pathData="M50,41h-0.06a18,18 0,1 0,-19.88 0H30A23,23 0,0 0,7 64v1H8.5V64A21.52,21.52 0,0 1,30 42.5h2.82a17.93,17.93 0,0 0,14.36 0H50A21.52,21.52 0,0 1,71.5 64v1H73V64A23,23 0,0 0,50 41ZM40,42.5A16.5,16.5 0,1 1,56.5 26,16.5 16.5,0 0,1 40,42.5Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:fillColor="?conversation_subtitle_color"
|
||||
android:pathData="M25.009,20.07a7.5,7.5 0,1 0,-10.018 0A8,8 0,0 0,8 28v1H9.5V28A6.508,6.508 0,0 1,16 21.5h8A6.508,6.508 0,0 1,30.5 28v1H32V28A8,8 0,0 0,25.009 20.07ZM20,20.5a6,6 0,1 1,6 -6A6,6 0,0 1,20 20.5Z"/>
|
||||
</vector>
|
|
@ -77,6 +77,7 @@
|
|||
android:layout="@layout/conversation_activity_attachment_editor_stub" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/conversation_activity_panel_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
|
@ -86,6 +87,14 @@
|
|||
|
||||
<include layout="@layout/conversation_search_nav" />
|
||||
|
||||
<org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView
|
||||
android:id="@+id/conversation_activity_message_request_bottom_bar"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:visibility="gone"
|
||||
android:clickable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<Button
|
||||
|
@ -140,14 +149,5 @@
|
|||
|
||||
</org.thoughtcrime.securesms.components.InputAwareLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_overlay_container"
|
||||
android:visibility="gone"
|
||||
android:focusableInTouchMode="true"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:background="?conversation_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<include layout="@layout/conversation_reaction_scrubber" />
|
||||
</FrameLayout>
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/message_request_avatar"
|
||||
android:layout_width="112dp"
|
||||
android:layout_height="112dp"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_request_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_request_avatar"
|
||||
tools:text="Cayce Pollard" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_request_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:gravity="center"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Subtitle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_request_title"
|
||||
tools:text="\@caycepollard" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/message_request_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:gravity="center"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Description"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_request_subtitle"
|
||||
tools:text="Member of NYC Rock Climbers, Dinner Party and Friends" />
|
||||
|
||||
</merge>
|
|
@ -5,6 +5,10 @@
|
|||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/conversation_item_banner"
|
||||
android:id="@+id/empty_conversation_banner"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.conversation.ConversationBannerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_request_question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="11dp"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Description"
|
||||
android:paddingTop="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_request_block"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="Do you want to let Cayce Pollard message you? They won't know you've seen their message until you accept." />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_block"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/MessageRequestBottomView_block"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/message_request_delete"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_delete"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/MessageRequestBottomView_delete"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_request_block"
|
||||
app:layout_constraintEnd_toStartOf="@+id/message_request_accept"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/message_request_block"
|
||||
app:layout_constraintTop_toTopOf="@id/message_request_block" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_accept"
|
||||
style="@style/Signal.MessageRequest.Button.Accept"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/MessageRequestBottomView_accept"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_request_block"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/message_request_delete"
|
||||
app:layout_constraintTop_toTopOf="@id/message_request_block" />
|
||||
</merge>
|
|
@ -1,119 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?conversation_background">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/message_request_avatar"
|
||||
android:layout_width="112dp"
|
||||
android:layout_height="112dp"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_request_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_request_avatar"
|
||||
tools:text="Cayce Pollard" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_request_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Subtitle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_request_title"
|
||||
tools:text="\@caycepollard" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/message_request_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_request_subtitle"
|
||||
tools:text="Member of NYC Rock Climbers, Dinner Party and Friends" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/message_request_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dip"
|
||||
android:layout_marginTop="27dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_request_question"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_request_description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_request_question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="11dp"
|
||||
android:textAppearance="@style/Signal.Text.MessageRequest.Description"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_request_block"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="Do you want to let Cayce Pollard message you? They won't know you've seen their message until you accept." />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_block"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/MessageRequestBottomView_block"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/message_request_delete"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_delete"
|
||||
style="@style/Signal.MessageRequest.Button.Deny"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/MessageRequestBottomView_delete"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_request_block"
|
||||
app:layout_constraintEnd_toStartOf="@+id/message_request_accept"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/message_request_block"
|
||||
app:layout_constraintTop_toTopOf="@id/message_request_block" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_request_accept"
|
||||
style="@style/Signal.MessageRequest.Button.Accept"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/MessageRequestBottomView_accept"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_request_block"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/message_request_delete"
|
||||
app:layout_constraintTop_toTopOf="@id/message_request_block" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/message_requests_lottie"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_requests_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_requests_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/MessageRequestsMegaphone__message_requests"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_requests_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_requests_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="30dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:text="@string/MessageRequestsMegaphone__users_can_now_choose_to_accept"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_requests_confirm_profile_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_requests_confirm_profile_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="33dp"
|
||||
android:background="@drawable/cta_button_background"
|
||||
android:text="@string/MessageRequestsMegaphone__add_profile_name"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?megaphone_background"
|
||||
android:clickable="true"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/popup_megaphone_image"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:scaleType="centerInside"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/profile_splash" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/popup_megaphone_title"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
app:layout_constraintEnd_toStartOf="@id/popup_x"
|
||||
app:layout_constraintStart_toEndOf="@id/popup_megaphone_image"
|
||||
app:layout_constraintTop_toTopOf="@id/popup_megaphone_image"
|
||||
tools:text="Avengers HQ Destroyed!" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/popup_megaphone_body"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?megaphone_body_text_color"
|
||||
app:layout_constraintEnd_toStartOf="@id/popup_x"
|
||||
app:layout_constraintStart_toStartOf="@id/popup_megaphone_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/popup_megaphone_title"
|
||||
tools:text="Where was the 'hero' Spider-Man during the battle?" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/popup_x"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:paddingStart="12.5dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingEnd="15.5dp"
|
||||
android:paddingBottom="14dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_x_20" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</merge>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:title="@string/conversation__menu_view_all_media"
|
||||
android:id="@+id/menu_view_media" />
|
||||
|
||||
</menu>
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:title="@string/convesation_group_options__recipients_list"
|
||||
android:id="@+id/menu_group_recipients"
|
||||
android:icon="@drawable/ic_group_solid_highlight_24"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item android:id="@+id/menu_leave"
|
||||
android:title="@string/conversation__menu_leave_group"
|
||||
app:showAsAction="collapseActionView"/>
|
||||
|
||||
</menu>
|
|
@ -285,6 +285,7 @@
|
|||
<string name="ConversationFragment_you_can_swipe_to_the_left_reply">You can swipe to the left on any message to quickly reply</string>
|
||||
<string name="ConversationFragment_outgoing_view_once_media_files_are_automatically_removed">Outgoing view-once media files are automatically removed after they are sent</string>
|
||||
<string name="ConversationFragment_you_already_viewed_this_message">You already viewed this message</string>
|
||||
<string name="ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation">You can add notes for yourself in this conversation. If your account has any linked devices, new notes will be synced.</string>
|
||||
|
||||
<!-- ConversationListActivity -->
|
||||
<string name="ConversationListActivity_there_is_no_browser_installed_on_your_device">There is no browser installed on your device.</string>
|
||||
|
@ -311,6 +312,8 @@
|
|||
<item quantity="one">Moved conversation to inbox</item>
|
||||
<item quantity="other">Moved %d conversations to inbox</item>
|
||||
</plurals>
|
||||
<string name="ConversationListFragment__your_profile_name_has_been_created">Your profile name has been created.</string>
|
||||
<string name="ConversationListFragment__your_profile_name_has_been_saved">Your profile name has been saved.</string>
|
||||
|
||||
<!-- ConversationListItem -->
|
||||
<string name="ConversationListItem_key_exchange_message">Key exchange message</string>
|
||||
|
@ -847,6 +850,8 @@
|
|||
<string name="ThreadRecord_you_marked_verified">You marked verified</string>
|
||||
<string name="ThreadRecord_you_marked_unverified">You marked unverified</string>
|
||||
<string name="ThreadRecord_message_could_not_be_processed">Message could not be processed</string>
|
||||
<string name="ThreadRecord_message_request">Message Request</string>
|
||||
<string name="ThreadRecord_s_added_you_to_the_group">%1$s added you to the group</string>
|
||||
|
||||
<!-- UpdateApkReadyListener -->
|
||||
<string name="UpdateApkReadyListener_Signal_update">Signal update</string>
|
||||
|
@ -1365,6 +1370,14 @@
|
|||
<string name="AndroidManifest_archived_conversations">Archived conversations</string>
|
||||
<string name="AndroidManifest_remove_photo">Remove photo</string>
|
||||
|
||||
<!-- Message Requests Megaphone -->
|
||||
<string name="MessageRequestsMegaphone__message_requests">Message requests</string>
|
||||
<string name="MessageRequestsMegaphone__users_can_now_choose_to_accept">Users can now choose to accept a new conversation. Profile names let people know who\'s messaging them.</string>
|
||||
<string name="MessageRequestsMegaphone__add_profile_name">Add profile name</string>
|
||||
<string name="MessageRequestsMegaphone__new_message_requests">New: Message requests</string>
|
||||
<string name="MessageRequestsMegaphone__add_name">Add name</string>
|
||||
<string name="MessageRequestsMegaphone__you_can_now_choose_whether_to_accept">You can now choose whether to accept a new conversation. You’ll see options to \"Accept,\" \"Delete,\" or \"Block.\"</string>
|
||||
|
||||
<!-- arrays.xml -->
|
||||
<string name="arrays__import_export">Import</string>
|
||||
<string name="arrays__use_default">Use default</string>
|
||||
|
@ -1923,7 +1936,10 @@
|
|||
<string name="MessageRequestProfileView_member_of_one_group">Member of %1$s</string>
|
||||
<string name="MessageRequestProfileView_member_of_two_groups">Member of %1$s and %2$s</string>
|
||||
<string name="MessageRequestProfileView_member_of_many_groups">Member of %1$s, %2$s, and %3$s</string>
|
||||
<string name="MessageRequestProfileView_members">%1$d members</string>
|
||||
<plurals name="MessageRequestProfileView_members">
|
||||
<item quantity="one">%1$d member</item>
|
||||
<item quantity="other">%1$d members</item>
|
||||
</plurals>
|
||||
<plurals name="MessageRequestProfileView_member_of_others">
|
||||
<item quantity="one">%d other</item>
|
||||
<item quantity="other">%d others</item>
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package org.thoughtcrime.securesms.jobmanager.migrations;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigration;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class SendReadReceiptsJobMigrationTest {
|
||||
|
||||
private final MmsSmsDatabase mockDatabase = mock(MmsSmsDatabase.class);
|
||||
private final SendReadReceiptsJobMigration testSubject = new SendReadReceiptsJobMigration(mockDatabase);
|
||||
|
||||
@Test
|
||||
public void givenSendReadReceiptJobDataWithoutThreadIdAndThreadIdFound_whenIMigrate_thenIInsertThreadId() {
|
||||
// GIVEN
|
||||
SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>());
|
||||
JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(),
|
||||
"asdf",
|
||||
new Data.Builder()
|
||||
.putString("recipient", RecipientId.from(2).serialize())
|
||||
.putLongArray("message_ids", new long[]{1, 2, 3, 4, 5})
|
||||
.putLong("timestamp", 292837649).build());
|
||||
when(mockDatabase.getThreadForMessageId(anyLong())).thenReturn(1234L);
|
||||
|
||||
// WHEN
|
||||
JobMigration.JobData result = testSubject.migrate(jobData);
|
||||
|
||||
// THEN
|
||||
assertEquals(1234L, result.getData().getLong("thread"));
|
||||
assertEquals(RecipientId.from(2).serialize(), result.getData().getString("recipient"));
|
||||
assertTrue(result.getData().hasLongArray("message_ids"));
|
||||
assertTrue(result.getData().hasLong("timestamp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSendReadReceiptJobDataWithoutThreadIdAndThreadIdNotFound_whenIMigrate_thenIGetAFailingJob() {
|
||||
// GIVEN
|
||||
SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>());
|
||||
JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(),
|
||||
"asdf",
|
||||
new Data.Builder()
|
||||
.putString("recipient", RecipientId.from(2).serialize())
|
||||
.putLongArray("message_ids", new long[]{})
|
||||
.putLong("timestamp", 292837649).build());
|
||||
when(mockDatabase.getThreadForMessageId(anyLong())).thenReturn(-1L);
|
||||
|
||||
// WHEN
|
||||
JobMigration.JobData result = testSubject.migrate(jobData);
|
||||
|
||||
// THEN
|
||||
assertEquals("FailingJob", result.getFactoryKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSendReadReceiptJobDataWithThreadId_whenIMigrate_thenIDoNotReplace() {
|
||||
// GIVEN
|
||||
SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>());
|
||||
JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(), "asdf", job.serialize());
|
||||
|
||||
// WHEN
|
||||
JobMigration.JobData result = testSubject.migrate(jobData);
|
||||
|
||||
// THEN
|
||||
assertEquals(jobData, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSomeOtherJobDataWithThreadId_whenIMigrate_thenIDoNotReplace() {
|
||||
// GIVEN
|
||||
JobMigration.JobData jobData = new JobMigration.JobData("SomeOtherJob", "asdf", new Data.Builder().putLong("thread", 1).build());
|
||||
|
||||
// WHEN
|
||||
JobMigration.JobData result = testSubject.migrate(jobData);
|
||||
|
||||
// THEN
|
||||
assertEquals(jobData, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSomeOtherJobDataWithoutThreadId_whenIMigrate_thenIDoNotReplace() {
|
||||
// GIVEN
|
||||
JobMigration.JobData jobData = new JobMigration.JobData("SomeOtherJob", "asdf", new Data.Builder().build());
|
||||
|
||||
// WHEN
|
||||
JobMigration.JobData result = testSubject.migrate(jobData);
|
||||
|
||||
// THEN
|
||||
assertEquals(jobData, result);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package org.thoughtcrime.securesms.notifications;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.powermock.api.mockito.PowerMockito.doAnswer;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest(ApplicationDependencies.class)
|
||||
public class MarkReadReceiverTest {
|
||||
|
||||
private final Context mockContext = mock(Context.class);
|
||||
private final JobManager mockJobManager = mock(JobManager.class);
|
||||
private final List<Job> jobs = new LinkedList<>();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mockStatic(ApplicationDependencies.class);
|
||||
when(ApplicationDependencies.getJobManager()).thenReturn(mockJobManager);
|
||||
doAnswer((Answer<Void>) invocation -> {
|
||||
jobs.add((Job) invocation.getArguments()[0]);
|
||||
return null;
|
||||
}).when(mockJobManager).add(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenMultipleThreadsWithMultipleMessagesEach_whenIProcess_thenIProperlyGroupByThreadAndRecipient() {
|
||||
// GIVEN
|
||||
List<RecipientId> recipients = Stream.range(1L, 4L).map(RecipientId::from).toList();
|
||||
List<Long> threads = Stream.range(4L, 7L).toList();
|
||||
int expected = recipients.size() * threads.size() + 1;
|
||||
|
||||
List<MessagingDatabase.MarkedMessageInfo> infoList = Stream.of(threads)
|
||||
.flatMap(threadId -> Stream.of(recipients)
|
||||
.map(recipientId -> createMarkedMessageInfo(threadId, recipientId)))
|
||||
.toList();
|
||||
|
||||
List<MessagingDatabase.MarkedMessageInfo> duplicatedList = Util.concatenatedList(infoList, infoList);
|
||||
|
||||
// WHEN
|
||||
MarkReadReceiver.process(mockContext, duplicatedList);
|
||||
|
||||
// THEN
|
||||
assertEquals("Should have 10 total jobs, including MultiDeviceReadUpdateJob", expected, jobs.size());
|
||||
|
||||
Set<Pair<Long, String>> threadRecipientPairs = new HashSet<>();
|
||||
Stream.of(jobs).forEach(job -> {
|
||||
if (job instanceof MultiDeviceReadUpdateJob) {
|
||||
return;
|
||||
}
|
||||
|
||||
Data data = job.serialize();
|
||||
|
||||
long threadId = data.getLong("thread");
|
||||
String recipientId = data.getString("recipient");
|
||||
long[] messageIds = data.getLongArray("message_ids");
|
||||
|
||||
assertEquals("Each job should contain two messages.", 2, messageIds.length);
|
||||
assertTrue("Each thread recipient pair should only exist once.", threadRecipientPairs.add(new Pair<>(threadId, recipientId)));
|
||||
});
|
||||
|
||||
assertEquals("Should have 9 total combinations.", 9, threadRecipientPairs.size());
|
||||
}
|
||||
|
||||
private MessagingDatabase.MarkedMessageInfo createMarkedMessageInfo(long threadId, @NonNull RecipientId recipientId) {
|
||||
return new MessagingDatabase.MarkedMessageInfo(threadId,
|
||||
new MessagingDatabase.SyncMessageId(recipientId, 0),
|
||||
new MessagingDatabase.ExpirationInfo(0, 0, 0, false));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({DatabaseFactory.class, FeatureFlags.class})
|
||||
public class RecipientUtilTest {
|
||||
|
||||
private Context context = mock(Context.class);
|
||||
private Recipient recipient = mock(Recipient.class);
|
||||
private ThreadDatabase mockThreadDatabase = mock(ThreadDatabase.class);
|
||||
private MmsSmsDatabase mockMmsSmsDatabase = mock(MmsSmsDatabase.class);
|
||||
private RecipientDatabase mockRecipientDatabase = mock(RecipientDatabase.class);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mockStatic(DatabaseFactory.class);
|
||||
when(DatabaseFactory.getThreadDatabase(any())).thenReturn(mockThreadDatabase);
|
||||
when(DatabaseFactory.getMmsSmsDatabase(any())).thenReturn(mockMmsSmsDatabase);
|
||||
when(DatabaseFactory.getRecipientDatabase(any())).thenReturn(mockRecipientDatabase);
|
||||
|
||||
mockStatic(FeatureFlags.class);
|
||||
when(FeatureFlags.messageRequests()).thenReturn(true);
|
||||
|
||||
when(recipient.getId()).thenReturn(RecipientId.from(5));
|
||||
when(recipient.resolve()).thenReturn(recipient);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenMessageRequestsFlagDisabled_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(FeatureFlags.messageRequests()).thenReturn(false);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenThreadIsNegativeOne_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, -1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenRecipientIsNullForThreadId_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenIHaveSentASecureMessageInThisThread_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(mockThreadDatabase.getRecipientForThreadId(anyLong())).thenReturn(recipient);
|
||||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(1L)).thenReturn(5);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenIHaveNotSentASecureMessageInThisThreadAndIAmProfileSharing_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(recipient.isProfileSharing()).thenReturn(true);
|
||||
when(mockThreadDatabase.getRecipientForThreadId(anyLong())).thenReturn(recipient);
|
||||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(1L)).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenIHaveNotSentASecureMessageInThisThreadAndRecipientIsSystemContact_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(recipient.isSystemContact()).thenReturn(true);
|
||||
when(mockThreadDatabase.getRecipientForThreadId(anyLong())).thenReturn(recipient);
|
||||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(1L)).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenIHaveReceivedASecureMessageIHaveNotSentASecureMessageAndRecipientIsNotSystemContactAndNotProfileSharing_whenIsThreadMessageRequestAccepted_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(mockThreadDatabase.getRecipientForThreadId(anyLong())).thenReturn(recipient);
|
||||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(1L)).thenReturn(0);
|
||||
when(mockMmsSmsDatabase.getSecureConversationCount(1L)).thenReturn(5);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenIHaveNotReceivedASecureMessageIHaveNotSentASecureMessageAndRecipientIsNotSystemContactAndNotProfileSharing_whenIsThreadMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(mockThreadDatabase.getRecipientForThreadId(anyLong())).thenReturn(recipient);
|
||||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(1L)).thenReturn(0);
|
||||
when(mockMmsSmsDatabase.getSecureConversationCount(1L)).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isThreadMessageRequestAccepted(context, 1L);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenRecipientIsNull_whenIsRecipientMessageRequestAccepted_thenIExpectTrue() {
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, null);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenMessageRequestsFlagIsOff_whenIsRecipientMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(FeatureFlags.messageRequests()).thenReturn(false);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenNonZeroOutgoingSecureMessageCount_whenIsRecipientMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(anyLong())).thenReturn(1);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenIAmProfileSharing_whenIsRecipientMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(recipient.isProfileSharing()).thenReturn(true);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenRecipientIsASystemContact_whenIsRecipientMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(recipient.isSystemContact()).thenReturn(true);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenNoSecureMessagesSentSomeSecureMessagesReceivedNotSharingAndNotSystemContact_whenIsRecipientMessageRequestAccepted_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(mockMmsSmsDatabase.getSecureConversationCount(anyLong())).thenReturn(5);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenNoSecureMessagesSentNoSecureMessagesReceivedNotSharingAndNotSystemContact_whenIsRecipientMessageRequestAccepted_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(mockMmsSmsDatabase.getSecureConversationCount(anyLong())).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
boolean result = RecipientUtil.isRecipientMessageRequestAccepted(context, recipient);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenNoSecureMessagesSent_whenIShareProfileIfFirstSecureMessage_thenIShareProfile() {
|
||||
// GIVEN
|
||||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(anyLong())).thenReturn(0);
|
||||
|
||||
// WHEN
|
||||
RecipientUtil.shareProfileIfFirstSecureMessage(context, recipient);
|
||||
|
||||
// THEN
|
||||
verify(mockRecipientDatabase).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSecureMessagesSent_whenIShareProfileIfFirstSecureMessage_thenIShareProfile() {
|
||||
// GIVEN
|
||||
when(mockMmsSmsDatabase.getOutgoingSecureConversationCount(anyLong())).thenReturn(5);
|
||||
|
||||
// WHEN
|
||||
RecipientUtil.shareProfileIfFirstSecureMessage(context, recipient);
|
||||
|
||||
// THEN
|
||||
verify(mockRecipientDatabase, never()).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
}
|