diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 28e4e79362..e5c6aa689f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -125,6 +125,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.ConversationS import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState +import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey.RecipientSearchKey import org.thoughtcrime.securesms.contactshare.Contact import org.thoughtcrime.securesms.contactshare.ContactUtil import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity @@ -327,7 +328,8 @@ class ConversationFragment : EmojiSearchFragment.Callback, ScheduleMessageTimePickerBottomSheet.ScheduleCallback, ScheduleMessageDialogCallback, - ConversationBottomSheetCallback { + ConversationBottomSheetCallback, + SafetyNumberBottomSheet.Callbacks { companion object { private val TAG = Log.tag(ConversationFragment::class.java) @@ -553,7 +555,7 @@ class ConversationFragment : motionEventRelay.setDrain(MotionEventRelayDrain()) - viewModel.updateIdentityRecords() + viewModel.updateIdentityRecordsInBackground() } override fun onPause() { @@ -712,6 +714,24 @@ class ConversationFragment : sendMessage(scheduledDate = scheduledDate) } + override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List) { + Log.d(TAG, "onSendAnywayAfterSafetyNumberChange") + viewModel + .updateIdentityRecords() + .subscribeBy( + onError = { t -> Log.w(TAG, "Error sending", t) }, + onComplete = { sendMessage() } + ) + .addTo(disposables) + } + + override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() { + Log.d(TAG, "onMessageResentAfterSafetyNumberChange") + viewModel.updateIdentityRecordsInBackground() + } + + override fun onCanceled() = Unit + //endregion private fun observeConversationThread() { @@ -859,6 +879,7 @@ class ConversationFragment : viewModel .identityRecords + .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) .subscribeBy { presentIdentityRecordsState(it) } .addTo(disposables) @@ -1574,7 +1595,8 @@ class ConversationFragment : bodyRanges = null, messageToEdit = null, quote = null, - linkPreviews = emptyList() + linkPreviews = emptyList(), + bypassPreSendSafetyNumberCheck = true ) } @@ -1590,6 +1612,7 @@ class ConversationFragment : clearCompose: Boolean = true, linkPreviews: List = linkPreviewViewModel.onSend(), preUploadResults: List = emptyList(), + bypassPreSendSafetyNumberCheck: Boolean = false, afterSendComplete: () -> Unit = {} ) { if (scheduledDate != -1L && ReenableScheduledMessagesDialogFragment.showIfNeeded(requireContext(), childFragmentManager, null, scheduledDate)) { @@ -1622,25 +1645,26 @@ class ConversationFragment : bodyRanges = bodyRanges, contacts = contacts, linkPreviews = linkPreviews, - preUploadResults = preUploadResults + preUploadResults = preUploadResults, + bypassPreSendSafetyNumberCheck = bypassPreSendSafetyNumberCheck ) disposables += send - .doOnSubscribe { - if (clearCompose) { - composeText.setText("") - inputPanel.clearQuote() - } - } .subscribeBy( onError = { t -> Log.w(TAG, "Error sending", t) when (t) { is InvalidMessageException -> toast(R.string.ConversationActivity_message_is_empty_exclamation) is RecipientFormattingException -> toast(R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation, Toast.LENGTH_LONG) + is RecentSafetyNumberChangeException -> handleRecentSafetyNumberChange(t.changedRecords) } }, onComplete = { + if (clearCompose) { + composeText.setText("") + inputPanel.clearQuote() + } + onSendComplete() afterSendComplete() } @@ -1667,6 +1691,13 @@ class ConversationFragment : inputPanel.exitEditMessageMode() } + private fun handleRecentSafetyNumberChange(changedRecords: List) { + val recipient = viewModel.recipientSnapshot ?: return + SafetyNumberBottomSheet + .forIdentityRecordsAndDestination(changedRecords, RecipientSearchKey(recipient.getId(), false)) + .show(childFragmentManager) + } + private fun toast(@StringRes toastTextId: Int, toastDuration: Int = Toast.LENGTH_SHORT) { ThreadUtil.runOnMain { if (context != null) { @@ -3617,7 +3648,7 @@ class ConversationFragment : @Subscribe(threadMode = ThreadMode.POSTING) fun onIdentityRecordUpdate(event: IdentityRecord?) { - viewModel.updateIdentityRecords() + viewModel.updateIdentityRecordsInBackground() } @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index e960073db7..0041d06bcc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -203,7 +203,8 @@ class ConversationRepository( bodyRanges: BodyRangeList?, contacts: List, linkPreviews: List, - preUploadResults: List + preUploadResults: List, + identityRecordsState: IdentityRecordsState? ): Completable { val sendCompletable = Completable.create { emitter -> if (body.isEmpty() && slideDeck?.containsMediaSlide() != true && preUploadResults.isEmpty()) { @@ -216,6 +217,11 @@ class ConversationRepository( return@create } + if (identityRecordsState != null && identityRecordsState.hasRecentSafetyNumberChange()) { + emitter.onError(RecentSafetyNumberChangeException(identityRecordsState.getRecentSafetyNumberChangeRecords())) + return@create + } + val splitMessage: MessageUtil.SplitResult = MessageUtil.getSplitMessage( applicationContext, body, @@ -369,7 +375,7 @@ class ConversationRepository( records.isVerified && !recipient.isSelf - IdentityRecordsState(isVerified, records, isGroup = groupRecord != null) + IdentityRecordsState(recipient, groupRecord, isVerified, records, isGroup = groupRecord != null) }.subscribeOn(Schedulers.io()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 78bfbad28b..84cf10849f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -126,7 +126,8 @@ class ConversationViewModel( val reminder: Observable> private val refreshIdentityRecords: Subject = PublishSubject.create() - val identityRecords: Observable + private val identityRecordsStore: RxStore = RxStore(IdentityRecordsState()) + val identityRecords: Observable = identityRecordsStore.stateFlowable.toObservable() private val _searchQuery = BehaviorSubject.createDefault("") val searchQuery: Observable = _searchQuery @@ -218,13 +219,17 @@ class ConversationViewModel( .flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) } .observeOn(AndroidSchedulers.mainThread()) - identityRecords = Observable.combineLatest( + Observable.combineLatest( refreshIdentityRecords.startWithItem(Unit).observeOn(Schedulers.io()), recipient, recipientRepository.groupRecord ) { _, r, g -> Pair(r, g) } + .subscribeOn(Schedulers.io()) .flatMapSingle { (r, g) -> repository.getIdentityRecords(r, g.orNull()) } - .distinctUntilChanged() + .subscribeBy { newState -> + identityRecordsStore.update { newState } + } + .addTo(disposables) } fun setSearchQuery(query: String?) { @@ -327,7 +332,8 @@ class ConversationViewModel( bodyRanges: BodyRangeList?, contacts: List, linkPreviews: List, - preUploadResults: List + preUploadResults: List, + bypassPreSendSafetyNumberCheck: Boolean ): Completable { return repository.sendMessage( threadId = threadId, @@ -342,7 +348,8 @@ class ConversationViewModel( bodyRanges = bodyRanges, contacts = contacts, linkPreviews = linkPreviews, - preUploadResults = preUploadResults + preUploadResults = preUploadResults, + identityRecordsState = if (bypassPreSendSafetyNumberCheck) null else identityRecordsStore.state ).observeOn(AndroidSchedulers.mainThread()) } @@ -353,10 +360,24 @@ class ConversationViewModel( } } - fun updateIdentityRecords() { + fun updateIdentityRecordsInBackground() { refreshIdentityRecords.onNext(Unit) } + fun updateIdentityRecords(): Completable { + val state: IdentityRecordsState = identityRecordsStore.state + if (state.recipient == null) { + return Completable.error(IllegalStateException("No recipient in records store")) + } + + return repository.getIdentityRecords(state.recipient, state.group) + .doOnSuccess { newState -> + identityRecordsStore.update { newState } + } + .flatMapCompletable { Completable.complete() } + .observeOn(AndroidSchedulers.mainThread()) + } + fun getTemporaryViewOnceUri(mmsMessageRecord: MmsMessageRecord): Maybe { return repository.getTemporaryViewOnceUri(mmsMessageRecord).observeOn(AndroidSchedulers.mainThread()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/IdentityRecordsState.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/IdentityRecordsState.kt index 0383491183..4d7de21e04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/IdentityRecordsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/IdentityRecordsState.kt @@ -6,14 +6,27 @@ package org.thoughtcrime.securesms.conversation.v2 import org.thoughtcrime.securesms.database.identity.IdentityRecordList +import org.thoughtcrime.securesms.database.model.GroupRecord +import org.thoughtcrime.securesms.database.model.IdentityRecord +import org.thoughtcrime.securesms.recipients.Recipient /** * Current state for all participants identity keys in a conversation excluding self. */ data class IdentityRecordsState( - val isVerified: Boolean, - val identityRecords: IdentityRecordList, - val isGroup: Boolean + val recipient: Recipient? = null, + val group: GroupRecord? = null, + val isVerified: Boolean = false, + val identityRecords: IdentityRecordList = IdentityRecordList(emptyList()), + val isGroup: Boolean = false ) { val isUnverified: Boolean = identityRecords.isUnverified + + fun hasRecentSafetyNumberChange(): Boolean { + return identityRecords.isUnverified(true) || identityRecords.isUntrusted(true) + } + + fun getRecentSafetyNumberChangeRecords(): List { + return identityRecords.unverifiedRecords + identityRecords.untrustedRecords + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/RecentSafetyNumberChangeException.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/RecentSafetyNumberChangeException.kt new file mode 100644 index 0000000000..e2bc18d558 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/RecentSafetyNumberChangeException.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.v2 + +import org.thoughtcrime.securesms.database.model.IdentityRecord + +/** + * Emitted when safety numbers changed recently before a send attempt. + */ +class RecentSafetyNumberChangeException(val changedRecords: List) : Exception()