List phone numbers and emails added to the Matrix account, and add Email to account (#44)

This commit is contained in:
Benoit Marty 2020-08-28 16:45:09 +02:00
parent 46d3608ccb
commit 175a5ab824
34 changed files with 1240 additions and 393 deletions

View File

@ -2,7 +2,7 @@ Changes in Element 1.0.6 (2020-XX-XX)
===================================================
Features ✨:
-
- List phone numbers and emails added to the Matrix account, and add Email to account (#44)
Improvements 🙌:
- You can now join room through permalink and within room directory search

View File

@ -12,7 +12,7 @@
}
```
### The email is already adding to an account
### The email is already added to an account
400
@ -84,6 +84,8 @@ User clicks on CONTINUE
POST https://homeserver.org/_matrix/client/r0/account/3pid/add
TODO: Remove "identifier"?
```json
{
"sid": "bxyDHuJKsdkjMlTJ",

View File

@ -18,9 +18,13 @@
package org.matrix.android.sdk.rx
import androidx.paging.PagedList
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function3
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
@ -43,10 +47,6 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function3
class RxSession(private val session: Session) {
@ -110,6 +110,11 @@ class RxSession(private val session: Session) {
.startWithCallable { session.getThreePids() }
}
fun livePendingThreePIds(): Observable<List<ThreePid>> {
return session.getPendingThreePidsLive().asObservable()
.startWithCallable { session.getPendingThreePids() }
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, it)
}

View File

@ -83,4 +83,32 @@ interface ProfileService {
* @param refreshData set to true to fetch data from the homeserver
*/
fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>>
/**
* Get the pending 3Pids, i.e. ThreePids that have requested a token, but not yet validated by the user.
*/
fun getPendingThreePids(): List<ThreePid>
/**
* Get the pending 3Pids Live
*/
fun getPendingThreePidsLive(): LiveData<List<ThreePid>>
/**
* Add a 3Pids. This is the first step to add a ThreePid to an account. Then the threePid will be added to the pending threePid list.
*/
fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
*/
fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Delete a 3Pids.
*/
fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
}

View File

@ -20,6 +20,7 @@ package org.matrix.android.sdk.internal.database
import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import timber.log.Timber
import javax.inject.Inject
@ -32,6 +33,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -63,4 +65,17 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
}
}
private fun migrateTo4(realm: DynamicRealm) {
Timber.d("Step 3 -> 4")
realm.schema.create("PendingThreePidEntity")
.addField(PendingThreePidEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingThreePidEntityFields.CLIENT_SECRET, true)
.addField(PendingThreePidEntityFields.EMAIL, String::class.java)
.addField(PendingThreePidEntityFields.MSISDN, String::class.java)
.addField(PendingThreePidEntityFields.SEND_ATTEMPT, Int::class.java)
.setRequired(PendingThreePidEntityFields.SEND_ATTEMPT, true)
.addField(PendingThreePidEntityFields.SID, String::class.java)
.setRequired(PendingThreePidEntityFields.SID, true)
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
* 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.database.model
import io.realm.RealmObject
/**
* This class is used to store pending threePid data, when user wants to add a threePid to his account
*/
internal open class PendingThreePidEntity(
var email: String? = null,
var msisdn: String? = null,
var clientSecret: String = "",
var sendAttempt: Int = 0,
var sid: String = ""
) : RealmObject()

View File

@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
RoomSummaryEntity::class,
RoomTagEntity::class,
SyncEntity::class,
PendingThreePidEntity::class,
UserEntity::class,
IgnoredUserEntity::class,
BreadcrumbsEntity::class,

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddEmailBody(
/**
* Required. A unique string generated by the client, and used to identify the validation attempt.
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
* 255 characters and it must not be empty.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The email address to validate.
*/
@Json(name = "email")
val email: String,
/**
* Required. The server will only send an email if the send_attempt is a number greater than the most
* recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly
* sending the same email in the case of request retries between the POSTing user and the identity server.
* The client should increment this value if they desire a new email (e.g. a reminder) to be sent.
* If they do not, the server should respond with success but not resend the email.
*/
@Json(name = "send_attempt")
val sendAttempt: Int
)

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddThreePidResponse(
/**
* Required. The session ID. Session IDs are opaque strings that must consist entirely
* of the characters [0-9a-zA-Z.=_-]. Their length must not exceed 255 characters and they must not be empty.
*/
@Json(name = "sid")
val sid: String
)

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.profile
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import java.util.UUID
import javax.inject.Inject
internal abstract class AddThreePidTask : Task<AddThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultAddThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
@SessionDatabase private val monarchy: Monarchy,
private val pendingThreePidMapper: PendingThreePidMapper,
private val eventBus: EventBus) : AddThreePidTask() {
override suspend fun execute(params: Params) {
val clientSecret = UUID.randomUUID().toString()
val sendAttempt = 1
val result = when (params.threePid) {
is ThreePid.Email ->
executeRequest<AddThreePidResponse>(eventBus) {
val body = AddEmailBody(
email = params.threePid.email,
sendAttempt = sendAttempt,
clientSecret = clientSecret
)
apiCall = profileAPI.addEmail(body)
}
is ThreePid.Msisdn -> TODO()
}
// Store as a pending three pid
monarchy.awaitTransaction { realm ->
PendingThreePid(
threePid = params.threePid,
clientSecret = clientSecret,
sendAttempt = sendAttempt,
sid = result.sid
)
.let { pendingThreePidMapper.map(it) }
.let { realm.copyToRealm(it) }
}
}
}

View File

@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.content.FileUploader
@ -44,6 +45,10 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
private val getProfileInfoTask: GetProfileInfoTask,
private val setDisplayNameTask: SetDisplayNameTask,
private val setAvatarUrlTask: SetAvatarUrlTask,
private val addThreePidTask: AddThreePidTask,
private val finalizeAddingThreePidTask: FinalizeAddingThreePidTask,
private val deleteThreePidTask: DeleteThreePidTask,
private val pendingThreePidMapper: PendingThreePidMapper,
private val fileUploader: FileUploader) : ProfileService {
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
@ -116,9 +121,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
override fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> {
if (refreshData) {
// Force a refresh of the values
refreshUserThreePidsTask
.configureWith()
.executeBy(taskExecutor)
refreshThreePids()
}
return monarchy.findAllMappedWithChanges(
@ -126,6 +129,69 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
{ it.asDomain() }
)
}
private fun refreshThreePids() {
refreshUserThreePidsTask
.configureWith()
.executeBy(taskExecutor)
}
override fun getPendingThreePids(): List<ThreePid> {
return monarchy.fetchAllMappedSync(
{ it.where<PendingThreePidEntity>() },
{ pendingThreePidMapper.map(it).threePid }
)
}
override fun getPendingThreePidsLive(): LiveData<List<ThreePid>> {
return monarchy.findAllMappedWithChanges(
{ it.where<PendingThreePidEntity>() },
{ pendingThreePidMapper.map(it).threePid }
)
}
override fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return addThreePidTask
.configureWith(AddThreePidTask.Params(threePid)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(threePid, uiaSession, accountPassword)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
/**
* Wrap the callback to fetch 3Pids from the server in case of success
*/
private fun alsoRefresh(callback: MatrixCallback<Unit>): MatrixCallback<Unit> {
return object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: Unit) {
refreshThreePids()
callback.onSuccess(data)
}
}
}
override fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return deleteThreePidTask
.configureWith(DeleteThreePidTask.Params(threePid)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
}
private fun UserThreePidEntity.asDomain(): ThreePid {

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class DeleteThreePidBody(
/**
* Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"]
*/
@Json(name = "medium") val medium: String,
/**
* Required. The third party address being removed.
*/
@Json(name = "address") val address: String
)

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class DeleteThreePidResponse(
/**
* Required. An indicator as to whether or not the homeserver was able to unbind the 3PID from
* the identity server. success indicates that the identity server has unbound the identifier
* whereas no-support indicates that the identity server refuses to support the request or the
* homeserver was not able to determine an identity server to unbind from. One of: ["no-support", "success"]
*/
@Json(name = "id_server_unbind_result")
val idServerUnbindResult: String? = null
)

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.profile
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal abstract class DeleteThreePidTask : Task<DeleteThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultDeleteThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
private val eventBus: EventBus) : DeleteThreePidTask() {
override suspend fun execute(params: Params) {
executeRequest<DeleteThreePidResponse>(eventBus) {
val body = DeleteThreePidBody(
medium = params.threePid.toMedium(),
address = params.threePid.value
)
apiCall = profileAPI.deleteThreePid(body)
}
// We do not really care about the result for the moment
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@JsonClass(generateAdapter = true)
internal data class FinalizeAddThreePidBody(
/**
* Required. The client secret used in the session with the homeserver.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The session identifier given by the homeserver.
*/
@Json(name = "sid")
val sid: String,
/**
* Additional authentication information for the user-interactive authentication API.
*/
@Json(name = "auth")
val auth: UserPasswordAuth?
)

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.profile
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid,
val session: String?,
val accountPassword: String?
)
}
internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
@SessionDatabase private val monarchy: Monarchy,
private val pendingThreePidMapper: PendingThreePidMapper,
@UserId private val userId: String,
private val eventBus: EventBus) : FinalizeAddingThreePidTask() {
override suspend fun execute(params: Params) {
// Get the required pending data
val pendingThreePids = monarchy.fetchAllMappedSync(
{ it.where(PendingThreePidEntity::class.java) },
{ pendingThreePidMapper.map(it) }
)
.firstOrNull { it.threePid == params.threePid }
?: throw IllegalArgumentException("unknown threepid")
try {
executeRequest<Unit>(eventBus) {
val body = FinalizeAddThreePidBody(
clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid,
auth = if (params.session != null && params.accountPassword != null) {
UserPasswordAuth(
session = params.session,
user = userId,
password = params.accountPassword
)
} else null
)
apiCall = profileAPI.finalizeAddThreePid(body)
}
} catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
}
// Delete the pending three pid
monarchy.awaitTransaction { realm ->
realm.where(PendingThreePidEntity::class.java)
.equalTo(PendingThreePidEntityFields.EMAIL, params.threePid.value)
.or()
.equalTo(PendingThreePidEntityFields.MSISDN, params.threePid.value)
.findAll()
.deleteAllFromRealm()
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 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 org.matrix.android.sdk.internal.session.profile
import org.matrix.android.sdk.api.session.identity.ThreePid
internal data class PendingThreePid(
val threePid: ThreePid,
val clientSecret: String,
val sendAttempt: Int,
val sid: String
)

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 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 org.matrix.android.sdk.internal.session.profile
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import javax.inject.Inject
internal class PendingThreePidMapper @Inject constructor() {
fun map(entity: PendingThreePidEntity): PendingThreePid {
return PendingThreePid(
threePid = entity.email?.let { ThreePid.Email(it) }
?: entity.msisdn?.let { ThreePid.Msisdn(it) }
?: error("Invalid data"),
clientSecret = entity.clientSecret,
sendAttempt = entity.sendAttempt,
sid = entity.sid
)
}
fun map(domain: PendingThreePid): PendingThreePidEntity {
return PendingThreePidEntity(
email = domain.threePid.takeIf { it is ThreePid.Email }?.value,
msisdn = domain.threePid.takeIf { it is ThreePid.Msisdn }?.value,
clientSecret = domain.clientSecret,
sendAttempt = domain.sendAttempt,
sid = domain.sid
)
}
}

View File

@ -28,7 +28,6 @@ import retrofit2.http.PUT
import retrofit2.http.Path
internal interface ProfileAPI {
/**
* Get the combined profile information for this user.
* This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers.
@ -71,4 +70,22 @@ internal interface ProfileAPI {
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind")
fun unbindThreePid(@Body body: UnbindThreePidBody): Call<UnbindThreePidResponse>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-email-requesttoken
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken")
fun addEmail(@Body body: AddEmailBody): Call<AddThreePidResponse>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-add
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add")
fun finalizeAddThreePid(@Body body: FinalizeAddThreePidBody): Call<Unit>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-delete
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete")
fun deleteThreePid(@Body body: DeleteThreePidBody): Call<DeleteThreePidResponse>
}

View File

@ -58,4 +58,13 @@ internal abstract class ProfileModule {
@Binds
abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask
@Binds
abstract fun bindAddThreePidTask(task: DefaultAddThreePidTask): AddThreePidTask
@Binds
abstract fun bindFinalizeAddingThreePidTask(task: DefaultFinalizeAddingThreePidTask): FinalizeAddingThreePidTask
@Binds
abstract fun bindDeleteThreePidTask(task: DefaultDeleteThreePidTask): DeleteThreePidTask
}

View File

@ -103,6 +103,7 @@ import im.vector.app.features.settings.ignored.VectorSettingsIgnoredUsersFragmen
import im.vector.app.features.settings.locale.LocalePickerFragment
import im.vector.app.features.settings.push.PushGatewaysFragment
import im.vector.app.features.settings.push.PushRulesFragment
import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
import im.vector.app.features.share.IncomingShareFragment
import im.vector.app.features.signout.soft.SoftLogoutFragment
import im.vector.app.features.terms.ReviewTermsFragment
@ -313,6 +314,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsDevicesFragment::class)
fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
@Binds
@IntoMap
@FragmentKey(ThreePidsSettingsFragment::class)
fun bindThreePidsSettingsFragment(fragment: ThreePidsSettingsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(PublicRoomsFragment::class)

View File

@ -87,6 +87,10 @@ class DefaultErrorFormatter @Inject constructor(
throwable.error.code == MatrixError.M_USER_DEACTIVATED -> {
stringProvider.getString(R.string.auth_invalid_login_deactivated_account)
}
throwable.error.code == MatrixError.M_THREEPID_IN_USE
&& throwable.error.message == "Email is already in use" -> {
stringProvider.getString(R.string.account_email_already_used_error)
}
else -> {
throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() }

View File

@ -70,6 +70,9 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
@EpoxyAttribute
var buttonAction: Action? = null
@EpoxyAttribute
var destructiveButtonAction: Action? = null
@EpoxyAttribute
var itemClickAction: Action? = null
@ -109,6 +112,11 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
buttonAction?.perform?.run()
}
holder.destructiveButton.setTextOrHide(destructiveButtonAction?.title)
holder.destructiveButton.setOnClickListener {
destructiveButtonAction?.perform?.run()
}
holder.root.setOnClickListener {
itemClickAction?.perform?.run()
}
@ -122,5 +130,6 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
val accessoryImage by bind<ImageView>(R.id.item_generic_accessory_image)
val progressBar by bind<ProgressBar>(R.id.item_generic_progress_bar)
val actionButton by bind<Button>(R.id.item_generic_action_button)
val destructiveButton by bind<Button>(R.id.item_generic_destructive_action_button)
}
}

View File

@ -23,13 +23,11 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.util.Patterns
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.preference.EditTextPreference
@ -54,13 +52,11 @@ import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.app.core.utils.TextUtils
import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.getSizeOfFiles
import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.media.createUCropWithDefaultSettings
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.SignOutUiWorker
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.entity.MultiPickerImageType
@ -187,44 +183,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
mPasswordPreference.isVisible = false
}
// Add Email
findPreference<EditTextPreference>(ADD_EMAIL_PREFERENCE_KEY)!!.let {
// It does not work on XML, do it here
it.icon = activity?.let {
ThemeUtils.tintDrawable(it,
ContextCompat.getDrawable(it, R.drawable.ic_material_add)!!, R.attr.colorAccent)
}
// Unfortunately, this is not supported in lib v7
// it.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
it.setOnPreferenceClickListener {
notImplemented()
true
}
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
notImplemented()
// addEmail((newValue as String).trim())
false
}
}
// Add phone number
findPreference<VectorPreference>(ADD_PHONE_NUMBER_PREFERENCE_KEY)!!.let {
// It does not work on XML, do it here
it.icon = activity?.let {
ThemeUtils.tintDrawable(it,
ContextCompat.getDrawable(it, R.drawable.ic_material_add)!!, R.attr.colorAccent)
}
it.setOnPreferenceClickListener {
notImplemented()
// TODO val intent = PhoneNumberAdditionActivity.getIntent(activity, session.credentials.userId)
// startActivityForResult(intent, REQUEST_NEW_PHONE_NUMBER)
true
}
}
// Advanced settings
// user account
@ -235,8 +193,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_HOME_SERVER_PREFERENCE_KEY)!!
.summary = session.sessionParams.homeServerUrl
refreshEmailsList()
refreshPhoneNumbersList()
// Contacts
setContactsPreferences()
@ -533,295 +489,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
* Refresh phone number list
*/
private fun refreshPhoneNumbersList() {
/* TODO
val currentPhoneNumber3PID = ArrayList(session.myUser.getlinkedPhoneNumbers())
val phoneNumberList = ArrayList<String>()
for (identifier in currentPhoneNumber3PID) {
phoneNumberList.add(identifier.address)
}
// check first if there is an update
var isNewList = true
if (phoneNumberList.size == mDisplayedPhoneNumber.size) {
isNewList = !mDisplayedPhoneNumber.containsAll(phoneNumberList)
}
if (isNewList) {
// remove the displayed one
run {
var index = 0
while (true) {
val preference = mUserSettingsCategory.findPreference(PHONE_NUMBER_PREFERENCE_KEY_BASE + index)
if (null != preference) {
mUserSettingsCategory.removePreference(preference)
} else {
break
}
index++
}
}
// add new phone number list
mDisplayedPhoneNumber = phoneNumberList
val addPhoneBtn = mUserSettingsCategory.findPreference(ADD_PHONE_NUMBER_PREFERENCE_KEY)
?: return
var order = addPhoneBtn.order
for ((index, phoneNumber3PID) in currentPhoneNumber3PID.withIndex()) {
val preference = VectorPreference(activity!!)
preference.title = getString(R.string.settings_phone_number)
var phoneNumberFormatted = phoneNumber3PID.address
try {
// Attempt to format phone number
val phoneNumber = PhoneNumberUtil.getInstance().parse("+$phoneNumberFormatted", null)
phoneNumberFormatted = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
} catch (e: NumberParseException) {
// Do nothing, we will display raw version
}
preference.summary = phoneNumberFormatted
preference.key = PHONE_NUMBER_PREFERENCE_KEY_BASE + index
preference.order = order
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
displayDelete3PIDConfirmationDialog(phoneNumber3PID, preference.summary)
true
}
preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
override fun onPreferenceLongClick(preference: Preference): Boolean {
activity?.let { copyToClipboard(it, phoneNumber3PID.address) }
return true
}
}
order++
mUserSettingsCategory.addPreference(preference)
}
addPhoneBtn.order = order
} */
}
// ==============================================================================================================
// Email management
// ==============================================================================================================
/**
* Refresh the emails list
*/
private fun refreshEmailsList() {
val currentEmail3PID = emptyList<String>() // TODO ArrayList(session.myUser.getlinkedEmails())
val newEmailsList = ArrayList<String>()
for (identifier in currentEmail3PID) {
// TODO newEmailsList.add(identifier.address)
}
// check first if there is an update
var isNewList = true
if (newEmailsList.size == mDisplayedEmails.size) {
isNewList = !mDisplayedEmails.containsAll(newEmailsList)
}
if (isNewList) {
// remove the displayed one
run {
var index = 0
while (true) {
val preference = mUserSettingsCategory.findPreference<VectorPreference>(EMAIL_PREFERENCE_KEY_BASE + index)
if (null != preference) {
mUserSettingsCategory.removePreference(preference)
} else {
break
}
index++
}
}
// add new emails list
mDisplayedEmails = newEmailsList
val addEmailBtn = mUserSettingsCategory.findPreference<VectorPreference>(ADD_EMAIL_PREFERENCE_KEY) ?: return
var order = addEmailBtn.order
for ((index, email3PID) in currentEmail3PID.withIndex()) {
val preference = VectorPreference(requireActivity())
preference.title = getString(R.string.settings_email_address)
preference.summary = "TODO" // email3PID.address
preference.key = EMAIL_PREFERENCE_KEY_BASE + index
preference.order = order
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { pref ->
displayDelete3PIDConfirmationDialog(/* TODO email3PID, */ pref.summary)
true
}
preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
override fun onPreferenceLongClick(preference: Preference): Boolean {
activity?.let { copyToClipboard(it, "TODO") } // email3PID.address) }
return true
}
}
mUserSettingsCategory.addPreference(preference)
order++
}
addEmailBtn.order = order
}
}
/**
* Attempt to add a new email to the account
*
* @param email the email to add.
*/
private fun addEmail(email: String) {
// check first if the email syntax is valid
// if email is null , then also its invalid email
if (email.isBlank() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
activity?.toast(R.string.auth_invalid_email)
return
}
// check first if the email syntax is valid
if (mDisplayedEmails.indexOf(email) >= 0) {
activity?.toast(R.string.auth_email_already_defined)
return
}
notImplemented()
/* TODO
val pid = ThreePid(email, ThreePid.MEDIUM_EMAIL)
displayLoadingView()
session.myUser.requestEmailValidationToken(pid, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
activity?.runOnUiThread { showEmailValidationDialog(pid) }
}
override fun onNetworkError(e: Exception) {
onCommonDone(e.localizedMessage)
}
override fun onMatrixError(e: MatrixError) {
if (TextUtils.equals(MatrixError.THREEPID_IN_USE, e.errcode)) {
onCommonDone(getString(R.string.account_email_already_used_error))
} else {
onCommonDone(e.localizedMessage)
}
}
override fun onUnexpectedError(e: Exception) {
onCommonDone(e.localizedMessage)
}
})
*/
}
/**
* Show an email validation dialog to warn the user tho valid his email link.
*
* @param pid the used pid.
*/
/* TODO
private fun showEmailValidationDialog(pid: ThreePid) {
activity?.let {
AlertDialog.Builder(it)
.setTitle(R.string.account_email_validation_title)
.setMessage(R.string.account_email_validation_message)
.setPositiveButton(R.string._continue) { _, _ ->
session.myUser.add3Pid(pid, true, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
it.runOnUiThread {
hideLoadingView()
refreshEmailsList()
}
}
override fun onNetworkError(e: Exception) {
onCommonDone(e.localizedMessage)
}
override fun onMatrixError(e: MatrixError) {
if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) {
it.runOnUiThread {
hideLoadingView()
it.toast(R.string.account_email_validation_error)
}
} else {
onCommonDone(e.localizedMessage)
}
}
override fun onUnexpectedError(e: Exception) {
onCommonDone(e.localizedMessage)
}
})
}
.setNegativeButton(R.string.cancel) { _, _ ->
hideLoadingView()
}
.show()
}
} */
/**
* Display a dialog which asks confirmation for the deletion of a 3pid
*
* @param pid the 3pid to delete
* @param preferenceSummary the displayed 3pid
*/
private fun displayDelete3PIDConfirmationDialog(/* TODO pid: ThirdPartyIdentifier,*/ preferenceSummary: CharSequence) {
val mediumFriendlyName = "TODO" // ThreePid.getMediumFriendlyName(pid.medium, activity).toLowerCase(VectorLocale.applicationLocale)
val dialogMessage = getString(R.string.settings_delete_threepid_confirmation, mediumFriendlyName, preferenceSummary)
activity?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_confirmation)
.setMessage(dialogMessage)
.setPositiveButton(R.string.remove) { _, _ ->
notImplemented()
/* TODO
displayLoadingView()
session.myUser.delete3Pid(pid, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
when (pid.medium) {
ThreePid.MEDIUM_EMAIL -> refreshEmailsList()
ThreePid.MEDIUM_MSISDN -> refreshPhoneNumbersList()
}
onCommonDone(null)
}
override fun onNetworkError(e: Exception) {
onCommonDone(e.localizedMessage)
}
override fun onMatrixError(e: MatrixError) {
onCommonDone(e.localizedMessage)
}
override fun onUnexpectedError(e: Exception) {
onCommonDone(e.localizedMessage)
}
})
*/
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
/**
@ -985,12 +652,6 @@ private fun showEmailValidationDialog(pid: ThreePid) {
}
companion object {
private const val ADD_EMAIL_PREFERENCE_KEY = "ADD_EMAIL_PREFERENCE_KEY"
private const val ADD_PHONE_NUMBER_PREFERENCE_KEY = "ADD_PHONE_NUMBER_PREFERENCE_KEY"
private const val EMAIL_PREFERENCE_KEY_BASE = "EMAIL_PREFERENCE_KEY_BASE"
private const val PHONE_NUMBER_PREFERENCE_KEY_BASE = "PHONE_NUMBER_PREFERENCE_KEY_BASE"
private const val REQUEST_NEW_PHONE_NUMBER = 456
private const val REQUEST_PHONEBOOK_COUNTRY = 789
}

View File

@ -28,6 +28,11 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
@ -41,11 +46,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.util.awaitCallback
import im.vector.app.core.platform.VectorViewModel
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.launch
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
import java.util.concurrent.TimeUnit
@ -309,7 +309,7 @@ class DevicesViewModel @AssistedInject constructor(
}
if (!isPasswordRequestFound) {
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
setState {
copy(
request = Fail(failure)

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 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.app.features.settings.threepids
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.identity.ThreePid
sealed class ThreePidsSettingsAction : VectorViewModelAction {
data class AddThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
data class ContinueThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
data class AccountPassword(val password: String) : ThreePidsSettingsAction()
data class DeleteThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
}

View File

@ -0,0 +1,145 @@
/*
* Copyright (c) 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.app.features.settings.threepids
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import im.vector.app.R
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.GenericItem
import im.vector.app.core.ui.list.genericButtonItem
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericItem
import im.vector.app.features.discovery.settingsSectionTitleItem
import org.matrix.android.sdk.api.session.identity.ThreePid
import javax.inject.Inject
class ThreePidsSettingsController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider
) : TypedEpoxyController<ThreePidsSettingsViewState>() {
interface InteractionListener {
fun addEmail()
fun addMsisdn()
fun continueThreePid(threePid: ThreePid)
fun deleteThreePid(threePid: ThreePid)
}
var interactionListener: InteractionListener? = null
override fun buildModels(data: ThreePidsSettingsViewState?) {
if (data == null) return
when (data.threePids) {
is Loading -> {
loadingItem {
id("loading")
loadingText(stringProvider.getString(R.string.loading))
}
}
is Fail -> {
genericFooterItem {
id("fail")
text(data.threePids.error.localizedMessage)
}
}
is Success -> {
val dataList = data.threePids.invoke()
buildThreePids(dataList, data.pendingThreePids)
}
}
}
private fun buildThreePids(list: List<ThreePid>, pendingThreePids: Async<List<ThreePid>>) {
val splited = list.groupBy { it is ThreePid.Email }
val emails = splited[true].orEmpty()
val msisdn = splited[false].orEmpty()
settingsSectionTitleItem {
id("email")
title(stringProvider.getString(R.string.settings_emails))
}
emails.forEach { buildThreePid("email_", it) }
// Pending threePids
pendingThreePids.invoke()
?.filterIsInstance(ThreePid.Email::class.java)
?.forEach { buildPendingThreePid("email_", it) }
genericButtonItem {
id("addEmail")
text(stringProvider.getString(R.string.settings_add_email_address))
textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { interactionListener?.addEmail() })
}
settingsSectionTitleItem {
id("msisdn")
title(stringProvider.getString(R.string.settings_phone_numbers))
}
msisdn.forEach { buildThreePid("msisdn_", it) }
// Pending threePids
pendingThreePids.invoke()
?.filterIsInstance(ThreePid.Msisdn::class.java)
?.forEach { buildPendingThreePid("msisdn_", it) }
genericButtonItem {
id("addMsisdn")
text(stringProvider.getString(R.string.settings_add_phone_number))
textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { interactionListener?.addMsisdn() })
}
}
private fun buildThreePid(idPrefix: String, threePid: ThreePid) {
genericItem {
id(idPrefix + threePid.value)
title(threePid.value)
destructiveButtonAction(
GenericItem.Action(stringProvider.getString(R.string.remove))
.apply {
perform = Runnable { interactionListener?.deleteThreePid(threePid) }
}
)
}
}
private fun buildPendingThreePid(idPrefix: String, threePid: ThreePid) {
genericItem {
id(idPrefix + threePid.value)
title(threePid.value)
if (threePid is ThreePid.Email) {
description(stringProvider.getString(R.string.account_email_validation_message))
}
buttonAction(
GenericItem.Action(stringProvider.getString(R.string._continue))
.apply {
perform = Runnable { interactionListener?.continueThreePid(threePid) }
}
)
}
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 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.app.features.settings.threepids
import android.content.DialogInterface
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.dialogs.PromptPasswordDialog
import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.isEmail
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import org.matrix.android.sdk.api.session.identity.ThreePid
import javax.inject.Inject
class ThreePidsSettingsFragment @Inject constructor(
private val viewModelFactory: ThreePidsSettingsViewModel.Factory,
private val epoxyController: ThreePidsSettingsController
) :
VectorBaseFragment(),
ThreePidsSettingsViewModel.Factory by viewModelFactory,
ThreePidsSettingsController.InteractionListener {
private val viewModel: ThreePidsSettingsViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_generic_recycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController)
epoxyController.interactionListener = this
viewModel.observeViewEvents {
when (it) {
is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable)
ThreePidsSettingsViewEvents.RequestPassword -> askUserPassword()
}.exhaustive
}
}
private fun askUserPassword() {
PromptPasswordDialog().show(requireActivity()) { password ->
viewModel.handle(ThreePidsSettingsAction.AccountPassword(password))
}
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
epoxyController.interactionListener = null
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_emails_and_phone_numbers_title)
}
override fun invalidate() = withState(viewModel) { state ->
if (state.isLoading) {
showLoadingDialog()
} else {
dismissLoadingDialog()
}
epoxyController.setData(state)
}
override fun addEmail() {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.editText)
input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_add_email_address)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
val email = input.text.toString()
doAddEmail(email)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun doAddEmail(email: String) {
// Check that email is valid
if (!email.isEmail()) {
requireActivity().toast(R.string.auth_invalid_email)
return
}
viewModel.handle(ThreePidsSettingsAction.AddThreePid(ThreePid.Email(email)))
}
override fun addMsisdn() {
TODO("Not yet implemented")
}
override fun continueThreePid(threePid: ThreePid) {
viewModel.handle(ThreePidsSettingsAction.ContinueThreePid(threePid))
}
override fun deleteThreePid(threePid: ThreePid) {
AlertDialog.Builder(requireActivity())
.setMessage(getString(R.string.settings_remove_three_pid_confirmation_content, threePid.value))
.setPositiveButton(R.string.remove) { _, _ ->
viewModel.handle(ThreePidsSettingsAction.DeleteThreePid(threePid))
}
.setNegativeButton(R.string.cancel, null)
.show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 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.app.features.settings.threepids
import im.vector.app.core.platform.VectorViewEvents
sealed class ThreePidsSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : ThreePidsSettingsViewEvents()
object RequestPassword : ThreePidsSettingsViewEvents()
}

View File

@ -0,0 +1,168 @@
/*
* Copyright (c) 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.app.features.settings.threepids
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
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.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.rx.rx
class ThreePidsSettingsViewModel @AssistedInject constructor(
@Assisted initialState: ThreePidsSettingsViewState,
private val session: Session
) : VectorViewModel<ThreePidsSettingsViewState, ThreePidsSettingsAction, ThreePidsSettingsViewEvents>(initialState) {
// UIA session
private var pendingThreePid: ThreePid? = null
private var pendingSession: String? = null
private val loadingCallback: MatrixCallback<Unit> = object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
isLoading(false)
if (failure is Failure.RegistrationFlowError) {
var isPasswordRequestFound = false
// We only support LoginFlowTypes.PASSWORD
// Check if we can provide the user password
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
}
if (isPasswordRequestFound) {
pendingSession = failure.registrationFlowResponse.session
_viewEvents.post(ThreePidsSettingsViewEvents.RequestPassword)
} else {
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure))
}
} else {
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure))
}
}
override fun onSuccess(data: Unit) {
pendingThreePid = null
pendingSession = null
isLoading(false)
}
}
private fun isLoading(isLoading: Boolean) {
setState {
copy(
isLoading = isLoading
)
}
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: ThreePidsSettingsViewState): ThreePidsSettingsViewModel
}
companion object : MvRxViewModelFactory<ThreePidsSettingsViewModel, ThreePidsSettingsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: ThreePidsSettingsViewState): ThreePidsSettingsViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
init {
observeThreePids()
observePendingThreePids()
}
private fun observeThreePids() {
session.rx()
.liveThreePIds(true)
.execute {
copy(
threePids = it
)
}
}
private fun observePendingThreePids() {
session.rx()
.livePendingThreePIds()
.execute {
copy(
pendingThreePids = it
)
}
}
override fun handle(action: ThreePidsSettingsAction) {
when (action) {
is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action)
is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action)
is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action)
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
}.exhaustive
}
private fun handleAddThreePid(action: ThreePidsSettingsAction.AddThreePid) {
isLoading(true)
viewModelScope.launch {
session.addThreePid(action.threePid, loadingCallback)
}
}
private fun handleContinueThreePid(action: ThreePidsSettingsAction.ContinueThreePid) {
isLoading(true)
pendingThreePid = action.threePid
viewModelScope.launch {
session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback)
}
}
private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) {
val safeSession = pendingSession ?: return Unit
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending session"))) }
val safeThreePid = pendingThreePid ?: return Unit
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) }
isLoading(true)
viewModelScope.launch {
session.finalizeAddingThreePid(safeThreePid, safeSession, action.password, loadingCallback)
}
}
private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) {
isLoading(true)
viewModelScope.launch {
session.deleteThreePid(action.threePid, loadingCallback)
}
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 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.app.features.settings.threepids
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.identity.ThreePid
data class ThreePidsSettingsViewState(
val isLoading: Boolean = false,
val threePids: Async<List<ThreePid>> = Uninitialized,
val pendingThreePids: Async<List<ThreePid>> = Uninitialized
) : MvRxState

View File

@ -91,7 +91,7 @@
app:layout_constraintTop_toTopOf="@+id/item_generic_title_text"
tools:visibility="visible" />
<!-- Set a maw width because the text can be long -->
<!-- Set a max width because the text can be long -->
<com.google.android.material.button.MaterialButton
android:id="@+id/item_generic_action_button"
style="@style/VectorButtonStyle"
@ -102,10 +102,26 @@
android:layout_marginBottom="16dp"
android:maxWidth="@dimen/button_max_width"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/item_generic_destructive_action_button"
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
app:layout_constraintTop_toBottomOf="@+id/item_generic_description_text"
tools:text="@string/settings_troubleshoot_test_device_settings_quickfix"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/item_generic_destructive_action_button"
style="@style/VectorButtonStyleDestructive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:maxWidth="@dimen/button_max_width"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
app:layout_constraintTop_toBottomOf="@+id/item_generic_action_button"
tools:text="@string/delete"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -682,6 +682,9 @@
<string name="settings_add_3pid_flow_not_supported">You can\'t do this from Element mobile</string>
<string name="settings_add_3pid_authentication_needed">Authentication is required</string>
<string name="settings_emails">Email addresses</string>
<string name="settings_phone_numbers">Phone numbers</string>
<string name="settings_remove_three_pid_confirmation_content">Remove %s?</string>
<string name="settings_notification_advanced">Advanced Notification Settings</string>
<string name="settings_notification_by_event">Notification importance by event</string>
@ -927,6 +930,9 @@
<string name="settings_unignore_user">Show all messages from %s?\n\nNote that this action will restart the app and it may take some time.</string>
<string name="passwords_do_not_match">Passwords do not match</string>
<string name="settings_emails_and_phone_numbers_title">Emails and phone numbers</string>
<string name="settings_emails_and_phone_numbers_summary">Manage emails and phone numbers linked to your Matrix account</string>
<string name="settings_delete_notification_targets_confirmation">Are you sure you want to remove this notification target?</string>
<string name="settings_delete_threepid_confirmation">Are you sure you want to remove the %1$s %2$s?</string>

View File

@ -22,33 +22,11 @@
android:summary="@string/change_password_summary"
android:title="@string/settings_password" />
<!-- Email will be added here -->
<!-- Note: inputType does not work, it is set also in code, as well as iconTint -->
<im.vector.app.core.preference.VectorEditTextPreference
android:icon="@drawable/ic_material_add"
android:inputType="textEmailAddress"
android:key="ADD_EMAIL_PREFERENCE_KEY"
android:order="100"
android:title="@string/settings_add_email_address"
app:iconTint="@color/riotx_accent" />
<!-- Phone will be added here -->
<!-- Note: iconTint does not work, it is also done in code -->
<im.vector.app.core.preference.VectorPreference
android:icon="@drawable/ic_material_add"
android:key="ADD_PHONE_NUMBER_PREFERENCE_KEY"
android:order="200"
android:title="@string/settings_add_phone_number"
app:iconTint="@color/riotx_accent" />
<im.vector.app.core.preference.VectorPreference
android:order="1000"
android:persistent="false"
android:summary="@string/settings_discovery_manage"
android:title="@string/settings_discovery_category"
app:fragment="im.vector.app.features.discovery.DiscoverySettingsFragment" />
android:key="SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY"
android:summary="@string/settings_emails_and_phone_numbers_summary"
android:title="@string/settings_emails_and_phone_numbers_title"
app:fragment="im.vector.app.features.settings.threepids.ThreePidsSettingsFragment" />
</im.vector.app.core.preference.VectorPreferenceCategory>