This commit is contained in:
Valere 2019-12-19 10:19:12 +01:00
parent 0997d9abf4
commit 38906084d1
28 changed files with 833 additions and 37 deletions

View file

@ -34,6 +34,8 @@ allprojects {
includeGroupByRegex "com\\.github\\.jaiselrahman"
// And monarchy
includeGroupByRegex "com\\.github\\.Zhuinden"
// And QR lib
includeGroupByRegex "com\\.github\\.kenglxn\\.QRGen"
}
}
maven {

View file

@ -59,6 +59,8 @@ interface SasVerificationService {
otherDeviceId: String,
callback: MatrixCallback<String>?): String?
fun readyPendingVerificationInDMs(transactionId: String)
// fun transactionUpdated(tx: SasVerificationTransaction)
interface SasVerificationListener {

View file

@ -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"

View file

@ -75,6 +75,16 @@ internal class DefaultSasVerificationService @Inject constructor(
// map [sender : [transaction]]
private val txMap = HashMap<String, HashMap<String, VerificationTransaction>>()
/**
* Map [sender: [PendingVerificationRequest]]
*/
private val incomingRequests = HashMap<String, ArrayList<PendingVerificationRequest>>()
/**
* Map [sender: [PendingVerificationRequest]]
*/
private val outgoingRequests = HashMap<String, ArrayList<PendingVerificationRequest>>()
// 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<MessageVerificationRequestContent>()
?: return
val senderId = event.senderId ?: return
// Remember this request
val requestsForUser = incomingRequests[senderId]
?: ArrayList<PendingVerificationRequest>().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<String>?) {
val requestsForUser = outgoingRequests[userId]
?: ArrayList<PendingVerificationRequest>().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<SendResponse> {
override fun onSuccess(data: SendResponse) {
params.event.getClearContent().toModel<MessageVerificationRequestContent>()?.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
*/

View file

@ -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
)

View file

@ -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<String>?
}

View file

@ -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?
/**
* Alices device ID

View file

@ -46,6 +46,7 @@ internal interface EventRelationsAggregationTask : Task<EventRelationsAggregatio
}
enum class VerificationState {
UNKNOWN,
REQUEST,
WAITING,
CANCELED_BY_ME,

View file

@ -21,4 +21,21 @@
<string name="key_verification_request_fallback_message">%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.</string>
<!-- Sender name of a message when it is send by you, e.g. You: Hello!-->
<string name="you">You</string>
<string name="verify_by_scanning_title">Verify by scanning</string>
<!-- the %s will be replaced by verify_open_camera_link that will be clickable -->
<string name="verify_by_scanning_description">Ask the other user to scan this code, or %s to scan theirs</string>
<!-- This part is inserted in verify_by_scanning_description-->
<string name="verify_open_camera_link">open your camera</string>
<string name="verify_by_emoji_title">Verify by emoji</string>
<string name="verify_by_emoji_description">If you cant scan the code above, verify by comparing a short, unique selection of emoji.</string>
<string name="aria_qr_code_description">QR code image</string>
<string name="verification_request_alert_title">Verify %s</string>
<string name="verification_request_waiting_for">Waiting for %s…</string>
<string name="verification_request_alert_description">For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person.</string>
</resources>

View file

@ -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'

View file

@ -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
}

View file

@ -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

View file

@ -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 <T, reified VM : BaseMvRxViewModel<S>, reified S : MvRxState> T.parentFragmentViewModel(
viewModelClass: KClass<VM> = 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() }) }
}

View file

@ -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
}

View file

@ -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"
)
}
}
}

View file

@ -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<Boolean> = 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<VerificationRequestViewState, VerificationAction>(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<OutgoingVerificationRequestViewModel, VerificationRequestViewState> {
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<String>()
return VerificationRequestViewState(otherUserId = userID)
}
}
override fun handle(action: VerificationAction) {
}
}

View file

@ -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
}

View file

@ -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<TimelineEvent> = Uninitialized
) : MvRxState
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
private val session: Session)
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction>(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<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
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.
}
}

View file

@ -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"
)
}
}
}

View file

@ -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()
}

View file

@ -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")
}
}
}
}

View file

@ -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()
}

View file

@ -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"
}
}
}

View file

@ -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()

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottomSheetScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:fadeScrollbars="false"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp">
<ImageView
android:id="@+id/verificationRequestAvatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:contentDescription="@string/avatar"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/verificationRequestName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/verification_request_alert_title"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
<FrameLayout
android:id="@+id/bottomSheetFragmentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- <ImageView-->
<!-- android:id="@+id/verificationRequestAvatar"-->
<!-- android:layout_width="32dp"-->
<!-- android:layout_height="32dp"-->
<!-- android:adjustViewBounds="true"-->
<!-- android:background="@drawable/circle"-->
<!-- android:contentDescription="@string/avatar"-->
<!-- android:scaleType="centerCrop"-->
<!-- android:transitionName="bottomSheetAvatar"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:layout_constraintVertical_bias="0"-->
<!-- tools:src="@tools:sample/avatars" />-->
<!-- <TextView-->
<!-- android:id="@+id/verificationRequestName"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="16dp"-->
<!-- android:text="@string/verification_request_alert_title"-->
<!-- android:textColor="?riotx_text_primary"-->
<!-- android:textSize="20sp"-->
<!-- android:textStyle="bold"-->
<!-- android:transitionName="bottomSheetDisplayName"-->
<!-- app:layout_constraintBottom_toBottomOf="@id/verificationRequestAvatar"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toEndOf="@id/verificationRequestAvatar"-->
<!-- app:layout_constraintTop_toTopOf="@id/verificationRequestAvatar" />-->
<TextView
android:id="@+id/verificationQRTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/verify_by_scanning_title"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/verifyQRDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/verify_by_scanning_description"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toBottomOf="@id/verificationQRTitle"
tools:text="@string/verify_by_scanning_description" />
<ImageView
android:id="@+id/verifyQRImageView"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:background="?riotx_header_panel_background"
android:contentDescription="@string/aria_qr_code_description"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/verifyQRDescription" />
<TextView
android:id="@+id/verificationEmojiTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="@string/verify_by_emoji_title"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@id/verifyQRImageView"
app:layout_goneMarginTop="0dp" />
<TextView
android:id="@+id/verifyEmojiDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/verify_by_emoji_description"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toBottomOf="@id/verificationEmojiTitle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/verificationByEmojiButton"
style="@style/VectorButtonStylePositive"
android:layout_width="match_parent"
android:layout_marginTop="16dp"
android:text="@string/accept"
app:layout_constraintTop_toBottomOf="@id/verifyEmojiDescription" />
<androidx.constraintlayout.widget.Group
android:id="@+id/verifyQRGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="verifyQRDescription,verificationQRTitle,verifyQRImageView" />
<androidx.constraintlayout.widget.Group
android:id="@+id/verifyEmojiGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="verifyEmojiDescription,verificationEmojiTitle,verificationByEmojiButton" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- <ImageView-->
<!-- android:id="@+id/verificationRequestAvatar"-->
<!-- android:layout_width="32dp"-->
<!-- android:layout_height="32dp"-->
<!-- android:adjustViewBounds="true"-->
<!-- android:background="@drawable/circle"-->
<!-- android:contentDescription="@string/avatar"-->
<!-- android:scaleType="centerCrop"-->
<!-- android:transitionName="bottomSheetAvatar"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:layout_constraintVertical_bias="0"-->
<!-- tools:src="@tools:sample/avatars" />-->
<!-- <TextView-->
<!-- android:id="@+id/verificationRequestName"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="16dp"-->
<!-- android:text="@string/verification_request_alert_title"-->
<!-- android:textColor="?riotx_text_primary"-->
<!-- android:textSize="20sp"-->
<!-- android:textStyle="bold"-->
<!-- android:transitionName="bottomSheetDisplayName"-->
<!-- app:layout_constraintBottom_toBottomOf="@id/verificationRequestAvatar"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toEndOf="@id/verificationRequestAvatar"-->
<!-- app:layout_constraintTop_toTopOf="@id/verificationRequestAvatar" />-->
<TextView
android:id="@+id/verificationRequestText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/verification_request_alert_description" />
<!-- app:layout_constraintTop_toBottomOf="@id/verificationRequestAvatar"-->
<com.google.android.material.button.MaterialButton
android:id="@+id/verificationStartButton"
style="@style/VectorButtonStylePositive"
android:layout_width="match_parent"
android:layout_marginTop="16dp"
android:text="@string/start_verification"
app:layout_constraintTop_toBottomOf="@id/verificationRequestText"
tools:visibility="visible" />
<TextView
android:id="@+id/verificationWaitingText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textColor="?vctr_notice_secondary"
android:textSize="17sp"
android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/verificationStartButton"
app:layout_constraintTop_toTopOf="@id/verificationStartButton"
tools:text="@string/verification_request_waiting_for" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1168,7 +1168,7 @@
<string name="your_unverified_device_requesting">Your unverified device \'%s\' is requesting encryption keys.</string>
<string name="your_unverified_device_requesting_with_info">An unverified device is requesting encryption keys.\nDevice name: %1$s\nLast seen: %2$s\nIf you didnt log in on another device, ignore this request.</string>
<string name="start_verification">Start verification</string>
<string name="start_verification">Start Verification</string>
<!-- Keep the label as small as possible-->
<string name="start_verification_short_label">Verify</string>
<string name="share_without_verifying">Share without verifying</string>