Merge pull request #924 from vector-im/feature/crossing_fix_2

Feature/crossing fix 2
This commit is contained in:
Benoit Marty 2020-02-01 17:17:30 +01:00 committed by GitHub
commit 8a9bd97a88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 274 additions and 71 deletions

View File

@ -41,6 +41,9 @@ dependencies {
// Paging // Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0" implementation "androidx.paging:paging-runtime-ktx:2.1.0"
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View File

@ -30,9 +30,11 @@ import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import timber.log.Timber
class RxRoom(private val room: Room, private val session: Session) { class RxRoom(private val room: Room, private val session: Session) {
@ -40,39 +42,45 @@ class RxRoom(private val room: Room, private val session: Session) {
val summaryObservable = room.getRoomSummaryLive() val summaryObservable = room.getRoomSummaryLive()
.asObservable() .asObservable()
.startWith(room.roomSummary().toOptional()) .startWith(room.roomSummary().toOptional())
.doOnNext { Timber.d("RX: summary emitted for: ${it.getOrNull()?.roomId}") }
val memberIdsChangeObservable = summaryObservable val memberIdsChangeObservable = summaryObservable
.map { .map {
it.getOrNull()?.let { roomSummary -> it.getOrNull()?.let { roomSummary ->
if (roomSummary.isEncrypted) { if (roomSummary.isEncrypted) {
// Return the list of other users // Return the list of other users
roomSummary.otherMemberIds roomSummary.otherMemberIds + listOf(session.myUserId)
} else { } else {
// Return an empty list, the room is not encrypted // Return an empty list, the room is not encrypted
emptyList() emptyList()
} }
}.orEmpty() }.orEmpty()
}.distinctUntilChanged() }.distinctUntilChanged()
.doOnNext { Timber.d("RX: memberIds emitted. Size: ${it.size}") }
// Observe the device info of the users in the room // Observe the device info of the users in the room
val cryptoDeviceInfoObservable = memberIdsChangeObservable val cryptoDeviceInfoObservable = memberIdsChangeObservable
.switchMap { otherUserIds -> .switchMap { membersIds ->
session.getLiveCryptoDeviceInfo(otherUserIds) session.getLiveCryptoDeviceInfo(membersIds)
.asObservable() .asObservable()
.map { .map {
// If any key change, emit the userIds list // If any key change, emit the userIds list
otherUserIds membersIds
} }
.startWith(membersIds)
.doOnNext { Timber.d("RX: CryptoDeviceInfo emitted. Size: ${it.size}") }
} }
.doOnNext { Timber.d("RX: cryptoDeviceInfo emitted 2. Size: ${it.size}") }
val roomEncryptionTrustLevelObservable = cryptoDeviceInfoObservable val roomEncryptionTrustLevelObservable = cryptoDeviceInfoObservable
.map { otherUserIds -> .map { userIds ->
if (otherUserIds.isEmpty()) { if (userIds.isEmpty()) {
Optional<RoomEncryptionTrustLevel>(null) Optional<RoomEncryptionTrustLevel>(null)
} else { } else {
session.getCrossSigningService().getTrustLevelForUsers(otherUserIds).toOptional() session.getCrossSigningService().getTrustLevelForUsers(userIds).toOptional()
} }
} }
.doOnNext { Timber.d("RX: roomEncryptionTrustLevel emitted: ${it.getOrNull()?.name}") }
return Observable return Observable
.combineLatest<Optional<RoomSummary>, Optional<RoomEncryptionTrustLevel>, Optional<RoomSummary>>( .combineLatest<Optional<RoomSummary>, Optional<RoomEncryptionTrustLevel>, Optional<RoomSummary>>(
@ -84,11 +92,37 @@ class RxRoom(private val room: Room, private val session: Session) {
).toOptional() ).toOptional()
} }
) )
.doOnNext { Timber.d("RX: final room summary emitted for ${it.getOrNull()?.roomId}") }
} }
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> { fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
return room.getRoomMembersLive(queryParams).asObservable() val roomMembersObservable = room.getRoomMembersLive(queryParams).asObservable()
.startWith(room.getRoomMembers(queryParams)) .startWith(room.getRoomMembers(queryParams))
.doOnNext { Timber.d("RX: room members emitted. Size: ${it.size}") }
// TODO Do it only for room members of the room (switchMap)
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
.startWith(emptyList<CryptoDeviceInfo>())
.doOnNext { Timber.d("RX: cryptoDeviceInfo emitted. Size: ${it.size}") }
return Observable
.combineLatest<List<RoomMemberSummary>, List<CryptoDeviceInfo>, List<RoomMemberSummary>>(
roomMembersObservable,
cryptoDeviceInfoObservable,
BiFunction { summaries, _ ->
summaries.map {
if (room.isEncrypted()) {
it.copy(
// Get the trust level of a virtual room with only this user
userEncryptionTrustLevel = session.getCrossSigningService().getTrustLevelForUsers(listOf(it.userId))
)
} else {
it
}
}
}
)
.doOnNext { Timber.d("RX: final room members emitted. Size: ${it.size}") }
} }
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> { fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {

View File

@ -34,14 +34,18 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import timber.log.Timber
class RxSession(private val session: Session) { class RxSession(private val session: Session) {
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> { fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable() val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable()
.startWith(session.getRoomSummaries(queryParams)) .startWith(session.getRoomSummaries(queryParams))
.doOnNext { Timber.d("RX: summaries emitted: size: ${it.size}") }
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable() val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
.startWith(emptyList<CryptoDeviceInfo>())
.doOnNext { Timber.d("RX: crypto device info emitted: size: ${it.size}") }
return Observable return Observable
.combineLatest<List<RoomSummary>, List<CryptoDeviceInfo>, List<RoomSummary>>( .combineLatest<List<RoomSummary>, List<CryptoDeviceInfo>, List<RoomSummary>>(
@ -51,7 +55,8 @@ class RxSession(private val session: Session) {
summaries.map { summaries.map {
if (it.isEncrypted) { if (it.isEncrypted) {
it.copy( it.copy(
roomEncryptionTrustLevel = session.getCrossSigningService().getTrustLevelForUsers(it.otherMemberIds) roomEncryptionTrustLevel = session.getCrossSigningService()
.getTrustLevelForUsers(it.otherMemberIds + session.myUserId)
) )
} else { } else {
it it
@ -59,6 +64,7 @@ class RxSession(private val session: Session) {
} }
} }
) )
.doOnNext { Timber.d("RX: final summaries emitted: size: ${it.size}") }
} }
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> { fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {

View File

@ -16,12 +16,16 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
/** /**
* Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content
*/ */
data class RoomMemberSummary( data class RoomMemberSummary constructor(
val membership: Membership, val membership: Membership,
val userId: String, val userId: String,
val displayName: String? = null, val displayName: String? = null,
val avatarUrl: String? = null val avatarUrl: String? = null,
// TODO Warning: Will not be populated if not using RxRoom
val userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
) )

View File

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
interface MessageContent { interface MessageContent {
// TODO Rename to msgType
val type: String val type: String
val body: String val body: String
val relatesTo: RelationDefaultContent? val relatesTo: RelationDefaultContent?

View File

@ -659,16 +659,18 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
override fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel { override fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel {
val atLeastOneTrusted = userIds val allTrusted = userIds
.filter { it != userId } .filter { getUserCrossSigningKeys(it)?.isTrusted() == true }
.map { getUserCrossSigningKeys(it) }
.any { it?.isTrusted() == true }
return if (!atLeastOneTrusted) { val allUsersAreVerified = userIds.size == allTrusted.size
return if (allTrusted.isEmpty()) {
RoomEncryptionTrustLevel.Default RoomEncryptionTrustLevel.Default
} else { } else {
// I have verified at least one other user // If one of the verified user as an untrusted device -> warning
val allDevices = userIds.mapNotNull { // Green if all devices of all verified users are trusted -> green
// else black
val allDevices = allTrusted.mapNotNull {
cryptoStore.getUserDeviceList(it) cryptoStore.getUserDeviceList(it)
}.flatten() }.flatten()
if (getMyCrossSigningKeys() != null) { if (getMyCrossSigningKeys() != null) {
@ -676,14 +678,14 @@ internal class DefaultCrossSigningService @Inject constructor(
if (hasWarning) { if (hasWarning) {
RoomEncryptionTrustLevel.Warning RoomEncryptionTrustLevel.Warning
} else { } else {
RoomEncryptionTrustLevel.Trusted if (allUsersAreVerified) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Default
} }
} else { } else {
val hasWarningLegacy = allDevices.any { !it.isVerified } val hasWarningLegacy = allDevices.any { !it.isVerified }
if (hasWarningLegacy) { if (hasWarningLegacy) {
RoomEncryptionTrustLevel.Warning RoomEncryptionTrustLevel.Warning
} else { } else {
RoomEncryptionTrustLevel.Trusted if (allUsersAreVerified) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Default
} }
} }
} }

View File

@ -26,7 +26,8 @@ internal object RoomMemberSummaryMapper {
userId = roomMemberSummaryEntity.userId, userId = roomMemberSummaryEntity.userId,
avatarUrl = roomMemberSummaryEntity.avatarUrl, avatarUrl = roomMemberSummaryEntity.avatarUrl,
displayName = roomMemberSummaryEntity.displayName, displayName = roomMemberSummaryEntity.displayName,
membership = roomMemberSummaryEntity.membership membership = roomMemberSummaryEntity.membership,
userEncryptionTrustLevel = null
) )
} }
} }

View File

@ -52,6 +52,7 @@ internal class RoomSummaryUpdater @Inject constructor(
// TODO: maybe allow user of SDK to give that list // TODO: maybe allow user of SDK to give that list
private val PREVIEWABLE_TYPES = listOf( private val PREVIEWABLE_TYPES = listOf(
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
EventType.MESSAGE, EventType.MESSAGE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,

View File

@ -369,7 +369,8 @@ dependencies {
implementation "androidx.emoji:emoji-appcompat:1.0.0" implementation "androidx.emoji:emoji-appcompat:1.0.0"
// QR-code // QR-code
implementation 'com.google.zxing:core:3.4.0' // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
implementation 'com.google.zxing:core:3.3.3'
implementation 'me.dm7.barcodescanner:zxing:1.9.13' implementation 'me.dm7.barcodescanner:zxing:1.9.13'
// TESTS // TESTS

View File

@ -22,11 +22,13 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.crypto.util.toImageRes
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) @EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
@ -34,6 +36,7 @@ abstract class ProfileMatrixItem : VectorEpoxyModel<ProfileMatrixItem.Holder>()
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
@ -43,11 +46,13 @@ abstract class ProfileMatrixItem : VectorEpoxyModel<ProfileMatrixItem.Holder>()
holder.titleView.text = bestName holder.titleView.text = bestName
holder.subtitleView.setTextOrHide(matrixId) holder.subtitleView.setTextOrHide(matrixId)
avatarRenderer.render(matrixItem, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.matrixItemTitle) val titleView by bind<TextView>(R.id.matrixItemTitle)
val subtitleView by bind<TextView>(R.id.matrixItemSubtitle) val subtitleView by bind<TextView>(R.id.matrixItemSubtitle)
val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar) val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar)
val avatarDecorationImageView by bind<ImageView>(R.id.matrixItemAvatarDecoration)
} }
} }

View File

@ -23,5 +23,5 @@ sealed class RoomMemberProfileAction : VectorViewModelAction {
object RetryFetchingInfo: RoomMemberProfileAction() object RetryFetchingInfo: RoomMemberProfileAction()
object IgnoreUser: RoomMemberProfileAction() object IgnoreUser: RoomMemberProfileAction()
data class VerifyUser(val userId: String? = null, val roomId: String? = null): RoomMemberProfileAction() data class VerifyUser(val userId: String? = null, val roomId: String? = null, val canCrossSign: Boolean? = true): RoomMemberProfileAction()
} }

View File

@ -79,11 +79,17 @@ class RoomMemberProfileController @Inject constructor(
// Cross signing is enabled for this user // Cross signing is enabled for this user
if (state.userMXCrossSigningInfo.isTrusted()) { if (state.userMXCrossSigningInfo.isTrusted()) {
// User is trusted // User is trusted
val icon = if (state.allDevicesAreTrusted.invoke() == true) R.drawable.ic_shield_trusted val icon = if (state.allDevicesAreTrusted) {
else R.drawable.ic_shield_warning R.drawable.ic_shield_trusted
} else {
R.drawable.ic_shield_warning
}
val titleRes = if (state.allDevicesAreTrusted.invoke() == true) R.string.verification_profile_verified val titleRes = if (state.allDevicesAreTrusted) {
else R.string.verification_profile_warning R.string.verification_profile_verified
} else {
R.string.verification_profile_warning
}
buildProfileAction( buildProfileAction(
id = "learn_more", id = "learn_more",
@ -106,6 +112,15 @@ class RoomMemberProfileController @Inject constructor(
divider = false, divider = false,
action = { callback?.onTapVerify() } action = { callback?.onTapVerify() }
) )
} else {
buildProfileAction(
id = "learn_more",
title = stringProvider.getString(R.string.room_profile_section_security_learn_more),
dividerColor = dividerColor,
editable = false,
divider = false,
action = { callback?.onShowDeviceListNoCrossSigning() }
)
} }
genericFooterItem { genericFooterItem {

View File

@ -20,6 +20,8 @@ package im.vector.riotx.features.roommemberprofile
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
@ -79,8 +81,13 @@ class RoomMemberProfileFragment @Inject constructor(
memberProfileStateView.contentView = memberProfileInfoContainer memberProfileStateView.contentView = memberProfileInfoContainer
matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true) matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true)
roomMemberProfileController.callback = this roomMemberProfileController.callback = this
appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView, listOf(matrixProfileToolbarAvatarImageView, appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView,
matrixProfileToolbarTitleView)) listOf(
matrixProfileToolbarAvatarImageView,
matrixProfileToolbarTitleView,
matrixProfileDecorationToolbarAvatarImageView
)
)
matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
@ -94,9 +101,22 @@ class RoomMemberProfileFragment @Inject constructor(
is Success -> { is Success -> {
when (val action = async.invoke()) { when (val action = async.invoke()) {
is RoomMemberProfileAction.VerifyUser -> { is RoomMemberProfileAction.VerifyUser -> {
VerificationBottomSheet if (action.canCrossSign == true) {
.withArgs(roomId = null, otherUserId = action.userId!!) VerificationBottomSheet
.show(parentFragmentManager, "VERIF") .withArgs(roomId = null, otherUserId = action.userId!!)
.show(parentFragmentManager, "VERIF")
} else {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.verify_cannot_cross_sign)
.setPositiveButton(R.string.verification_profile_verify) { _, _ ->
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = action.userId!!)
.show(parentFragmentManager, "VERIF")
}
.setNegativeButton(R.string.cancel, null)
.show()
}
} }
} }
} }
@ -133,6 +153,37 @@ class RoomMemberProfileFragment @Inject constructor(
matrixProfileToolbarTitleView.text = bestName matrixProfileToolbarTitleView.text = bestName
avatarRenderer.render(userMatrixItem, memberProfileAvatarView) avatarRenderer.render(userMatrixItem, memberProfileAvatarView)
avatarRenderer.render(userMatrixItem, matrixProfileToolbarAvatarImageView) avatarRenderer.render(userMatrixItem, matrixProfileToolbarAvatarImageView)
if (state.isRoomEncrypted) {
memberProfileDecorationImageView.isVisible = true
if (state.userMXCrossSigningInfo != null) {
// Cross signing is enabled for this user
val icon = if (state.userMXCrossSigningInfo.isTrusted()) {
// User is trusted
if (state.allDevicesAreCrossSignedTrusted) {
R.drawable.ic_shield_trusted
} else {
R.drawable.ic_shield_warning
}
} else {
R.drawable.ic_shield_black
}
memberProfileDecorationImageView.setImageResource(icon)
matrixProfileDecorationToolbarAvatarImageView.setImageResource(icon)
} else {
// Legacy
if (state.allDevicesAreTrusted) {
memberProfileDecorationImageView.setImageResource(R.drawable.ic_shield_trusted)
matrixProfileDecorationToolbarAvatarImageView.setImageResource(R.drawable.ic_shield_trusted)
} else {
memberProfileDecorationImageView.setImageResource(R.drawable.ic_shield_warning)
matrixProfileDecorationToolbarAvatarImageView.setImageResource(R.drawable.ic_shield_warning)
}
}
} else {
memberProfileDecorationImageView.isVisible = false
}
} }
} }
memberProfilePowerLevelView.setTextOrHide(state.userPowerLevelString()) memberProfilePowerLevelView.setTextOrHide(state.userPowerLevelString())

View File

@ -24,6 +24,7 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
@ -85,7 +86,12 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
} }
init { init {
setState { copy(isMine = session.myUserId == this.userId) } setState {
copy(
isMine = session.myUserId == this.userId,
userMatrixItem = room?.getRoomMember(initialState.userId)?.toMatrixItem()?.let { Success(it) } ?: Uninitialized
)
}
observeIgnoredState() observeIgnoredState()
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
// Do we have a room member for this id. // Do we have a room member for this id.
@ -101,20 +107,26 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
observeRoomMemberSummary(room) observeRoomMemberSummary(room)
observeRoomSummaryAndPowerLevels(room) observeRoomSummaryAndPowerLevels(room)
} }
session.rx().liveUserCryptoDevices(initialState.userId)
.map {
it.fold(true, { prev, dev -> prev && dev.isVerified })
}
.execute {
copy(allDevicesAreTrusted = it)
}
session.rx().liveCrossSigningInfo(initialState.userId)
.execute {
copy(userMXCrossSigningInfo = it.invoke()?.getOrNull())
}
} }
session.rx().liveUserCryptoDevices(initialState.userId)
.map {
Pair(
it.fold(true, { prev, dev -> prev && dev.isVerified }),
it.fold(true, { prev, dev -> prev && (dev.trustLevel?.crossSigningVerified == true) })
)
}
.execute { it ->
copy(
allDevicesAreTrusted = it()?.first == true,
allDevicesAreCrossSignedTrusted = it()?.second == true
)
}
session.rx().liveCrossSigningInfo(initialState.userId)
.execute {
copy(userMXCrossSigningInfo = it.invoke()?.getOrNull())
}
} }
private fun observeIgnoredState() { private fun observeIgnoredState() {
@ -143,7 +155,12 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted() == false) { if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted() == false) {
// ok, let's find or create the DM room // ok, let's find or create the DM room
_actionResultLiveData.postValue( _actionResultLiveData.postValue(
LiveEvent(Success(action.copy(userId = state.userId))) LiveEvent(Success(
action.copy(
userId = state.userId,
canCrossSign = session.getCrossSigningService().canCrossSign()
)
))
) )
} }
} }

View File

@ -35,7 +35,8 @@ data class RoomMemberProfileViewState(
val userPowerLevelString: Async<String> = Uninitialized, val userPowerLevelString: Async<String> = Uninitialized,
val userMatrixItem: Async<MatrixItem> = Uninitialized, val userMatrixItem: Async<MatrixItem> = Uninitialized,
val userMXCrossSigningInfo: MXCrossSigningInfo? = null, val userMXCrossSigningInfo: MXCrossSigningInfo? = null,
val allDevicesAreTrusted: Async<Boolean> = Uninitialized val allDevicesAreTrusted: Boolean = false,
val allDevicesAreCrossSignedTrusted: Boolean = false
) : MvRxState { ) : MvRxState {
constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId) constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId)

View File

@ -23,6 +23,7 @@ import im.vector.riotx.core.epoxy.profiles.buildProfileAction
import im.vector.riotx.core.epoxy.profiles.buildProfileSection import im.vector.riotx.core.epoxy.profiles.buildProfileSection
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem
import javax.inject.Inject import javax.inject.Inject
class RoomProfileController @Inject constructor( class RoomProfileController @Inject constructor(
@ -55,13 +56,11 @@ class RoomProfileController @Inject constructor(
} else { } else {
R.string.room_profile_not_encrypted_subtitle R.string.room_profile_not_encrypted_subtitle
} }
buildProfileAction( genericFooterItem {
id = "learn_more", id("e2e info")
title = stringProvider.getString(R.string.room_profile_section_security_learn_more), centered(false)
dividerColor = dividerColor, text(stringProvider.getString(learnMoreSubtitle))
subtitle = stringProvider.getString(learnMoreSubtitle), }
action = { callback?.onLearnMoreClicked() }
)
// More // More
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))

View File

@ -62,6 +62,7 @@ class RoomMemberListController @Inject constructor(
id(roomMember.userId) id(roomMember.userId)
matrixItem(roomMember.toMatrixItem()) matrixItem(roomMember.toMatrixItem())
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
userEncryptionTrustLevel(roomMember.userEncryptionTrustLevel)
clickListener { _ -> clickListener { _ ->
callback?.onRoomMemberClicked(roomMember) callback?.onRoomMemberClicked(roomMember)
} }

View File

@ -143,7 +143,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
} else if (xSigningKeysAreTrusted) { } else if (xSigningKeysAreTrusted) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_warning) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
} else if (xSigningIsEnableInAccount) { } else if (xSigningIsEnableInAccount) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)

View File

@ -63,7 +63,7 @@ class CrossSigningEpoxyController @Inject constructor(
} else if (data.xSigningKeysAreTrusted) { } else if (data.xSigningKeysAreTrusted) {
genericItem { genericItem {
id("trusted") id("trusted")
titleIconResourceId(R.drawable.ic_shield_warning) titleIconResourceId(R.drawable.ic_shield_custom)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
} }
if (!data.isUploadingKeys) { if (!data.isUploadingKeys) {

View File

@ -120,6 +120,12 @@ class CrossSigningSettingsFragment @Inject constructor(
} }
override fun onResetCrossSigningKeys() { override fun onResetCrossSigningKeys() {
viewModel.handle(CrossSigningAction.InitializeCrossSigning()) AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_confirmation)
.setMessage(R.string.are_you_sure)
.setPositiveButton(R.string.ok) { _, _ ->
viewModel.handle(CrossSigningAction.InitializeCrossSigning())
}
.show()
} }
} }

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M12,21C12,21 21,17.2 21,11.5V4.85L12,2L3,4.85V11.5C3,17.2 12,21 12,21Z"
android:strokeLineJoin="round"
android:fillColor="#03B381"
android:fillType="evenOdd"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -63,9 +63,11 @@
android:id="@+id/matrixProfileDecorationToolbarAvatarImageView" android:id="@+id/matrixProfileDecorationToolbarAvatarImageView"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:alpha="0"
app:layout_constraintCircle="@+id/matrixProfileToolbarAvatarImageView" app:layout_constraintCircle="@+id/matrixProfileToolbarAvatarImageView"
app:layout_constraintCircleAngle="135" app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp" app:layout_constraintCircleRadius="20dp"
tools:alpha="1"
tools:ignore="MissingConstraints" tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" /> tools:src="@drawable/ic_shield_trusted" />

View File

@ -25,6 +25,16 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/matrixItemAvatarDecoration"
android:layout_width="20dp"
android:layout_height="20dp"
app:layout_constraintCircle="@+id/matrixItemAvatar"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="16dp"
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<TextView <TextView
android:id="@+id/matrixItemTitle" android:id="@+id/matrixItemTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<im.vector.riotx.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android" <im.vector.riotx.core.platform.StateView 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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/memberProfileStateView" android:id="@+id/memberProfileStateView"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -7,68 +8,94 @@
android:background="?riotx_background" android:background="?riotx_background"
android:padding="16dp"> android:padding="16dp">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/memberProfileInfoContainer" android:id="@+id/memberProfileInfoContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintBottom_toTopOf="@+id/memberProfileNameView"
app:layout_constraintTop_toTopOf="@+id/memberProfileNameView">
<ImageView <ImageView
android:id="@+id/memberProfileAvatarView" android:id="@+id/memberProfileAvatarView"
android:layout_width="128dp" android:layout_width="128dp"
android:layout_height="128dp" android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/memberProfileNameView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/memberProfileDecorationImageView"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintCircle="@+id/memberProfileAvatarView"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="64dp"
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<TextView <TextView
android:id="@+id/memberProfileNameView" android:id="@+id/memberProfileNameView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:gravity="center"
android:gravity="center_vertical"
android:textAppearance="@style/Vector.Toolbar.Title" android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/memberProfileIdView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/memberProfileAvatarView"
tools:text="@sample/matrix.json/data/displayName" /> tools:text="@sample/matrix.json/data/displayName" />
<TextView <TextView
android:id="@+id/memberProfileIdView" android:id="@+id/memberProfileIdView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:gravity="center"
android:textAppearance="@style/Vector.Toolbar.Title" android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/memberProfilePowerLevelView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/memberProfileNameView"
tools:text="@sample/matrix.json/data/mxid" /> tools:text="@sample/matrix.json/data/mxid" />
<TextView <TextView
android:id="@+id/memberProfilePowerLevelView" android:id="@+id/memberProfilePowerLevelView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:gravity="center" android:gravity="center"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBottom_toTopOf="@+id/memberProfileStatusView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/memberProfileIdView"
tools:text="Admin in Matrix" /> tools:text="Admin in Matrix" />
<TextView <TextView
android:id="@+id/memberProfileStatusView" android:id="@+id/memberProfileStatusView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:gravity="center" android:gravity="center"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/memberProfilePowerLevelView"
tools:text="Here is a profile status" tools:text="Here is a profile status"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</im.vector.riotx.core.platform.StateView> </im.vector.riotx.core.platform.StateView>

View File

@ -91,6 +91,8 @@
<string name="unignore">Unignore</string> <string name="unignore">Unignore</string>
<string name="verify_cannot_cross_sign">This session is unable to share this verification with your other sessions.\nThe verification will be saved locally and shared in a future version of the app.</string>
<string name="room_list_sharing_header_recent_rooms">Recent rooms</string> <string name="room_list_sharing_header_recent_rooms">Recent rooms</string>
<string name="room_list_sharing_header_other_rooms">Other rooms</string> <string name="room_list_sharing_header_other_rooms">Other rooms</string>