Profile Screen / Add show device list trust screen

This commit is contained in:
Valere 2020-01-27 09:25:58 +01:00
parent 665c577747
commit 08ae0b485a
13 changed files with 418 additions and 28 deletions

View file

@ -18,6 +18,7 @@ package im.vector.matrix.rx
import androidx.paging.PagedList import androidx.paging.PagedList
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.pushers.Pusher
@ -103,6 +104,11 @@ class RxSession(private val session: Session) {
fun liveUserCryptoDevices(userId: String): Observable<List<CryptoDeviceInfo>> { fun liveUserCryptoDevices(userId: String): Observable<List<CryptoDeviceInfo>> {
return session.getLiveCryptoDeviceInfo(userId).asObservable() return session.getLiveCryptoDeviceInfo(userId).asObservable()
} }
fun liveCrossSigningInfo(userId: String): Observable<Optional<MXCrossSigningInfo>> {
return session.getCrossSigningService().getLiveCrossSigningKeys(userId).asObservable()
.startWith(session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional())
}
} }
fun Session.rx(): RxSession { fun Session.rx(): RxSession {

View file

@ -52,6 +52,7 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.riotx.features.settings.devices.DeviceVerificationInfoBottomSheet
import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.share.IncomingShareActivity
@ -148,6 +149,8 @@ interface ScreenComponent {
fun inject(deviceVerificationInfoBottomSheet: DeviceVerificationInfoBottomSheet) fun inject(deviceVerificationInfoBottomSheet: DeviceVerificationInfoBottomSheet)
fun inject(deviceListBottomSheet: DeviceListBottomSheet)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View file

@ -0,0 +1,86 @@
/*
* 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.core.ui.list
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.themes.ThemeUtils
/**
* A generic list item.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
@EpoxyModelClass(layout = R.layout.item_generic_with_value)
abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Holder>() {
@EpoxyAttribute
var title: String? = null
@EpoxyAttribute
var value: CharSequence? = null
@EpoxyAttribute
@ColorInt
var valueColorInt: Int? = null
@EpoxyAttribute
@DrawableRes
var titleIconResourceId: Int = -1
@EpoxyAttribute
var itemClickAction: View.OnClickListener? = null
override fun bind(holder: Holder) {
holder.titleText.setTextOrHide(title)
if (titleIconResourceId != -1) {
holder.titleIcon.setImageResource(titleIconResourceId)
holder.titleIcon.isVisible = true
} else {
holder.titleIcon.isVisible = false
}
holder.valueText.setTextOrHide(value)
if (valueColorInt != null) {
holder.valueText.setTextColor(valueColorInt!!)
} else {
holder.valueText.setTextColor(ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_primary))
}
holder.view.setOnClickListener(itemClickAction?.let { DebouncedClickListener(it) })
}
class Holder : VectorEpoxyHolder() {
val titleIcon by bind<ImageView>(R.id.itemGenericWithValueTitleIcon)
val titleText by bind<TextView>(R.id.itemGenericWithValueLabelText)
val valueText by bind<TextView>(R.id.itemGenericWithValueValueText)
}
}

View file

@ -75,9 +75,9 @@ class RoomMemberProfileController @Inject constructor(
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
if (state.isRoomEncrypted) { if (state.isRoomEncrypted) {
if (state.userMXCrossSigningInfo != null) { if (state.userMXCrossSigningInfo.invoke() != null) {
// Cross signing is enabled for this user // Cross signing is enabled for this user
if (state.userMXCrossSigningInfo.isTrusted) { if (state.userMXCrossSigningInfo.invoke()?.isTrusted() == true) {
//User is trusted //User is trusted
val icon = if (state.allDevicesAreTrusted.invoke() == true) R.drawable.ic_shield_trusted val icon = if (state.allDevicesAreTrusted.invoke() == true) R.drawable.ic_shield_trusted
else R.drawable.ic_shield_warning else R.drawable.ic_shield_warning
@ -126,14 +126,6 @@ class RoomMemberProfileController @Inject constructor(
) )
} }
} else { } else {
// buildProfileAction(
// id = "learn_more",
// title = stringProvider.getString(R.string.room_profile_section_security_learn_more),
// dividerColor = dividerColor,
// editable = false,
// divider = false,
// subtitle = stringProvider.getString(R.string.room_profile_not_encrypted_subtitle)
// )
genericFooterItem { genericFooterItem {
id("verify_footer_not_encrypted") id("verify_footer_not_encrypted")
text(stringProvider.getString(R.string.room_profile_not_encrypted_subtitle)) text(stringProvider.getString(R.string.room_profile_not_encrypted_subtitle))

View file

@ -38,6 +38,7 @@ import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_matrix_profile.* import kotlinx.android.synthetic.main.fragment_matrix_profile.*
import kotlinx.android.synthetic.main.view_stub_room_member_profile_header.* import kotlinx.android.synthetic.main.view_stub_room_member_profile_header.*
@ -175,8 +176,8 @@ class RoomMemberProfileFragment @Inject constructor(
// } // }
// } // }
override fun onShowDeviceList() { override fun onShowDeviceList() = withState(viewModel) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST")
} }
override fun onShowDeviceListNoCrossSigning() { override fun onShowDeviceListNoCrossSigning() {

View file

@ -76,16 +76,6 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
return fragment.viewModelFactory.create(state) return fragment.viewModelFactory.create(state)
} }
override fun initialState(viewModelContext: ViewModelContext): RoomMemberProfileViewState? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
val args = viewModelContext.args<RoomMemberProfileArgs>()
return RoomMemberProfileViewState(
userId = args.userId,
roomId = args.roomId,
userMXCrossSigningInfo = session.getCrossSigningService().getUserCrossSigningKeys(args.userId)
)
}
} }
private val _viewEvents = PublishDataSource<RoomMemberProfileViewEvents>() private val _viewEvents = PublishDataSource<RoomMemberProfileViewEvents>()
@ -126,6 +116,14 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
.execute { .execute {
copy(allDevicesAreTrusted = it) copy(allDevicesAreTrusted = it)
} }
session.rx().liveCrossSigningInfo(initialState.userId)
.map {
it.getOrNull()
}
.execute {
copy(userMXCrossSigningInfo = it)
}
} }
} }
@ -152,7 +150,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
private fun prepareVerification(action: RoomMemberProfileAction.VerifyUser) = withState { state -> private fun prepareVerification(action: RoomMemberProfileAction.VerifyUser) = withState { state ->
// Sanity // Sanity
if (state.isRoomEncrypted) { if (state.isRoomEncrypted) {
if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted == false) { if (!state.isMine && state.userMXCrossSigningInfo.invoke()?.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)))

View file

@ -35,9 +35,9 @@ data class RoomMemberProfileViewState(
val powerLevelsContent: Async<PowerLevelsContent> = Uninitialized, val powerLevelsContent: Async<PowerLevelsContent> = Uninitialized,
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: Async<MXCrossSigningInfo?> = Uninitialized,
val allDevicesAreTrusted: Async<Boolean> = Uninitialized val allDevicesAreTrusted: Async<Boolean> = Uninitialized
) : MvRxState { ) : MvRxState {
//constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId) constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId)
} }

View file

@ -0,0 +1,87 @@
/*
* Copyright 2020 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.roommemberprofile.devices
import android.os.Bundle
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.core.utils.DimensionConverter
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
import javax.inject.Inject
class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment(), DeviceListEpoxyController.InteractionListener {
override fun getLayoutResId() = R.layout.bottom_sheet_generic_list_with_title
private val viewModel: DeviceListBottomSheetViewModel by fragmentViewModel(DeviceListBottomSheetViewModel::class)
@Inject lateinit var viewModelFactory: DeviceListBottomSheetViewModel.Factory
@Inject lateinit var dimensionConverter: DimensionConverter
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
@Inject lateinit var epoxyController: DeviceListEpoxyController
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.setPadding(0, dimensionConverter.dpToPx(16 ),0, dimensionConverter.dpToPx(16 ))
recyclerView.configureWith(
epoxyController,
showDivider = false,
hasFixedSize = false)
epoxyController.interactionListener = this
bottomSheetTitle.isVisible = false
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
super.invalidate()
}
override fun onDeviceSelected(device: CryptoDeviceInfo) {
// TODO
}
companion object {
fun newInstance(userId: String): DeviceListBottomSheet {
val args = Bundle()
args.putString(MvRx.KEY_ARG, userId)
return DeviceListBottomSheet().apply { arguments = args }
}
}
}

View file

@ -0,0 +1,49 @@
package im.vector.riotx.features.roommemberprofile.devices
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
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.CryptoDeviceInfo
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
data class DeviceListViewState(
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Loading()
) : MvRxState
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
@Assisted private val userId: String,
private val stringProvider: StringProvider,
private val session: Session) : VectorViewModel<DeviceListViewState, EmptyAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel
}
init {
session.rx().liveUserCryptoDevices(userId)
.execute {
copy(cryptoDevices = it)
}
}
override fun handle(action: EmptyAction) {}
companion object : MvRxViewModelFactory<DeviceListBottomSheetViewModel, DeviceListViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: DeviceListViewState): DeviceListBottomSheetViewModel? {
val fragment: DeviceListBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
val userId = viewModelContext.args<String>()
return fragment.viewModelFactory.create(state, userId)
}
}
}

View file

@ -0,0 +1,119 @@
package im.vector.riotx.features.roommemberprofile.devices
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemWithValue
import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Inject
class DeviceListEpoxyController @Inject constructor(private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val vectorPreferences: VectorPreferences)
: TypedEpoxyController<DeviceListViewState>() {
interface InteractionListener {
fun onDeviceSelected(device: CryptoDeviceInfo)
}
var interactionListener: InteractionListener? = null
override fun buildModels(data: DeviceListViewState?) {
if (data == null) {
return
}
when (data.cryptoDevices) {
Uninitialized -> {
}
is Loading -> {
loadingItem {
id("loading")
loadingText(stringProvider.getString(R.string.loading))
}
}
is Success -> {
val deviceList = data.cryptoDevices.invoke()
// Build top header
val allGreen = deviceList.fold(true, { prev, device ->
prev && device.isVerified
})
genericItem {
id("title")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(if (allGreen) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
title(stringProvider.getString(R.string.verification_profile_verified))
description(stringProvider.getString(R.string.verification_conclusion_ok_notice))
}
genericItem {
id("sessions")
style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.room_member_profile_sessions_section_title))
}
if (deviceList.isEmpty()) {
// Can this really happen?
genericFooterItem {
id("empty")
text(stringProvider.getString(R.string.search_no_results))
}
} else {
// Build list of device with status
deviceList.forEach { device ->
genericItemWithValue {
id(device.deviceId)
titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
title(
buildString {
append(device.displayName() ?: device.deviceId)
apply {
if (vectorPreferences.developerMode()) {
append("\n")
append(device.deviceId)
}
}
}
)
value(
stringProvider.getString(
if (device.isVerified) R.string.trusted else R.string.not_trusted
)
)
valueColorInt(
colorProvider.getColor(
if (device.isVerified) R.color.riotx_positive_accent else R.color.riotx_destructive_accent
)
)
}
}
}
}
is Fail -> {
errorWithRetryItem {
id("error")
text(stringProvider.getString(R.string.room_member_profile_failed_to_get_devices))
listener {
// TODO
}
}
}
}
}
}

View file

@ -22,6 +22,7 @@ import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
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.resources.UserPreferencesProvider
import im.vector.riotx.core.ui.list.GenericItem import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
@ -30,8 +31,7 @@ import javax.inject.Inject
class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider, class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val session: Session, private val session: Session)
private val avatarRender: AvatarRenderer)
: TypedEpoxyController<DeviceVerificationInfoBottomSheetViewState>() { : TypedEpoxyController<DeviceVerificationInfoBottomSheetViewState>() {
var callback: Callback? = null var callback: Callback? = null

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:foreground="?attr/selectableItemBackground"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:minHeight="40dp">
<ImageView
android:id="@+id/itemGenericWithValueTitleIcon"
android:layout_width="20sp"
android:layout_height="20sp"
android:src="@drawable/ic_shield_trusted"
/>
<TextView
android:id="@+id/itemGenericWithValueLabelText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="?riotx_text_primary"
android:layout_marginStart="8dp"
android:layout_marginEnd="0dp"
android:layout_weight="1"
tools:text="Label" />
<TextView
android:id="@+id/itemGenericWithValueValueText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="15sp"
android:textColor="?riotx_text_primary"
android:layout_marginStart="8dp"
android:layout_marginEnd="0dp"
tools:textColor="@color/riotx_positive_accent"
tools:text="Value" />
</LinearLayout>

View file

@ -124,4 +124,9 @@
<string name="verification_profile_verified">Verified</string> <string name="verification_profile_verified">Verified</string>
<string name="verification_profile_warning">Warning</string> <string name="verification_profile_warning">Warning</string>
<string name="room_member_profile_failed_to_get_devices">Failed to get devices</string>
<string name="room_member_profile_sessions_section_title">Sessions</string>
<string name="trusted">Trusted</string>
<string name="not_trusted">Not Trusted</string>
</resources> </resources>