BottomSheet UX

This commit is contained in:
Valere 2019-12-30 17:20:43 +01:00
parent 2152af8851
commit 6bf3a703df
31 changed files with 2034 additions and 153 deletions

View file

@ -39,6 +39,10 @@ interface SasVerificationService {
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction? fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>?
fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest?
/** /**
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS * Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
* @see beginKeyVerification * @see beginKeyVerification

View file

@ -0,0 +1,164 @@
/*
* 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.tasks
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration
import timber.log.Timber
import java.util.*
import javax.inject.Inject
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
data class Params(
val events: List<Event>,
val sasVerificationService: DefaultSasVerificationService,
val cryptoService: CryptoService
)
}
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
@UserId private val userId: String,
@DeviceId private val deviceId: String?,
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
companion object {
// XXX what about multi-account?
private val transactionsHandledByOtherDevice = ArrayList<String>()
}
override suspend fun execute(params: RoomVerificationUpdateTask.Params): Unit {
// TODO ignore initial sync or back pagination?
val now = System.currentTimeMillis()
val tooInThePast = now - (10 * 60 * 1000)
val fiveMinInMs = 5 * 60 * 1000
val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs
params.events.forEach { event ->
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
Timber.v("## SAS Verification live observer: received msgId: $event")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
// the message should be ignored by the receiver.
val ageLocalTs = event.ageLocalTs
if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})")
return@forEach
} else {
val eventOrigin = event.originServerTs ?: -1
if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin")
return@forEach
}
}
// decrypt if needed?
if (event.isEncrypted() && event.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
}
}
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
if (event.senderId == userId) {
// If it's send from me, we need to keep track of Requests or Start
// done from another device of mine
if (EventType.MESSAGE == event.type) {
val msgType = event.getClearContent().toModel<MessageContent>()?.type
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is requested from another device
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
}
} else if (EventType.KEY_VERIFICATION_START == event.type) {
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_READY == event.type) {
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
transactionsHandledByOtherDevice.remove(it)
}
}
return@forEach
}
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
// Ignore this event, it is directed to another of my devices
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
return@forEach
}
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_DONE -> {
params.sasVerificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
params.sasVerificationService.onRoomRequestReceived(event)
}
}
}
}
}
}

View file

@ -189,9 +189,49 @@ internal class DefaultSasVerificationService @Inject constructor(
} }
} }
fun onRoomRequestReceived(event: Event) { suspend fun onRoomRequestReceived(event: Event) {
// TODO
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
?: return
val senderId = event.senderId ?: return
if (requestInfo.toUserId != credentials.userId) {
//I should ignore this, it's not for me
Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me")
return
}
if(checkKeysAreDownloaded(senderId, requestInfo.fromDevice) == null) {
//I should ignore this, it's not for me
Timber.e("## SAS Verification device ${requestInfo.fromDevice} is not knwon")
// TODO cancel?
return
}
// Remember this request
val requestsForUser = pendingRequests[senderId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[event.senderId] = it
}
val pendingVerificationRequest = PendingVerificationRequest(
isIncoming = true,
otherUserId = senderId,//requestInfo.toUserId,
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) { private suspend fun onRoomStartRequestReceived(event: Event) {
@ -263,7 +303,7 @@ internal class DefaultSasVerificationService @Inject constructor(
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? { private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? {
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}") Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) {
Timber.v("## SAS onStartRequestReceived $startReq") Timber.v("## SAS onStartRequestReceived $startReq")
val tid = startReq.transactionID!! val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid) val existing = getExistingTransaction(otherUserId, tid)
@ -311,11 +351,11 @@ internal class DefaultSasVerificationService @Inject constructor(
} }
private suspend fun checkKeysAreDownloaded(otherUserId: String, private suspend fun checkKeysAreDownloaded(otherUserId: String,
startReq: VerificationInfoStart): MXUsersDevicesMap<MXDeviceInfo>? { fromDevice: String): MXUsersDevicesMap<MXDeviceInfo>? {
return try { return try {
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null
keys.takeIf { deviceIds.contains(startReq.fromDevice) } keys.takeIf { deviceIds.contains(fromDevice) }
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@ -333,6 +373,10 @@ internal class DefaultSasVerificationService @Inject constructor(
// TODO should we cancel? // TODO should we cancel?
return return
} }
getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let {
updateOutgoingPendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code)))
// Should we remove it from the list?
}
handleOnCancel(event.senderId!!, cancelReq) handleOnCancel(event.senderId!!, cancelReq)
} }
@ -456,6 +500,28 @@ internal class DefaultSasVerificationService @Inject constructor(
handleMacReceived(event.senderId, macReq) handleMacReceived(event.senderId, macReq)
} }
private suspend fun onRoomReadyReceived(event: Event) {
val readyReq = event.getClearContent().toModel<MessageVerificationReadyContent>()
?.copy(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) {
// ignore
Timber.e("## SAS Received invalid ready request")
// TODO should we cancel?
return
}
if(checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) {
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown")
// TODO cancel?
return
}
handleReadyReceived(event.senderId, readyReq)
}
private fun onMacReceived(event: Event) { private fun onMacReceived(event: Event) {
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!! val macReq = event.getClearContent().toModel<KeyVerificationMac>()!!
@ -487,6 +553,18 @@ internal class DefaultSasVerificationService @Inject constructor(
} }
} }
override fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>? {
synchronized(lock = pendingRequests) {
return pendingRequests[otherUser]
}
}
override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? {
synchronized(lock = pendingRequests) {
return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } }
}
}
private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? { private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? {
synchronized(txMap) { synchronized(txMap) {
return txMap[otherUser]?.values return txMap[otherUser]?.values
@ -536,7 +614,30 @@ internal class DefaultSasVerificationService @Inject constructor(
} }
} }
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) { override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
: PendingVerificationRequest {
Timber.i("## SAS Requesting verification to user: $userId in room ${roomId}")
val requestsForUser = pendingRequests[userId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[userId] = it
}
val params = requestVerificationDMTask.createParamsAndLocalEcho(
roomId = roomId,
from = credentials.deviceId ?: "",
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
to = userId,
cryptoService = cryptoService
)
val verificationRequest = PendingVerificationRequest(
isIncoming = false,
localID = params.event.eventId ?: "",
otherUserId = userId
)
requestsForUser.add(verificationRequest)
dispatchRequestAdded(verificationRequest)
requestVerificationDMTask.configureWith( requestVerificationDMTask.configureWith(
requestVerificationDMTask.createParamsAndLocalEcho( requestVerificationDMTask.createParamsAndLocalEcho(
roomId = roomId, roomId = roomId,

View file

@ -0,0 +1,39 @@
/*
* 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.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import java.util.*
/**
* Stores current pending verification requests
*/
data class PendingVerificationRequest(
val isIncoming: Boolean = false,
val localID: String = UUID.randomUUID().toString(),
val otherUserId: String,
val transactionId: String? = null,
val requestInfo: MessageVerificationRequestContent? = null,
val readyInfo: VerificationInfoReady? = null,
val cancelConclusion: CancelCode? = null,
val isSuccessful : Boolean = false
) {
val isReady: Boolean = readyInfo != null
val isSent: Boolean = transactionId != null
}

View file

@ -222,13 +222,14 @@ internal abstract class SASVerificationTransaction(
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
if (otherDeviceKey == null) { if (otherDeviceKey == null) {
Timber.e("Verification: Could not find device $keyIDNoPrefix to verify") Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
// just ignore and continue // just ignore and continue
return@forEach return@forEach
} }
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it)
if (mac != theirMac?.mac?.get(it)) { if (mac != theirMac?.mac?.get(it)) {
// WRONG! // WRONG!
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} }

View file

@ -17,38 +17,31 @@ package im.vector.matrix.android.internal.crypto.verification
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmResults import io.realm.RealmResults
import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList
internal class VerificationMessageLiveObserver @Inject constructor( internal class VerificationMessageLiveObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration, @SessionDatabase realmConfiguration: RealmConfiguration,
@UserId private val userId: String, private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
@DeviceId private val deviceId: String?,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val sasVerificationService: DefaultSasVerificationService, private val sasVerificationService: DefaultSasVerificationService,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) { ) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> { override val query = Monarchy.Query {
EventEntity.types(it, listOf( EventEntity.types(it, listOf(
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
@ -61,11 +54,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(
) )
} }
val transactionsHandledByOtherDevice = ArrayList<String>()
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) { override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
// TODO do that in a task // Should we ignore when it's an initial sync?
// TODO how to ignore when it's an initial sync?
val events = changeSet.insertions val events = changeSet.insertions
.asSequence() .asSequence()
.mapNotNull { results[it]?.asDomain() } .mapNotNull { results[it]?.asDomain() }
@ -75,102 +65,9 @@ internal class VerificationMessageLiveObserver @Inject constructor(
} }
.toList() .toList()
// TODO ignore initial sync or back pagination? roomVerificationUpdateTask.configureWith(
RoomVerificationUpdateTask.Params(events, sasVerificationService, cryptoService)
).executeBy(taskExecutor)
val now = System.currentTimeMillis()
val tooInThePast = now - (10 * 60 * 1000)
val fiveMinInMs = 5 * 60 * 1000
val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs
events.forEach { event ->
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
Timber.v("## SAS Verification live observer: received msgId: $event")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
// the message should be ignored by the receiver.
val ageLocalTs = event.ageLocalTs
if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})")
return@forEach
} else {
val eventOrigin = event.originServerTs ?: -1
if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin")
return@forEach
}
}
// decrypt if needed?
if (event.isEncrypted() && event.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
}
}
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
if (event.senderId == userId) {
// If it's send from me, we need to keep track of Requests or Start
// done from another device of mine
if (EventType.MESSAGE == event.type) {
val msgType = event.getClearContent().toModel<MessageContent>()?.type
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is requested from another device
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
}
} else if (EventType.KEY_VERIFICATION_START == event.type) {
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
transactionsHandledByOtherDevice.remove(it)
}
}
return@forEach
}
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
// Ignore this event, it is directed to another of my devices
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
return@forEach
}
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> {
sasVerificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
sasVerificationService.onRoomRequestReceived(event)
}
}
}
}
} }
} }

View file

@ -272,4 +272,27 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(SoftLogoutFragment::class) @FragmentKey(SoftLogoutFragment::class)
fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VerificationRequestFragment::class)
fun bindVerificationRequestFragment(fragment: VerificationRequestFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VerificationChooseMethodFragment::class)
fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SASVerificationCodeFragment::class)
fun bindVerificationSasCodeFragment(fragment: SASVerificationCodeFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VerificationConclusionFragment::class)
fun bindVerificationSasConclusionFragment(fragment: VerificationConclusionFragment): Fragment
} }

View file

@ -0,0 +1,58 @@
/*
* 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.BulletSpan
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import androidx.annotation.ColorInt
import me.gujun.android.span.Span
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
}
fun Spannable.tappableMatchingText(match: String, clickSpan: ClickableSpan): Spannable {
if (match.isEmpty()) return this
indexOf(match).takeIf { it != -1 }?.let { start ->
this.setSpan(clickSpan, start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return this
}
fun Span.bullet(text: CharSequence = "",
init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(BulletSpan())
init()
build()
})
}

View file

@ -0,0 +1,164 @@
/*
* 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.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import butterknife.BindView
import butterknife.OnClick
import com.airbnb.mvrx.*
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.platform.parentFragmentViewModel
import kotlinx.android.synthetic.main.fragment_bottom_sas_verification_code.*
import javax.inject.Inject
class SASVerificationCodeFragment @Inject constructor(
val viewModelFactory: SASVerificationCodeViewModel.Factory
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_bottom_sas_verification_code
@BindView(R.id.sas_emoji_grid)
lateinit var emojiGrid: ViewGroup
@BindView(R.id.sas_decimal_code)
lateinit var decimalTextView: TextView
@BindView(R.id.emoji0)
lateinit var emoji0View: ViewGroup
@BindView(R.id.emoji1)
lateinit var emoji1View: ViewGroup
@BindView(R.id.emoji2)
lateinit var emoji2View: ViewGroup
@BindView(R.id.emoji3)
lateinit var emoji3View: ViewGroup
@BindView(R.id.emoji4)
lateinit var emoji4View: ViewGroup
@BindView(R.id.emoji5)
lateinit var emoji5View: ViewGroup
@BindView(R.id.emoji6)
lateinit var emoji6View: ViewGroup
private val viewModel by fragmentViewModel(SASVerificationCodeViewModel::class)
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
override fun invalidate() = withState(viewModel) { state ->
if (state.supportsEmoji) {
decimalTextView.isVisible = false
when(val emojiDescription = state.emojiDescription) {
is Success -> {
sasLoadingProgress.isVisible = false
emojiGrid.isVisible = true
ButtonsVisibilityGroup.isVisible = true
emojiDescription.invoke().forEachIndexed { index, emojiRepresentation ->
when (index) {
0 -> {
emoji0View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji0View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
1 -> {
emoji1View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji1View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
2 -> {
emoji2View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji2View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
3 -> {
emoji3View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji3View.findViewById<TextView>(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId)
}
4 -> {
emoji4View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji4View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
5 -> {
emoji5View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji5View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
6 -> {
emoji6View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji6View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
}
}
if (state.isWaitingFromOther) {
//hide buttons
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
} else {
ButtonsVisibilityGroup.isVisible = true
sasCodeWaitingPartnerText.isVisible = false
}
}
is Fail -> {
sasLoadingProgress.isVisible = false
emojiGrid.isInvisible = true
ButtonsVisibilityGroup.isInvisible = true
//TODO?
}
else -> {
sasLoadingProgress.isVisible = true
emojiGrid.isInvisible = true
ButtonsVisibilityGroup.isInvisible = true
}
}
} else {
//Decimal
emojiGrid.isInvisible = true
decimalTextView.isVisible = true
val decimalCode = state.decimalDescription.invoke()
decimalTextView.text = decimalCode
//TODO
if (state.isWaitingFromOther) {
//hide buttons
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
} else {
ButtonsVisibilityGroup.isVisible = decimalCode != null
sasCodeWaitingPartnerText.isVisible = false
}
}
}
@OnClick(R.id.sas_request_continue_button)
fun onMatchButtonTapped() = withState(viewModel) { state ->
//UX echo
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
sharedViewModel.handle(VerificationAction.SASMatchAction(state.otherUserId, state.transactionId))
}
@OnClick(R.id.sas_request_cancel_button)
fun onDoNotMatchButtonTapped() = withState(viewModel) { state ->
//UX echo
ButtonsVisibilityGroup.isInvisible = true
sasCodeWaitingPartnerText.isVisible = true
sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(state.otherUserId, state.transactionId))
}
}

View file

@ -0,0 +1,170 @@
/*
* 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.session.crypto.sas.EmojiRepresentation
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
data class SASVerificationCodeViewState(
val transactionId: String,
val otherUserId: String,
val otherUser: MatrixItem? = null,
val supportsEmoji: Boolean = true,
val emojiDescription: Async<List<EmojiRepresentation>> = Uninitialized,
val decimalDescription: Async<String> = Uninitialized,
val isWaitingFromOther: Boolean = false
) : MvRxState
class SASVerificationCodeViewModel @AssistedInject constructor(
@Assisted initialState: SASVerificationCodeViewState,
private val session: Session
) : VectorViewModel<SASVerificationCodeViewState, EmptyAction>(initialState)
, SasVerificationService.SasVerificationListener {
init {
withState { state ->
val matrixItem = session.getUser(state.otherUserId)?.toMatrixItem()
setState {
copy(otherUser = matrixItem)
}
val sasTx = session.getSasVerificationService()
.getExistingTransaction(state.otherUserId, state.transactionId)
if (sasTx == null) {
setState {
copy(
isWaitingFromOther = false,
emojiDescription = Fail(Throwable("Unknown Transaction")),
decimalDescription = Fail(Throwable("Unknown Transaction"))
)
}
} else {
refreshStateFromTx(sasTx)
}
}
session.getSasVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
super.onCleared()
}
private fun refreshStateFromTx(sasTx: SasVerificationTransaction) {
when (sasTx.state) {
SasVerificationTxState.None,
SasVerificationTxState.SendingStart,
SasVerificationTxState.Started,
SasVerificationTxState.OnStarted,
SasVerificationTxState.SendingAccept,
SasVerificationTxState.Accepted,
SasVerificationTxState.OnAccepted,
SasVerificationTxState.SendingKey,
SasVerificationTxState.KeySent,
SasVerificationTxState.OnKeyReceived -> {
setState {
copy(
isWaitingFromOther = false,
supportsEmoji = sasTx.supportsEmoji(),
emojiDescription = Loading<List<EmojiRepresentation>>()
.takeIf { sasTx.supportsEmoji() }
?: Uninitialized,
decimalDescription = Loading<String>()
.takeIf { sasTx.supportsEmoji().not() }
?: Uninitialized
)
}
}
SasVerificationTxState.ShortCodeReady -> {
setState {
copy(
isWaitingFromOther = false,
supportsEmoji = sasTx.supportsEmoji(),
emojiDescription = if (sasTx.supportsEmoji()) Success(sasTx.getEmojiCodeRepresentation())
else Uninitialized,
decimalDescription = if (!sasTx.supportsEmoji()) Success(sasTx.getDecimalCodeRepresentation())
else Uninitialized
)
}
}
SasVerificationTxState.ShortCodeAccepted,
SasVerificationTxState.SendingMac,
SasVerificationTxState.MacSent,
SasVerificationTxState.Verifying,
SasVerificationTxState.Verified -> {
setState {
copy(isWaitingFromOther = true)
}
}
SasVerificationTxState.Cancelled,
SasVerificationTxState.OnCancelled -> {
// The fragment should not be rendered in this state,
// it should have been replaced by a conclusion fragment
setState {
copy(
isWaitingFromOther = false,
supportsEmoji = sasTx.supportsEmoji(),
emojiDescription = Fail(Throwable("Transaction Cancelled")),
decimalDescription = Fail(Throwable("Transaction Cancelled"))
)
}
}
}
}
override fun transactionCreated(tx: SasVerificationTransaction) {
transactionUpdated(tx)
}
override fun transactionUpdated(tx: SasVerificationTransaction) {
refreshStateFromTx(tx)
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: SASVerificationCodeViewState): SASVerificationCodeViewModel
}
companion object : MvRxViewModelFactory<SASVerificationCodeViewModel, SASVerificationCodeViewState> {
override fun create(viewModelContext: ViewModelContext, state: SASVerificationCodeViewState): SASVerificationCodeViewModel? {
val factory = (viewModelContext as FragmentViewModelContext).fragment<SASVerificationCodeFragment>().viewModelFactory
return factory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): SASVerificationCodeViewState? {
val args = viewModelContext.args<VerificationBottomSheet.VerificationArgs>()
return SASVerificationCodeViewState(
transactionId = args.verificationId ?: "",
otherUserId = args.otherUserId
)
}
}
override fun handle(action: EmptyAction) {
}
}

View file

@ -91,7 +91,7 @@ class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
(requireActivity() as VectorBaseActivity).notImplemented() (requireActivity() as VectorBaseActivity).notImplemented()
/* /*
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserMxItem ?: "", viewModel.otherDeviceId
?: "", object : SimpleApiCallback<MXDeviceInfo>() { ?: "", object : SimpleApiCallback<MXDeviceInfo>() {
override fun onSuccess(info: MXDeviceInfo?) { override fun onSuccess(info: MXDeviceInfo?) {
info?.let { info?.let {

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import javax.inject.Inject import javax.inject.Inject

View file

@ -0,0 +1,195 @@
/*
* 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.os.Bundle
import android.os.Parcelable
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 androidx.lifecycle.Observer
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransactionNow
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 kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_verification.*
import javax.inject.Inject
import kotlin.reflect.KClass
class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Parcelize
data class VerificationArgs(
val otherUserId: String,
val verificationId: String? = null,
val roomId: String? = null
) : Parcelable
@Inject
lateinit var verificationRequestViewModelFactory: VerificationRequestViewModel.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 onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.requestLiveData.observe(this, Observer {
it.peekContent().let { va ->
when (va) {
is Success -> {
if (va.invoke() is VerificationAction.GotItConclusion) {
dismiss()
}
}
}
}
})
}
override fun invalidate() = withState(viewModel) {
it.otherUserMxItem?.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)
}
// Did the request result in a SAS transaction?
if (it.sasTransactionState != null) {
when (it.sasTransactionState) {
SasVerificationTxState.None,
SasVerificationTxState.SendingStart,
SasVerificationTxState.Started,
SasVerificationTxState.OnStarted,
SasVerificationTxState.SendingAccept,
SasVerificationTxState.Accepted,
SasVerificationTxState.OnAccepted,
SasVerificationTxState.SendingKey,
SasVerificationTxState.KeySent,
SasVerificationTxState.OnKeyReceived,
SasVerificationTxState.ShortCodeReady,
SasVerificationTxState.ShortCodeAccepted,
SasVerificationTxState.SendingMac,
SasVerificationTxState.MacSent,
SasVerificationTxState.Verifying -> {
showFragment(SASVerificationCodeFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(
it.otherUserMxItem?.id ?: "",
it.pendingRequest?.transactionId))
})
}
SasVerificationTxState.Verified,
SasVerificationTxState.Cancelled,
SasVerificationTxState.OnCancelled -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
it.sasTransactionState == SasVerificationTxState.Verified,
it.cancelCode?.value))
})
}
}
return@withState
}
// Transaction has not yet started
if (it.pendingRequest == null || !it.pendingRequest.isReady) {
showFragment(VerificationRequestFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: ""))
})
} else if (it.pendingRequest.isReady) {
showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id
?: "", it.pendingRequest.transactionId))
})
}
super.invalidate()
}
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
// We want to animate the bottomsheet bound changes
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
}
// Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
childFragmentManager.commitTransactionNow {
replace(R.id.bottomSheetFragmentContainer,
fragmentClass.java,
bundle,
fragmentClass.simpleName
)
}
}
}
}
fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
var current = this as? View
while (current != null) {
if (current is CoordinatorLayout) return current
current = current.parent as? View
}
return null
}

View file

@ -0,0 +1,178 @@
/*
* 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 androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
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.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.utils.LiveEvent
data class VerificationBottomSheetViewState(
val otherUserMxItem: MatrixItem? = null,
val roomId: String? = null,
val pendingRequest: PendingVerificationRequest? = null,
val sasTransactionState: SasVerificationTxState? = null,
val cancelCode: CancelCode? = null
) : MvRxState
sealed class VerificationAction : VectorViewModelAction {
data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction()
data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction()
data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
object GotItConclusion : VerificationAction()
}
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
private val session: Session)
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction>(initialState),
SasVerificationService.SasVerificationListener {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
get() = _requestLiveData
init {
session.getSasVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
super.onCleared()
}
@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 args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
val userItem = session.getUser(args.otherUserId)
val sasTx = state.pendingRequest?.transactionId?.let {
session.getSasVerificationService().getExistingTransaction(args.otherUserId, it)
}
val pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId)
?.firstOrNull { it.transactionId == args.verificationId }
return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(
otherUserMxItem = userItem?.toMatrixItem(),
sasTransactionState = sasTx?.state,
pendingRequest = pr,
roomId = args.roomId)
)
}
}
override fun handle(action: VerificationAction) = withState { state ->
val otherUserId = state.otherUserMxItem?.id ?: return@withState
val roomId = state.roomId
?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId
?: return@withState
when (action) {
is VerificationAction.RequestVerificationByDM -> {
// session
setState {
copy(pendingRequest = session.getSasVerificationService().requestKeyVerificationInDMs(otherUserId, roomId, null))
}
}
is VerificationAction.StartSASVerification -> {
val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId)
?.firstOrNull { it.transactionId == action.pendingRequestTransactionId }
?: return@withState
val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
session.getSasVerificationService().beginKeyVerificationInDMs(
KeyVerificationStart.VERIF_METHOD_SAS,
transactionId = action.pendingRequestTransactionId,
roomId = roomId,
otherUserId = request.otherUserId,
otherDeviceId = otherDevice ?: "",
callback = null
)
}
is VerificationAction.SASMatchAction -> {
session.getSasVerificationService()
.getExistingTransaction(action.userID, action.sasTransactionId)
?.userHasVerifiedShortCode()
}
is VerificationAction.SASDoNotMatchAction -> {
session.getSasVerificationService()
.getExistingTransaction(action.userID, action.sasTransactionId)
?.shortCodeDoNotMatch()
}
is VerificationAction.GotItConclusion -> {
_requestLiveData.postValue(LiveEvent(Success(action)))
}
}
}
override fun transactionCreated(tx: SasVerificationTransaction) {
transactionUpdated(tx)
}
override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state ->
if (tx.transactionId == state.pendingRequest?.transactionId) {
// A SAS tx has been started following this request
setState {
copy(
sasTransactionState = tx.state,
cancelCode = tx.cancelledReason
)
}
}
}
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
verificationRequestUpdated(pr)
}
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
if (pr.localID == state.pendingRequest?.localID || state.pendingRequest?.transactionId == pr.transactionId) {
setState {
copy(pendingRequest = pr)
}
}
}
}

View file

@ -0,0 +1,69 @@
/*
* 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.text.style.ClickableSpan
import android.view.View
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import butterknife.OnClick
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.platform.parentFragmentViewModel
import im.vector.riotx.core.utils.tappableMatchingText
import kotlinx.android.synthetic.main.fragment_verification_choose_method.*
import javax.inject.Inject
class VerificationChooseMethodFragment @Inject constructor(
val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_verification_choose_method
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
private val viewModel by fragmentViewModel(VerificationChooseMethodViewModel::class)
override fun invalidate() = withState(viewModel) { state ->
if (state.QRModeAvailable) {
val cSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
}
}
val openLink = getString(R.string.verify_open_camera_link)
val descCharSequence =
getString(R.string.verify_by_scanning_description, openLink)
.toSpannable()
.tappableMatchingText(openLink, cSpan)
verifyQRDescription.text = descCharSequence
verifyQRGroup.isVisible = true
} else {
verifyQRGroup.isVisible = false
}
verifyEmojiGroup.isVisible = state.SASMOdeAvailable
}
@OnClick(R.id.verificationByEmojiButton)
fun doVerifyBySas() = withState(sharedViewModel) {
sharedViewModel.handle(VerificationAction.StartSASVerification(it.otherUserMxItem?.id ?: "", it.pendingRequest?.transactionId
?: ""))
}
}

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 com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
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.internal.crypto.model.rest.KeyVerificationStart
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
data class VerificationChooseMethodViewState(
val otherUserId: String = "",
val transactionId: String = "",
val QRModeAvailable: Boolean = false,
val SASMOdeAvailable: Boolean = false
) : MvRxState
class VerificationChooseMethodViewModel @AssistedInject constructor(
@Assisted initialState: VerificationChooseMethodViewState,
private val session: Session
) : VectorViewModel<VerificationChooseMethodViewState, EmptyAction>(initialState) {
init {
withState { state ->
val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId)?.first {
it.transactionId == state.transactionId
}
val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN) ?: false
val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS) ?: false
setState {
copy(QRModeAvailable = qrAvailable, SASMOdeAvailable = emojiAvailable)
}
}
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel
}
companion object : MvRxViewModelFactory<VerificationChooseMethodViewModel, VerificationChooseMethodViewState> {
override fun create(viewModelContext: ViewModelContext, state: VerificationChooseMethodViewState): VerificationChooseMethodViewModel? {
val fragment: VerificationChooseMethodFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.verificationChooseMethodViewModelFactory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? {
val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
return VerificationChooseMethodViewState(otherUserId = args.otherUserId, transactionId = args.verificationId ?: "")
}
}
override fun handle(action: EmptyAction) {}
}

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 android.os.Parcelable
import androidx.core.content.ContextCompat
import butterknife.OnClick
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.platform.parentFragmentViewModel
import io.noties.markwon.Markwon
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_verification_conclusion.*
import javax.inject.Inject
class VerificationConclusionFragment @Inject constructor() : VectorBaseFragment() {
@Parcelize
data class Args(
val isSuccessFull: Boolean,
val cancelReason: String?
) : Parcelable
override fun getLayoutResId() = R.layout.fragment_verification_conclusion
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
private val viewModel by fragmentViewModel(VerificationConclusionViewModel::class)
override fun invalidate() = withState(viewModel) {
when (it.conclusionState) {
ConclusionState.SUCCESS -> {
verificationConclusionTitle.text = getString(R.string.sas_verified)
verifyConclusionDescription.setTextOrHide(getString(R.string.sas_verified_successful_description))
verifyConclusionBottomDescription.text = getString(R.string.verification_green_shield)
verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_trusted))
}
ConclusionState.WARNING -> {
verificationConclusionTitle.text = getString(R.string.verification_conclusion_not_secure)
verifyConclusionDescription.setTextOrHide(null)
verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_warning))
verifyConclusionBottomDescription.text = Markwon.builder(requireContext()).build().toMarkdown(getString(R.string.verification_conclusion_compromised))
}
ConclusionState.CANCELLED -> {
// Just dismiss in this case
sharedViewModel.handle(VerificationAction.GotItConclusion)
}
}
}
@OnClick(R.id.verificationConclusionButton)
fun onButtonTapped() {
sharedViewModel.handle(VerificationAction.GotItConclusion)
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
data class SASVerificationConclusionViewState(
val conclusionState: ConclusionState = ConclusionState.CANCELLED
) : MvRxState
enum class ConclusionState {
SUCCESS,
WARNING,
CANCELLED
}
class VerificationConclusionViewModel(initialState: SASVerificationConclusionViewState)
: VectorViewModel<SASVerificationConclusionViewState, EmptyAction>(initialState) {
companion object : MvRxViewModelFactory<VerificationConclusionViewModel, SASVerificationConclusionViewState> {
override fun initialState(viewModelContext: ViewModelContext): SASVerificationConclusionViewState? {
val args = viewModelContext.args<VerificationConclusionFragment.Args>()
return when (safeValueOf(args.cancelReason)) {
CancelCode.MismatchedSas,
CancelCode.MismatchedCommitment,
CancelCode.MismatchedKeys -> {
SASVerificationConclusionViewState(ConclusionState.WARNING)
}
else -> {
SASVerificationConclusionViewState(
if (args.isSuccessFull) ConclusionState.SUCCESS
else ConclusionState.CANCELLED
)
}
}
}
}
override fun handle(action: EmptyAction) {}
}

View file

@ -0,0 +1,83 @@
/*
* 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 androidx.core.text.toSpannable
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import butterknife.OnClick
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
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 VerificationRequestFragment @Inject constructor(
val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory,
val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() {
private val viewModel by fragmentViewModel(VerificationRequestViewModel::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
}
when (state.started) {
is Loading -> {
//Hide the start button, show waiting
verificationStartButton.isInvisible = true
verificationWaitingText.isVisible = true
val otherUser = state.matrixItem.displayName ?: state.matrixItem.id
verificationWaitingText.text = getString(R.string.verification_request_waiting_for, otherUser)
.toSpannable()
.styleMatchingText(otherUser, Typeface.BOLD)
.colorizeMatchingText(otherUser, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
}
else -> {
verificationStartButton.isEnabled = true
verificationStartButton.isVisible = true
verificationWaitingText.isInvisible = true
}
}
Unit
}
@OnClick(R.id.verificationStartButton)
fun onClickOnVerificationStart() = withState(viewModel) { state ->
verificationStartButton.isEnabled = false
sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId))
}
}

View file

@ -0,0 +1,109 @@
/*
* 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.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.VectorViewModel
data class VerificationRequestViewState(
val roomId: String? = null,
val matrixItem: MatrixItem,
val started: Async<Boolean> = Success(false)
) : MvRxState
class VerificationRequestViewModel @AssistedInject constructor(
@Assisted initialState: VerificationRequestViewState,
private val session: Session
) : VectorViewModel<VerificationRequestViewState, VerificationAction>(initialState), SasVerificationService.SasVerificationListener {
@AssistedInject.Factory
interface Factory {
fun create(initialState: VerificationRequestViewState): VerificationRequestViewModel
}
init {
withState {
val pr = session.getSasVerificationService()
.getExistingVerificationRequest(it.matrixItem.id)
?.firstOrNull()
setState {
copy(
started = Success(false).takeIf { pr == null }
?: Success(true).takeIf { pr?.isReady == true }
?: Loading<Boolean>()
)
}
}
session.getSasVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
super.onCleared()
}
companion object : MvRxViewModelFactory<VerificationRequestViewModel, VerificationRequestViewState> {
override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): VerificationRequestViewModel? {
val fragment: VerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.verificationRequestViewModelFactory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? {
val otherUserId = viewModelContext.args<VerificationBottomSheet.VerificationArgs>().otherUserId
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return session.getUser(otherUserId)?.let {
VerificationRequestViewState(matrixItem = it.toMatrixItem())
}
}
}
override fun handle(action: VerificationAction) {
}
override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) {}
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
verificationRequestUpdated(pr)
}
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
if (pr.otherUserId == state.matrixItem.id) {
if (pr.isReady) {
setState {
copy(started = Success(true))
}
} else {
setState {
copy(started = Loading())
}
}
}
}
}

View file

@ -923,7 +923,7 @@ class RoomDetailFragment @Inject constructor(
} }
is Success -> { is Success -> {
when (val data = result.invoke()) { when (val data = result.invoke()) {
is RoomDetailAction.ReportContent -> { is RoomDetailAction.ReportContent -> {
when { when {
data.spam -> { data.spam -> {
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
@ -960,6 +960,22 @@ class RoomDetailFragment @Inject constructor(
} }
} }
} }
is RoomDetailAction.RequestVerification -> {
VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(data.userId, roomId = roomDetailArgs.roomId))
}
// setArguments()
}.show(parentFragmentManager, "REQ")
}
is RoomDetailAction.AcceptVerificationRequest -> {
VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
data.otherUserId, data.transactionId, roomId = roomDetailArgs.roomId))
}
}.show(parentFragmentManager, "REQ")
}
} }
} }
} }
@ -1114,7 +1130,8 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onAvatarClicked(informationData: MessageInformationData) { override fun onAvatarClicked(informationData: MessageInformationData) {
vectorBaseActivity.notImplemented("Click on user avatar") //vectorBaseActivity.notImplemented("Click on user avatar")
roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId))
} }
override fun onMemberNameClicked(informationData: MessageInformationData) { override fun onMemberNameClicked(informationData: MessageInformationData) {

View file

@ -49,7 +49,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.R import im.vector.riotx.R
@ -184,8 +183,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
} }
} }
@ -796,20 +796,27 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
session.getSasVerificationService().beginKeyVerificationInDMs( session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId,room.roomId,
KeyVerificationStart.VERIF_METHOD_SAS, action.transactionId)
action.transactionId, _requestLiveData.postValue(LiveEvent(Success(action)))
room.roomId, // session.getSasVerificationService().beginKeyVerificationInDMs(
action.otherUserId, // KeyVerificationStart.VERIF_METHOD_SAS,
action.otherdDeviceId, // action.transactionId,
null // room.roomId,
) // action.otherUserMxItem,
// action.otherdDeviceId,
// null
// )
} }
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) { private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
Timber.e("TODO implement $action") Timber.e("TODO implement $action")
} }
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
_requestLiveData.postValue(LiveEvent(Success(action)))
}
private fun observeSyncState() { private fun observeSyncState() {
session.rx() session.rx()
.liveSyncState() .liveSyncState()

View file

@ -151,18 +151,18 @@ class MessageItemFactory @Inject constructor(
return VerificationRequestItem_() return VerificationRequestItem_()
.attributes( .attributes(
VerificationRequestItem.Attributes( VerificationRequestItem.Attributes(
otherUserId, otherUserId = otherUserId,
otherUserName.toString(), otherUserName = otherUserName.toString(),
messageContent.fromDevice, fromDevide = messageContent.fromDevice,
informationData.eventId, referenceId = informationData.eventId,
informationData, informationData = informationData,
attributes.avatarRenderer, avatarRenderer = attributes.avatarRenderer,
attributes.colorProvider, colorProvider = attributes.colorProvider,
attributes.itemLongClickListener, itemLongClickListener = attributes.itemLongClickListener,
attributes.itemClickListener, itemClickListener = attributes.itemClickListener,
attributes.reactionPillCallback, reactionPillCallback = attributes.reactionPillCallback,
attributes.readReceiptsCallback, readReceiptsCallback = attributes.readReceiptsCallback,
attributes.emojiTypeFace emojiTypeFace = attributes.emojiTypeFace
) )
) )
.callback(callback) .callback(callback)

View file

@ -70,6 +70,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC -> { EventType.KEY_VERIFICATION_MAC -> {
// These events are filtered from timeline in normal case // These events are filtered from timeline in normal case
// Only visible in developer mode // Only visible in developer mode

View file

@ -52,6 +52,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.REDACTION -> formatDebug(timelineEvent.root) EventType.REDACTION -> formatDebug(timelineEvent.root)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")

View file

@ -50,7 +50,8 @@ object TimelineDisplayableEvents {
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_KEY EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY
) )
} }

View file

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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="match_parent"
android:background="?android:colorBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/sas_emoji_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_horizontal_margin"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/verify_by_emoji_title"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/sas_emoji_description_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:text="@string/verify_user_sas_emoji_help_text"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_emoji_description" />
<TextView
android:id="@+id/sas_decimal_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="?riotx_text_primary"
android:textSize="28sp"
android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid"
tools:text="1234-4320-3905"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/sasLoadingProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid"
/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/sas_emoji_grid"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:visibility="invisible"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_description_2"
tools:visibility="visible">
<include
android:id="@+id/emoji0"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji1"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji2"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji3"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji4"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji5"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji6"
layout="@layout/item_emoji_verif" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/sas_emoji_grid_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:constraint_referenced_ids="emoji0,emoji1,emoji2,emoji3,emoji4,emoji5,emoji6"
app:flow_horizontalBias="0.5"
app:flow_horizontalGap="16dp"
app:flow_horizontalStyle="packed"
app:flow_verticalBias="0"
app:flow_verticalGap="8dp"
app:flow_wrapMode="chain"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/sas_request_continue_button"
style="@style/VectorButtonStylePositive"
android:layout_width="0dp"
android:layout_margin="@dimen/layout_vertical_margin"
android:minWidth="160dp"
android:text="@string/verification_sas_match"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/centerGuideLine"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid"
tools:text="A very long translation thats too big" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/centerGuideLine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<com.google.android.material.button.MaterialButton
android:id="@+id/sas_request_cancel_button"
style="@style/VectorButtonStyleDestructive"
android:layout_width="0dp"
android:layout_margin="@dimen/layout_vertical_margin"
android:text="@string/verification_sas_do_not_match"
app:layout_constraintEnd_toStartOf="@+id/centerGuideLine"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bottomBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:barrierMargin="8dp"
app:constraint_referenced_ids="sas_request_cancel_button,sas_request_continue_button" />
<TextView
android:id="@+id/sasCodeWaitingPartnerText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?attr/vctr_notice_secondary"
android:textSize="15sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
app:layout_constraintTop_toBottomOf="@id/sas_emoji_grid"
android:text="@string/sas_waiting_for_partner"
tools:visibility="visible" />
<TextView
android:id="@+id/sasEmojiSecurityTip"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:gravity="center"
android:text="@string/verify_user_sas_emoji_security_tip"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottomBarrier" />
<androidx.constraintlayout.widget.Group
android:id="@+id/ButtonsVisibilityGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="sas_request_continue_button,sas_request_cancel_button"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,117 @@
<?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/verify_by_emoji_title"
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="gone"
tools: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,65 @@
<?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">
<TextView
android:id="@+id/verificationConclusionTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/sas_verified"
tools:text="@string/sas_verified"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/verifyConclusionDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="@string/sas_verified_successful_description"
android:text="@string/sas_verified_successful_description"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toBottomOf="@id/verificationConclusionTitle"/>
<ImageView
android:id="@+id/verifyConclusionImageView"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
tools:background="@drawable/ic_shield_trusted"
android:background="@drawable/ic_shield_trusted"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/verifyConclusionDescription" />
<TextView
android:id="@+id/verifyConclusionBottomDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="@string/verification_green_shield"
android:text="@string/verification_green_shield"
android:textColor="?riotx_text_secondary"
app:layout_constraintTop_toBottomOf="@id/verifyConclusionImageView"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/verificationConclusionButton"
style="@style/VectorButtonStylePositive"
android:layout_width="match_parent"
android:layout_marginTop="16dp"
android:text="@string/sas_got_it"
app:layout_constraintTop_toBottomOf="@id/verifyConclusionBottomDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1500,7 +1500,7 @@ Why choose Riot.im?
<string name="sas_verified">Verified!</string> <string name="sas_verified">Verified!</string>
<string name="sas_verified_successful">You\'ve successfully verified this device.</string> <string name="sas_verified_successful">You\'ve successfully verified this device.</string>
<string name="sas_verified_successful_description">Secure messages with this user are end-to-end encrypted and not able to be read by third parties.</string> <string name="sas_verified_successful_description">Messages with this user in this room are end-to-end encrypted and cant be read by third parties.</string>
<string name="sas_got_it">Got it</string> <string name="sas_got_it">Got it</string>
<string name="sas_verifying_keys">Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification.</string> <string name="sas_verifying_keys">Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification.</string>

View file

@ -5,15 +5,25 @@
<string name="command_description_verify">Request to verify the given userID</string> <string name="command_description_verify">Request to verify the given userID</string>
<string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string> <string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string>
<string name="notification_initial_sync">Initial Sync…</string> <string name="notification_initial_sync">Initial Sync…</string>
<string name="sent_a_file">File</string>
<string name="sent_an_audio_file">Audio</string>
<string name="sent_an_image">Image.</string>
<string name="sent_a_video">Video.</string>
<string name="verification_conclusion_warning">Untrusted sign in</string> <string name="verification_conclusion_warning">Untrusted sign in</string>
<string name="verification_sas_match">They match</string>
<string name="verification_sas_do_not_match">They dont match</string>
<string name="verify_user_sas_emoji_help_text">Verify this user by confirming the following unique emoji appear on their screen, in the same order."</string>
<string name="verify_user_sas_emoji_security_tip">For ultimate security, use another trusted means of communication or do this in person.</string>
<string name="verification_green_shield">Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure.</string>
<string name="verification_conclusion_not_secure">Not secure</string>
<string name="verification_conclusion_compromised">One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user youre verifying is connected to\n - Yours, or the other users internet connection\n - Yours, or the other users device
</string>
<string name="sent_a_video">Video.</string>
<string name="sent_an_image">Image.</string>
<string name="sent_an_audio_file">Audio</string>
<string name="sent_a_file">File</string>
<string name="verification_request_waiting">Waiting…</string> <string name="verification_request_waiting">Waiting…</string>
<string name="verification_request_other_cancelled">%s cancelled</string> <string name="verification_request_other_cancelled">%s cancelled</string>
<string name="verification_request_you_cancelled">You cancelled</string> <string name="verification_request_you_cancelled">You cancelled</string>