Improve messaging and UX around safety number changes.
This commit is contained in:
parent
819f0f68f6
commit
bbe003a454
22 changed files with 713 additions and 83 deletions
|
@ -47,5 +47,6 @@ public interface BindableConversationItem extends Unbindable {
|
|||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
|
@ -15,7 +14,6 @@ import androidx.appcompat.app.AlertDialog;
|
|||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
|
@ -105,7 +103,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
|||
}
|
||||
|
||||
processMessageRecord(messageRecord);
|
||||
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -115,26 +112,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
|||
else processIncomingMessageRecord(messageRecord);
|
||||
}
|
||||
|
||||
private void processPendingMessageRecords(long threadId, IdentityKeyMismatch mismatch) {
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(getContext());
|
||||
Cursor cursor = mmsSmsDatabase.getIdentityConflictMessagesForThread(threadId);
|
||||
MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(cursor);
|
||||
MessageRecord record;
|
||||
|
||||
try {
|
||||
while ((record = reader.getNext()) != null) {
|
||||
for (IdentityKeyMismatch recordMismatch : record.getIdentityKeyMismatches()) {
|
||||
if (mismatch.equals(recordMismatch)) {
|
||||
processMessageRecord(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
|
|
|
@ -6,14 +6,15 @@ import android.content.Context;
|
|||
import android.content.res.TypedArray;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -94,9 +95,17 @@ public class ConversationItemFooter extends LinearLayout {
|
|||
|
||||
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
||||
dateView.forceLayout();
|
||||
|
||||
if (messageRecord.isFailed()) {
|
||||
dateView.setText(R.string.ConversationItem_error_not_delivered);
|
||||
int errorMsg;
|
||||
if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
errorMsg = R.string.ConversationItem_error_network_not_delivered;
|
||||
} else if (messageRecord.getRecipient().isPushGroup() && messageRecord.isIdentityMismatchFailure()) {
|
||||
errorMsg = R.string.ConversationItem_error_partially_not_delivered;
|
||||
} else {
|
||||
errorMsg = R.string.ConversationItem_error_not_sent_tap_for_details;
|
||||
}
|
||||
|
||||
dateView.setText(errorMsg);
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
||||
} else {
|
||||
|
|
|
@ -108,9 +108,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
|
|||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.identity.UntrustedSendDialog;
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedSendDialog;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
|
@ -125,6 +123,7 @@ import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
|||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -173,6 +172,7 @@ 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.messagedetails.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||
|
@ -273,13 +273,16 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
StickerKeyboardProvider.StickerEventListener,
|
||||
AttachmentKeyboard.Callback,
|
||||
ConversationReactionOverlay.OnReactionSelectedListener,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
|
||||
SafetyNumberChangeDialog.Callback
|
||||
{
|
||||
|
||||
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
|
||||
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String SAFETY_NUMBER_DIALOG = "SAFETY_NUMBER";
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String TEXT_EXTRA = "draft_text";
|
||||
|
@ -1306,50 +1309,28 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void handleUnverifiedRecipients() {
|
||||
List<Recipient> unverifiedRecipients = identityRecords.getUnverifiedRecipients();
|
||||
List<IdentityRecord> unverifiedRecords = identityRecords.getUnverifiedRecords();
|
||||
String message = IdentityUtil.getUnverifiedSendDialogDescription(this, unverifiedRecipients);
|
||||
|
||||
if (message == null) return;
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
new UnverifiedSendDialog(this, message, unverifiedRecords, () -> {
|
||||
initializeIdentityRecords().addListener(new ListenableFuture.Listener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
});
|
||||
}).show();
|
||||
private void handleRecentSafetyNumberChange() {
|
||||
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
|
||||
records.addAll(identityRecords.getUntrustedRecords());
|
||||
SafetyNumberChangeDialog.create(records).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
private void handleUntrustedRecipients() {
|
||||
List<Recipient> untrustedRecipients = identityRecords.getUntrustedRecipients();
|
||||
List<IdentityRecord> untrustedRecords = identityRecords.getUntrustedRecords();
|
||||
String untrustedMessage = IdentityUtil.getUntrustedSendDialogDescription(this, untrustedRecipients);
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange() {
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (untrustedMessage == null) return;
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
new UntrustedSendDialog(this, untrustedMessage, untrustedRecords, () -> {
|
||||
initializeIdentityRecords().addListener(new ListenableFuture.Listener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
});
|
||||
}).show();
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() {
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) { }
|
||||
});
|
||||
}
|
||||
|
||||
private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) {
|
||||
|
@ -2329,10 +2310,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
|
||||
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
|
||||
handleManualMmsRequired();
|
||||
} else if (!forceSms && identityRecords.isUnverified()) {
|
||||
handleUnverifiedRecipients();
|
||||
} else if (!forceSms && identityRecords.isUntrusted()) {
|
||||
handleUntrustedRecipients();
|
||||
} else if (!forceSms && (identityRecords.isUnverified() || identityRecords.isUntrusted())) {
|
||||
handleRecentSafetyNumberChange();
|
||||
} else if (isMediaMessage) {
|
||||
sendMediaMessage(forceSms, expiresIn, false, subscriptionId, initiating);
|
||||
} else {
|
||||
|
@ -2886,6 +2865,21 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
reactionOverlay.setListVerticalTranslation(translationY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.conversation_activity__send, (dialog, which) -> MessageSender.resend(this, messageRecord))
|
||||
.show();
|
||||
} else if (messageRecord.isIdentityMismatchFailure()) {
|
||||
SafetyNumberChangeDialog.create(this, messageRecord).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
} else {
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(this, messageRecord, messageRecord.getRecipient().getId(), messageRecord.getThreadId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorChanged() {
|
||||
if (!reactionOverlay.isShowing()) {
|
||||
|
|
|
@ -989,6 +989,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
||||
void onCursorChanged();
|
||||
void onListVerticalTranslationChanged(float translationY);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
|
||||
private class ConversationScrollListener extends OnScrollListener {
|
||||
|
@ -1249,6 +1250,11 @@ public class ConversationFragment extends LoggingFragment {
|
|||
|
||||
RecipientBottomSheetDialogFragment.create(recipientId, groupId).show(requireFragmentManager(), "BOTTOM");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
listener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -90,7 +90,6 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
|||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
|
@ -1375,7 +1374,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
if (!shouldInterceptClicks(messageRecord) && parent != null) {
|
||||
parent.onClick(v);
|
||||
} else if (messageRecord.isFailed()) {
|
||||
context.startActivity(MessageDetailsActivity.getIntentForMessageDetails(context, messageRecord, conversationRecipient.getId(), messageRecord.getThreadId()));
|
||||
if (eventListener != null) {
|
||||
eventListener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
} else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) {
|
||||
handleApproveIdentity();
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
/**
|
||||
* Wrapper class for helping show a list of recipients that had recent safety number changes.
|
||||
*
|
||||
* Also provides helper methods for behavior used in multiple spots.
|
||||
*/
|
||||
final class ChangedRecipient {
|
||||
private final Recipient recipient;
|
||||
private final IdentityRecord record;
|
||||
|
||||
ChangedRecipient(@NonNull Recipient recipient, @NonNull IdentityRecord record) {
|
||||
this.recipient = recipient;
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
@NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
@NonNull IdentityRecord getIdentityRecord() {
|
||||
return record;
|
||||
}
|
||||
|
||||
boolean isUnverified() {
|
||||
return record.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.UNVERIFIED;
|
||||
}
|
||||
|
||||
boolean isVerified() {
|
||||
return record.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
final class SafetyNumberChangeAdapter extends ListAdapter<ChangedRecipient, SafetyNumberChangeAdapter.ViewHolder> {
|
||||
|
||||
private final Callbacks callbacks;
|
||||
|
||||
SafetyNumberChangeAdapter(@NonNull Callbacks callbacks) {
|
||||
super(new AlwaysChangedDiffUtil<>());
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.safety_number_change_recipient, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
final ChangedRecipient changedRecipient = getItem(position);
|
||||
holder.bind(changedRecipient);
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
final AvatarImageView avatar;
|
||||
final FromTextView name;
|
||||
final TextView subtitle;
|
||||
final View viewButton;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
avatar = itemView.findViewById(R.id.safety_number_change_recipient_avatar);
|
||||
name = itemView.findViewById(R.id.safety_number_change_recipient_name);
|
||||
subtitle = itemView.findViewById(R.id.safety_number_change_recipient_subtitle);
|
||||
viewButton = itemView.findViewById(R.id.safety_number_change_recipient_view);
|
||||
}
|
||||
|
||||
void bind(@NonNull ChangedRecipient changedRecipient) {
|
||||
avatar.setRecipient(changedRecipient.getRecipient());
|
||||
name.setText(changedRecipient.getRecipient());
|
||||
|
||||
if (changedRecipient.isUnverified() || changedRecipient.isVerified()) {
|
||||
subtitle.setText(R.string.safety_number_change_dialog__previous_verified);
|
||||
|
||||
Drawable check = ContextCompat.getDrawable(itemView.getContext(), R.drawable.check);
|
||||
if (check != null) {
|
||||
check.setBounds(0, 0, ViewUtil.dpToPx(12), ViewUtil.dpToPx(12));
|
||||
subtitle.setCompoundDrawables(check, null, null, null);
|
||||
}
|
||||
} else if (changedRecipient.getRecipient().hasAUserSetDisplayName(itemView.getContext())) {
|
||||
subtitle.setText(changedRecipient.getRecipient().getE164().or(""));
|
||||
subtitle.setCompoundDrawables(null, null, null, null);
|
||||
} else {
|
||||
subtitle.setText("");
|
||||
}
|
||||
subtitle.setVisibility(TextUtils.isEmpty(subtitle.getText()) ? View.GONE : View.VISIBLE);
|
||||
|
||||
viewButton.setOnClickListener(view -> callbacks.onViewIdentityRecord(changedRecipient.getIdentityRecord()));
|
||||
}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SafetyNumberChangeDialog extends DialogFragment implements SafetyNumberChangeAdapter.Callbacks {
|
||||
|
||||
private static final String RECIPIENT_IDS_EXTRA = "recipient_ids";
|
||||
private static final String MESSAGE_ID_EXTRA = "message_id";
|
||||
|
||||
private SafetyNumberChangeViewModel viewModel;
|
||||
private SafetyNumberChangeAdapter adapter;
|
||||
private View dialogView;
|
||||
|
||||
public static @NonNull SafetyNumberChangeDialog create(List<IdentityDatabase.IdentityRecord> identityRecords) {
|
||||
List<String> ids = Stream.of(identityRecords)
|
||||
.map(record -> record.getRecipientId().serialize())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static @NonNull SafetyNumberChangeDialog create(Context context, MessageRecord messageRecord) {
|
||||
List<String> ids = Stream.of(messageRecord.getIdentityKeyMismatches())
|
||||
.map(mismatch -> mismatch.getRecipientId(context).serialize())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
|
||||
arguments.putLong(MESSAGE_ID_EXTRA, messageRecord.getId());
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private SafetyNumberChangeDialog() { }
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
List<RecipientId> recipientIds = Stream.of(getArguments().getStringArray(RECIPIENT_IDS_EXTRA)).map(RecipientId::from).toList();
|
||||
long messageId = getArguments().getLong(MESSAGE_ID_EXTRA, -1);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new SafetyNumberChangeViewModel.Factory(recipientIds, (messageId != -1) ? messageId : null)).get(SafetyNumberChangeViewModel.class);
|
||||
viewModel.getChangedRecipients().observe(getViewLifecycleOwner(), adapter::submitList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
dialogView = LayoutInflater.from(requireActivity()).inflate(R.layout.safety_number_change_dialog, null);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), getTheme());
|
||||
|
||||
configureView(dialogView);
|
||||
|
||||
builder.setTitle(R.string.safety_number_change_dialog__safety_number_changes)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.safety_number_change_dialog__send_anyway, this::handleSendAnyway)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override public void onDestroyView() {
|
||||
dialogView = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void configureView(View view) {
|
||||
RecyclerView list = view.findViewById(R.id.safety_number_change_dialog_list);
|
||||
adapter = new SafetyNumberChangeAdapter(this);
|
||||
list.setAdapter(adapter);
|
||||
list.setItemAnimator(null);
|
||||
list.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
}
|
||||
|
||||
private void handleSendAnyway(DialogInterface dialogInterface, int which) {
|
||||
Activity activity = getActivity();
|
||||
Callback callback;
|
||||
if (activity instanceof Callback) {
|
||||
callback = (Callback) activity;
|
||||
} else {
|
||||
callback = null;
|
||||
}
|
||||
|
||||
LiveData<TrustAndVerifyResult> trustOrVerifyResultLiveData = viewModel.trustOrVerifyChangedRecipients();
|
||||
|
||||
Observer<TrustAndVerifyResult> observer = new Observer<TrustAndVerifyResult>() {
|
||||
@Override
|
||||
public void onChanged(TrustAndVerifyResult result) {
|
||||
if (callback != null) {
|
||||
switch (result) {
|
||||
case TRUST_AND_VERIFY:
|
||||
callback.onSendAnywayAfterSafetyNumberChange();
|
||||
break;
|
||||
case TRUST_VERIFY_AND_RESEND:
|
||||
callback.onMessageResentAfterSafetyNumberChange();
|
||||
break;
|
||||
}
|
||||
}
|
||||
trustOrVerifyResultLiveData.removeObserver(this);
|
||||
}
|
||||
};
|
||||
|
||||
trustOrVerifyResultLiveData.observeForever(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord) {
|
||||
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord));
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onSendAnywayAfterSafetyNumberChange();
|
||||
void onMessageResentAfterSafetyNumberChange();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
final class SafetyNumberChangeRepository {
|
||||
|
||||
private final Context context;
|
||||
|
||||
SafetyNumberChangeRepository(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@NonNull LiveData<SafetyNumberChangeState> getSafetyNumberChangeState(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
MutableLiveData<SafetyNumberChangeState> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(getSafetyNumberChangeStateInternal(recipientIds, messageId)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients(@NonNull List<ChangedRecipient> changedRecipients) {
|
||||
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsInternal(changedRecipients)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipientsAndResend(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
|
||||
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsAndResendInternal(changedRecipients, messageRecord)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull SafetyNumberChangeState getSafetyNumberChangeStateInternal(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
MessageRecord messageRecord = null;
|
||||
if (messageId != null) {
|
||||
messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageRecord(messageId);
|
||||
}
|
||||
|
||||
List<Recipient> recipients = Stream.of(recipientIds).map(Recipient::resolved).toList();
|
||||
|
||||
List<ChangedRecipient> changedRecipients = Stream.of(DatabaseFactory.getIdentityDatabase(context).getIdentities(recipients).getIdentityRecords())
|
||||
.map(record -> new ChangedRecipient(Recipient.resolved(record.getRecipientId()), record))
|
||||
.toList();
|
||||
|
||||
return new SafetyNumberChangeState(changedRecipients, messageRecord);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private TrustAndVerifyResult trustOrVerifyChangedRecipientsInternal(@NonNull List<ChangedRecipient> changedRecipients) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
IdentityRecord identityRecord = changedRecipient.getIdentityRecord();
|
||||
|
||||
if (changedRecipient.isUnverified()) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
} else {
|
||||
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TrustAndVerifyResult.TRUST_AND_VERIFY;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private TrustAndVerifyResult trustOrVerifyChangedRecipientsAndResendInternal(@NonNull List<ChangedRecipient> changedRecipients,
|
||||
@NonNull MessageRecord messageRecord) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(changedRecipient.getRecipient().requireServiceId(), 1);
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context);
|
||||
identityKeyStore.saveIdentity(mismatchAddress, changedRecipient.getIdentityRecord().getIdentityKey(), true);
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRecord.isOutgoing()) {
|
||||
processOutgoingMessageRecord(changedRecipients, messageRecord);
|
||||
}
|
||||
|
||||
return TrustAndVerifyResult.TRUST_VERIFY_AND_RESEND;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void processOutgoingMessageRecord(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
RecipientId id = changedRecipient.getRecipient().getId();
|
||||
IdentityKey identityKey = changedRecipient.getIdentityRecord().getIdentityKey();
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
|
||||
|
||||
if (messageRecord.getRecipient().isPushGroup()) {
|
||||
MessageSender.resendGroupMessage(context, messageRecord, id);
|
||||
} else {
|
||||
MessageSender.resend(context, messageRecord);
|
||||
}
|
||||
} else {
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
|
||||
|
||||
MessageSender.resend(context, messageRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class SafetyNumberChangeState {
|
||||
|
||||
private final List<ChangedRecipient> changedRecipients;
|
||||
private final MessageRecord messageRecord;
|
||||
|
||||
SafetyNumberChangeState(List<ChangedRecipient> changedRecipients, @Nullable MessageRecord messageRecord) {
|
||||
this.changedRecipients = changedRecipients;
|
||||
this.messageRecord = messageRecord;
|
||||
}
|
||||
|
||||
@NonNull List<ChangedRecipient> getChangedRecipients() {
|
||||
return changedRecipients;
|
||||
}
|
||||
|
||||
@Nullable MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeRepository.SafetyNumberChangeState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SafetyNumberChangeViewModel extends ViewModel {
|
||||
|
||||
private final SafetyNumberChangeRepository safetyNumberChangeRepository;
|
||||
private final LiveData<SafetyNumberChangeState> safetyNumberChangeState;
|
||||
|
||||
private SafetyNumberChangeViewModel(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId, SafetyNumberChangeRepository safetyNumberChangeRepository) {
|
||||
this.safetyNumberChangeRepository = safetyNumberChangeRepository;
|
||||
safetyNumberChangeState = this.safetyNumberChangeRepository.getSafetyNumberChangeState(recipientIds, messageId);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<ChangedRecipient>> getChangedRecipients() {
|
||||
return Transformations.map(safetyNumberChangeState, SafetyNumberChangeState::getChangedRecipients);
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients() {
|
||||
SafetyNumberChangeState state = Objects.requireNonNull(safetyNumberChangeState.getValue());
|
||||
if (state.getMessageRecord() != null) {
|
||||
return safetyNumberChangeRepository.trustOrVerifyChangedRecipientsAndResend(state.getChangedRecipients(), state.getMessageRecord());
|
||||
} else {
|
||||
return safetyNumberChangeRepository.trustOrVerifyChangedRecipients(state.getChangedRecipients());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements ViewModelProvider.Factory {
|
||||
private final List<RecipientId> recipientIds;
|
||||
private final Long messageId;
|
||||
|
||||
public Factory(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
this.recipientIds = recipientIds;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
SafetyNumberChangeRepository repo = new SafetyNumberChangeRepository(ApplicationDependencies.getApplication());
|
||||
return Objects.requireNonNull(modelClass.cast(new SafetyNumberChangeViewModel(recipientIds, messageId, repo)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
public enum TrustAndVerifyResult {
|
||||
TRUST_AND_VERIFY,
|
||||
TRUST_VERIFY_AND_RESEND,
|
||||
UNKNOWN
|
||||
}
|
|
@ -280,6 +280,18 @@ public class MmsSmsDatabase extends Database {
|
|||
else return id;
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getMessageRecord(long messageId) {
|
||||
try {
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
} catch (NoSuchMessageException e1) {
|
||||
try {
|
||||
return DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
} catch (NoSuchMessageException e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId, long timestamp) {
|
||||
DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, true);
|
||||
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true);
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package org.thoughtcrime.securesms.database.identity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -24,6 +27,10 @@ public final class IdentityRecordList {
|
|||
identityRecords.addAll(identityRecordList.identityRecords);
|
||||
}
|
||||
|
||||
public List<IdentityRecord> getIdentityRecords() {
|
||||
return Collections.unmodifiableList(identityRecords);
|
||||
}
|
||||
|
||||
public boolean isVerified() {
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (identityRecord.getVerifiedStatus() != VerifiedStatus.VERIFIED) {
|
||||
|
|
|
@ -285,6 +285,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
return networkFailures != null && !networkFailures.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFailedWithNetworkFailures() {
|
||||
return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure());
|
||||
}
|
||||
|
||||
protected SpannableString emphasisAdded(String sequence) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
|
|
@ -84,11 +84,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
private void bindErrorState(MessageRecord messageRecord) {
|
||||
boolean isPushGroup = messageRecord.getRecipient().isPushGroup();
|
||||
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
|
||||
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
|
||||
|
||||
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
|
||||
if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
errorText.setVisibility(View.VISIBLE);
|
||||
resendButton.setVisibility(View.VISIBLE);
|
||||
resendButton.setOnClickListener(unused -> {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package org.thoughtcrime.securesms.util.adapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
|
||||
public final class AlwaysChangedDiffUtil<T> extends DiffUtil.ItemCallback<T> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem) {
|
||||
return false;
|
||||
}
|
||||
}
|
23
app/src/main/res/layout/safety_number_change_dialog.xml
Normal file
23
app/src/main/res/layout/safety_number_change_dialog.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
tools:background="?dialog_background_color">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/safety_number_change_dialog__the_following_people_may_have_reinstalled_or_changed_devices" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/safety_number_change_dialog_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
tools:listitem="@layout/safety_number_change_recipient" />
|
||||
|
||||
</LinearLayout>
|
68
app/src/main/res/layout/safety_number_change_recipient.xml
Normal file
68
app/src/main/res/layout/safety_number_change_recipient.xml
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?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="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
tools:background="?dialog_background_color">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/safety_number_change_recipient_avatar"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
|
||||
android:cropToPadding="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/safety_number_change_recipient_name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_contact_picture" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.FromTextView
|
||||
android:id="@+id/safety_number_change_recipient_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:textColor="?title_text_color_primary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/safety_number_change_recipient_subtitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/safety_number_change_recipient_view"
|
||||
app:layout_constraintStart_toEndOf="@+id/safety_number_change_recipient_avatar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Jules Bonnot" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/safety_number_change_recipient_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="marquee"
|
||||
android:textColor="?title_text_color_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/safety_number_change_recipient_view"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="@+id/safety_number_change_recipient_name"
|
||||
app:layout_constraintTop_toBottomOf="@+id/safety_number_change_recipient_name"
|
||||
tools:text="+1 817-647-3790" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/safety_number_change_recipient_view"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="?attr/safety_number_change_dialog_button_background"
|
||||
android:elevation="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:text="@string/safety_number_change_dialog__view"
|
||||
android:textColor="?safety_number_change_dialog_button_text_color"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/safety_number_change_recipient_name"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:targetApi="lollipop" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -141,6 +141,9 @@
|
|||
<attr name="conversation_scroll_to_bottom_background" format="reference" />
|
||||
<attr name="conversation_scroll_to_bottom_foreground_color" format="color" />
|
||||
|
||||
<attr name="safety_number_change_dialog_button_background" format="reference|color" />
|
||||
<attr name="safety_number_change_dialog_button_text_color" format="color" />
|
||||
|
||||
<attr name="tinted_circle_background" format="reference" />
|
||||
|
||||
<attr name="dialog_info_icon" format="reference" />
|
||||
|
|
|
@ -187,7 +187,9 @@
|
|||
<string name="ContactShareEditActivity_invalid_contact">Selected contact was invalid</string>
|
||||
|
||||
<!-- ConversationItem -->
|
||||
<string name="ConversationItem_error_not_delivered">Send failed, tap for details</string>
|
||||
<string name="ConversationItem_error_not_sent_tap_for_details">Not sent, tap for details</string>
|
||||
<string name="ConversationItem_error_partially_not_delivered">Partially sent, tap for details</string>
|
||||
<string name="ConversationItem_error_network_not_delivered">Send failed</string>
|
||||
<string name="ConversationItem_received_key_exchange_message_tap_to_process">Received key exchange message, tap to process.</string>
|
||||
<string name="ConversationItem_group_action_left">%1$s has left the group.</string>
|
||||
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
|
||||
|
@ -1444,6 +1446,7 @@
|
|||
<string name="conversation_activity__quick_attachment_drawer_record_and_send_audio_description">Record and send audio attachment</string>
|
||||
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Lock recording of audio attachment</string>
|
||||
<string name="conversation_activity__enable_signal_for_sms">Enable Signal for SMS</string>
|
||||
<string name="conversation_activity__message_could_not_be_sent">Message could not be sent. Check your connection and try again.</string>
|
||||
|
||||
<!-- conversation_input_panel -->
|
||||
<string name="conversation_input_panel__slide_to_cancel">Slide to cancel</string>
|
||||
|
@ -1479,6 +1482,13 @@
|
|||
<!-- conversation_fragment -->
|
||||
<string name="conversation_fragment__scroll_to_the_bottom_content_description">Scroll to the bottom</string>
|
||||
|
||||
<!-- safety_number_change_dialog -->
|
||||
<string name="safety_number_change_dialog__safety_number_changes">Safety Number Changes</string>
|
||||
<string name="safety_number_change_dialog__send_anyway">Send anyway</string>
|
||||
<string name="safety_number_change_dialog__the_following_people_may_have_reinstalled_or_changed_devices">The following people may have reinstalled or changed devices. Verify your safety number with them to ensure privacy.</string>
|
||||
<string name="safety_number_change_dialog__view">View</string>
|
||||
<string name="safety_number_change_dialog__previous_verified">Previous verified</string>
|
||||
|
||||
<!-- country_selection_fragment -->
|
||||
<string name="country_selection_fragment__loading_countries">Loading countries…</string>
|
||||
<string name="country_selection_fragment__search">Search</string>
|
||||
|
|
|
@ -258,6 +258,9 @@
|
|||
<item name="conversation_title_color">@color/white</item>
|
||||
<item name="conversation_subtitle_color">@color/transparent_white_90</item>
|
||||
|
||||
<item name="safety_number_change_dialog_button_background">@color/core_grey_05</item>
|
||||
<item name="safety_number_change_dialog_button_text_color">@color/core_ultramarine</item>
|
||||
|
||||
<item name="tinted_circle_background">@drawable/tinted_circle_light</item>
|
||||
|
||||
<item name="contact_list_divider">@drawable/contact_list_divider_light</item>
|
||||
|
@ -546,6 +549,9 @@
|
|||
<item name="conversation_item_image_outline_color">@color/transparent_white_20</item>
|
||||
<item name="conversation_item_reveal_viewed_background_color">?conversation_background</item>
|
||||
|
||||
<item name="safety_number_change_dialog_button_background">@color/core_grey_75</item>
|
||||
<item name="safety_number_change_dialog_button_text_color">@color/core_grey_05</item>
|
||||
|
||||
<item name="contact_list_divider">@drawable/contact_list_divider_dark</item>
|
||||
|
||||
<item name="debuglog_color_none">@color/debuglog_dark_none</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue