mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-23 11:15:44 +00:00
Implement new Safety Number Changes bottom sheeet.
This commit is contained in:
@@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequest
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
@@ -344,7 +345,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall());
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
|
||||
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
|
||||
SafetyNumberBottomSheet.forGroupCall(((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords())
|
||||
.show(getSupportFragmentManager());
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) {
|
||||
callScreen.switchToSpeakerView();
|
||||
@@ -561,7 +563,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
|
||||
SafetyNumberBottomSheet.forCall(recipient.getId()).show(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
public void handleSafetyNumberChangeEvent(@NonNull WebRtcCallViewModel.SafetyNumberChangeEvent safetyNumberChangeEvent) {
|
||||
@@ -570,7 +572,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
GroupCallSafetyNumberChangeNotificationUtil.showNotification(this, viewModel.getRecipient().get());
|
||||
} else {
|
||||
GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(this, viewModel.getRecipient().get());
|
||||
SafetyNumberChangeDialog.showForDuringGroupCall(getSupportFragmentManager(), safetyNumberChangeEvent.getRecipientIds());
|
||||
SafetyNumberBottomSheet.forDuringGroupCall(safetyNumberChangeEvent.getRecipientIds()).show(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -40,6 +41,15 @@ abstract class WrapperDialogFragment : DialogFragment(R.layout.fragment_containe
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
onHandleBackPressed()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, getWrappedFragment())
|
||||
@@ -47,6 +57,10 @@ abstract class WrapperDialogFragment : DialogFragment(R.layout.fragment_containe
|
||||
}
|
||||
}
|
||||
|
||||
open fun onHandleBackPressed() {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
findListener<WrapperDialogFragmentCallback>()?.onWrapperDialogFragmentDismissed()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.components.settings.models
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
@@ -18,9 +20,9 @@ object SplashImage {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.splash_image))
|
||||
}
|
||||
|
||||
class Model(@DrawableRes val splashImageResId: Int) : PreferenceModel<Model>() {
|
||||
class Model(@DrawableRes val splashImageResId: Int, @ColorRes val splashImageTintResId: Int = -1) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.splashImageResId == splashImageResId
|
||||
return newItem.splashImageResId == splashImageResId && newItem.splashImageTintResId == splashImageTintResId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +32,12 @@ object SplashImage {
|
||||
|
||||
override fun bind(model: Model) {
|
||||
splashImageView.setImageResource(model.splashImageResId)
|
||||
|
||||
if (model.splashImageTintResId != -1) {
|
||||
splashImageView.setColorFilter(ContextCompat.getColor(context, model.splashImageTintResId))
|
||||
} else {
|
||||
splashImageView.clearColorFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ sealed class ContactSearchKey {
|
||||
*/
|
||||
open fun requireShareContact(): ShareContact = error("This key cannot be converted into a ShareContact")
|
||||
|
||||
open fun requireParcelable(): Parcelable = error("This key cannot be parcelized")
|
||||
open fun requireParcelable(): ParcelableRecipientSearchKey = error("This key cannot be parcelized")
|
||||
|
||||
sealed class RecipientSearchKey : ContactSearchKey() {
|
||||
|
||||
@@ -29,7 +29,7 @@ sealed class ContactSearchKey {
|
||||
return ShareContact(recipientId)
|
||||
}
|
||||
|
||||
override fun requireParcelable(): Parcelable {
|
||||
override fun requireParcelable(): ParcelableRecipientSearchKey {
|
||||
return ParcelableRecipientSearchKey(ParcelableType.STORY, recipientId)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ sealed class ContactSearchKey {
|
||||
return ShareContact(recipientId)
|
||||
}
|
||||
|
||||
override fun requireParcelable(): Parcelable {
|
||||
override fun requireParcelable(): ParcelableRecipientSearchKey {
|
||||
return ParcelableRecipientSearchKey(ParcelableType.KNOWN_RECIPIENT, recipientId)
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
@@ -1874,7 +1875,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
@Override
|
||||
public void onIncomingIdentityMismatchClicked(@NonNull RecipientId recipientId) {
|
||||
SafetyNumberChangeDialog.show(getParentFragmentManager(), recipientId);
|
||||
SafetyNumberBottomSheet.forRecipientId(recipientId)
|
||||
.show(getParentFragmentManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -152,6 +152,7 @@ import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
||||
@@ -161,7 +162,6 @@ import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupA
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository;
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
@@ -263,6 +263,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@@ -348,7 +349,7 @@ public class ConversationParentFragment extends Fragment
|
||||
AttachmentKeyboard.Callback,
|
||||
ConversationReactionOverlay.OnReactionSelectedListener,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
|
||||
SafetyNumberChangeDialog.Callback,
|
||||
SafetyNumberBottomSheet.Callbacks,
|
||||
ReactionsBottomSheetDialogFragment.Callback,
|
||||
MediaKeyboard.MediaKeyboardListener,
|
||||
EmojiEventListener,
|
||||
@@ -1594,22 +1595,16 @@ public class ConversationParentFragment extends Fragment
|
||||
private void handleRecentSafetyNumberChange() {
|
||||
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
|
||||
records.addAll(identityRecords.getUntrustedRecords());
|
||||
SafetyNumberChangeDialog.show(getChildFragmentManager(), records);
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestination(
|
||||
records,
|
||||
new ContactSearchKey.RecipientSearchKey.KnownRecipient(recipient.getId())
|
||||
)
|
||||
.show(getChildFragmentManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange(@NonNull List<RecipientId> changedRecipients) {
|
||||
Log.d(TAG, "onSendAnywayAfterSafetyNumberChange");
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() {
|
||||
public void onMessageResentAfterSafetyNumberChangeInBottomSheet() {
|
||||
Log.d(TAG, "onMessageResentAfterSafetyNumberChange");
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
@@ -3629,6 +3624,17 @@ public class ConversationParentFragment extends Fragment
|
||||
material3OnScrollHelper.setColorImmediate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendAnywayAfterSafetyNumberChangedInBottomSheet(@NonNull List<? extends ContactSearchKey.RecipientSearchKey> destinations) {
|
||||
Log.d(TAG, "onSendAnywayAfterSafetyNumberChange");
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listeners
|
||||
|
||||
private final class DeleteCanceledVoiceNoteListener implements ListenableFuture.Listener<VoiceNoteDraft> {
|
||||
@@ -3888,7 +3894,9 @@ public class ConversationParentFragment extends Fragment
|
||||
@Override
|
||||
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isIdentityMismatchFailure()) {
|
||||
SafetyNumberChangeDialog.show(requireContext(), getChildFragmentManager(), messageRecord);
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), messageRecord)
|
||||
.show(getChildFragmentManager());
|
||||
} else if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
|
||||
|
||||
@@ -34,12 +34,12 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
import org.thoughtcrime.securesms.sharing.ShareSelectionAdapter
|
||||
import org.thoughtcrime.securesms.sharing.ShareSelectionMappingModel
|
||||
@@ -74,7 +74,7 @@ import org.thoughtcrime.securesms.util.visible
|
||||
*/
|
||||
class MultiselectForwardFragment :
|
||||
Fragment(R.layout.multiselect_forward_fragment),
|
||||
SafetyNumberChangeDialog.Callback,
|
||||
SafetyNumberBottomSheet.Callbacks,
|
||||
ChooseStoryTypeBottomSheet.Callback,
|
||||
WrapperDialogFragment.WrapperDialogFragmentCallback,
|
||||
ChooseInitialMyStoryMembershipBottomSheetDialogFragment.Callback {
|
||||
@@ -204,7 +204,7 @@ class MultiselectForwardFragment :
|
||||
when (it.stage) {
|
||||
MultiselectForwardState.Stage.Selection -> {}
|
||||
MultiselectForwardState.Stage.FirstConfirmation -> displayFirstSendConfirmation()
|
||||
is MultiselectForwardState.Stage.SafetyConfirmation -> displaySafetyNumberConfirmation(it.stage.identities)
|
||||
is MultiselectForwardState.Stage.SafetyConfirmation -> displaySafetyNumberConfirmation(it.stage.identities, it.stage.selectedContacts)
|
||||
MultiselectForwardState.Stage.LoadingIdentities -> {}
|
||||
MultiselectForwardState.Stage.SendPending -> {
|
||||
handler?.removeCallbacksAndMessages(null)
|
||||
@@ -288,8 +288,10 @@ class MultiselectForwardFragment :
|
||||
viewModel.send(addMessage.text.toString(), contactSearchMediator.getSelectedContacts())
|
||||
}
|
||||
|
||||
private fun displaySafetyNumberConfirmation(identityRecords: List<IdentityRecord>) {
|
||||
SafetyNumberChangeDialog.show(childFragmentManager, identityRecords)
|
||||
private fun displaySafetyNumberConfirmation(identityRecords: List<IdentityRecord>, selectedContacts: List<ContactSearchKey>) {
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestinations(identityRecords, selectedContacts)
|
||||
.show(childFragmentManager)
|
||||
}
|
||||
|
||||
private fun dismissWithSuccess(@PluralsRes toastTextResId: Int) {
|
||||
@@ -332,11 +334,11 @@ class MultiselectForwardFragment :
|
||||
callback.exitFlow()
|
||||
}
|
||||
|
||||
override fun onSendAnywayAfterSafetyNumberChange(changedRecipients: MutableList<RecipientId>) {
|
||||
viewModel.confirmSafetySend(addMessage.text.toString(), contactSearchMediator.getSelectedContacts())
|
||||
override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List<ContactSearchKey.RecipientSearchKey>) {
|
||||
viewModel.confirmSafetySend(addMessage.text.toString(), destinations.toSet())
|
||||
}
|
||||
|
||||
override fun onMessageResentAfterSafetyNumberChange() {
|
||||
override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ data class MultiselectForwardState(
|
||||
object Selection : Stage()
|
||||
object FirstConfirmation : Stage()
|
||||
object LoadingIdentities : Stage()
|
||||
data class SafetyConfirmation(val identities: List<IdentityRecord>) : Stage()
|
||||
data class SafetyConfirmation(val identities: List<IdentityRecord>, val selectedContacts: List<ContactSearchKey>) : Stage()
|
||||
object SendPending : Stage()
|
||||
object SomeFailed : Stage()
|
||||
object AllFailed : Stage()
|
||||
|
||||
@@ -44,7 +44,14 @@ class MultiselectForwardViewModel(
|
||||
if (identityRecords.isEmpty()) {
|
||||
performSend(additionalMessage, selectedContacts)
|
||||
} else {
|
||||
store.update { it.copy(stage = MultiselectForwardState.Stage.SafetyConfirmation(identityRecords)) }
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
stage = MultiselectForwardState.Stage.SafetyConfirmation(
|
||||
identityRecords,
|
||||
selectedContacts.filterIsInstance<ContactSearchKey.RecipientSearchKey>()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -28,9 +27,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
||||
|
||||
@@ -64,37 +61,6 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
|
||||
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
public static void show(@NonNull FragmentManager fragmentManager, @NonNull List<IdentityRecord> identityRecords) {
|
||||
List<String> ids = Stream.of(identityRecords)
|
||||
.filterNot(IdentityRecord::isFirstUse)
|
||||
.map(record -> record.getRecipientId().serialize())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
|
||||
arguments.putInt(CONTINUE_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__send_anyway);
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
public static void show(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull 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());
|
||||
arguments.putString(MESSAGE_TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||
arguments.putInt(CONTINUE_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__send_anyway);
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
public static void showForCall(@NonNull FragmentManager fragmentManager, @NonNull RecipientId recipientId) {
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, new String[] { recipientId.serialize() });
|
||||
@@ -140,7 +106,7 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
|
||||
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
private SafetyNumberChangeDialog() { }
|
||||
private SafetyNumberChangeDialog() {}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
||||
@@ -22,10 +22,12 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberRecipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
@@ -35,17 +37,39 @@ import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
final class SafetyNumberChangeRepository {
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class SafetyNumberChangeRepository {
|
||||
|
||||
private static final String TAG = Log.tag(SafetyNumberChangeRepository.class);
|
||||
|
||||
private final Context context;
|
||||
|
||||
SafetyNumberChangeRepository(Context context) {
|
||||
public SafetyNumberChangeRepository(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Single<TrustAndVerifyResult> trustOrVerifyChangedRecipientsRx(@NonNull List<SafetyNumberRecipient> safetyNumberRecipients) {
|
||||
Log.d(TAG, "Trust or verify changed recipients for: " + Util.join(safetyNumberRecipients, ","));
|
||||
return Single.fromCallable(() -> trustOrVerifyChangedRecipientsInternal(fromSafetyNumberRecipients(safetyNumberRecipients)))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Single<TrustAndVerifyResult> trustOrVerifyChangedRecipientsAndResendRx(@NonNull List<SafetyNumberRecipient> safetyNumberRecipients, @NonNull MessageId messageId) {
|
||||
Log.d(TAG, "Trust or verify changed recipients and resend message: " + messageId + " for: " + Util.join(safetyNumberRecipients, ","));
|
||||
return Single.fromCallable(() -> {
|
||||
MessageRecord messageRecord = messageId.isMms() ? SignalDatabase.mms().getMessageRecord(messageId.getId())
|
||||
: SignalDatabase.sms().getMessageRecord(messageId.getId());
|
||||
|
||||
return trustOrVerifyChangedRecipientsAndResendInternal(fromSafetyNumberRecipients(safetyNumberRecipients), messageRecord);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients(@NonNull List<ChangedRecipient> changedRecipients) {
|
||||
Log.d(TAG, "Trust or verify changed recipients for: " + Util.join(changedRecipients, ","));
|
||||
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
|
||||
@@ -78,6 +102,14 @@ final class SafetyNumberChangeRepository {
|
||||
return new SafetyNumberChangeState(changedRecipients, messageRecord);
|
||||
}
|
||||
|
||||
private @NonNull List<ChangedRecipient> fromSafetyNumberRecipients(@NonNull List<SafetyNumberRecipient> safetyNumberRecipients) {
|
||||
return safetyNumberRecipients.stream().map(this::fromSafetyNumberRecipient).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private @NonNull ChangedRecipient fromSafetyNumberRecipient(@NonNull SafetyNumberRecipient safetyNumberRecipient) {
|
||||
return new ChangedRecipient(safetyNumberRecipient.getRecipient(), safetyNumberRecipient.getIdentityRecord());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable MessageRecord getMessageRecord(Long messageId, String messageType) {
|
||||
try {
|
||||
@@ -99,7 +131,7 @@ final class SafetyNumberChangeRepository {
|
||||
private TrustAndVerifyResult trustOrVerifyChangedRecipientsInternal(@NonNull List<ChangedRecipient> changedRecipients) {
|
||||
SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities();
|
||||
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
IdentityRecord identityRecord = changedRecipient.getIdentityRecord();
|
||||
|
||||
@@ -126,7 +158,7 @@ final class SafetyNumberChangeRepository {
|
||||
Log.d(TAG, "No changed recipients to process, will still process message record");
|
||||
}
|
||||
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
SignalProtocolAddress mismatchAddress = changedRecipient.getRecipient().requireServiceId().toProtocolAddress(SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
|
||||
|
||||
@@ -488,6 +488,13 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
.run()
|
||||
}
|
||||
|
||||
fun removeAllMembers(listId: DistributionListId, privacyMode: DistributionListPrivacyMode) {
|
||||
writableDatabase
|
||||
.delete(MembershipTable.TABLE_NAME)
|
||||
.where("${MembershipTable.LIST_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ?", listId.serialize(), privacyMode.serialize())
|
||||
.run()
|
||||
}
|
||||
|
||||
fun remapRecipient(oldId: RecipientId, newId: RecipientId) {
|
||||
val values = ContentValues().apply {
|
||||
put(MembershipTable.RECIPIENT_ID, newId.serialize())
|
||||
@@ -644,4 +651,33 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
private fun createUniqueNameForUnknownDistributionId(): String {
|
||||
return "DELETED-${UUID.randomUUID()}"
|
||||
}
|
||||
|
||||
fun excludeFromStory(recipientId: RecipientId, record: DistributionListRecord) {
|
||||
excludeAllFromStory(listOf(recipientId), record)
|
||||
}
|
||||
|
||||
fun excludeAllFromStory(recipientIds: List<RecipientId>, record: DistributionListRecord) {
|
||||
writableDatabase.withinTransaction {
|
||||
when (record.privacyMode) {
|
||||
DistributionListPrivacyMode.ONLY_WITH -> {
|
||||
recipientIds.forEach {
|
||||
removeMemberFromList(record.id, record.privacyMode, it)
|
||||
}
|
||||
}
|
||||
DistributionListPrivacyMode.ALL_EXCEPT -> {
|
||||
recipientIds.forEach {
|
||||
addMemberToList(record.id, record.privacyMode, it)
|
||||
}
|
||||
}
|
||||
DistributionListPrivacyMode.ALL -> {
|
||||
removeAllMembers(record.id, DistributionListPrivacyMode.ALL_EXCEPT)
|
||||
setPrivacyMode(record.id, DistributionListPrivacyMode.ALL_EXCEPT)
|
||||
|
||||
recipientIds.forEach {
|
||||
addMemberToList(record.id, DistributionListPrivacyMode.ALL_EXCEPT, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents a pair of values that can be used to find a message. Because we have two tables,
|
||||
* that means this has both the primary key and a boolean indicating which table it's in.
|
||||
*/
|
||||
@Parcelize
|
||||
data class MessageId(
|
||||
val id: Long,
|
||||
@get:JvmName("isMms") val mms: Boolean
|
||||
) {
|
||||
) : Parcelable {
|
||||
fun serialize(): String {
|
||||
return "$id|$mms"
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFullScreenDialogFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.SearchConfigurationProvider
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
|
||||
@@ -41,6 +40,7 @@ import org.thoughtcrime.securesms.mediasend.v2.review.MediaReviewFragment
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendRepository
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
@@ -245,7 +245,9 @@ class MediaSelectionActivity :
|
||||
override fun onSendError(error: Throwable) {
|
||||
if (error is UntrustedRecords.UntrustedRecordsException) {
|
||||
Log.w(TAG, "Send failed due to untrusted identities.")
|
||||
SafetyNumberChangeDialog.show(supportFragmentManager, error.untrustedRecords)
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestinations(error.untrustedRecords, error.destinations.toList())
|
||||
.show(supportFragmentManager)
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ object UntrustedRecords {
|
||||
return Completable.fromAction {
|
||||
val untrustedRecords: List<IdentityRecord> = checkForBadIdentityRecordsSync(contactSearchKeys)
|
||||
if (untrustedRecords.isNotEmpty()) {
|
||||
throw UntrustedRecordsException(untrustedRecords)
|
||||
throw UntrustedRecordsException(untrustedRecords, contactSearchKeys)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
@@ -39,8 +39,8 @@ object UntrustedRecords {
|
||||
}
|
||||
.flatten()
|
||||
|
||||
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients).untrustedRecords
|
||||
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients).identityRecords
|
||||
}
|
||||
|
||||
class UntrustedRecordsException(val untrustedRecords: List<IdentityRecord>) : Throwable()
|
||||
class UntrustedRecordsException(val untrustedRecords: List<IdentityRecord>, val destinations: Set<ContactSearchKey.RecipientSearchKey>) : Throwable()
|
||||
}
|
||||
|
||||
@@ -14,18 +14,18 @@ import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.HudCommand
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendRepository
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendResult
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostView
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creation_fragment), TextStoryPostTextEntryFragment.Callback {
|
||||
class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creation_fragment), TextStoryPostTextEntryFragment.Callback, SafetyNumberBottomSheet.Callbacks {
|
||||
|
||||
private lateinit var scene: ConstraintLayout
|
||||
private lateinit var backgroundButton: AppCompatImageView
|
||||
@@ -166,9 +166,21 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati
|
||||
}
|
||||
is TextStoryPostSendResult.UntrustedRecordsError -> {
|
||||
send.isClickable = true
|
||||
SafetyNumberChangeDialog.show(childFragmentManager, result.untrustedRecords)
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestinations(result.untrustedRecords, contacts.toList())
|
||||
.show(childFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List<ContactSearchKey.RecipientSearchKey>) {
|
||||
performSend(destinations.toSet())
|
||||
}
|
||||
|
||||
override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() {
|
||||
error("Unsupported, we do not hand in a message id.")
|
||||
}
|
||||
|
||||
override fun onCanceled() = Unit
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import org.thoughtcrime.securesms.components.WrapperDialogFragment
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationViewModel
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.sharing.ShareSelectionAdapter
|
||||
import org.thoughtcrime.securesms.sharing.ShareSelectionMappingModel
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
@@ -39,7 +39,8 @@ class TextStoryPostSendFragment :
|
||||
Fragment(R.layout.stories_send_text_post_fragment),
|
||||
ChooseStoryTypeBottomSheet.Callback,
|
||||
WrapperDialogFragment.WrapperDialogFragmentCallback,
|
||||
ChooseInitialMyStoryMembershipBottomSheetDialogFragment.Callback {
|
||||
ChooseInitialMyStoryMembershipBottomSheetDialogFragment.Callback,
|
||||
SafetyNumberBottomSheet.Callbacks {
|
||||
|
||||
private lateinit var shareListWrapper: View
|
||||
private lateinit var shareSelectionRecyclerView: RecyclerView
|
||||
@@ -93,8 +94,10 @@ class TextStoryPostSendFragment :
|
||||
send()
|
||||
}
|
||||
|
||||
disposables += viewModel.untrustedIdentities.subscribe {
|
||||
SafetyNumberChangeDialog.show(childFragmentManager, it)
|
||||
disposables += viewModel.untrustedIdentities.subscribe { records ->
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestinations(records, contactSearchMediator.getSelectedContacts().toList())
|
||||
.show(childFragmentManager)
|
||||
}
|
||||
|
||||
searchField.doAfterTextChanged {
|
||||
@@ -197,4 +200,14 @@ class TextStoryPostSendFragment :
|
||||
contactSearchMediator.setKeysSelected(setOf(ContactSearchKey.RecipientSearchKey.Story(recipientId)))
|
||||
contactSearchMediator.refresh()
|
||||
}
|
||||
|
||||
override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List<ContactSearchKey.RecipientSearchKey>) {
|
||||
send()
|
||||
}
|
||||
|
||||
override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() = error("Not supported here")
|
||||
|
||||
override fun onCanceled() {
|
||||
viewModel.onSendCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.FullScreenDialogFragment;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
|
||||
@@ -26,6 +25,7 @@ import org.thoughtcrime.securesms.messagedetails.MessageDetailsViewModel.Factory
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -88,8 +88,8 @@ public final class MessageDetailsFragment extends FullScreenDialogFragment {
|
||||
}
|
||||
|
||||
private void initializeList(@NonNull View view) {
|
||||
RecyclerView list = view.findViewById(R.id.message_details_list);
|
||||
View toolbarShadow = view.findViewById(R.id.toolbar_shadow);
|
||||
RecyclerView list = view.findViewById(R.id.message_details_list);
|
||||
View toolbarShadow = view.findViewById(R.id.toolbar_shadow);
|
||||
|
||||
colorizer = new Colorizer();
|
||||
adapter = new MessageDetailsAdapter(getViewLifecycleOwner(), glideRequests, colorizer, this::onErrorClicked);
|
||||
@@ -159,7 +159,9 @@ public final class MessageDetailsFragment extends FullScreenDialogFragment {
|
||||
}
|
||||
|
||||
private void onErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
SafetyNumberChangeDialog.show(requireContext(), getChildFragmentManager(), messageRecord);
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), messageRecord)
|
||||
.show(getChildFragmentManager());
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
/**
|
||||
* Object responsible for the construction of SafetyNumberBottomSheetFragment and Arg objects.
|
||||
*/
|
||||
object SafetyNumberBottomSheet {
|
||||
|
||||
private const val TAG = "SafetyNumberBottomSheet"
|
||||
private const val ARGS = "args"
|
||||
|
||||
/**
|
||||
* Create a factory to generate a legacy dialog for the given recipient id.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun forRecipientId(recipientId: RecipientId): Factory {
|
||||
return object : Factory {
|
||||
override fun show(fragmentManager: FragmentManager) {
|
||||
SafetyNumberChangeDialog.show(fragmentManager, recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory to generate a legacy dialog for a given recipient id to display when
|
||||
* trying to place an outgoing call.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun forCall(recipientId: RecipientId): Factory {
|
||||
return object : Factory {
|
||||
override fun show(fragmentManager: FragmentManager) {
|
||||
SafetyNumberChangeDialog.showForCall(fragmentManager, recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory to display a legacy dialog for a given list of records when trying
|
||||
* to place a group call.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun forGroupCall(identityRecords: List<IdentityRecord>): Factory {
|
||||
return object : Factory {
|
||||
override fun show(fragmentManager: FragmentManager) {
|
||||
SafetyNumberChangeDialog.showForGroupCall(fragmentManager, identityRecords)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory to display a legacy dialog for a given list of recipient ids during
|
||||
* a group call
|
||||
*/
|
||||
@JvmStatic
|
||||
fun forDuringGroupCall(recipientIds: Collection<RecipientId>): Factory {
|
||||
return object : Factory {
|
||||
override fun show(fragmentManager: FragmentManager) {
|
||||
SafetyNumberChangeDialog.showForDuringGroupCall(fragmentManager, recipientIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory to generate a sheet for the given message record. This will try
|
||||
* to resend the message automatically when the user confirms.
|
||||
*
|
||||
* @param context Not held on to, so any context is fine.
|
||||
* @param messageRecord The message record containing failed identities.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun forMessageRecord(context: Context, messageRecord: MessageRecord): Factory {
|
||||
val args = SafetyNumberBottomSheetArgs(
|
||||
untrustedRecipients = messageRecord.identityKeyMismatches.map { it.getRecipientId(context) },
|
||||
destinations = getDestinationFromRecord(messageRecord),
|
||||
messageId = MessageId(messageRecord.id, messageRecord.isMms)
|
||||
)
|
||||
|
||||
return SheetFactory(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory to generate a sheet for the given identity records and destinations.
|
||||
*
|
||||
* @param identityRecords The list of untrusted records from the thrown error
|
||||
* @param destinations The list of locations the user was trying to send content
|
||||
*/
|
||||
@JvmStatic
|
||||
fun forIdentityRecordsAndDestinations(identityRecords: List<IdentityRecord>, destinations: List<ContactSearchKey>): Factory {
|
||||
val args = SafetyNumberBottomSheetArgs(
|
||||
identityRecords.map { it.recipientId },
|
||||
destinations.filterIsInstance<ContactSearchKey.RecipientSearchKey>().map { it.requireParcelable() }
|
||||
)
|
||||
|
||||
return SheetFactory(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory to generate a sheet for the given identity records and single destination.
|
||||
*
|
||||
* @param identityRecords The list of untrusted records from the thrown error
|
||||
* @param destination The location the user was trying to send content
|
||||
*/
|
||||
@JvmStatic
|
||||
fun forIdentityRecordsAndDestination(identityRecords: List<IdentityRecord>, destination: ContactSearchKey): Factory {
|
||||
val args = SafetyNumberBottomSheetArgs(
|
||||
identityRecords.map { it.recipientId },
|
||||
listOf(destination).filterIsInstance<ContactSearchKey.RecipientSearchKey>().map { it.requireParcelable() }
|
||||
)
|
||||
|
||||
return SheetFactory(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The parcelized arguments inside the bundle
|
||||
* @throws IllegalArgumentException if the bundle does not contain the correct parcelized arguments.
|
||||
*/
|
||||
fun getArgsFromBundle(bundle: Bundle): SafetyNumberBottomSheetArgs {
|
||||
val args = bundle.getParcelable<SafetyNumberBottomSheetArgs>(ARGS)
|
||||
Preconditions.checkArgument(args != null)
|
||||
return args!!
|
||||
}
|
||||
|
||||
private fun getDestinationFromRecord(messageRecord: MessageRecord): List<ContactSearchKey.ParcelableRecipientSearchKey> {
|
||||
val key = if ((messageRecord as? MmsMessageRecord)?.storyType?.isStory == true) {
|
||||
ContactSearchKey.RecipientSearchKey.Story(messageRecord.recipient.id)
|
||||
} else {
|
||||
ContactSearchKey.RecipientSearchKey.KnownRecipient(messageRecord.recipient.id)
|
||||
}
|
||||
|
||||
return listOf(key.requireParcelable())
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to the normal companion object "show" methods, but with automatically provided arguments.
|
||||
*/
|
||||
interface Factory {
|
||||
fun show(fragmentManager: FragmentManager)
|
||||
}
|
||||
|
||||
private class SheetFactory(private val args: SafetyNumberBottomSheetArgs) : Factory {
|
||||
override fun show(fragmentManager: FragmentManager) {
|
||||
val dialogFragment = SafetyNumberBottomSheetFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARGS, args)
|
||||
}
|
||||
}
|
||||
|
||||
dialogFragment.show(fragmentManager, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callbacks for the bottom sheet. These are optional, and are invoked by the bottom sheet itself.
|
||||
*
|
||||
* Since the bottom sheet utilizes findListener to locate the callback implementor, child fragments will
|
||||
* get precedence over their parents and activities have the least precedence.
|
||||
*/
|
||||
interface Callbacks {
|
||||
/**
|
||||
* Invoked when the user presses "send anyway" and the parent should automatically perform a resend.
|
||||
*/
|
||||
fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List<ContactSearchKey.RecipientSearchKey>)
|
||||
|
||||
/**
|
||||
* Invoked when the user presses "send anyway" and a message was automatically resent.
|
||||
*/
|
||||
fun onMessageResentAfterSafetyNumberChangeInBottomSheet()
|
||||
|
||||
/**
|
||||
* Invoked when the user dismisses the sheet without performing a send.
|
||||
*/
|
||||
fun onCanceled()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Fragment argument for `SafetyNumberBottomSheetFragment`
|
||||
*/
|
||||
@Parcelize
|
||||
data class SafetyNumberBottomSheetArgs(
|
||||
val untrustedRecipients: List<RecipientId>,
|
||||
val destinations: List<ContactSearchKey.ParcelableRecipientSearchKey>,
|
||||
val messageId: MessageId? = null
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,218 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.view.View
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.WrapperDialogFragment
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.SplashImage
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeRepository
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.TrustAndVerifyResult
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.safety.review.SafetyNumberReviewConnectionsFragment
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityFragment
|
||||
|
||||
/**
|
||||
* Displays a bottom sheet containing information about safety number changes and allows the user to
|
||||
* address these changes.
|
||||
*/
|
||||
class SafetyNumberBottomSheetFragment : DSLSettingsBottomSheetFragment(layoutId = R.layout.safety_number_bottom_sheet), WrapperDialogFragment.WrapperDialogFragmentCallback {
|
||||
|
||||
private lateinit var sendAnyway: MaterialButton
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
@get:MainThread
|
||||
private val args: SafetyNumberBottomSheetArgs by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SafetyNumberBottomSheet.getArgsFromBundle(requireArguments())
|
||||
}
|
||||
|
||||
private val viewModel: SafetyNumberBottomSheetViewModel by viewModels(factoryProducer = {
|
||||
SafetyNumberBottomSheetViewModel.Factory(
|
||||
args,
|
||||
SafetyNumberChangeRepository(requireContext())
|
||||
)
|
||||
})
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
val reviewConnections: View = requireView().findViewById(R.id.review_connections)
|
||||
sendAnyway = requireView().findViewById(R.id.send_anyway)
|
||||
|
||||
reviewConnections.setOnClickListener {
|
||||
viewModel.setDone()
|
||||
SafetyNumberReviewConnectionsFragment.show(childFragmentManager)
|
||||
}
|
||||
|
||||
sendAnyway.setOnClickListener {
|
||||
sendAnyway.isEnabled = false
|
||||
lifecycleDisposable += viewModel.trustAndVerify().subscribe { trustAndVerifyResult ->
|
||||
when (trustAndVerifyResult.result) {
|
||||
TrustAndVerifyResult.Result.TRUST_AND_VERIFY -> {
|
||||
findListener<SafetyNumberBottomSheet.Callbacks>()?.sendAnywayAfterSafetyNumberChangedInBottomSheet(viewModel.destinationSnapshot)
|
||||
}
|
||||
TrustAndVerifyResult.Result.TRUST_VERIFY_AND_RESEND -> {
|
||||
findListener<SafetyNumberBottomSheet.Callbacks>()?.onMessageResentAfterSafetyNumberChangeInBottomSheet()
|
||||
}
|
||||
TrustAndVerifyResult.Result.UNKNOWN -> {
|
||||
Log.w(TAG, "Unknown Result")
|
||||
}
|
||||
}
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
SplashImage.register(adapter)
|
||||
SafetyNumberRecipientRowItem.register(adapter)
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
lifecycleDisposable += viewModel.state.subscribe { state ->
|
||||
reviewConnections.visible = state.hasLargeNumberOfUntrustedRecipients
|
||||
|
||||
if (state.isCheckupComplete()) {
|
||||
sendAnyway.setText(R.string.conversation_activity__send)
|
||||
}
|
||||
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
if (sendAnyway.isEnabled) {
|
||||
findListener<SafetyNumberBottomSheet.Callbacks>()?.onCanceled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWrapperDialogFragmentDismissed() = Unit
|
||||
|
||||
private fun getConfiguration(state: SafetyNumberBottomSheetState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
SplashImage.Model(
|
||||
R.drawable.ic_safety_number_24,
|
||||
R.color.signal_colorOnSurface
|
||||
)
|
||||
)
|
||||
|
||||
textPref(
|
||||
title = DSLSettingsText.from(
|
||||
when {
|
||||
state.isCheckupComplete() && state.hasLargeNumberOfUntrustedRecipients -> R.string.SafetyNumberBottomSheetFragment__safety_number_checkup_complete
|
||||
state.hasLargeNumberOfUntrustedRecipients -> R.string.SafetyNumberBottomSheetFragment__safety_number_checkup
|
||||
else -> R.string.SafetyNumberBottomSheetFragment__safety_number_changes
|
||||
},
|
||||
DSLSettingsText.TextAppearanceModifier(R.style.Signal_Text_TitleLarge),
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
textPref(
|
||||
title = DSLSettingsText.from(
|
||||
when {
|
||||
state.isCheckupComplete() && state.hasLargeNumberOfUntrustedRecipients -> ""
|
||||
state.hasLargeNumberOfUntrustedRecipients -> getString(R.string.SafetyNumberBottomSheetFragment__you_have_d_connections, args.untrustedRecipients.size)
|
||||
else -> getString(R.string.SafetyNumberBottomSheetFragment__the_following_people)
|
||||
},
|
||||
DSLSettingsText.TextAppearanceModifier(R.style.Signal_Text_BodyLarge),
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
if (state.isEmpty()) {
|
||||
space(DimensionUnit.DP.toPixels(96f).toInt())
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.SafetyNumberBottomSheetFragment__no_more_recipients_to_show,
|
||||
DSLSettingsText.TextAppearanceModifier(R.style.Signal_Text_BodyLarge),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.hasLargeNumberOfUntrustedRecipients) {
|
||||
state.destinationToRecipientMap.values.flatten().distinct().forEach {
|
||||
customPref(
|
||||
SafetyNumberRecipientRowItem.Model(
|
||||
recipient = it.recipient,
|
||||
isVerified = it.identityRecord.verifiedStatus == IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
distributionListMembershipCount = it.distributionListMembershipCount,
|
||||
groupMembershipCount = it.groupMembershipCount,
|
||||
getContextMenuActions = { model ->
|
||||
val actions = mutableListOf<ActionItem>()
|
||||
|
||||
actions.add(
|
||||
ActionItem(
|
||||
iconRes = R.drawable.ic_safety_number_24,
|
||||
title = getString(R.string.SafetyNumberBottomSheetFragment__verify_safety_number),
|
||||
tintRes = R.color.signal_colorOnSurface,
|
||||
action = {
|
||||
lifecycleDisposable += viewModel.getIdentityRecord(model.recipient.id).subscribe { record ->
|
||||
VerifyIdentityFragment.createDialog(
|
||||
model.recipient.id,
|
||||
IdentityKeyParcelable(record.identityKey),
|
||||
false
|
||||
).show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (model.distributionListMembershipCount > 0) {
|
||||
actions.add(
|
||||
ActionItem(
|
||||
iconRes = R.drawable.ic_circle_x_24,
|
||||
title = getString(R.string.SafetyNumberBottomSheetFragment__remove_from_story),
|
||||
tintRes = R.color.signal_colorOnSurface,
|
||||
action = {
|
||||
viewModel.removeRecipientFromSelectedStories(model.recipient.id)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (model.distributionListMembershipCount == 0 && model.groupMembershipCount == 0) {
|
||||
actions.add(
|
||||
ActionItem(
|
||||
iconRes = R.drawable.ic_circle_x_24,
|
||||
title = getString(R.string.SafetyNumberReviewConnectionsFragment__remove),
|
||||
tintRes = R.color.signal_colorOnSurface,
|
||||
action = {
|
||||
viewModel.removeDestination(model.recipient.id)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
actions
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SafetyNumberBottomSheetFragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Repository for dealing with recipients with changed safety numbers.
|
||||
*/
|
||||
class SafetyNumberBottomSheetRepository {
|
||||
|
||||
/**
|
||||
* Retrieves the IdentityRecord for a given recipient from the protocol store (if present)
|
||||
*/
|
||||
fun getIdentityRecord(recipientId: RecipientId): Maybe<IdentityRecord> {
|
||||
return Single.fromCallable {
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipientId)
|
||||
}.flatMapMaybe {
|
||||
if (it.isPresent) {
|
||||
Maybe.just(it.get())
|
||||
} else {
|
||||
Maybe.empty()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds out the list of UntrustedRecipients to display in either the bottom sheet or review fragment. This list will be automatically
|
||||
* republished whenever there is a recipient change, group change, or distribution list change, and is driven by Recipient.observe.
|
||||
*
|
||||
* It will automatically filter out any recipients who are no longer included in destinations as the user moves through the list and removes or
|
||||
* verifies senders.
|
||||
*/
|
||||
fun getBuckets(recipients: List<RecipientId>, destinations: List<ContactSearchKey.RecipientSearchKey>): Flowable<Map<SafetyNumberBucket, List<SafetyNumberRecipient>>> {
|
||||
val recipientObservable = getResolvedIdentities(recipients)
|
||||
val distributionListObservable = getDistributionLists(destinations)
|
||||
val groupObservable = getGroups(destinations)
|
||||
val destinationRecipientIds = destinations.map { it.recipientId }.toSet()
|
||||
|
||||
return Observable.combineLatest(recipientObservable, distributionListObservable, groupObservable) { recipientList, distributionLists, groups ->
|
||||
val map = mutableMapOf<SafetyNumberBucket, List<SafetyNumberRecipient>>()
|
||||
|
||||
recipientList.forEach {
|
||||
val distributionListMemberships = getDistributionMemberships(it.recipient, distributionLists)
|
||||
val groupMemberships = getGroupMemberships(it.recipient, groups)
|
||||
val isInContactsBucket = destinationRecipientIds.contains(it.recipient.id)
|
||||
|
||||
val safetyNumberRecipient = SafetyNumberRecipient(
|
||||
it.recipient,
|
||||
it.identityRecord.get(),
|
||||
distributionListMemberships.size,
|
||||
groupMemberships.size
|
||||
)
|
||||
|
||||
distributionListMemberships.forEach { distributionListRecord ->
|
||||
insert(map, SafetyNumberBucket.DistributionListBucket(distributionListRecord.id, distributionListRecord.name), safetyNumberRecipient)
|
||||
}
|
||||
|
||||
groupMemberships.forEach { group ->
|
||||
insert(map, SafetyNumberBucket.GroupBucket(group), safetyNumberRecipient)
|
||||
}
|
||||
|
||||
if (isInContactsBucket) {
|
||||
insert(map, SafetyNumberBucket.ContactsBucket, safetyNumberRecipient)
|
||||
}
|
||||
}
|
||||
|
||||
map.toMap()
|
||||
}.toFlowable(BackpressureStrategy.LATEST).subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given recipient from all stories they're a member of in destinations.
|
||||
*/
|
||||
fun removeFromStories(recipientId: RecipientId, destinations: List<ContactSearchKey.RecipientSearchKey>): Completable {
|
||||
return filterForDistributionLists(destinations).flatMapCompletable { distributionRecipients ->
|
||||
Completable.fromAction {
|
||||
distributionRecipients
|
||||
.mapNotNull { SignalDatabase.distributionLists.getList(it.requireDistributionListId()) }
|
||||
.filter { it.members.contains(recipientId) }
|
||||
.forEach {
|
||||
SignalDatabase.distributionLists.excludeFromStory(recipientId, it)
|
||||
Stories.onStorySettingsChanged(it.id)
|
||||
}
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given set of recipients from the specified distribution list
|
||||
*/
|
||||
fun removeAllFromStory(recipientIds: List<RecipientId>, distributionList: DistributionListId): Completable {
|
||||
return Completable.fromAction {
|
||||
val record = SignalDatabase.distributionLists.getList(distributionList)
|
||||
if (record != null) {
|
||||
SignalDatabase.distributionLists.excludeAllFromStory(recipientIds, record)
|
||||
Stories.onStorySettingsChanged(distributionList)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun insert(map: MutableMap<SafetyNumberBucket, List<SafetyNumberRecipient>>, bucket: SafetyNumberBucket, safetyNumberRecipient: SafetyNumberRecipient) {
|
||||
val bucketList = map.getOrDefault(bucket, emptyList())
|
||||
map[bucket] = bucketList + safetyNumberRecipient
|
||||
}
|
||||
|
||||
private fun filterForDistributionLists(destinations: List<ContactSearchKey.RecipientSearchKey>): Single<List<Recipient>> {
|
||||
return Single.fromCallable {
|
||||
val recipients = Recipient.resolvedList(destinations.map { it.recipientId })
|
||||
recipients.filter { it.isDistributionList }
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterForGroups(destinations: List<ContactSearchKey.RecipientSearchKey>): Single<List<Recipient>> {
|
||||
return Single.fromCallable {
|
||||
val recipients = Recipient.resolvedList(destinations.map { it.recipientId })
|
||||
recipients.filter { it.isGroup }
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeDistributionList(recipient: Recipient): Observable<DistributionListRecord> {
|
||||
return Recipient.observable(recipient.id).map { SignalDatabase.distributionLists.getList(it.requireDistributionListId())!! }
|
||||
}
|
||||
|
||||
private fun getDistributionLists(destinations: List<ContactSearchKey.RecipientSearchKey>): Observable<List<DistributionListRecord>> {
|
||||
val distributionListRecipients: Single<List<Recipient>> = filterForDistributionLists(destinations)
|
||||
|
||||
return distributionListRecipients.flatMapObservable { recipients ->
|
||||
if (recipients.isEmpty()) {
|
||||
Observable.just(emptyList())
|
||||
} else {
|
||||
val distributionListObservables = recipients.map { observeDistributionList(it) }
|
||||
Observable.combineLatest(distributionListObservables) {
|
||||
it.filterIsInstance<DistributionListRecord>()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGroups(destinations: List<ContactSearchKey.RecipientSearchKey>): Observable<List<Recipient>> {
|
||||
val groupRecipients: Single<List<Recipient>> = filterForGroups(destinations)
|
||||
return groupRecipients.flatMapObservable { recipients ->
|
||||
if (recipients.isEmpty()) {
|
||||
Observable.just(emptyList())
|
||||
} else {
|
||||
val recipientObservables = recipients.map {
|
||||
Recipient.observable(it.id)
|
||||
}
|
||||
Observable.combineLatest(recipientObservables) {
|
||||
it.filterIsInstance<Recipient>()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getResolvedIdentities(recipients: List<RecipientId>): Observable<List<ResolvedIdentity>> {
|
||||
val recipientObservables: List<Observable<ResolvedIdentity>> = recipients.map {
|
||||
Recipient.observable(it).switchMap { recipient ->
|
||||
Observable.fromCallable {
|
||||
val record = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.id)
|
||||
ResolvedIdentity(recipient, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Observable.combineLatest(recipientObservables) { identities ->
|
||||
identities.filterIsInstance<ResolvedIdentity>().filter { it.identityRecord.isPresent }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDistributionMemberships(recipient: Recipient, distributionLists: List<DistributionListRecord>): List<DistributionListRecord> {
|
||||
return distributionLists.filter { it.members.contains(recipient.id) }
|
||||
}
|
||||
|
||||
private fun getGroupMemberships(recipient: Recipient, groups: List<Recipient>): List<Recipient> {
|
||||
return groups.filter { it.participants.contains(recipient) }
|
||||
}
|
||||
|
||||
data class ResolvedIdentity(val recipient: Recipient, val identityRecord: Optional<IdentityRecord>)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
|
||||
/**
|
||||
* Screen state for SafetyNumberBottomSheetFragment and SafetyNumberReviewConnectionsFragment
|
||||
*/
|
||||
data class SafetyNumberBottomSheetState(
|
||||
val untrustedRecipientCount: Int,
|
||||
val hasLargeNumberOfUntrustedRecipients: Boolean,
|
||||
val destinationToRecipientMap: Map<SafetyNumberBucket, List<SafetyNumberRecipient>> = emptyMap(),
|
||||
val loadState: LoadState = LoadState.INIT
|
||||
) {
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return !hasLargeNumberOfUntrustedRecipients && destinationToRecipientMap.values.flatten().isEmpty() && loadState == LoadState.READY
|
||||
}
|
||||
|
||||
fun isCheckupComplete(): Boolean {
|
||||
return loadState == LoadState.DONE ||
|
||||
isEmpty() ||
|
||||
destinationToRecipientMap.values.flatten().all { it.identityRecord.verifiedStatus == IdentityDatabase.VerifiedStatus.VERIFIED }
|
||||
}
|
||||
|
||||
enum class LoadState {
|
||||
INIT,
|
||||
READY,
|
||||
DONE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeRepository
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.TrustAndVerifyResult
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class SafetyNumberBottomSheetViewModel(
|
||||
private val args: SafetyNumberBottomSheetArgs,
|
||||
private val repository: SafetyNumberBottomSheetRepository = SafetyNumberBottomSheetRepository(),
|
||||
private val trustAndVerifyRepository: SafetyNumberChangeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val MAX_RECIPIENTS_TO_DISPLAY = 5
|
||||
}
|
||||
|
||||
private val destinationStore = RxStore(args.destinations.map { it.asRecipientSearchKey() })
|
||||
val destinationSnapshot: List<ContactSearchKey.RecipientSearchKey>
|
||||
get() = destinationStore.state
|
||||
|
||||
private val store = RxStore(
|
||||
SafetyNumberBottomSheetState(
|
||||
untrustedRecipientCount = args.untrustedRecipients.size,
|
||||
hasLargeNumberOfUntrustedRecipients = args.untrustedRecipients.size > MAX_RECIPIENTS_TO_DISPLAY
|
||||
)
|
||||
)
|
||||
|
||||
val state: Flowable<SafetyNumberBottomSheetState> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val hasLargeNumberOfUntrustedRecipients
|
||||
get() = store.state.hasLargeNumberOfUntrustedRecipients
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
if (!store.state.hasLargeNumberOfUntrustedRecipients) {
|
||||
loadRecipients()
|
||||
}
|
||||
}
|
||||
|
||||
fun setDone() {
|
||||
store.update { it.copy(loadState = SafetyNumberBottomSheetState.LoadState.DONE) }
|
||||
}
|
||||
|
||||
fun trustAndVerify(): Single<TrustAndVerifyResult> {
|
||||
val recipients: List<SafetyNumberRecipient> = store.state.destinationToRecipientMap.values.flatten().distinct()
|
||||
return if (args.messageId != null) {
|
||||
trustAndVerifyRepository.trustOrVerifyChangedRecipientsAndResendRx(recipients, args.messageId)
|
||||
} else {
|
||||
trustAndVerifyRepository.trustOrVerifyChangedRecipientsRx(recipients).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadRecipients() {
|
||||
val bucketFlowable: Flowable<Map<SafetyNumberBucket, List<SafetyNumberRecipient>>> = destinationStore.stateFlowable.switchMap { repository.getBuckets(args.untrustedRecipients, it) }
|
||||
|
||||
disposables += store.update(bucketFlowable) { map, state ->
|
||||
state.copy(
|
||||
destinationToRecipientMap = map,
|
||||
untrustedRecipientCount = map.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun getIdentityRecord(recipientId: RecipientId): Maybe<IdentityRecord> {
|
||||
return repository.getIdentityRecord(recipientId).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun removeRecipientFromSelectedStories(recipientId: RecipientId) {
|
||||
disposables += repository.removeFromStories(recipientId, destinationStore.state).subscribe()
|
||||
}
|
||||
|
||||
fun removeDestination(destination: RecipientId) {
|
||||
destinationStore.update { list -> list.filterNot { it.recipientId == destination } }
|
||||
}
|
||||
|
||||
fun removeAll(distributionListBucket: SafetyNumberBucket.DistributionListBucket) {
|
||||
val toRemove = store.state.destinationToRecipientMap[distributionListBucket] ?: return
|
||||
disposables += repository.removeAllFromStory(toRemove.map { it.recipient.id }, distributionListBucket.distributionListId).subscribe()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val args: SafetyNumberBottomSheetArgs,
|
||||
private val trustAndVerifyRepository: SafetyNumberChangeRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(SafetyNumberBottomSheetViewModel(args = args, trustAndVerifyRepository = trustAndVerifyRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
sealed class SafetyNumberBucket {
|
||||
data class DistributionListBucket(val distributionListId: DistributionListId, val name: String) : SafetyNumberBucket()
|
||||
data class GroupBucket(val recipient: Recipient) : SafetyNumberBucket()
|
||||
object ContactsBucket : SafetyNumberBucket()
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
object SafetyNumberBucketRowItem {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(DistributionListModel::class.java, LayoutFactory(::DistributionListViewHolder, R.layout.safety_number_bucket_row_item))
|
||||
mappingAdapter.registerFactory(GroupModel::class.java, LayoutFactory(::GroupViewHolder, R.layout.safety_number_bucket_row_item))
|
||||
mappingAdapter.registerFactory(ContactsModel::class.java, LayoutFactory(::ContactsViewHolder, R.layout.safety_number_bucket_row_item))
|
||||
}
|
||||
|
||||
fun createModel(
|
||||
safetyNumberBucket: SafetyNumberBucket,
|
||||
actionItemsProvider: (SafetyNumberBucket) -> List<ActionItem>
|
||||
): MappingModel<*> {
|
||||
return when (safetyNumberBucket) {
|
||||
SafetyNumberBucket.ContactsBucket -> ContactsModel()
|
||||
is SafetyNumberBucket.DistributionListBucket -> DistributionListModel(safetyNumberBucket, actionItemsProvider)
|
||||
is SafetyNumberBucket.GroupBucket -> GroupModel(safetyNumberBucket)
|
||||
}
|
||||
}
|
||||
|
||||
private class DistributionListModel(
|
||||
val distributionListBucket: SafetyNumberBucket.DistributionListBucket,
|
||||
val actionItemsProvider: (SafetyNumberBucket) -> List<ActionItem>
|
||||
) : MappingModel<DistributionListModel> {
|
||||
override fun areItemsTheSame(newItem: DistributionListModel): Boolean {
|
||||
return distributionListBucket.distributionListId == newItem.distributionListBucket.distributionListId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: DistributionListModel): Boolean {
|
||||
return distributionListBucket == newItem.distributionListBucket
|
||||
}
|
||||
}
|
||||
|
||||
private class GroupModel(val groupBucket: SafetyNumberBucket.GroupBucket) : MappingModel<GroupModel> {
|
||||
override fun areItemsTheSame(newItem: GroupModel): Boolean {
|
||||
return groupBucket.recipient.id == newItem.groupBucket.recipient.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: GroupModel): Boolean {
|
||||
return groupBucket.recipient.hasSameContent(newItem.groupBucket.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
private class ContactsModel : MappingModel<ContactsModel> {
|
||||
override fun areItemsTheSame(newItem: ContactsModel): Boolean = true
|
||||
|
||||
override fun areContentsTheSame(newItem: ContactsModel): Boolean = true
|
||||
}
|
||||
|
||||
private class DistributionListViewHolder(itemView: View) : BaseViewHolder<DistributionListModel>(itemView) {
|
||||
override fun getTitle(model: DistributionListModel): String {
|
||||
return if (model.distributionListBucket.distributionListId == DistributionListId.MY_STORY) {
|
||||
context.getString(R.string.Recipient_my_story)
|
||||
} else {
|
||||
model.distributionListBucket.name
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindMenuListener(model: DistributionListModel, menuView: View) {
|
||||
menuView.setOnClickListener {
|
||||
SignalContextMenu.Builder(menuView, menuView.rootView as ViewGroup)
|
||||
.offsetX(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
.offsetY(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
.show(model.actionItemsProvider(model.distributionListBucket))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class GroupViewHolder(itemView: View) : BaseViewHolder<GroupModel>(itemView) {
|
||||
override fun getTitle(model: GroupModel): String {
|
||||
return model.groupBucket.recipient.getDisplayName(context)
|
||||
}
|
||||
|
||||
override fun bindMenuListener(model: GroupModel, menuView: View) {
|
||||
menuView.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
private class ContactsViewHolder(itemView: View) : BaseViewHolder<ContactsModel>(itemView) {
|
||||
override fun getTitle(model: ContactsModel): String {
|
||||
return context.getString(R.string.SafetyNumberBucketRowItem__contacts)
|
||||
}
|
||||
|
||||
override fun bindMenuListener(model: ContactsModel, menuView: View) {
|
||||
menuView.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class BaseViewHolder<T : MappingModel<*>>(itemView: View) : MappingViewHolder<T>(itemView) {
|
||||
|
||||
private val titleView: TextView = findViewById(R.id.safety_number_bucket_header)
|
||||
private val menuView: View = findViewById(R.id.safety_number_bucket_menu)
|
||||
|
||||
override fun bind(model: T) {
|
||||
titleView.text = getTitle(model)
|
||||
bindMenuListener(model, menuView)
|
||||
}
|
||||
|
||||
abstract fun getTitle(model: T): String
|
||||
abstract fun bindMenuListener(model: T, menuView: View)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Represents a Recipient who had a safety number change. Also includes information used in
|
||||
* order to determine whether the recipient should be shown on a given screen and what menu
|
||||
* options it should have.
|
||||
*/
|
||||
data class SafetyNumberRecipient(
|
||||
val recipient: Recipient,
|
||||
val identityRecord: IdentityRecord,
|
||||
val distributionListMembershipCount: Int,
|
||||
val groupMembershipCount: Int
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.thoughtcrime.securesms.safety
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import org.signal.core.util.or
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* An untrusted recipient who can be verified or removed.
|
||||
*/
|
||||
object SafetyNumberRecipientRowItem {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.safety_number_recipient_row_item))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val recipient: Recipient,
|
||||
val isVerified: Boolean,
|
||||
val distributionListMembershipCount: Int,
|
||||
val groupMembershipCount: Int,
|
||||
val getContextMenuActions: (Model) -> List<ActionItem>
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return recipient.id == newItem.recipient.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return recipient.hasSameContent(newItem.recipient) &&
|
||||
isVerified == newItem.isVerified &&
|
||||
distributionListMembershipCount == newItem.distributionListMembershipCount &&
|
||||
groupMembershipCount == newItem.groupMembershipCount
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val avatar: AvatarImageView = itemView.findViewById(R.id.safety_number_recipient_avatar)
|
||||
private val name: TextView = itemView.findViewById(R.id.safety_number_recipient_name)
|
||||
private val identifier: TextView = itemView.findViewById(R.id.safety_number_recipient_identifier)
|
||||
private val menu: View = itemView.findViewById(R.id.safety_number_recipient_menu)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
avatar.setRecipient(model.recipient)
|
||||
name.text = model.recipient.getDisplayName(context)
|
||||
|
||||
val identifierText = model.recipient.e164.or(model.recipient.username).orElse(null)
|
||||
val subLineText = when {
|
||||
model.isVerified && identifierText.isNullOrBlank() -> context.getString(R.string.SafetyNumberRecipientRowItem__verified)
|
||||
model.isVerified -> context.getString(R.string.SafetyNumberRecipientRowItem__s_dot_verified, identifierText)
|
||||
else -> identifierText
|
||||
}
|
||||
|
||||
identifier.text = subLineText
|
||||
identifier.visible = !subLineText.isNullOrBlank()
|
||||
menu.setOnClickListener {
|
||||
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
|
||||
.show(model.getContextMenuActions(model))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.thoughtcrime.securesms.safety.review
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.WrapperDialogFragment
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheetState
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheetViewModel
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBucket
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBucketRowItem
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberRecipientRowItem
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityFragment
|
||||
|
||||
/**
|
||||
* Full-screen fragment which displays the list of users who have safety number changes.
|
||||
* Consider this an extension of the bottom sheet.
|
||||
*/
|
||||
class SafetyNumberReviewConnectionsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.SafetyNumberReviewConnectionsFragment__safety_number_changes,
|
||||
layoutId = R.layout.safety_number_review_fragment
|
||||
) {
|
||||
|
||||
private val viewModel: SafetyNumberBottomSheetViewModel by viewModels(ownerProducer = {
|
||||
requireParentFragment().requireParentFragment()
|
||||
})
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
SafetyNumberBucketRowItem.register(adapter)
|
||||
SafetyNumberRecipientRowItem.register(adapter)
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
val done = requireView().findViewById<View>(R.id.done)
|
||||
done.setOnClickListener {
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
|
||||
lifecycleDisposable += viewModel.state.subscribe { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: SafetyNumberBottomSheetState): DSLConfiguration {
|
||||
return configure {
|
||||
textPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.SafetyNumberReviewConnectionsFragment__d_recipients_may_have, state.destinationToRecipientMap.values.flatten().size),
|
||||
DSLSettingsText.TextAppearanceModifier(R.style.Signal_Text_BodyMedium),
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
|
||||
state.destinationToRecipientMap.forEach { (bucket, recipients) ->
|
||||
customPref(SafetyNumberBucketRowItem.createModel(bucket, this@SafetyNumberReviewConnectionsFragment::getActionItemsForBucket))
|
||||
|
||||
recipients.forEach {
|
||||
customPref(
|
||||
SafetyNumberRecipientRowItem.Model(
|
||||
recipient = it.recipient,
|
||||
isVerified = it.identityRecord.verifiedStatus == IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
distributionListMembershipCount = it.distributionListMembershipCount,
|
||||
groupMembershipCount = it.groupMembershipCount,
|
||||
getContextMenuActions = { model ->
|
||||
val actions = mutableListOf<ActionItem>()
|
||||
|
||||
actions.add(
|
||||
ActionItem(
|
||||
iconRes = R.drawable.ic_safety_number_24,
|
||||
title = getString(R.string.SafetyNumberBottomSheetFragment__verify_safety_number),
|
||||
action = {
|
||||
lifecycleDisposable += viewModel.getIdentityRecord(model.recipient.id).subscribe { record ->
|
||||
VerifyIdentityFragment.createDialog(
|
||||
model.recipient.id,
|
||||
IdentityKeyParcelable(record.identityKey),
|
||||
false
|
||||
).show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (model.distributionListMembershipCount > 0) {
|
||||
actions.add(
|
||||
ActionItem(
|
||||
iconRes = R.drawable.ic_circle_x_24,
|
||||
title = getString(R.string.SafetyNumberBottomSheetFragment__remove_from_story),
|
||||
action = {
|
||||
viewModel.removeRecipientFromSelectedStories(model.recipient.id)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (model.distributionListMembershipCount == 0 && model.groupMembershipCount == 0) {
|
||||
actions.add(
|
||||
ActionItem(
|
||||
iconRes = R.drawable.ic_circle_x_24,
|
||||
title = getString(R.string.SafetyNumberReviewConnectionsFragment__remove),
|
||||
tintRes = R.color.signal_colorOnSurface,
|
||||
action = {
|
||||
viewModel.removeDestination(model.recipient.id)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
actions
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActionItemsForBucket(bucket: SafetyNumberBucket): List<ActionItem> {
|
||||
return when (bucket) {
|
||||
is SafetyNumberBucket.DistributionListBucket -> {
|
||||
listOf(
|
||||
ActionItem(
|
||||
iconRes = R.drawable.ic_circle_x_24,
|
||||
title = getString(R.string.SafetyNumberReviewConnectionsFragment__remove_all),
|
||||
tintRes = R.color.signal_colorOnSurface,
|
||||
action = {
|
||||
viewModel.removeAll(bucket)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
class Dialog : WrapperDialogFragment() {
|
||||
override fun getWrappedFragment(): Fragment {
|
||||
return SafetyNumberReviewConnectionsFragment()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun show(fragmentManager: FragmentManager) {
|
||||
Dialog().show(fragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,13 +32,13 @@ import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
||||
import org.thoughtcrime.securesms.stories.StoryViewerArgs
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||
@@ -211,7 +211,9 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
startActivityIfAble(Intent(requireContext(), MyStoriesActivity::class.java))
|
||||
} else if (model.data.primaryStory.messageRecord.isOutgoing && model.data.primaryStory.messageRecord.isFailed) {
|
||||
if (model.data.primaryStory.messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberChangeDialog.show(requireContext(), childFragmentManager, model.data.primaryStory.messageRecord)
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), model.data.primaryStory.messageRecord)
|
||||
.show(childFragmentManager)
|
||||
} else {
|
||||
StoryDialogs.resendStory(requireContext()) {
|
||||
lifecycleDisposable += viewModel.resend(model.data.primaryStory.messageRecord).subscribe()
|
||||
|
||||
@@ -15,10 +15,10 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
||||
import org.thoughtcrime.securesms.stories.StoryViewerArgs
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||
@@ -80,7 +80,9 @@ class MyStoriesFragment : DSLSettingsFragment(
|
||||
onClick = { it, preview ->
|
||||
if (it.distributionStory.messageRecord.isOutgoing && it.distributionStory.messageRecord.isFailed) {
|
||||
if (it.distributionStory.messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberChangeDialog.show(requireContext(), childFragmentManager, it.distributionStory.messageRecord)
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), it.distributionStory.messageRecord)
|
||||
.show(childFragmentManager)
|
||||
} else {
|
||||
StoryDialogs.resendStory(requireContext()) {
|
||||
lifecycleDisposable += viewModel.resend(it.distributionStory.messageRecord).subscribe()
|
||||
|
||||
@@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class MyStorySettingsFragment : DSLSettingsFragment(
|
||||
@@ -111,10 +110,6 @@ class MyStorySettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onToolbarNavigationClicked() {
|
||||
findListener<WrapperDialogFragment>()?.dismiss() ?: super.onToolbarNavigationClicked()
|
||||
}
|
||||
|
||||
class Dialog : WrapperDialogFragment() {
|
||||
override fun getWrappedFragment(): Fragment {
|
||||
return NavHostFragment.create(R.navigation.my_story_settings)
|
||||
|
||||
@@ -24,9 +24,9 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.MarkReadHelper
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDial
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerChild
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerParent
|
||||
@@ -67,7 +68,7 @@ class StoryGroupReplyFragment :
|
||||
StoryReplyComposer.Callback,
|
||||
EmojiKeyboardCallback,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
|
||||
SafetyNumberChangeDialog.Callback {
|
||||
SafetyNumberBottomSheet.Callbacks {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StoryGroupReplyFragment::class.java)
|
||||
@@ -311,7 +312,9 @@ class StoryGroupReplyFragment :
|
||||
}
|
||||
|
||||
if (messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberChangeDialog.show(requireContext(), childFragmentManager, messageRecord)
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), messageRecord)
|
||||
.show(childFragmentManager)
|
||||
} else if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
|
||||
@@ -370,7 +373,9 @@ class StoryGroupReplyFragment :
|
||||
if (error is UntrustedRecords.UntrustedRecordsException) {
|
||||
resendReaction = emoji
|
||||
|
||||
SafetyNumberChangeDialog.show(childFragmentManager, error.untrustedRecords)
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestination(error.untrustedRecords, ContactSearchKey.RecipientSearchKey.Story(groupRecipientId))
|
||||
.show(childFragmentManager)
|
||||
} else {
|
||||
Log.w(TAG, "Failed to send reply", error)
|
||||
val context = context
|
||||
@@ -463,14 +468,16 @@ class StoryGroupReplyFragment :
|
||||
lifecycleDisposable += StoryGroupReplySender.sendReply(requireContext(), storyId, body, mentions)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(
|
||||
onError = {
|
||||
if (it is UntrustedRecords.UntrustedRecordsException) {
|
||||
onError = { throwable ->
|
||||
if (throwable is UntrustedRecords.UntrustedRecordsException) {
|
||||
resendBody = body
|
||||
resendMentions = mentions
|
||||
|
||||
SafetyNumberChangeDialog.show(childFragmentManager, it.untrustedRecords)
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestination(throwable.untrustedRecords, ContactSearchKey.RecipientSearchKey.Story(groupRecipientId))
|
||||
.show(childFragmentManager)
|
||||
} else {
|
||||
Log.w(TAG, "Failed to send reply", it)
|
||||
Log.w(TAG, "Failed to send reply", throwable)
|
||||
val context = context
|
||||
if (context != null) {
|
||||
Toast.makeText(context, R.string.message_details_recipient__failed_to_send, Toast.LENGTH_SHORT).show()
|
||||
@@ -480,7 +487,7 @@ class StoryGroupReplyFragment :
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSendAnywayAfterSafetyNumberChange(changedRecipients: MutableList<RecipientId>) {
|
||||
override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List<ContactSearchKey.RecipientSearchKey>) {
|
||||
val resendBody = resendBody
|
||||
val resendReaction = resendReaction
|
||||
if (resendBody != null) {
|
||||
@@ -490,7 +497,7 @@ class StoryGroupReplyFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageResentAfterSafetyNumberChange() {
|
||||
override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() {
|
||||
Log.i(TAG, "Message resent")
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import android.Manifest
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.qr.kitkat.ScanListener
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.WrapperDialogFragment
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
@@ -20,6 +22,14 @@ import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
*/
|
||||
class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListener, VerifyDisplayFragment.Callback {
|
||||
|
||||
class Dialog : WrapperDialogFragment() {
|
||||
override fun getWrappedFragment(): Fragment {
|
||||
return VerifyIdentityFragment().apply {
|
||||
arguments = this@Dialog.requireArguments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_RECIPIENT = "extra.recipient.id"
|
||||
private const val EXTRA_IDENTITY = "extra.recipient.identity"
|
||||
@@ -32,11 +42,25 @@ class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListen
|
||||
verified: Boolean
|
||||
): VerifyIdentityFragment {
|
||||
return VerifyIdentityFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(EXTRA_RECIPIENT, recipientId)
|
||||
putParcelable(EXTRA_IDENTITY, remoteIdentity)
|
||||
putBoolean(EXTRA_VERIFIED, verified)
|
||||
}
|
||||
arguments = bundleOf(
|
||||
EXTRA_RECIPIENT to recipientId,
|
||||
EXTRA_IDENTITY to remoteIdentity,
|
||||
EXTRA_VERIFIED to verified
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createDialog(
|
||||
recipientId: RecipientId,
|
||||
remoteIdentity: IdentityKeyParcelable,
|
||||
verified: Boolean
|
||||
): Dialog {
|
||||
return Dialog().apply {
|
||||
arguments = bundleOf(
|
||||
EXTRA_RECIPIENT to recipientId,
|
||||
EXTRA_IDENTITY to remoteIdentity,
|
||||
EXTRA_VERIFIED to verified
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user