Merge pull request #2377 from vector-im/feature/bma/consent
Feature/bma/consent
This commit is contained in:
commit
0022777a4f
|
@ -6,6 +6,7 @@ Features ✨:
|
|||
|
||||
Improvements 🙌:
|
||||
- Open an existing DM instead of creating a new one (#2319)
|
||||
- Ask for explicit user consent to send their contact details to the identity server (#2375)
|
||||
- Handle events of type "m.room.server_acl" (#890)
|
||||
|
||||
Bugfix 🐛:
|
||||
|
|
|
@ -92,9 +92,29 @@ interface IdentityService {
|
|||
|
||||
/**
|
||||
* Search MatrixId of users providing email and phone numbers
|
||||
* Note the the user consent has to be set to true, or it will throw a UserConsentNotProvided failure
|
||||
* Application has to explicitly ask for the user consent, and the answer can be stored using [setUserConsent]
|
||||
* Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details.
|
||||
*/
|
||||
fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the current user consent for the current identity server, which has been stored using [setUserConsent].
|
||||
* If [setUserConsent] has not been called, the returned value will be false.
|
||||
* Note that if the identity server is changed, the user consent is reset to false.
|
||||
* @return the value stored using [setUserConsent] or false if [setUserConsent] has never been called, or if the identity server
|
||||
* has been changed
|
||||
*/
|
||||
fun getUserConsent(): Boolean
|
||||
|
||||
/**
|
||||
* Set the user consent to the provided value. Application MUST explicitly ask for the user consent to send their private data
|
||||
* (email and phone numbers) to the identity server.
|
||||
* Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details.
|
||||
* @param newValue true if the user explicitly give their consent, false if the user wants to revoke their consent.
|
||||
*/
|
||||
fun setUserConsent(newValue: Boolean)
|
||||
|
||||
/**
|
||||
* Get the status of the current user's threePid
|
||||
* A lookup will be performed, but also pending binding state will be restored
|
||||
|
|
|
@ -24,6 +24,7 @@ sealed class IdentityServiceError : Failure.FeatureFailure() {
|
|||
object NoIdentityServerConfigured : IdentityServiceError()
|
||||
object TermsNotSignedException : IdentityServiceError()
|
||||
object BulkLookupSha256NotSupported : IdentityServiceError()
|
||||
object UserConsentNotProvided : IdentityServiceError()
|
||||
object BindingError : IdentityServiceError()
|
||||
object NoCurrentBindingError : IdentityServiceError()
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
|||
import org.matrix.android.sdk.internal.util.ensureProtocol
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
@ -243,7 +244,20 @@ internal class DefaultIdentityService @Inject constructor(
|
|||
))
|
||||
}
|
||||
|
||||
override fun getUserConsent(): Boolean {
|
||||
return identityStore.getIdentityData()?.userConsent.orFalse()
|
||||
}
|
||||
|
||||
override fun setUserConsent(newValue: Boolean) {
|
||||
identityStore.setUserConsent(newValue)
|
||||
}
|
||||
|
||||
override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable {
|
||||
if (!getUserConsent()) {
|
||||
callback.onFailure(IdentityServiceError.UserConsentNotProvided)
|
||||
return NoOpCancellable
|
||||
}
|
||||
|
||||
if (threePids.isEmpty()) {
|
||||
callback.onSuccess(emptyList())
|
||||
return NoOpCancellable
|
||||
|
@ -255,6 +269,9 @@ internal class DefaultIdentityService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable {
|
||||
// Note: we do not require user consent here, because it is used for emails and phone numbers that the user has already sent
|
||||
// to the home server, and not emails and phone numbers from the contact book of the user
|
||||
|
||||
if (threePids.isEmpty()) {
|
||||
callback.onSuccess(emptyMap())
|
||||
return NoOpCancellable
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.identity.db.IdentityRealmModule
|
|||
import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore
|
||||
import io.realm.RealmConfiguration
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStoreMigration
|
||||
import java.io.File
|
||||
|
||||
@Module
|
||||
|
@ -59,6 +60,7 @@ internal abstract class IdentityModule {
|
|||
@SessionScope
|
||||
fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils,
|
||||
@SessionFilesDirectory directory: File,
|
||||
migration: RealmIdentityStoreMigration,
|
||||
@UserMd5 userMd5: String): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(directory)
|
||||
|
@ -66,6 +68,8 @@ internal abstract class IdentityModule {
|
|||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
|
||||
}
|
||||
.schemaVersion(RealmIdentityStoreMigration.IDENTITY_STORE_SCHEMA_VERSION)
|
||||
.migration(migration)
|
||||
.allowWritesOnUiThread(true)
|
||||
.modules(IdentityRealmModule())
|
||||
.build()
|
||||
|
|
|
@ -20,5 +20,6 @@ internal data class IdentityData(
|
|||
val identityServerUrl: String?,
|
||||
val token: String?,
|
||||
val hashLookupPepper: String?,
|
||||
val hashLookupAlgorithm: List<String>
|
||||
val hashLookupAlgorithm: List<String>,
|
||||
val userConsent: Boolean
|
||||
)
|
||||
|
|
|
@ -27,6 +27,8 @@ internal interface IdentityStore {
|
|||
|
||||
fun setToken(token: String?)
|
||||
|
||||
fun setUserConsent(consent: Boolean)
|
||||
|
||||
fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse)
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,7 +23,8 @@ internal open class IdentityDataEntity(
|
|||
var identityServerUrl: String? = null,
|
||||
var token: String? = null,
|
||||
var hashLookupPepper: String? = null,
|
||||
var hashLookupAlgorithm: RealmList<String> = RealmList()
|
||||
var hashLookupAlgorithm: RealmList<String> = RealmList(),
|
||||
var userConsent: Boolean = false
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
|
|
@ -52,6 +52,13 @@ internal fun IdentityDataEntity.Companion.setToken(realm: Realm,
|
|||
}
|
||||
}
|
||||
|
||||
internal fun IdentityDataEntity.Companion.setUserConsent(realm: Realm,
|
||||
newConsent: Boolean) {
|
||||
get(realm)?.apply {
|
||||
userConsent = newConsent
|
||||
}
|
||||
}
|
||||
|
||||
internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm,
|
||||
pepper: String,
|
||||
algorithms: List<String>) {
|
||||
|
|
|
@ -26,7 +26,8 @@ internal object IdentityMapper {
|
|||
identityServerUrl = entity.identityServerUrl,
|
||||
token = entity.token,
|
||||
hashLookupPepper = entity.hashLookupPepper,
|
||||
hashLookupAlgorithm = entity.hashLookupAlgorithm.toList()
|
||||
hashLookupAlgorithm = entity.hashLookupAlgorithm.toList(),
|
||||
userConsent = entity.userConsent
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,14 @@ internal class RealmIdentityStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun setUserConsent(consent: Boolean) {
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
it.executeTransaction { realm ->
|
||||
IdentityDataEntity.setUserConsent(realm, consent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) {
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
it.executeTransaction { realm ->
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.identity.db
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RealmIdentityStoreMigration @Inject constructor() : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val IDENTITY_STORE_SCHEMA_VERSION = 1L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.v("Migrating Realm Identity from $oldVersion to $newVersion")
|
||||
|
||||
if (oldVersion <= 0) migrateTo1(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
Timber.d("Step 0 -> 1")
|
||||
Timber.d("Add field userConsent (Boolean) and set the value to false")
|
||||
|
||||
realm.schema.get("IdentityDataEntity")
|
||||
?.addField(IdentityDataEntityFields.USER_CONSENT, Boolean::class.java)
|
||||
}
|
||||
}
|
|
@ -136,6 +136,7 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported
|
||||
IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error
|
||||
IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error
|
||||
IdentityServiceError.UserConsentNotProvided -> R.string.identity_server_user_consent_not_provided
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
sealed class ContactsBookAction : VectorViewModelAction {
|
||||
data class FilterWith(val filter: String) : ContactsBookAction()
|
||||
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
|
||||
object UserConsentGranted : ContactsBookAction()
|
||||
}
|
||||
|
|
|
@ -52,11 +52,10 @@ class ContactsBookController @Inject constructor(
|
|||
|
||||
override fun buildModels() {
|
||||
val currentState = state ?: return
|
||||
val hasSearch = currentState.searchTerm.isNotEmpty()
|
||||
when (val asyncMappedContacts = currentState.mappedContacts) {
|
||||
is Uninitialized -> renderEmptyState(false)
|
||||
is Loading -> renderLoading()
|
||||
is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts)
|
||||
is Success -> renderSuccess(currentState)
|
||||
is Fail -> renderFailure(asyncMappedContacts.error)
|
||||
}
|
||||
}
|
||||
|
@ -75,13 +74,13 @@ class ContactsBookController @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun renderSuccess(mappedContacts: List<MappedContact>,
|
||||
hasSearch: Boolean,
|
||||
onlyBoundContacts: Boolean) {
|
||||
private fun renderSuccess(state: ContactsBookViewState) {
|
||||
val mappedContacts = state.filteredMappedContacts
|
||||
|
||||
if (mappedContacts.isEmpty()) {
|
||||
renderEmptyState(hasSearch)
|
||||
renderEmptyState(state.searchTerm.isNotEmpty())
|
||||
} else {
|
||||
renderContacts(mappedContacts, onlyBoundContacts)
|
||||
renderContacts(mappedContacts, state.onlyBoundContacts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.contactsbook
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
|
@ -57,10 +58,26 @@ class ContactsBookFragment @Inject constructor(
|
|||
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
|
||||
setupRecyclerView()
|
||||
setupFilterView()
|
||||
setupConsentView()
|
||||
setupOnlyBoundContactsView()
|
||||
setupCloseView()
|
||||
}
|
||||
|
||||
private fun setupConsentView() {
|
||||
phoneBookSearchForMatrixContacts.setOnClickListener {
|
||||
withState(contactsBookViewModel) { state ->
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.identity_server_consent_dialog_title)
|
||||
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: ""))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupOnlyBoundContactsView() {
|
||||
phoneBookOnlyBoundContacts.checkedChanges()
|
||||
.subscribe {
|
||||
|
@ -98,6 +115,7 @@ class ContactsBookFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun invalidate() = withState(contactsBookViewModel) { state ->
|
||||
phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent
|
||||
phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
|
||||
contactsBookController.setData(state)
|
||||
}
|
||||
|
|
|
@ -38,11 +38,10 @@ import kotlinx.coroutines.launch
|
|||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.identity.FoundThreePid
|
||||
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import timber.log.Timber
|
||||
|
||||
private typealias PhoneBookSearch = String
|
||||
|
||||
class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: ContactsBookViewState,
|
||||
private val contactsDataSource: ContactsDataSource,
|
||||
|
@ -85,7 +84,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
|||
private fun loadContacts() {
|
||||
setState {
|
||||
copy(
|
||||
mappedContacts = Loading()
|
||||
mappedContacts = Loading(),
|
||||
identityServerUrl = session.identityService().getCurrentIdentityServerUrl(),
|
||||
userConsent = session.identityService().getUserConsent()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -109,6 +110,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
|
||||
private fun performLookup(data: List<MappedContact>) {
|
||||
if (!session.identityService().getUserConsent()) {
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val threePids = data.flatMap { contact ->
|
||||
contact.emails.map { ThreePid.Email(it.email) } +
|
||||
|
@ -116,8 +120,14 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// Ignore
|
||||
Timber.w(failure, "Unable to perform the lookup")
|
||||
|
||||
// Should not happen, but just to be sure
|
||||
if (failure is IdentityServiceError.UserConsentNotProvided) {
|
||||
setState {
|
||||
copy(userConsent = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: List<FoundThreePid>) {
|
||||
|
@ -171,9 +181,21 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
|||
when (action) {
|
||||
is ContactsBookAction.FilterWith -> handleFilterWith(action)
|
||||
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
|
||||
ContactsBookAction.UserConsentGranted -> handleUserConsentGranted()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleUserConsentGranted() {
|
||||
session.identityService().setUserConsent(true)
|
||||
|
||||
setState {
|
||||
copy(userConsent = true)
|
||||
}
|
||||
|
||||
// Perform the lookup
|
||||
performLookup(allContacts)
|
||||
}
|
||||
|
||||
private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
|
||||
setState {
|
||||
copy(
|
||||
|
|
|
@ -26,10 +26,14 @@ data class ContactsBookViewState(
|
|||
val mappedContacts: Async<List<MappedContact>> = Loading(),
|
||||
// Use to filter contacts by display name
|
||||
val searchTerm: String = "",
|
||||
// Tru to display only bound contacts with their bound 2pid
|
||||
// True to display only bound contacts with their bound 2pid
|
||||
val onlyBoundContacts: Boolean = false,
|
||||
// All contacts, filtered by searchTerm and onlyBoundContacts
|
||||
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
||||
// True when the identity service has return some data
|
||||
val isBoundRetrieved: Boolean = false
|
||||
val isBoundRetrieved: Boolean = false,
|
||||
// The current identity server url if any
|
||||
val identityServerUrl: String? = null,
|
||||
// User consent to perform lookup (send emails to the identity server)
|
||||
val userConsent: Boolean = false
|
||||
) : MvRxState
|
||||
|
|
|
@ -25,6 +25,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction {
|
|||
|
||||
object DisconnectIdentityServer : DiscoverySettingsAction()
|
||||
data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction()
|
||||
data class UpdateUserConsent(val newConsent: Boolean) : DiscoverySettingsAction()
|
||||
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||
|
|
|
@ -65,6 +65,7 @@ class DiscoverySettingsController @Inject constructor(
|
|||
buildIdentityServerSection(data)
|
||||
val hasIdentityServer = data.identityServer().isNullOrBlank().not()
|
||||
if (hasIdentityServer && !data.termsNotSigned) {
|
||||
buildConsentSection(data)
|
||||
buildEmailsSection(data.emailList)
|
||||
buildMsisdnSection(data.phoneNumbersList)
|
||||
}
|
||||
|
@ -72,6 +73,38 @@ class DiscoverySettingsController @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun buildConsentSection(data: DiscoverySettingsState) {
|
||||
settingsSectionTitleItem {
|
||||
id("idConsentTitle")
|
||||
titleResId(R.string.settings_discovery_consent_title)
|
||||
}
|
||||
|
||||
if (data.userConsent) {
|
||||
settingsInfoItem {
|
||||
id("idConsentInfo")
|
||||
helperTextResId(R.string.settings_discovery_consent_notice_on)
|
||||
}
|
||||
settingsButtonItem {
|
||||
id("idConsentButton")
|
||||
colorProvider(colorProvider)
|
||||
buttonTitleId(R.string.settings_discovery_consent_action_revoke)
|
||||
buttonStyle(ButtonStyle.DESTRUCTIVE)
|
||||
buttonClickListener { listener?.onTapUpdateUserConsent(false) }
|
||||
}
|
||||
} else {
|
||||
settingsInfoItem {
|
||||
id("idConsentInfo")
|
||||
helperTextResId(R.string.settings_discovery_consent_notice_off)
|
||||
}
|
||||
settingsButtonItem {
|
||||
id("idConsentButton")
|
||||
colorProvider(colorProvider)
|
||||
buttonTitleId(R.string.settings_discovery_consent_action_give_consent)
|
||||
buttonClickListener { listener?.onTapUpdateUserConsent(true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildIdentityServerSection(data: DiscoverySettingsState) {
|
||||
val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none)
|
||||
|
||||
|
@ -359,6 +392,7 @@ class DiscoverySettingsController @Inject constructor(
|
|||
fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String)
|
||||
fun onTapChangeIdentityServer()
|
||||
fun onTapDisconnectIdentityServer()
|
||||
fun onTapUpdateUserConsent(newValue: Boolean)
|
||||
fun onTapRetryToRetrieveBindings()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,6 +170,23 @@ class DiscoverySettingsFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onTapUpdateUserConsent(newValue: Boolean) {
|
||||
if (newValue) {
|
||||
withState(viewModel) { state ->
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.identity_server_consent_dialog_title)
|
||||
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke()))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true))
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTapRetryToRetrieveBindings() {
|
||||
viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
|
||||
}
|
||||
|
|
|
@ -25,5 +25,6 @@ data class DiscoverySettingsState(
|
|||
val emailList: Async<List<PidInfo>> = Uninitialized,
|
||||
val phoneNumbersList: Async<List<PidInfo>> = Uninitialized,
|
||||
// Can be true if terms are updated
|
||||
val termsNotSigned: Boolean = false
|
||||
val termsNotSigned: Boolean = false,
|
||||
val userConsent: Boolean = false
|
||||
) : MvRxState
|
||||
|
|
|
@ -63,7 +63,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
|||
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
|
||||
val currentIS = state.identityServer()
|
||||
setState {
|
||||
copy(identityServer = Success(identityServerUrl))
|
||||
copy(
|
||||
identityServer = Success(identityServerUrl),
|
||||
userConsent = false
|
||||
)
|
||||
}
|
||||
if (currentIS != identityServerUrl) retrieveBinding()
|
||||
}
|
||||
|
@ -71,7 +74,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
|||
|
||||
init {
|
||||
setState {
|
||||
copy(identityServer = Success(identityService.getCurrentIdentityServerUrl()))
|
||||
copy(
|
||||
identityServer = Success(identityService.getCurrentIdentityServerUrl()),
|
||||
userConsent = identityService.getUserConsent()
|
||||
)
|
||||
}
|
||||
startListenToIdentityManager()
|
||||
observeThreePids()
|
||||
|
@ -97,6 +103,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
|||
DiscoverySettingsAction.RetrieveBinding -> retrieveBinding()
|
||||
DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer()
|
||||
is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action)
|
||||
is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action)
|
||||
is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action)
|
||||
is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action)
|
||||
is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true)
|
||||
|
@ -105,13 +112,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
|||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleUpdateUserConsent(action: DiscoverySettingsAction.UpdateUserConsent) {
|
||||
identityService.setUserConsent(action.newConsent)
|
||||
setState { copy(userConsent = action.newConsent) }
|
||||
}
|
||||
|
||||
private fun disconnectIdentityServer() {
|
||||
setState { copy(identityServer = Loading()) }
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
awaitCallback<Unit> { session.identityService().disconnect(it) }
|
||||
setState { copy(identityServer = Success(null)) }
|
||||
setState {
|
||||
copy(
|
||||
identityServer = Success(null),
|
||||
userConsent = false
|
||||
)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
setState { copy(identityServer = Fail(failure)) }
|
||||
}
|
||||
|
@ -126,7 +143,12 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
|||
val data = awaitCallback<String?> {
|
||||
session.identityService().setNewIdentityServer(action.url, it)
|
||||
}
|
||||
setState { copy(identityServer = Success(data)) }
|
||||
setState {
|
||||
copy(
|
||||
identityServer = Success(data),
|
||||
userConsent = false
|
||||
)
|
||||
}
|
||||
retrieveBinding()
|
||||
} catch (failure: Throwable) {
|
||||
setState { copy(identityServer = Fail(failure)) }
|
||||
|
|
|
@ -93,6 +93,27 @@
|
|||
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/phoneBookSearchForMatrixContacts"
|
||||
style="@style/VectorButtonStyleText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:text="@string/phone_book_perform_lookup"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/phoneBookBottomBarrier"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="phoneBookSearchForMatrixContacts,phoneBookOnlyBoundContacts" />
|
||||
|
||||
<View
|
||||
android:id="@+id/phoneBookFilterDivider"
|
||||
android:layout_width="0dp"
|
||||
|
@ -101,7 +122,7 @@
|
|||
android:background="?attr/vctr_list_divider_color"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/phoneBookOnlyBoundContacts" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/phoneBookBottomBarrier" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/phoneBookRecyclerView"
|
||||
|
|
|
@ -1793,6 +1793,14 @@
|
|||
<string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string>
|
||||
<string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string>
|
||||
<string name="settings_discovery_mail_pending">Pending</string>
|
||||
<string name="settings_discovery_consent_title">Send emails and phone numbers</string>
|
||||
<string name="settings_discovery_consent_notice_on">You have given your consent to send emails and phone numbers to this identity server to discover other users from your contacts.</string>
|
||||
<string name="settings_discovery_consent_notice_off">You have not given your consent to send emails and phone numbers to this identity server to discover other users from your contacts.</string>
|
||||
<string name="settings_discovery_consent_action_revoke">Revoke my consent</string>
|
||||
<string name="settings_discovery_consent_action_give_consent">Give consent</string>
|
||||
|
||||
<string name="identity_server_consent_dialog_title">Send emails and phone numbers</string>
|
||||
<string name="identity_server_consent_dialog_content">In order to discover existing contacts you know, do you accept to send your contact data (phone numbers and/or emails) to the configured Identity Server (%1$s)?\n\nFor more privacy, the sent data will be hashed before being sent.</string>
|
||||
|
||||
<string name="settings_discovery_enter_identity_server">Enter an identity server URL</string>
|
||||
<string name="settings_discovery_bad_identity_server">Could not connect to identity server</string>
|
||||
|
@ -2527,6 +2535,7 @@
|
|||
<string name="identity_server_error_bulk_sha256_not_supported">For your privacy, Element only supports sending hashed user emails and phone number.</string>
|
||||
<string name="identity_server_error_binding_error">The association has failed.</string>
|
||||
<string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string>
|
||||
<string name="identity_server_user_consent_not_provided">The user consent has not been provided.</string>
|
||||
|
||||
<string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string>
|
||||
<string name="identity_server_set_default_submit">Use %1$s</string>
|
||||
|
@ -2593,6 +2602,7 @@
|
|||
<string name="loading_contact_book">Retrieving your contacts…</string>
|
||||
<string name="empty_contact_book">Your contact book is empty</string>
|
||||
<string name="contacts_book_title">Contacts book</string>
|
||||
<string name="phone_book_perform_lookup">Search for contacts on Matrix</string>
|
||||
|
||||
<string name="three_pid_revoke_invite_dialog_title">Revoke invite</string>
|
||||
<string name="three_pid_revoke_invite_dialog_content">Revoke invite to %1$s?</string>
|
||||
|
|
Loading…
Reference in New Issue