RoomMember decoration

This commit is contained in:
Benoit Marty 2020-02-01 11:36:07 +01:00
parent 59abee10f8
commit cd606ba8a1
12 changed files with 117 additions and 15 deletions

View file

@ -30,6 +30,7 @@ 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
@ -87,8 +88,29 @@ class RxRoom(private val room: Room, private val session: Session) {
} }
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))
// TODO Do it only for room members of the room (switchMap)
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
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
}
}
}
)
} }
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> { fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {

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

@ -667,7 +667,6 @@ internal class DefaultCrossSigningService @Inject constructor(
return if (allTrusted.isEmpty()) { return if (allTrusted.isEmpty()) {
RoomEncryptionTrustLevel.Default RoomEncryptionTrustLevel.Default
} else { } else {
// If one of the verified user as an untrusted device -> warning // If one of the verified user as an untrusted device -> warning
// Green if all devices of all verified users are trusted -> green // Green if all devices of all verified users are trusted -> green
// else black // else black

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

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

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

View file

@ -20,6 +20,7 @@ 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.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 +80,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) {
@ -133,6 +139,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

@ -104,10 +104,16 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
session.rx().liveUserCryptoDevices(initialState.userId) session.rx().liveUserCryptoDevices(initialState.userId)
.map { .map {
it.fold(true, { prev, dev -> prev && dev.isVerified }) Pair(
it.fold(true, { prev, dev -> prev && dev.isVerified }),
it.fold(true, { prev, dev -> prev && (dev.trustLevel?.crossSigningVerified == true) })
)
} }
.execute { .execute { it ->
copy(allDevicesAreTrusted = it) copy(
allDevicesAreTrusted = it()?.first == true,
allDevicesAreCrossSignedTrusted = it()?.second == true
)
} }
session.rx().liveCrossSigningInfo(initialState.userId) session.rx().liveCrossSigningInfo(initialState.userId)

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

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

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

@ -29,6 +29,16 @@
app:layout_constraintVertical_chainStyle="spread_inside" 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"