Profile detailed device info + verify manually
This commit is contained in:
parent
08ae0b485a
commit
6cece03998
@ -57,6 +57,8 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
|
|||||||
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
||||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
||||||
import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment
|
import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment
|
||||||
|
import im.vector.riotx.features.roommemberprofile.devices.DeviceListFragment
|
||||||
|
import im.vector.riotx.features.roommemberprofile.devices.DeviceTrustInfoActionFragment
|
||||||
import im.vector.riotx.features.roomprofile.RoomProfileFragment
|
import im.vector.riotx.features.roomprofile.RoomProfileFragment
|
||||||
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
|
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
|
||||||
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
|
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
|
||||||
@ -319,4 +321,14 @@ interface FragmentModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(QrCodeScannerFragment::class)
|
@FragmentKey(QrCodeScannerFragment::class)
|
||||||
fun bindQrCodeScannerFragment(fragment: QrCodeScannerFragment): Fragment
|
fun bindQrCodeScannerFragment(fragment: QrCodeScannerFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(DeviceListFragment::class)
|
||||||
|
fun bindDeviceListFragment(fragment: DeviceListFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(DeviceTrustInfoActionFragment::class)
|
||||||
|
fun bindDeviceTrustInfoActionFragment(fragment: DeviceTrustInfoActionFragment): Fragment
|
||||||
}
|
}
|
||||||
|
@ -226,11 +226,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
|||||||
mainActivityStarted = true
|
mainActivityStarted = true
|
||||||
|
|
||||||
MainActivity.restartApp(this,
|
MainActivity.restartApp(this,
|
||||||
MainActivityArgs(
|
MainActivityArgs(
|
||||||
clearCredentials = !globalError.softLogout,
|
clearCredentials = !globalError.softLogout,
|
||||||
isUserLoggedOut = true,
|
isUserLoggedOut = true,
|
||||||
isSoftLogout = globalError.softLogout
|
isSoftLogout = globalError.softLogout
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,12 +17,14 @@ package im.vector.riotx.core.ui.list
|
|||||||
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
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.themes.ThemeUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic list item.
|
* A generic list item.
|
||||||
@ -45,13 +47,23 @@ abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>()
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var centered: Boolean = true
|
var centered: Boolean = true
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
@ColorInt
|
||||||
|
var textColor: Int? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
holder.text.setTextOrHide(text)
|
holder.text.setTextOrHide(text)
|
||||||
when (style) {
|
when (style) {
|
||||||
GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f
|
GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f
|
||||||
GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f
|
GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f
|
||||||
}
|
}
|
||||||
holder.text.gravity = if(centered) Gravity.CENTER_HORIZONTAL else Gravity.START
|
holder.text.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START
|
||||||
|
|
||||||
|
if (textColor != null) {
|
||||||
|
holder.text.setTextColor(textColor!!)
|
||||||
|
} else {
|
||||||
|
holder.text.setTextColor(ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary))
|
||||||
|
}
|
||||||
|
|
||||||
holder.view.setOnClickListener {
|
holder.view.setOnClickListener {
|
||||||
itemClickAction?.perform?.run()
|
itemClickAction?.perform?.run()
|
||||||
|
@ -40,7 +40,7 @@ import im.vector.riotx.features.themes.ThemeUtils
|
|||||||
abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Holder>() {
|
abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var title: String? = null
|
var title: CharSequence? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var value: CharSequence? = null
|
var value: CharSequence? = null
|
||||||
|
@ -180,8 +180,8 @@ class RoomMemberProfileFragment @Inject constructor(
|
|||||||
DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST")
|
DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowDeviceListNoCrossSigning() {
|
override fun onShowDeviceListNoCrossSigning() = 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 onJumpToReadReceiptClicked() {
|
override fun onJumpToReadReceiptClicked() {
|
||||||
|
@ -16,65 +16,98 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.riotx.features.roommemberprofile.devices
|
package im.vector.riotx.features.roommemberprofile.devices
|
||||||
|
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.view.isVisible
|
import android.view.KeyEvent
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.fragment.app.Fragment
|
||||||
import butterknife.BindView
|
|
||||||
import com.airbnb.mvrx.MvRx
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.extensions.cleanup
|
import im.vector.riotx.core.extensions.commitTransaction
|
||||||
import im.vector.riotx.core.extensions.configureWith
|
import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
import im.vector.riotx.core.utils.DimensionConverter
|
import im.vector.riotx.features.crypto.verification.VerificationAction
|
||||||
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment(), DeviceListEpoxyController.InteractionListener {
|
class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.bottom_sheet_generic_list_with_title
|
override fun getLayoutResId() = R.layout.bottom_sheet_with_fragments
|
||||||
|
|
||||||
private val viewModel: DeviceListBottomSheetViewModel by fragmentViewModel(DeviceListBottomSheetViewModel::class)
|
private val viewModel: DeviceListBottomSheetViewModel by fragmentViewModel(DeviceListBottomSheetViewModel::class)
|
||||||
|
|
||||||
@Inject lateinit var viewModelFactory: DeviceListBottomSheetViewModel.Factory
|
@Inject lateinit var viewModelFactory: DeviceListBottomSheetViewModel.Factory
|
||||||
|
|
||||||
@Inject lateinit var dimensionConverter: DimensionConverter
|
|
||||||
|
|
||||||
@BindView(R.id.bottomSheetRecyclerView)
|
|
||||||
lateinit var recyclerView: RecyclerView
|
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
injector.inject(this)
|
injector.inject(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var epoxyController: DeviceListEpoxyController
|
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
recyclerView.setPadding(0, dimensionConverter.dpToPx(16 ),0, dimensionConverter.dpToPx(16 ))
|
viewModel.requestLiveData.observeEvent(this) { async ->
|
||||||
recyclerView.configureWith(
|
when (async) {
|
||||||
epoxyController,
|
is Success -> {
|
||||||
showDivider = false,
|
when (val action = async.invoke()) {
|
||||||
hasFixedSize = false)
|
is VerificationAction.StartSASVerification -> {
|
||||||
epoxyController.interactionListener = this
|
VerificationBottomSheet.withArgs(
|
||||||
bottomSheetTitle.isVisible = false
|
roomId = null,
|
||||||
|
otherUserId = action.userID,
|
||||||
|
transactionId = action.pendingRequestTransactionId
|
||||||
|
).show(requireActivity().supportFragmentManager, "REQPOP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
private val onKeyListener = DialogInterface.OnKeyListener { _, keyCode, _ ->
|
||||||
recyclerView.cleanup()
|
withState(viewModel) {
|
||||||
super.onDestroyView()
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
if (it.selectedDevice != null) {
|
||||||
|
viewModel.selectDevice(null)
|
||||||
|
return@withState true
|
||||||
|
} else {
|
||||||
|
return@withState false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@withState false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
dialog?.setOnKeyListener(onKeyListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
dialog?.setOnKeyListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) {
|
override fun invalidate() = withState(viewModel) {
|
||||||
epoxyController.setData(it)
|
|
||||||
super.invalidate()
|
super.invalidate()
|
||||||
|
if (it.selectedDevice == null) {
|
||||||
|
showFragment(DeviceListFragment::class, arguments ?: Bundle())
|
||||||
|
} else {
|
||||||
|
showFragment(DeviceTrustInfoActionFragment::class, arguments ?: Bundle())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeviceSelected(device: CryptoDeviceInfo) {
|
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
||||||
// TODO
|
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
||||||
|
childFragmentManager.commitTransaction {
|
||||||
|
replace(R.id.bottomSheetFragmentContainer,
|
||||||
|
fragmentClass.java,
|
||||||
|
bundle,
|
||||||
|
fragmentClass.simpleName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -1,22 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
package im.vector.riotx.features.roommemberprofile.devices
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
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
|
||||||
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.crypto.sas.VerificationMethod
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotx.core.di.HasScreenInjector
|
||||||
import im.vector.riotx.core.platform.EmptyAction
|
import im.vector.riotx.core.platform.EmptyAction
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.core.utils.LiveEvent
|
||||||
|
import im.vector.riotx.features.crypto.verification.VerificationAction
|
||||||
|
|
||||||
data class DeviceListViewState(
|
data class DeviceListViewState(
|
||||||
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Loading()
|
val userItem: MatrixItem? = null,
|
||||||
|
val isMine: Boolean = false,
|
||||||
|
val memberCrossSigningKey: MXCrossSigningInfo? = null,
|
||||||
|
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Loading(),
|
||||||
|
val selectedDevice: CryptoDeviceInfo? = null
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
|
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
|
||||||
@ -24,16 +54,56 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
|
|||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val session: Session) : VectorViewModel<DeviceListViewState, EmptyAction>(initialState) {
|
private val session: Session) : VectorViewModel<DeviceListViewState, EmptyAction>(initialState) {
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel
|
fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
session.rx().liveUserCryptoDevices(userId)
|
session.rx().liveUserCryptoDevices(userId)
|
||||||
.execute {
|
.execute {
|
||||||
copy(cryptoDevices = it)
|
copy(cryptoDevices = it).also {
|
||||||
|
refreshSelectedId()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.rx().liveCrossSigningInfo(userId)
|
||||||
|
.map {
|
||||||
|
it.getOrNull()
|
||||||
|
}
|
||||||
|
.execute {
|
||||||
|
copy(memberCrossSigningKey = it.invoke())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshSelectedId() = withState { state ->
|
||||||
|
if (state.selectedDevice != null) {
|
||||||
|
state.cryptoDevices.invoke()?.firstOrNull { state.selectedDevice.deviceId == it.deviceId }?.let {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
selectedDevice = it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectDevice(device: CryptoDeviceInfo?) {
|
||||||
|
setState {
|
||||||
|
copy(selectedDevice = device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manuallyVerify(device: CryptoDeviceInfo) {
|
||||||
|
session.getSasVerificationService().beginKeyVerification(VerificationMethod.SAS, deviceID = device.deviceId, userId = userId)?.let { txID ->
|
||||||
|
_requestLiveData.postValue(LiveEvent(Success(VerificationAction.StartSASVerification(userId, txID))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: EmptyAction) {}
|
override fun handle(action: EmptyAction) {}
|
||||||
@ -45,5 +115,16 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
|
|||||||
val userId = viewModelContext.args<String>()
|
val userId = viewModelContext.args<String>()
|
||||||
return fragment.viewModelFactory.create(state, userId)
|
return fragment.viewModelFactory.create(state, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? {
|
||||||
|
val userId = viewModelContext.args<String>()
|
||||||
|
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||||
|
return session.getUser(userId)?.toMatrixItem()?.let {
|
||||||
|
DeviceListViewState(
|
||||||
|
userItem = it,
|
||||||
|
isMine = userId == session.myUserId
|
||||||
|
)
|
||||||
|
} ?: return super.initialState(viewModelContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
package im.vector.riotx.features.roommemberprofile.devices
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
@ -11,16 +28,18 @@ import im.vector.riotx.core.epoxy.errorWithRetryItem
|
|||||||
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.genericFooterItem
|
import im.vector.riotx.core.ui.list.genericFooterItem
|
||||||
import im.vector.riotx.core.ui.list.genericItem
|
import im.vector.riotx.core.ui.list.genericItem
|
||||||
import im.vector.riotx.core.ui.list.genericItemWithValue
|
import im.vector.riotx.core.ui.list.genericItemWithValue
|
||||||
|
import im.vector.riotx.core.utils.DimensionConverter
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DeviceListEpoxyController @Inject constructor(private val stringProvider: StringProvider,
|
class DeviceListEpoxyController @Inject constructor(private val stringProvider: StringProvider,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
|
private val dimensionConverter: DimensionConverter,
|
||||||
private val vectorPreferences: VectorPreferences)
|
private val vectorPreferences: VectorPreferences)
|
||||||
: TypedEpoxyController<DeviceListViewState>() {
|
: TypedEpoxyController<DeviceListViewState>() {
|
||||||
|
|
||||||
@ -46,7 +65,10 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
|
|||||||
}
|
}
|
||||||
is Success -> {
|
is Success -> {
|
||||||
|
|
||||||
val deviceList = data.cryptoDevices.invoke()
|
val deviceList = data.cryptoDevices.invoke().sortedByDescending {
|
||||||
|
it.isVerified
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Build top header
|
// Build top header
|
||||||
val allGreen = deviceList.fold(true, { prev, device ->
|
val allGreen = deviceList.fold(true, { prev, device ->
|
||||||
@ -57,10 +79,19 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
|
|||||||
id("title")
|
id("title")
|
||||||
style(GenericItem.STYLE.BIG_TEXT)
|
style(GenericItem.STYLE.BIG_TEXT)
|
||||||
titleIconResourceId(if (allGreen) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
|
titleIconResourceId(if (allGreen) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
|
||||||
title(stringProvider.getString(R.string.verification_profile_verified))
|
title(
|
||||||
|
stringProvider.getString(
|
||||||
|
if (allGreen) R.string.verification_profile_verified else R.string.verification_profile_warning
|
||||||
|
)
|
||||||
|
)
|
||||||
description(stringProvider.getString(R.string.verification_conclusion_ok_notice))
|
description(stringProvider.getString(R.string.verification_conclusion_ok_notice))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (vectorPreferences.developerMode()) {
|
||||||
|
// Display the cross signing keys
|
||||||
|
addDebugInfo(data)
|
||||||
|
}
|
||||||
|
|
||||||
genericItem {
|
genericItem {
|
||||||
id("sessions")
|
id("sessions")
|
||||||
style(GenericItem.STYLE.BIG_TEXT)
|
style(GenericItem.STYLE.BIG_TEXT)
|
||||||
@ -79,18 +110,22 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
|
|||||||
genericItemWithValue {
|
genericItemWithValue {
|
||||||
id(device.deviceId)
|
id(device.deviceId)
|
||||||
titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
|
titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
|
||||||
title(
|
apply {
|
||||||
buildString {
|
if (vectorPreferences.developerMode()) {
|
||||||
append(device.displayName() ?: device.deviceId)
|
val seq = span {
|
||||||
apply {
|
+(device.displayName() ?: device.deviceId)
|
||||||
if (vectorPreferences.developerMode()) {
|
+"\n"
|
||||||
append("\n")
|
span {
|
||||||
append(device.deviceId)
|
text = "(${device.deviceId})"
|
||||||
}
|
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||||
|
textSize = dimensionConverter.spToPx(14)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
title(seq)
|
||||||
)
|
} else {
|
||||||
|
title(device.displayName() ?: device.deviceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
value(
|
value(
|
||||||
stringProvider.getString(
|
stringProvider.getString(
|
||||||
if (device.isVerified) R.string.trusted else R.string.not_trusted
|
if (device.isVerified) R.string.trusted else R.string.not_trusted
|
||||||
@ -101,6 +136,9 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
|
|||||||
if (device.isVerified) R.color.riotx_positive_accent else R.color.riotx_destructive_accent
|
if (device.isVerified) R.color.riotx_positive_accent else R.color.riotx_destructive_accent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
itemClickAction(View.OnClickListener {
|
||||||
|
interactionListener?.onDeviceSelected(device)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,4 +154,55 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addDebugInfo(data: DeviceListViewState) {
|
||||||
|
data.memberCrossSigningKey?.masterKey()?.let {
|
||||||
|
genericItemWithValue {
|
||||||
|
id("msk")
|
||||||
|
titleIconResourceId(R.drawable.key_small)
|
||||||
|
title(
|
||||||
|
span {
|
||||||
|
+"Master Key:\n"
|
||||||
|
span {
|
||||||
|
text = it.unpaddedBase64PublicKey ?: ""
|
||||||
|
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||||
|
textSize = dimensionConverter.spToPx(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.memberCrossSigningKey?.userKey()?.let {
|
||||||
|
genericItemWithValue {
|
||||||
|
id("usk")
|
||||||
|
titleIconResourceId(R.drawable.key_small)
|
||||||
|
title(
|
||||||
|
span {
|
||||||
|
+"User Key:\n"
|
||||||
|
span {
|
||||||
|
text = it.unpaddedBase64PublicKey ?: ""
|
||||||
|
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||||
|
textSize = dimensionConverter.spToPx(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.memberCrossSigningKey?.selfSigningKey()?.let {
|
||||||
|
genericItemWithValue {
|
||||||
|
id("ssk")
|
||||||
|
titleIconResourceId(R.drawable.key_small)
|
||||||
|
title(
|
||||||
|
span {
|
||||||
|
+"Self Signed Key:\n"
|
||||||
|
span {
|
||||||
|
text = it.unpaddedBase64PublicKey ?: ""
|
||||||
|
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||||
|
textSize = dimensionConverter.spToPx(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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.recyclerview.widget.RecyclerView
|
||||||
|
import butterknife.BindView
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
import im.vector.riotx.core.extensions.configureWith
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.core.utils.DimensionConverter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DeviceListFragment @Inject constructor(
|
||||||
|
val dimensionConverter: DimensionConverter,
|
||||||
|
val epoxyController: DeviceListEpoxyController
|
||||||
|
) : VectorBaseFragment(), DeviceListEpoxyController.InteractionListener {
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.bottom_sheet_generic_list
|
||||||
|
|
||||||
|
private val viewModel: DeviceListBottomSheetViewModel by parentFragmentViewModel(DeviceListBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
@BindView(R.id.bottomSheetRecyclerView)
|
||||||
|
lateinit var recyclerView: RecyclerView
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
recyclerView.cleanup()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
epoxyController.setData(it)
|
||||||
|
super.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeviceSelected(device: CryptoDeviceInfo) {
|
||||||
|
viewModel.selectDevice(device)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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.recyclerview.widget.RecyclerView
|
||||||
|
import butterknife.BindView
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
import im.vector.riotx.core.extensions.configureWith
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.core.utils.DimensionConverter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DeviceTrustInfoActionFragment @Inject constructor(
|
||||||
|
val dimensionConverter: DimensionConverter,
|
||||||
|
val epoxyController: DeviceTrustInfoEpoxyController
|
||||||
|
) : VectorBaseFragment(), DeviceTrustInfoEpoxyController.InteractionListener {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.bottom_sheet_generic_list
|
||||||
|
|
||||||
|
private val viewModel: DeviceListBottomSheetViewModel by parentFragmentViewModel(DeviceListBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
@BindView(R.id.bottomSheetRecyclerView)
|
||||||
|
lateinit var recyclerView: RecyclerView
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
recyclerView.cleanup()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
epoxyController.setData(it)
|
||||||
|
super.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVerifyManually(device: CryptoDeviceInfo) {
|
||||||
|
viewModel.manuallyVerify(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
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.core.utils.DimensionConverter
|
||||||
|
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||||
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
|
import me.gujun.android.span.span
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
|
||||||
|
private val colorProvider: ColorProvider,
|
||||||
|
private val dimensionConverter: DimensionConverter,
|
||||||
|
private val vectorPreferences: VectorPreferences)
|
||||||
|
: TypedEpoxyController<DeviceListViewState>() {
|
||||||
|
|
||||||
|
interface InteractionListener {
|
||||||
|
fun onVerifyManually(device: CryptoDeviceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
var interactionListener: InteractionListener? = null
|
||||||
|
|
||||||
|
override fun buildModels(data: DeviceListViewState?) {
|
||||||
|
data?.selectedDevice?.let {
|
||||||
|
val isVerified = it.trustLevel?.isVerified() == true
|
||||||
|
genericItem {
|
||||||
|
id("title")
|
||||||
|
style(GenericItem.STYLE.BIG_TEXT)
|
||||||
|
titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
|
||||||
|
title(
|
||||||
|
stringProvider.getString(
|
||||||
|
if (isVerified) R.string.verification_profile_verified else R.string.verification_profile_warning
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
genericFooterItem {
|
||||||
|
id("desc")
|
||||||
|
centered(false)
|
||||||
|
textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||||
|
apply {
|
||||||
|
if (isVerified) {
|
||||||
|
// TODO FORMAT
|
||||||
|
text(stringProvider.getString(R.string.verification_profile_device_verified_because,
|
||||||
|
data.userItem?.displayName ?: "",
|
||||||
|
data.userItem?.id ?: ""))
|
||||||
|
} else {
|
||||||
|
// TODO what if mine
|
||||||
|
text(stringProvider.getString(R.string.verification_profile_device_new_signing,
|
||||||
|
data.userItem?.displayName ?: "",
|
||||||
|
data.userItem?.id ?: ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// text(stringProvider.getString(R.string.verification_profile_device_untrust_info))
|
||||||
|
}
|
||||||
|
|
||||||
|
genericItemWithValue {
|
||||||
|
id(it.deviceId)
|
||||||
|
titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
|
||||||
|
title(
|
||||||
|
span {
|
||||||
|
+(it.displayName() ?: "")
|
||||||
|
span {
|
||||||
|
text = " (${it.deviceId})"
|
||||||
|
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||||
|
textSize = dimensionConverter.spToPx(14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVerified) {
|
||||||
|
genericFooterItem {
|
||||||
|
id("warn")
|
||||||
|
centered(false)
|
||||||
|
textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||||
|
text(stringProvider.getString(R.string.verification_profile_device_untrust_info))
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheetVerificationActionItem {
|
||||||
|
id("verify")
|
||||||
|
title(stringProvider.getString(R.string.verification_verify_device_manually))
|
||||||
|
titleColor(colorProvider.getColor(R.color.riotx_accent))
|
||||||
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
|
iconColor(colorProvider.getColor(R.color.riotx_accent))
|
||||||
|
listener {
|
||||||
|
interactionListener?.onVerifyManually(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/bottomSheetFragmentContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<string name="verification_verify_device">Verify this device</string>
|
<string name="verification_verify_device">Verify this device</string>
|
||||||
|
<string name="verification_verify_device_manually">Manually verify</string>
|
||||||
|
|
||||||
<!-- Sender name of a message when it is send by you, e.g. You: Hello!-->
|
<!-- Sender name of a message when it is send by you, e.g. You: Hello!-->
|
||||||
<string name="you">You</string>
|
<string name="you">You</string>
|
||||||
@ -129,4 +130,8 @@
|
|||||||
<string name="trusted">Trusted</string>
|
<string name="trusted">Trusted</string>
|
||||||
<string name="not_trusted">Not Trusted</string>
|
<string name="not_trusted">Not Trusted</string>
|
||||||
|
|
||||||
|
<string name="verification_profile_device_verified_because">This device is trusted for secure messaging because %1$s (%2$s) verified it:</string>
|
||||||
|
<string name="verification_profile_device_new_signing">%1$s (%2$s) signed in using a new device:</string>
|
||||||
|
<string name="verification_profile_device_untrust_info">Until this user trusts this device, messages sent to and from it are labelled with warnings. Alternatively, you can manually verify it.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user