From 38906084d133b4397684a73f15c33a86760cf4ef Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 19 Dec 2019 10:19:12 +0100 Subject: [PATCH] WIP --- build.gradle | 2 + .../crypto/sas/SasVerificationService.kt | 2 + .../api/session/events/model/EventType.kt | 1 + .../DefaultSasVerificationService.kt | 65 ++++++++-- .../PendingVerificationRequest.kt | 27 ++++ .../verification/VerificationInfoReady.kt | 38 ++++++ .../verification/VerificationInfoStart.kt | 3 + .../room/EventRelationsAggregationTask.kt | 1 + .../src/main/res/values/strings_RiotX.xml | 17 +++ vector/build.gradle | 3 + .../im/vector/riotx/core/di/FragmentModule.kt | 16 ++- .../vector/riotx/core/di/ScreenComponent.kt | 3 + .../VectorBaseBottomSheetDialogFragment.kt | 20 ++- .../vector/riotx/core/utils/SpannableUtils.kt | 37 ++++++ .../OutgoingVerificationRequestFragment.kt | 77 ++++++++++++ .../OutgoingVerificationRequestViewModel.kt | 73 +++++++++++ .../verification/VerificationBottomSheet.kt | 90 ++++++++++++++ .../VerificationBottomSheetViewModel.kt | 50 ++++++++ .../VerificationChooseMethodFragment.kt | 37 ++++++ .../home/room/detail/RoomDetailAction.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 11 +- .../home/room/detail/RoomDetailViewModel.kt | 8 +- .../format/DisplayableEventFormatter.kt | 28 +++-- .../notifications/NotifiableEventResolver.kt | 13 +- .../res/layout/bottom_sheet_verification.xml | 55 +++++++++ .../fragment_verification_choose_method.xml | 116 ++++++++++++++++++ .../layout/fragment_verification_request.xml | 73 +++++++++++ vector/src/main/res/values/strings.xml | 2 +- 28 files changed, 833 insertions(+), 37 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_verification.xml create mode 100644 vector/src/main/res/layout/fragment_verification_choose_method.xml create mode 100644 vector/src/main/res/layout/fragment_verification_request.xml diff --git a/build.gradle b/build.gradle index 29351e403f..5b663740e1 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,8 @@ allprojects { includeGroupByRegex "com\\.github\\.jaiselrahman" // And monarchy includeGroupByRegex "com\\.github\\.Zhuinden" + // And QR lib + includeGroupByRegex "com\\.github\\.kenglxn\\.QRGen" } } maven { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt index 3c3c43dbd4..42b5372aae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt @@ -59,6 +59,8 @@ interface SasVerificationService { otherDeviceId: String, callback: MatrixCallback?): String? + fun readyPendingVerificationInDMs(transactionId: String) + // fun transactionUpdated(tx: SasVerificationTransaction) interface SasVerificationListener { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 60d333ec96..1939b1f0e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -73,6 +73,7 @@ object EventType { const val KEY_VERIFICATION_MAC = "m.key.verification.mac" const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" const val KEY_VERIFICATION_DONE = "m.key.verification.done" + const val KEY_VERIFICATION_READY = "m.key.verification.ready" // Relation Events const val REACTION = "m.reaction" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index 1d50fc89fe..41925eaa86 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -75,6 +75,16 @@ internal class DefaultSasVerificationService @Inject constructor( // map [sender : [transaction]] private val txMap = HashMap>() + /** + * Map [sender: [PendingVerificationRequest]] + */ + private val incomingRequests = HashMap>() + + /** + * Map [sender: [PendingVerificationRequest]] + */ + private val outgoingRequests = HashMap>() + // Event received from the sync fun onToDeviceEvent(event: Event) { GlobalScope.launch(coroutineDispatchers.crypto) { @@ -190,8 +200,32 @@ internal class DefaultSasVerificationService @Inject constructor( } fun onRoomRequestReceived(event: Event) { - // TODO Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") + val requestInfo = event.getClearContent().toModel() + ?: return + val senderId = event.senderId ?: return + // Remember this request + val requestsForUser = incomingRequests[senderId] + ?: ArrayList().also { + incomingRequests[event.senderId] = it + } + + val pendingVerificationRequest = PendingVerificationRequest( + transactionId = event.eventId, + requestInfo = requestInfo + ) + requestsForUser.add(pendingVerificationRequest) + + /* + * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event + * to begin the verification. + * If both parties send an m.key.verification.start event, and they both specify the same verification method, + * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start + * event is ignored. + * In the case of a single user verifying two of their devices, the device ID is compared instead. + * If both parties send an m.key.verification.start event, but they specify different verification methods, + * the verification should be cancelled with a code of m.unexpected_message. + */ } private suspend fun onRoomStartRequestReceived(event: Event) { @@ -537,17 +571,29 @@ internal class DefaultSasVerificationService @Inject constructor( } override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) { + val requestsForUser = outgoingRequests[userId] + ?: ArrayList().also { + outgoingRequests[userId] = it + } + + val params = requestVerificationDMTask.createParamsAndLocalEcho( + roomId = roomId, + from = credentials.deviceId ?: "", + methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), + to = userId, + cryptoService = cryptoService + ) requestVerificationDMTask.configureWith( - requestVerificationDMTask.createParamsAndLocalEcho( - roomId = roomId, - from = credentials.deviceId ?: "", - methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), - to = userId, - cryptoService = cryptoService - ) + params ) { this.callback = object : MatrixCallback { override fun onSuccess(data: SendResponse) { + params.event.getClearContent().toModel()?.let { + requestsForUser.add(PendingVerificationRequest( + transactionId = data.eventId, + requestInfo = it + )) + } callback?.onSuccess(data.eventId) } @@ -582,6 +628,9 @@ internal class DefaultSasVerificationService @Inject constructor( } } + override fun readyPendingVerificationInDMs(transactionId: String) { + // + } /** * This string must be unique for the pair of users performing verification for the duration that the transaction is valid */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt new file mode 100644 index 0000000000..1d37bcfbfd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.verification + +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent + +/** + * Stores current pending verification requests + */ +internal data class PendingVerificationRequest( + val transactionId: String?, + val requestInfo: MessageVerificationRequestContent?, + val readyInfo: VerificationInfoReady? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt new file mode 100644 index 0000000000..3f3c45901f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.verification + +/** + * A new event type is added to the key verification framework: m.key.verification.ready, + * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. + * + * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly + * with a m.key.verification.start event instead. + */ +internal interface VerificationInfoReady : VerificationInfo { + + val transactionID: String? + + /** + * The ID of the device that sent the m.key.verification.ready message + */ + val fromDevice: String? + + /** + * An array of verification methods that the device supports + */ + val methods: List? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt index 2248a239fb..3160834d57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt @@ -17,6 +17,9 @@ package im.vector.matrix.android.internal.crypto.verification internal interface VerificationInfoStart : VerificationInfo { + /** + * An array of verification methods that the device supports + */ val method: String? /** * Alice’s device ID diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index cf7a8a9275..f72232b228 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -46,6 +46,7 @@ internal interface EventRelationsAggregationTask : Task%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. + + You + + Verify by scanning + + Ask the other user to scan this code, or %s to scan theirs + + open your camera + + Verify by emoji + If you can’t scan the code above, verify by comparing a short, unique selection of emoji. + + QR code image + + Verify %s + Waiting for %s… + For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person. diff --git a/vector/build.gradle b/vector/build.gradle index c8d474088f..10a17acf2e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -345,6 +345,9 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.0.0" + // QR codes +// implementation 'com.github.kenglxn.QRGen:javase:2.6.0' + // TESTS testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index d457581c8e..2c3061ee55 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -23,10 +23,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment -import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment -import im.vector.riotx.features.crypto.verification.SASVerificationShortCodeFragment -import im.vector.riotx.features.crypto.verification.SASVerificationStartFragment -import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragment +import im.vector.riotx.features.crypto.verification.* import im.vector.riotx.features.home.HomeDetailFragment import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.LoadingFragment @@ -272,4 +269,15 @@ interface FragmentModule { @IntoMap @FragmentKey(SoftLogoutFragment::class) fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment + + + @Binds + @IntoMap + @FragmentKey(OutgoingVerificationRequestFragment::class) + fun bindVerificationRequestFragment(fragment: OutgoingVerificationRequestFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VerificationChooseMethodFragment::class) + fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index e0b14af9d0..3ac9e7c7e9 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -25,6 +25,7 @@ import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.features.MainActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeModule import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity @@ -133,6 +134,8 @@ interface ScreenComponent { fun inject(activity: SoftLogoutActivity) + fun inject(verificationBottomSheet: VerificationBottomSheet) + fun inject(permalinkHandlerActivity: PermalinkHandlerActivity) @Component.Factory diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index b3a56f48ee..4052a259a7 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -21,17 +21,17 @@ import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout import androidx.annotation.CallSuper +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.MvRxView -import com.airbnb.mvrx.MvRxViewId +import com.airbnb.mvrx.* import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.utils.DimensionConverter +import kotlin.reflect.KClass import timber.log.Timber /** @@ -70,6 +70,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() override fun onAttach(context: Context) { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) viewModelFactory = screenComponent.viewModelFactory() + childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() super.onAttach(context) injectWith(screenComponent) } @@ -121,3 +122,16 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } } } + +inline fun , reified S : MvRxState> T.parentFragmentViewModel( + viewModelClass: KClass = VM::class, + crossinline keyFactory: () -> String = { viewModelClass.java.name } +) where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { + MvRxViewModelProvider.get( + viewModelClass.java, + S::class.java, + FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), this.parentFragment + ?: this), + keyFactory() + ).apply { subscribe(this@parentFragmentViewModel, subscriber = { postInvalidate() }) } +} diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt new file mode 100644 index 0000000000..91b5ae2ffd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.core.utils + +import android.text.Spannable +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import androidx.annotation.ColorInt + +fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable { + if (match.isEmpty()) return this + indexOf(match).takeIf { it != -1 }?.let { start -> + this.setSpan(StyleSpan(typeFace), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this +} + +fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannable { + if (match.isEmpty()) return this + indexOf(match).takeIf { it != -1 }?.let { start -> + this.setSpan(ForegroundColorSpan(color), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt new file mode 100644 index 0000000000..f652c7bfdd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.crypto.verification + +import android.graphics.Typeface +import android.os.Bundle +import androidx.core.text.toSpannable +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager +import butterknife.OnClick +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.core.utils.styleMatchingText +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.synthetic.main.fragment_verification_request.* +import javax.inject.Inject + +class OutgoingVerificationRequestFragment @Inject constructor( + val outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory, + val avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { + + private val viewModel by fragmentViewModel(OutgoingVerificationRequestViewModel::class) + + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_verification_request + + override fun invalidate() = withState(viewModel) { state -> + state.matrixItem?.let { + val styledText = getString(R.string.verification_request_alert_description, it.id) + .toSpannable() + .styleMatchingText(it.id, Typeface.BOLD) + .colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) + verificationRequestText.text = styledText + } + Unit + } + + @OnClick(R.id.verificationStartButton) + fun onClickOnVerificationStart() = withState(viewModel) { state -> + + sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.otherUserId)) + + getParentCoordinatorLayout()?.let { + TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) + } + parentFragmentManager.commitTransaction { + replace(R.id.bottomSheetFragmentContainer, + VerificationChooseMethodFragment::class.java, + Bundle().apply { putString(MvRx.KEY_ARG, state.otherUserId) }, + "REQUEST" + ) + } + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt new file mode 100644 index 0000000000..0f16b0786b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.crypto.verification + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction + + +data class VerificationRequestViewState( + val otherUserId: String = "", + val matrixItem: MatrixItem? = null, + val started: Async = Success(false) +) : MvRxState + +sealed class VerificationAction : VectorViewModelAction { + data class RequestVerificationByDM(val userID: String) : VerificationAction() +} + +class OutgoingVerificationRequestViewModel @AssistedInject constructor( + @Assisted initialState: VerificationRequestViewState, + private val session: Session +) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationRequestViewState): OutgoingVerificationRequestViewModel + } + + init { + withState { + val user = session.getUser(it.otherUserId) + setState { + copy(matrixItem = user?.toMatrixItem()) + } + } + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): OutgoingVerificationRequestViewModel? { + val fragment: OutgoingVerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.outgoingVerificationRequestViewModelFactory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? { + val userID: String = viewModelContext.args() + return VerificationRequestViewState(otherUserId = userID) + } + } + + + override fun handle(action: VerificationAction) { + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt new file mode 100644 index 0000000000..abac6c6b72 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -0,0 +1,90 @@ +package im.vector.riotx.features.crypto.verification + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.text.toSpannable +import androidx.fragment.app.Fragment +import butterknife.BindView +import butterknife.ButterKnife +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.themes.ThemeUtils +import javax.inject.Inject + +class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Inject lateinit var outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory + @Inject lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory + @Inject lateinit var avatarRenderer: AvatarRenderer + + + private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + @BindView(R.id.verificationRequestName) + lateinit var otherUserNameText: TextView + + @BindView(R.id.verificationRequestAvatar) + lateinit var otherUserAvatarImageView: ImageView + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.bottom_sheet_verification, container, false) + ButterKnife.bind(this, view) + return view + } + + override fun invalidate() = withState(viewModel) { + when (it.verificationRequestEvent) { + is Uninitialized -> { + if (childFragmentManager.findFragmentByTag("REQUEST") == null) { + //Verification not yet started, put outgoing verification + childFragmentManager.commitTransaction { + setCustomAnimations(R.anim.fade_in, R.anim.fade_out) + replace(R.id.bottomSheetFragmentContainer, + OutgoingVerificationRequestFragment::class.java, + Bundle().apply { putString(MvRx.KEY_ARG, it.userId) }, + "REQUEST" + ) + } + } + } + } + + it.otherUserId?.let { matrixItem -> + val displayName = matrixItem.displayName ?: "" + otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName) + .toSpannable() + .colorizeMatchingText(displayName, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) + + avatarRenderer.render(matrixItem, otherUserAvatarImageView) + } + + super.invalidate() + } +} + + +fun Fragment.getParentCoordinatorLayout(): CoordinatorLayout? { + var current = view?.parent as? View + while (current != null) { + if (current is CoordinatorLayout) return current + current = current.parent as? View + } + return null +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt new file mode 100644 index 0000000000..578cedc07e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -0,0 +1,50 @@ +package im.vector.riotx.features.crypto.verification + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.platform.VectorViewModel + + +data class VerificationBottomSheetViewState( + val userId: String = "", + val otherUserId: MatrixItem? = null, + val verificationRequestEvent: Async = Uninitialized +) : MvRxState + +class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState, + private val session: Session) + : VectorViewModel(initialState) { + + init { + withState { + session.getUser(it.userId).let { user -> + setState { + copy(otherUserId = user?.toMatrixItem()) + } + } + } + } + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel + } + + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { + val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + val userId: String = viewModelContext.args() + return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(userId)) + } + } + + override fun handle(action: VerificationAction) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt new file mode 100644 index 0000000000..ca441b8a24 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt @@ -0,0 +1,37 @@ +package im.vector.riotx.features.crypto.verification + +import android.os.Bundle +import androidx.transition.AutoTransition +import androidx.transition.ChangeBounds +import androidx.transition.TransitionManager +import butterknife.OnClick +import com.airbnb.mvrx.MvRx +import im.vector.riotx.R +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.platform.VectorBaseFragment +import javax.inject.Inject + +class VerificationChooseMethodFragment @Inject constructor() : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_verification_choose_method + +// init { +// sharedElementEnterTransition = ChangeBounds() +// sharedElementReturnTransition = ChangeBounds() +// } + + @OnClick(R.id.verificationByEmojiButton) + fun test() { //withState(viewModel) { state -> + getParentCoordinatorLayout()?.let { + TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) + } + parentFragmentManager.commitTransaction { + // setCustomAnimations(R.anim.fade_in, R.anim.fade_out) + replace(R.id.bottomSheetFragmentContainer, + OutgoingVerificationRequestFragment::class.java, + Bundle().apply { putString(MvRx.KEY_ARG, "@valere35:matrix.org") }, + "REQUEST" + ) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 5d00b09204..013c908f16 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -66,4 +66,6 @@ sealed class RoomDetailAction : VectorViewModelAction { data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction() data class DeclineVerificationRequest(val transactionId: String) : RoomDetailAction() + + data class RequestVerification(val userId: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index e983542ad2..1db153e594 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -90,6 +90,7 @@ import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.room.detail.composer.TextComposerAction @@ -431,7 +432,8 @@ class RoomDetailFragment @Inject constructor( composerLayout.sendButton.setContentDescription(getString(descriptionRes)) avatarRenderer.render( - MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), + MatrixItem.UserItem(event.root.senderId + ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), composerLayout.composerRelatedMessageAvatar ) composerLayout.expand { @@ -923,7 +925,7 @@ class RoomDetailFragment @Inject constructor( } is Success -> { when (val data = result.invoke()) { - is RoomDetailAction.ReportContent -> { + is RoomDetailAction.ReportContent -> { when { data.spam -> { AlertDialog.Builder(requireActivity()) @@ -960,6 +962,11 @@ class RoomDetailFragment @Inject constructor( } } } + is RoomDetailAction.RequestVerification -> { + VerificationBottomSheet().apply { + arguments = Bundle().apply { putString(MvRx.KEY_ARG, data.userId) } + }.show(parentFragmentManager, "REQ") + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index b0c0144d66..a8623c4cb2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -184,8 +184,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) } } @@ -398,7 +398,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro popDraft() } is ParsedCommand.VerifyUser -> { - session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null) +// + _requestLiveData.postValue(LiveEvent(Success(RoomDetailAction.RequestVerification(slashCommandResult.userId)))) +// session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) popDraft() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index ed6bc9df62..2dfe908365 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -23,13 +23,14 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import me.gujun.android.span.span import javax.inject.Inject class DisplayableEventFormatter @Inject constructor( -// private val sessionHolder: ActiveSessionHolder, + private val sessionHolder: ActiveSessionHolder, private val stringProvider: StringProvider, private val colorProvider: ColorProvider, private val noticeEventFormatter: NoticeEventFormatter @@ -41,32 +42,36 @@ class DisplayableEventFormatter @Inject constructor( return stringProvider.getString(R.string.encrypted_message) } - val senderName = timelineEvent.getDisambiguatedDisplayName() + sessionHolder.getActiveSession().myUserId + val senderName = if (sessionHolder.getActiveSession().myUserId == timelineEvent.root.senderId) + stringProvider.getString(R.string.you) + else + timelineEvent.getDisambiguatedDisplayName() when (timelineEvent.root.getClearType()) { EventType.MESSAGE -> { timelineEvent.getLastMessageContent()?.let { messageContent -> when (messageContent.type) { MessageType.MSGTYPE_VERIFICATION_REQUEST -> { - return simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.verification_request).italicSpan(), appendAuthor) } MessageType.MSGTYPE_IMAGE -> { - return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image).italicSpan(), appendAuthor) } MessageType.MSGTYPE_AUDIO -> { - return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file).italicSpan(), appendAuthor) } MessageType.MSGTYPE_VIDEO -> { - return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video).italicSpan(), appendAuthor) } MessageType.MSGTYPE_FILE -> { - return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file).italicSpan(), appendAuthor) } MessageType.MSGTYPE_TEXT -> { if (messageContent.isReply()) { // Skip reply prefix, and show important // TODO add a reply image span ? - return simpleFormat(senderName, timelineEvent.getTextEditableContent() + return simpleFormat(senderName, timelineEvent.getTextEditableContent()?.let { "↩︎ $it" } ?: messageContent.body, appendAuthor) } else { return simpleFormat(senderName, messageContent.body, appendAuthor) @@ -101,4 +106,11 @@ class DisplayableEventFormatter @Inject constructor( body } } + + private fun String.italicSpan(): CharSequence { + return span { + text = this@italicSpan + textStyle = "italic" + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index e38e7d548a..a75c23f65c 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -26,14 +26,14 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getEditedEventId -import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import timber.log.Timber -import java.util.UUID +import java.util.* import javax.inject.Inject /** @@ -43,6 +43,7 @@ import javax.inject.Inject * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. */ class NotifiableEventResolver @Inject constructor(private val stringProvider: StringProvider, + private val displayableEventFormatter: DisplayableEventFormatter, private val noticeEventFormatter: NoticeEventFormatter) { // private val eventDisplay = RiotEventDisplay(context) @@ -86,13 +87,11 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? { // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) - + val body = displayableEventFormatter.format(event, false).toString() if (room == null) { Timber.e("## Unable to resolve room for eventId [$event]") // Ok room is not known in store, but we can still display something - val body = - event.getLastMessageBody() - ?: stringProvider.getString(R.string.notification_unknown_new_event) + val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.getDisambiguatedDisplayName() @@ -125,8 +124,6 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } - val body = event.getLastMessageBody() - ?: stringProvider.getString(R.string.notification_unknown_new_event) val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.getDisambiguatedDisplayName() diff --git a/vector/src/main/res/layout/bottom_sheet_verification.xml b/vector/src/main/res/layout/bottom_sheet_verification.xml new file mode 100644 index 0000000000..7293434f0d --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_verification.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_verification_choose_method.xml b/vector/src/main/res/layout/fragment_verification_choose_method.xml new file mode 100644 index 0000000000..70411c69b6 --- /dev/null +++ b/vector/src/main/res/layout/fragment_verification_choose_method.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_verification_request.xml b/vector/src/main/res/layout/fragment_verification_request.xml new file mode 100644 index 0000000000..1ea7a1233d --- /dev/null +++ b/vector/src/main/res/layout/fragment_verification_request.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 07a2f40bbd..8c1b2b819d 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1168,7 +1168,7 @@ Your unverified device \'%s\' is requesting encryption keys. An unverified device is requesting encryption keys.\nDevice name: %1$s\nLast seen: %2$s\nIf you didn’t log in on another device, ignore this request. - Start verification + Start Verification Verify Share without verifying