diff --git a/CHANGES.md b/CHANGES.md index 1ffb6bcad0..b96337097b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/docs/add_email.md b/docs/add_email.md index 64227418a3..06b01b3026 100644 --- a/docs/add_email.md +++ b/docs/add_email.md @@ -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", diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 45efd125ee..55ede52c0c 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt index 449c670983..95f142e877 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 7d2a4ea581..00c4fdac84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -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) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt new file mode 100644 index 0000000000..bf2f11dedd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index ea466db352..2c45cfcdbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule RoomSummaryEntity::class, RoomTagEntity::class, SyncEntity::class, + PendingThreePidEntity::class, UserEntity::class, IgnoredUserEntity::class, BreadcrumbsEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt new file mode 100644 index 0000000000..ff81ad6a5c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidResponse.kt new file mode 100644 index 0000000000..109b3d5343 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidResponse.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt new file mode 100644 index 0000000000..75c829c785 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt @@ -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) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 633b047994..cd81496814 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt new file mode 100644 index 0000000000..e7d4568f8b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt new file mode 100644 index 0000000000..3817277a9d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt new file mode 100644 index 0000000000..69ff7d82da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt @@ -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 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt new file mode 100644 index 0000000000..73e9b39cea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt @@ -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? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt new file mode 100644 index 0000000000..e6f4141ab1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt @@ -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() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt new file mode 100644 index 0000000000..d9c3a5d656 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt @@ -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 +) + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt new file mode 100644 index 0000000000..c9d6381bd3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt @@ -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 + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt index 31e1f09bbd..125b1a47e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt @@ -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> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt index 57a86d03e0..baeaf9fd58 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 591d1c0474..d0e4c938cd 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 051847321a..14939eaff9 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -59,35 +59,39 @@ class DefaultErrorFormatter @Inject constructor( } is Failure.ServerError -> { when { - throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { + throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { // Special case for terms and conditions stringProvider.getString(R.string.error_terms_not_accepted) } - throwable.isInvalidPassword() -> { + throwable.isInvalidPassword() -> { stringProvider.getString(R.string.auth_invalid_login_param) } - throwable.error.code == MatrixError.M_USER_IN_USE -> { + throwable.error.code == MatrixError.M_USER_IN_USE -> { stringProvider.getString(R.string.login_signup_error_user_in_use) } - throwable.error.code == MatrixError.M_BAD_JSON -> { + throwable.error.code == MatrixError.M_BAD_JSON -> { stringProvider.getString(R.string.login_error_bad_json) } - throwable.error.code == MatrixError.M_NOT_JSON -> { + throwable.error.code == MatrixError.M_NOT_JSON -> { stringProvider.getString(R.string.login_error_not_json) } - throwable.error.code == MatrixError.M_THREEPID_DENIED -> { + throwable.error.code == MatrixError.M_THREEPID_DENIED -> { stringProvider.getString(R.string.login_error_threepid_denied) } - throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { + throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { limitExceededError(throwable.error) } - throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { + throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { stringProvider.getString(R.string.login_reset_password_error_not_found) } - throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { + throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { stringProvider.getString(R.string.auth_invalid_login_deactivated_account) } - else -> { + 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() } } diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt index 832250fab4..492df9eb00 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index e31b81b162..c4df3a8d6e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index 237c8c218d..37a0d392a1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt new file mode 100644 index 0000000000..07ce6b6744 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt new file mode 100644 index 0000000000..dc59dd4ad2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt @@ -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) } + } + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt new file mode 100644 index 0000000000..875237bdc4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt new file mode 100644 index 0000000000..1ac2d10458 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt new file mode 100644 index 0000000000..85eb855569 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt new file mode 100644 index 0000000000..10ac51f229 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt @@ -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 diff --git a/vector/src/main/res/layout/item_generic_list.xml b/vector/src/main/res/layout/item_generic_list.xml index 4b206c352d..f89413f15f 100644 --- a/vector/src/main/res/layout/item_generic_list.xml +++ b/vector/src/main/res/layout/item_generic_list.xml @@ -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> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 7506a4d502..c22dfc6be8 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -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> diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index 042ea5c77c..5a9d016842 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -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>