From 3673520ef69cc13e1112af57aa18dd177c9f3770 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 18 Nov 2019 13:30:03 +0000 Subject: [PATCH 001/189] Small typo fix --- vector/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 83ce65783f..231575ff2b 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1518,7 +1518,7 @@ Why choose Riot.im? Welcome home! Catch up on unread messages here Conversations - Your direct message conversation will be displayed here + Your direct message conversations will be displayed here Rooms Your rooms will be displayed here From ec2954200e0b24eef556b7298fcb2ded165b5b8c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 09:58:48 +0100 Subject: [PATCH 002/189] Version++ --- CHANGES.md | 21 +++++++++++++++++++++ vector/build.gradle | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 33ba41e778..ed85d018a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,24 @@ +Changes in RiotX 0.9.0 (2019-XX-XX) +=================================================== + +Features ✨: + - + +Improvements 🙌: + - + +Other changes: + - + +Bugfix 🐛: + - + +Translations 🗣: + - + +Build 🧱: + - + Changes in RiotX 0.8.0 (2019-11-19) =================================================== diff --git a/vector/build.gradle b/vector/build.gradle index e425d53a62..ed489f84ac 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 8 +ext.versionMinor = 9 ext.versionPatch = 0 static def getGitTimestamp() { From 507134407bdb12730cab50d622da3c22f4582fdf Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 19 Nov 2019 15:10:41 +0000 Subject: [PATCH 003/189] Update CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ed85d018a3..6ad2dbf390 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - Other changes: - - + - Fix a small grammatical error when an empty room list is shown. Bugfix 🐛: - From 2e87e0b4c172fa143f800cff7a9c64bf926091c3 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 21 Nov 2019 01:41:59 +0000 Subject: [PATCH 004/189] fix typo --- .../internal/session/room/EventRelationsAggregationTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index 224b3bcfeb..3d7c5df5fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -298,7 +298,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } } else { - Timber.e("Unknwon relation type ${content.relatesTo?.type} for event ${event.eventId}") + Timber.e("Unknown relation type ${content.relatesTo?.type} for event ${event.eventId}") } } From 8b63f78d7647b62fd2aa85db02b0773db34b3164 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 12 Nov 2019 15:31:46 +0100 Subject: [PATCH 005/189] Add documentation on the sign up flow --- docs/signup.md | 395 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 docs/signup.md diff --git a/docs/signup.md b/docs/signup.md new file mode 100644 index 0000000000..7d35f6b19b --- /dev/null +++ b/docs/signup.md @@ -0,0 +1,395 @@ +# Sign up to a homeserver + +This document describes the flow of registration to a homeserver. Examples come from the matrix.org homeserver, and the logs come from Riot-Android. + +Note that it contains bugs: + - "password" and "initial_device_display_name" values are sent a bit too much + - the first received "sessionId" is not reused + - The order of stages returned by the homeserver is not strictly followed + +Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management + +## Sign up flows + +### First step + +Client request the sign-up flows, once the homeserver is chosen by the user and its url is knwon (in the example it's https://matrix.org) + +> curl -X POST --data $'{"initial_device_display_name":"Mobile device","x_show_msisdn":true}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "initial_device_display_name": "Mobile device", + "x_show_msisdn": true +} +``` + +401 + +```json +{ + "session": "vwehdKMtkRedactedAMwgCACZ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + } +} +``` + +### Step 1: entering user name and password + +The app is displaying a form with login and password. Only the login is sent for the first request + +> curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "initial_device_display_name": "Mobile device", + "username": "alice" +} +``` + +401 + +```json +{ + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + } +} +``` + +#### If username already exists + +```json +{ + "errcode": "M_USER_IN_USE", + "error": "User ID already taken." +} +``` + +### Step 2: entering email + +User is proposed to enter an email. We skip this step. + +> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"},"initial_device_display_name":"Mobile device","password":"azerty","username":"alice"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "type": "m.login.dummy" + }, + "initial_device_display_name": "Mobile device", + "password": "password_REDACTED", + "username":"alice" +} +``` + +401 + +```json +{ + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + }, + "completed": [ + "m.login.dummy" + ] +} +``` + +### Step 2 bis: we enter an email + +> curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@yopmail.com","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken' + +```json +{ + "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", + "email": "alice@yopmail.com", + "send_attempt": 0 +} +``` + +200 + +```json +{ + "sid": "qlBCREDACTEDEtgxD" +} +``` + +And + +> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"},"initial_device_display_name":"Mobile device","password":"password_REDACTED","username":"alice"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "threepid_creds": { + "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", + "sid": "qlBCREDACTEDEtgxD" + }, + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "type": "m.login.email.identity" + }, + "initial_device_display_name": "Mobile device", + "password": "password_REDACTED", + "username": "alice" +} +``` + +401 + +```json +{ + "errcode": "M_UNAUTHORIZED", + "error": "" +} +``` + +The app is now polling on + +> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"},"initial_device_display_name":"Mobile device","password":"password_REDACTED","username":"alice"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "threepid_creds": { + "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", + "sid": "qlBCREDACTEDEtgxD" + }, + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "type": "m.login.email.identity" + }, + "initial_device_display_name": "Mobile device", + "password": "password_REDACTED", + "username": "alice" +} +``` + +We click on the link received by email https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD which contains: +- A token vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ +- a client secret: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa +- A sid: qlBCREDACTEDEtgxD + +Once the link is clicked, the registration request (polling) returns a 401 with the following content: + +```json +{ + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + }, + "completed": [ + "m.login.email.identity" + ] +} +``` + +### Step 3: Accepting T&C + +User is proposed to accept T&C and he accepts them + +> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"},"initial_device_display_name":"Mobile device"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "type": "m.login.terms" + }, + "initial_device_display_name": "Mobile device" +} +``` + +401 + +```json +{ + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + }, + "completed": [ + "m.login.dummy", + "m.login.terms" + ] +} +``` + +### Step 4: Captcha + +User is proposed to prove he is not a robot and he does it: + +> curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"iLHmdwNlXZoREDACTEDoouwMi","type":"m.login.recaptcha"},"initial_device_display_name":"Mobile device"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "response": "03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q", + "session": "iLHmdwNlXZoREDACTEDoouwMi", + "type": "m.login.recaptcha" + }, + "initial_device_display_name": "Mobile device" +} +``` + +200 + +```json +{ + "user_id": "@alice:matrix.org", + "home_server": "matrix.org", + "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmcKMoREDACTEDo50aWZpZXIga2V5CjAwMTBjaWQgZ2VuID0gMQowMDI5Y2lkIHVzZXJfaWQgPSBAYmVub2l0eHh4eDptYXRoREDACTEDoCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNHVSVm00aVFDaWlKdoREDACTEDoJmc2lnbmF0dXJlIOmHnTLRfxiPjhrWhS-dThUX-qAzZktfRThzH1YyAsxaCg", + "device_id": "FLBAREDAJZ" +} +``` + +The account is created! From 4485d1c68544845f0bcf3e8dbaba1f5cef0d7150 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 13 Nov 2019 18:40:17 +0100 Subject: [PATCH 006/189] Registration flow: SDK side --- .idea/dictionaries/bmarty.xml | 2 + .../android/api/auth/data/Credentials.kt | 3 +- .../auth/registration/RegistrationService.kt | 25 +++++ .../auth/registration/RegistrationWizard.kt | 31 +++++ .../android/api/auth/registration/Stage.kt | 51 +++++++++ .../matrix/android/api/util/Cancelable.kt | 3 + .../matrix/android/internal/auth/AuthAPI.kt | 8 ++ .../internal/auth/registration/AuthParams.kt | 32 ++++++ .../auth/registration/AuthParamsCaptcha.kt | 30 +++++ .../registration/AuthParamsEmailIdentity.kt | 40 +++++++ .../DefaultRegistrationService.kt | 46 ++++++++ .../registration/DefaultRegistrationWizard.kt | 106 ++++++++++++++++++ .../LocalizedFlowDataLoginTerms.kt | 31 +++++ .../auth/registration/RegisterTask.kt | 63 +++++++++++ .../auth/registration/RegistrationParams.kt | 47 ++++++++ 15 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 01981ada12..784e5c1182 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -4,6 +4,7 @@ backstack bytearray ciphertext + coroutine decryptor emoji emojis @@ -12,6 +13,7 @@ linkified linkify megolm + msisdn pbkdf pkcs diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index d5962e261b..089129967b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -30,4 +30,5 @@ data class Credentials( @Json(name = "home_server") val homeServer: String, @Json(name = "access_token") val accessToken: String, @Json(name = "refresh_token") val refreshToken: String?, - @Json(name = "device_id") val deviceId: String?) + @Json(name = "device_id") val deviceId: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt new file mode 100644 index 0000000000..7b131b922d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.registration + +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig + +interface RegistrationService { + + fun getOrCreateRegistrationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): RegistrationWizard + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt new file mode 100644 index 0000000000..879bd5d74b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.registration + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.Cancelable + +interface RegistrationWizard { + + fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback): Cancelable + + fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable + + // TODO Add other method here + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt new file mode 100644 index 0000000000..f302b953c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.registration + +import im.vector.matrix.android.api.util.JsonDict + + +sealed class Stage(open val mandatory: Boolean) { + + // m.login.password + data class Password(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) + + // m.login.recaptcha + data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) + + // m.login.oauth2 + // m.login.email.identity + data class Email(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + + // m.login.msisdn + data class Msisdn(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + // m.login.token + // m.login.dummy + + // Undocumented yet: m.login.terms + data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + + // TODO SSO + + // For unknown stages + data class Other(override val mandatory: Boolean, val type: String, val params: JsonDict?) : Stage(mandatory) +} + + +class TermPolicies { + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt index 7f3543dec2..7ec01cca10 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt @@ -29,3 +29,6 @@ interface Cancelable { // no-op } } + + +object NoOpCancellable : Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index bfc2b76db7..8316589ad4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.auth import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.auth.registration.RegistrationParams import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body @@ -31,6 +32,13 @@ import retrofit2.http.POST */ internal interface AuthAPI { + /** + * Register to the homeserver + * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") + fun register(registrationParams: RegistrationParams): Call + /** * Get the supported login flow * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt new file mode 100644 index 0000000000..69ef4e2238 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 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.matrix.android.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Open class, parent to all possible authentication parameters + */ +@JsonClass(generateAdapter = true) +open class AuthParams( + @Json(name = "type") + val type: String, + + @Json(name = "session") + val session: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt new file mode 100644 index 0000000000..daf9f911c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2018 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.matrix.android.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes + +/** + * Class to define the authentication parameters for "m.login.recaptcha" type + */ +@JsonClass(generateAdapter = true) +class AuthParamsCaptcha(session: String, + + @Json(name = "response") + val response: String) + : AuthParams(LoginFlowTypes.RECAPTCHA, session) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt new file mode 100644 index 0000000000..981b8682f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018 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.matrix.android.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes + +/** + * Class to define the authentication parameters for "m.login.email.identity" type + */ +@JsonClass(generateAdapter = true) +class AuthParamsEmailIdentity(session: String, + + @Json(name = "threepid_creds") + val threePidCredentials: ThreePidCredentials) + : AuthParams(LoginFlowTypes.EMAIL_IDENTITY, session) + +data class ThreePidCredentials( + @Json(name = "client_secret") + val clientSecret: String? = null, + + @Json(name = "id_server") + val idServer: String? = null, + + val sid: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt new file mode 100644 index 0000000000..89dbda077b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.registration + +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.registration.RegistrationService +import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.internal.SessionManager +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import okhttp3.OkHttpClient +import javax.inject.Provider + +internal class DefaultRegistrationService(@Unauthenticated + private val okHttpClient: Provider, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager) : RegistrationService { + + override fun getOrCreateRegistrationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): RegistrationWizard { + // TODO Persist the wizard? + return DefaultRegistrationWizard(homeServerConnectionConfig, + okHttpClient, + retrofitFactory, + coroutineDispatchers, + sessionParamsStore, + sessionManager) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt new file mode 100644 index 0000000000..d856d9211a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2018 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.matrix.android.internal.auth.registration + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.internal.SessionManager +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.util.CancelableCoroutine +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import javax.inject.Provider + +internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: HomeServerConnectionConfig, + @Unauthenticated + private val okHttpClient: Provider, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager) : RegistrationWizard { + + private var currentSession: String? = null + + private val authAPI = buildAuthAPI() + private val registerTask = DefaultRegisterTask(authAPI) + + override fun createAccount(userName: String, + password: String, + initialDeviceDisplayName: String?, + callback: MatrixCallback): Cancelable { + return performRegistrationRequest(RegistrationParams( + username = userName, + password = password, + initialDeviceDisplayName = initialDeviceDisplayName + ), callback) + } + + override fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + return performRegistrationRequest( + RegistrationParams( + auth = AuthParamsCaptcha( + session = safeSession, + response = response) + ), callback) + } + + + private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { + val job = GlobalScope.launch(coroutineDispatchers.main) { + val result = runCatching { + registerTask.execute(RegisterTask.Params(registrationParams)) + } + result.fold( + { + val sessionParams = SessionParams(it, homeServerConnectionConfig) + sessionParamsStore.save(sessionParams) + val session = sessionManager.getOrCreateSession(sessionParams) + + callback.onSuccess(session) + }, + { + if (it is Failure.RegistrationFlowError) { + currentSession = it.registrationFlowResponse.session + } + callback.onFailure(it) + } + ) + } + return CancelableCoroutine(job) + } + + private fun buildAuthAPI(): AuthAPI { + val retrofit = retrofitFactory.create(okHttpClient.get(), homeServerConnectionConfig.homeServerUri.toString()) + return retrofit.create(AuthAPI::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt new file mode 100644 index 0000000000..dd125e3c74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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.androidsdk.rest.model.login + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * This class represent a localized privacy policy for registration Flow. + */ +@Parcelize +data class LocalizedFlowDataLoginTerms( + var policyName: String? = null, + var version: String? = null, + var localizedUrl: String? = null, + var localizedName: String? = null +) : Parcelable \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt new file mode 100644 index 0000000000..4c3cc59e7a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.registration + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface RegisterTask : Task { + data class Params( + val registrationParams: RegistrationParams + ) +} + +internal class DefaultRegisterTask @Inject constructor(private val authAPI: AuthAPI) + : RegisterTask { + + override suspend fun execute(params: RegisterTask.Params): Credentials { + try { + return executeRequest { + apiCall = authAPI.register(params.registrationParams) + } + } catch (throwable: Throwable) { + if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { + // Parse to get a RegistrationFlowResponse + val registrationFlowResponse = try { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(throwable.errorBody) + } catch (e: Exception) { + null + } + // check if the server response can be cast + if (registrationFlowResponse != null) { + throw Failure.RegistrationFlowError(registrationFlowResponse) + } else { + throw throwable + } + } else { + // Other error + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt new file mode 100644 index 0000000000..db8475e06c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 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.matrix.android.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to pass parameters to the different registration types for /register. + */ +@JsonClass(generateAdapter = true) +data class RegistrationParams( + // authentication parameters + @Json(name = "auth") + val auth: AuthParams? = null, + + // the account username + @Json(name = "username") + val username: String? = null, + + // the account password + @Json(name = "password") + val password: String? = null, + + // device name + @Json(name = "initial_device_display_name") + val initialDeviceDisplayName: String? = null, + + // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app + // versions to end up in fallback because the HS returns the msisdn flow which they don't support + val x_show_msisdn: Boolean? = null +) \ No newline at end of file From 6ab7209e4d8327d2505eab5d1dac668f7cc78460 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 13 Nov 2019 19:00:59 +0100 Subject: [PATCH 007/189] Handle navigation with VectorSharedAction --- .../vector/riotx/core/di/ViewModelModule.kt | 6 +++++ .../riotx/features/login/LoginAction.kt | 1 - .../riotx/features/login/LoginActivity.kt | 23 ++++++++--------- .../riotx/features/login/LoginFragment.kt | 5 +++- .../riotx/features/login/LoginNavigation.kt | 25 +++++++++++++++++++ .../login/LoginSharedActionViewModel.kt | 22 ++++++++++++++++ .../login/LoginSsoFallbackFragment.kt | 4 ++- .../riotx/features/login/LoginViewModel.kt | 12 --------- 8 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index cc1e4dabc7..0876701504 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -31,6 +31,7 @@ import im.vector.riotx.features.home.HomeSharedActionViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.riotx.features.login.LoginSharedActionViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel @@ -112,4 +113,9 @@ interface ViewModelModule { @IntoMap @ViewModelKey(RoomDirectorySharedActionViewModel::class) fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(LoginSharedActionViewModel::class) + fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index bb42bc8e0c..63d19ea148 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -23,6 +23,5 @@ sealed class LoginAction : VectorViewModelAction { data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class Login(val login: String, val password: String) : LoginAction() data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() - data class NavigateTo(val target: LoginActivity.Navigation) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index abed22cb5e..4e5b5621ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -25,7 +25,6 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack -import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity @@ -33,13 +32,8 @@ import javax.inject.Inject class LoginActivity : VectorBaseActivity() { - // Supported navigation actions for this Activity - sealed class Navigation { - object OpenSsoLoginFallback : Navigation() - object GoBack : Navigation() - } - private val loginViewModel: LoginViewModel by viewModel() + private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory @@ -60,12 +54,15 @@ class LoginActivity : VectorBaseActivity() { loginViewModel.handle(LoginAction.InitWith(loginConfig)) } - loginViewModel.navigationLiveData.observeEvent(this) { - when (it) { - is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) - is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - } - } + loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java) + loginSharedActionViewModel.observe() + .subscribe { + when (it) { + is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) + is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + } + .disposeOnDestroy() loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) { if (it is Success) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 456e4b2bb3..aa9aabe5dd 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -44,6 +44,7 @@ import javax.inject.Inject class LoginFragment @Inject constructor() : VectorBaseFragment() { private val viewModel: LoginViewModel by activityViewModel() + private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel private var passwordShown = false @@ -52,6 +53,8 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) + setupNotice() setupAuthButton() setupPasswordReveal() @@ -114,7 +117,7 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() { } private fun openSso() { - viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback)) + loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) } private fun setupPasswordReveal() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt new file mode 100644 index 0000000000..c9de4695f9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import im.vector.riotx.core.platform.VectorSharedAction + +// Supported navigation actions for this Activity +sealed class LoginNavigation : VectorSharedAction { + object OpenSsoLoginFallback : LoginNavigation() + object GoBack : LoginNavigation() +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt new file mode 100644 index 0000000000..625208b682 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import im.vector.riotx.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class LoginSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index 38deccccaf..f77b36b1d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -47,6 +47,7 @@ import javax.inject.Inject */ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnBackPressed { + private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel private val viewModel: LoginViewModel by activityViewModel() var homeServerUrl: String = "" @@ -69,6 +70,7 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB login_sso_fallback_toolbar.title = getString(R.string.login) setupWebview() + loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) } @SuppressLint("SetJavaScriptEnabled") @@ -143,7 +145,7 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB super.onReceivedError(view, errorCode, description, failingUrl) // on error case, close this fragment - viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.GoBack)) + loginSharedActionViewModel.post(LoginNavigation.GoBack) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index a0a7258e2a..b3d7e56029 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -16,8 +16,6 @@ package im.vector.riotx.features.login -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import arrow.core.Try import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted @@ -32,7 +30,6 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import timber.log.Timber @@ -60,10 +57,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private var loginConfig: LoginConfig? = null - private val _navigationLiveData = MutableLiveData>() - val navigationLiveData: LiveData> - get() = _navigationLiveData - private var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null @@ -73,7 +66,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) is LoginAction.SsoLoginSuccess -> handleSsoLoginSuccess(action) - is LoginAction.NavigateTo -> handleNavigation(action) } } @@ -202,10 +194,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } - private fun handleNavigation(action: LoginAction.NavigateTo) { - _navigationLiveData.postValue(LiveEvent(action.target)) - } - override fun onCleared() { super.onCleared() From bdfc4ad8a7d70a0f0853d149b932d5858ef9e1d8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 10:23:48 +0100 Subject: [PATCH 008/189] Login screens: splash screen --- .../features/login/AbstractLoginFragment.kt | 40 ++++++ .../riotx/features/login/LoginActivity.kt | 3 +- .../riotx/features/login/LoginFragment.kt | 8 +- .../riotx/features/login/LoginNavigation.kt | 3 +- .../features/login/LoginSplashFragment.kt | 34 +++++ .../main/res/layout/fragment_login_splash.xml | 126 ++++++++++++++++++ vector/src/main/res/values/strings_riotX.xml | 5 + 7 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_splash.xml diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt new file mode 100644 index 0000000000..25472fa9a1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.os.Bundle +import android.view.View +import androidx.annotation.CallSuper +import com.airbnb.mvrx.activityViewModel +import im.vector.riotx.core.platform.VectorBaseFragment + +/** + * Parent Fragment for all the login/registration screens + */ +abstract class AbstractLoginFragment() : VectorBaseFragment() { + + protected val viewModel: LoginViewModel by activityViewModel() + protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel + + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 4e5b5621ce..d371b459eb 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -45,7 +45,7 @@ class LoginActivity : VectorBaseActivity() { override fun initUiAndData() { if (isFirstCreation()) { - addFragment(R.id.simpleFragmentContainer, LoginFragment::class.java) + addFragment(R.id.simpleFragmentContainer, LoginSplashFragment::class.java) } // Get config extra @@ -58,6 +58,7 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index aa9aabe5dd..4f918baa8a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -28,7 +28,6 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R import im.vector.riotx.core.extensions.setTextWithColoredPart import im.vector.riotx.core.extensions.showPassword -import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.homeserver.ServerUrlsRepository import io.reactivex.Observable @@ -41,10 +40,7 @@ import javax.inject.Inject * What can be improved: * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect */ -class LoginFragment @Inject constructor() : VectorBaseFragment() { - - private val viewModel: LoginViewModel by activityViewModel() - private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel +class LoginFragment @Inject constructor() : AbstractLoginFragment() { private var passwordShown = false @@ -53,8 +49,6 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) - setupNotice() setupAuthButton() setupPasswordReveal() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index c9de4695f9..28d583a749 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -18,8 +18,9 @@ package im.vector.riotx.features.login import im.vector.riotx.core.platform.VectorSharedAction -// Supported navigation actions for this Activity +// Supported navigation actions for LoginActivity sealed class LoginNavigation : VectorSharedAction { + object OpenServerSelection : LoginNavigation() object OpenSsoLoginFallback : LoginNavigation() object GoBack : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt new file mode 100644 index 0000000000..672502a167 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import butterknife.OnClick +import im.vector.riotx.R +import javax.inject.Inject + +/** + * + */ +class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_splash + + @OnClick(R.id.loginSplashSubmit) + fun getStarted() { + loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) + } +} diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml new file mode 100644 index 0000000000..fd40e0bca3 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8d8be693e1..4e7143fa56 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -23,5 +23,10 @@ %1$s made the room public to whoever knows the link. %1$s made the room invite only. + Liberate your communication + Chat with people directly or in groups + Keep conversations private with encryption + Extend & customise your experience + Get started From fa6a9cab7ee2db3fe7d158b143b44b6fdb2fc936 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 11:56:19 +0100 Subject: [PATCH 009/189] Login screens: server selection --- .idea/dictionaries/bmarty.xml | 1 + .../riotx/features/login/LoginAction.kt | 1 + .../riotx/features/login/LoginActivity.kt | 16 +- .../riotx/features/login/LoginNavigation.kt | 1 + .../login/LoginServerSelectionFragment.kt | 81 +++++++ .../riotx/features/login/LoginViewModel.kt | 9 + .../riotx/features/login/LoginViewState.kt | 1 + .../vector/riotx/features/login/ServerType.kt | 23 ++ .../src/main/res/drawable/bg_login_server.xml | 12 ++ .../res/drawable/bg_login_server_checked.xml | 12 ++ .../res/drawable/bg_login_server_selector.xml | 8 + .../src/main/res/drawable/ic_logo_modular.xml | 34 +++ .../fragment_login_server_selection.xml | 197 ++++++++++++++++++ .../main/res/layout/fragment_login_splash.xml | 17 +- vector/src/main/res/values/strings_riotX.xml | 10 + vector/src/main/res/values/styles_login.xml | 14 ++ .../src/main/res/values/text_appearances.xml | 19 ++ 17 files changed, 442 insertions(+), 14 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/ServerType.kt create mode 100644 vector/src/main/res/drawable/bg_login_server.xml create mode 100644 vector/src/main/res/drawable/bg_login_server_checked.xml create mode 100644 vector/src/main/res/drawable/bg_login_server_selector.xml create mode 100644 vector/src/main/res/drawable/ic_logo_modular.xml create mode 100644 vector/src/main/res/layout/fragment_login_server_selection.xml create mode 100644 vector/src/main/res/values/styles_login.xml diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 784e5c1182..7c9f6489ee 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -3,6 +3,7 @@ backstack bytearray + checkables ciphertext coroutine decryptor diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 63d19ea148..8192d26a30 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -24,4 +24,5 @@ sealed class LoginAction : VectorViewModelAction { data class Login(val login: String, val password: String) : LoginAction() data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() + data class UpdateServerType(val serverType: ServerType) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index d371b459eb..ecf3fa45e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment @@ -58,9 +59,10 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) - is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) - is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) + is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } } .disposeOnDestroy() @@ -74,6 +76,14 @@ class LoginActivity : VectorBaseActivity() { } } + private fun onServerSelectionDone() = withState(loginViewModel) { + when (it.serverType) { + ServerType.MatrixOrg -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) + ServerType.Modular, + ServerType.Other -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginEnterHomeServerFragment::class.java) + } + } + override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index 28d583a749..46a6c213d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -21,6 +21,7 @@ import im.vector.riotx.core.platform.VectorSharedAction // Supported navigation actions for LoginActivity sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() + object OnServerSelectionDone : LoginNavigation() object OpenSsoLoginFallback : LoginNavigation() object GoBack : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt new file mode 100644 index 0000000000..011100a6f5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.os.Bundle +import android.view.View +import butterknife.OnClick +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import kotlinx.android.synthetic.main.fragment_login_server_selection.* +import me.gujun.android.span.span +import javax.inject.Inject + +/** + * + */ +class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_server_selection + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initTextViews() + } + + private fun updateSelectedChoice(serverType: ServerType) { + loginServerChoiceMatrixOrg.isChecked = serverType == ServerType.MatrixOrg + loginServerChoiceModular.isChecked = serverType == ServerType.Modular + loginServerChoiceOther.isChecked = serverType == ServerType.Other + } + + private fun initTextViews() { + loginServerChoiceModularLearnMore.text = span { + text = getString(R.string.login_server_modular_learn_more) + textDecorationLine = "underline" + onClick = { + // TODO + } + } + + } + + @OnClick(R.id.loginServerChoiceMatrixOrg) + fun selectMatrixOrg() { + viewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + } + + @OnClick(R.id.loginServerChoiceModular) + fun selectModular() { + viewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + } + + @OnClick(R.id.loginServerChoiceOther) + fun selectOther() { + viewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + } + + @OnClick(R.id.loginServerSubmit) + fun submit() { + loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + } + + override fun invalidate() = withState(viewModel) { + updateSelectedChoice(it.serverType) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index b3d7e56029..a596603bc6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -62,6 +62,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi override fun handle(action: LoginAction) { when (action) { + is LoginAction.UpdateServerType -> handleUpdateServerType(action) is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) @@ -69,6 +70,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { + setState { + copy( + serverType = action.serverType + ) + } + } + private fun handleInitWith(action: LoginAction.InitWith) { loginConfig = action.loginConfig } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 0cc0476254..5c00614b09 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized data class LoginViewState( + val serverType: ServerType = ServerType.MatrixOrg, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt b/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt new file mode 100644 index 0000000000..4c7007c137 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +enum class ServerType { + MatrixOrg, + Modular, + Other +} diff --git a/vector/src/main/res/drawable/bg_login_server.xml b/vector/src/main/res/drawable/bg_login_server.xml new file mode 100644 index 0000000000..5aecd26292 --- /dev/null +++ b/vector/src/main/res/drawable/bg_login_server.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_login_server_checked.xml b/vector/src/main/res/drawable/bg_login_server_checked.xml new file mode 100644 index 0000000000..1aea622462 --- /dev/null +++ b/vector/src/main/res/drawable/bg_login_server_checked.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_login_server_selector.xml b/vector/src/main/res/drawable/bg_login_server_selector.xml new file mode 100644 index 0000000000..57be1e5d54 --- /dev/null +++ b/vector/src/main/res/drawable/bg_login_server_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_logo_modular.xml b/vector/src/main/res/drawable/ic_logo_modular.xml new file mode 100644 index 0000000000..c95ee66b86 --- /dev/null +++ b/vector/src/main/res/drawable/ic_logo_modular.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml new file mode 100644 index 0000000000..58bc682d2a --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index fd40e0bca3..5c025ef57b 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -25,9 +25,7 @@ android:layout_height="wrap_content" android:layout_marginTop="48dp" android:text="@string/login_splash_title" - android:textColor="?riotx_text_primary" - android:textSize="20sp" - android:textStyle="bold" + android:textAppearance="@style/TextAppearance.Vector.Login.Title" app:layout_constraintBottom_toTopOf="@+id/loginSplashText1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashLogo" /> @@ -51,8 +49,7 @@ android:layout_marginTop="32dp" android:gravity="start" android:text="@string/login_splash_text1" - android:textColor="?vctr_notice_secondary" - android:textSize="16sp" + android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@+id/loginSplashText2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/loginSplashPicto1" @@ -76,8 +73,7 @@ android:layout_marginTop="16dp" android:gravity="start" android:text="@string/login_splash_text2" - android:textColor="?vctr_notice_secondary" - android:textSize="16sp" + android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@id/loginSplashText3" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/loginSplashPicto2" @@ -100,9 +96,8 @@ android:layout_marginStart="12dp" android:layout_marginTop="16dp" android:gravity="start" - android:text="@string/login_splash_text1" - android:textColor="?vctr_notice_secondary" - android:textSize="16sp" + android:text="@string/login_splash_text3" + android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/loginSplashPicto3" @@ -110,7 +105,7 @@ Extend & customise your experience Get started + Select a server + Just like email, accounts have one home, although you can talk to anyone + Join millions free on the largest public server + Premium hosting for organisations + Learn more + Other + Custom & advanced settings + Continue + + diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml new file mode 100644 index 0000000000..ffcd5bc29e --- /dev/null +++ b/vector/src/main/res/values/styles_login.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/text_appearances.xml b/vector/src/main/res/values/text_appearances.xml index 606aef6511..6c2a71631d 100644 --- a/vector/src/main/res/values/text_appearances.xml +++ b/vector/src/main/res/values/text_appearances.xml @@ -37,4 +37,23 @@ ?riotx_text_secondary + + + + + + \ No newline at end of file From da8d6fb4f437e313dee7747dbae1e2699d319d4e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 13:16:01 +0100 Subject: [PATCH 010/189] Login screens: signup signin selection --- .idea/dictionaries/bmarty.xml | 2 + .../riotx/features/login/LoginAction.kt | 1 + .../riotx/features/login/LoginActivity.kt | 7 +- .../riotx/features/login/LoginNavigation.kt | 1 + .../LoginSignUpSignInSelectionFragment.kt | 70 +++++++++++++++ .../riotx/features/login/LoginViewModel.kt | 9 ++ .../riotx/features/login/LoginViewState.kt | 1 + .../vector/riotx/features/login/SignMode.kt | 24 ++++++ .../main/res/drawable/ic_logo_matrix_org.xml | 30 +++++++ .../fragment_login_server_selection.xml | 3 +- ...fragment_login_signup_signin_selection.xml | 86 +++++++++++++++++++ vector/src/main/res/values/strings_riotX.xml | 3 + vector/src/main/res/values/styles_login.xml | 4 + 13 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/SignMode.kt create mode 100644 vector/src/main/res/drawable/ic_logo_matrix_org.xml create mode 100644 vector/src/main/res/layout/fragment_login_signup_signin_selection.xml diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 7c9f6489ee..00c6f6c865 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -17,6 +17,8 @@ msisdn pbkdf pkcs + signin + signup \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 8192d26a30..d1690f46af 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -25,4 +25,5 @@ sealed class LoginAction : VectorViewModelAction { data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() data class UpdateServerType(val serverType: ServerType) : LoginAction() + data class UpdateSignMode(val signMode: SignMode) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index ecf3fa45e2..65db61a1d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -61,6 +61,7 @@ class LoginActivity : VectorBaseActivity() { when (it) { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerSelectionFragment::class.java) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } @@ -76,11 +77,15 @@ class LoginActivity : VectorBaseActivity() { } } + private fun onSignModeSelected() { + // TODO + } + private fun onServerSelectionDone() = withState(loginViewModel) { when (it.serverType) { ServerType.MatrixOrg -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) ServerType.Modular, - ServerType.Other -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginEnterHomeServerFragment::class.java) + ServerType.Other -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, LoginEnterHomeServerFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index 46a6c213d5..6f7fc174d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -22,6 +22,7 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() + object OnSignModeSelected : LoginNavigation() object OpenSsoLoginFallback : LoginNavigation() object GoBack : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt new file mode 100644 index 0000000000..ad99037f53 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* +import javax.inject.Inject + +/** + * + */ +class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection + + private fun updateViews(serverType: ServerType) { + when (serverType) { + ServerType.MatrixOrg -> { + loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginSignupSigninServerIcon.isVisible = true + loginSignupSigninTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_modular) + loginSignupSigninServerIcon.isVisible = true + loginSignupSigninTitle.text = getString(R.string.login_connect_to, "TODO MODULAR NAME") + loginSignupSigninText.text = "TODO MODULAR URL" + } + ServerType.Other -> { + loginSignupSigninServerIcon.isVisible = false + loginSignupSigninTitle.text = getString(R.string.login_server_other_title) + loginSignupSigninText.text = "TODO SERVER URL" + } + } + } + + @OnClick(R.id.loginSignupSigninSignUp) + fun signUp() { + viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + } + + @OnClick(R.id.loginSignupSigninSignIn) + fun signIn() { + viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + } + + override fun invalidate() = withState(viewModel) { + updateViews(it.serverType) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index a596603bc6..0dad62a816 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -63,6 +63,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi override fun handle(action: LoginAction) { when (action) { is LoginAction.UpdateServerType -> handleUpdateServerType(action) + is LoginAction.UpdateSignMode -> handleUpdateSignMode(action) is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) @@ -70,6 +71,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun handleUpdateSignMode(action: LoginAction.UpdateSignMode) { + setState { + copy( + signMode = action.signMode + ) + } + } + private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 5c00614b09..bfa81a55ef 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Uninitialized data class LoginViewState( val serverType: ServerType = ServerType.MatrixOrg, + val signMode: SignMode = SignMode.SignUp, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt new file mode 100644 index 0000000000..e8c9b9a734 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +enum class SignMode { + // Account creation + SignUp, + // Login + SignIn +} diff --git a/vector/src/main/res/drawable/ic_logo_matrix_org.xml b/vector/src/main/res/drawable/ic_logo_matrix_org.xml new file mode 100644 index 0000000000..13a05fba4f --- /dev/null +++ b/vector/src/main/res/drawable/ic_logo_matrix_org.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index 58bc682d2a..3cd7bd3fb9 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -57,12 +57,11 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginServerText"> - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1b92dca1ed..36318ec52d 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -38,5 +38,8 @@ Custom & advanced settings Continue + Connect to %1$s + Sign Up + Sign In diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml index ffcd5bc29e..cac4351c3f 100644 --- a/vector/src/main/res/values/styles_login.xml +++ b/vector/src/main/res/values/styles_login.xml @@ -11,4 +11,8 @@ false + + \ No newline at end of file From 6525314af840e98ce0203e0c4f6e65d5270d8f76 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 15:25:43 +0100 Subject: [PATCH 011/189] Login screens: server ur form --- vector/src/main/AndroidManifest.xml | 4 +- .../homeserver/ServerUrlsRepository.kt | 6 +- .../riotx/features/login/LoginActivity.kt | 13 +- .../riotx/features/login/LoginFragment.kt | 57 +------- .../login/LoginServerSelectionFragment.kt | 2 + .../login/LoginServerUrlFormFragment.kt | 94 +++++++++++++ vector/src/main/res/layout/fragment_login.xml | 87 ++++++------ .../fragment_login_server_selection.xml | 5 +- .../layout/fragment_login_server_url_form.xml | 125 ++++++++++++++++++ ...fragment_login_signup_signin_selection.xml | 5 +- .../main/res/layout/fragment_login_splash.xml | 3 +- vector/src/main/res/values/config.xml | 1 - vector/src/main/res/values/strings_riotX.xml | 12 +- vector/src/main/res/values/styles_login.xml | 2 + 14 files changed, 304 insertions(+), 112 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_server_url_form.xml diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 0c9bac61a1..5f1687c9c9 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -33,7 +33,9 @@ - + addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) ServerType.Modular, - ServerType.Other -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, LoginEnterHomeServerFragment::class.java) + ServerType.Other -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerUrlFormFragment::class.java) + } + } + + private fun onSignModeSelected() = withState(loginViewModel) { + when (it.signMode) { + SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, SignUpFragment::class.java) + SignMode.SignIn -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 4f918baa8a..582567aba5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -18,20 +18,15 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View -import android.view.inputmethod.EditorInfo import android.widget.Toast import androidx.core.view.isVisible import androidx.transition.TransitionManager import com.airbnb.mvrx.* -import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R -import im.vector.riotx.core.extensions.setTextWithColoredPart import im.vector.riotx.core.extensions.showPassword -import im.vector.riotx.core.utils.openUrlInExternalBrowser -import im.vector.riotx.features.homeserver.ServerUrlsRepository import io.reactivex.Observable -import io.reactivex.functions.Function3 +import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject @@ -49,41 +44,8 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupNotice() - setupAuthButton() + setupLoginButton() setupPasswordReveal() - - homeServerField.focusChanges() - .subscribe { - if (!it) { - viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) - } - } - .disposeOnDestroyView() - - homeServerField.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) - return@setOnEditorActionListener true - } - return@setOnEditorActionListener false - } - - val initHsUrl = viewModel.getInitialHomeServerUrl() - if (initHsUrl != null) { - homeServerField.setText(initHsUrl) - } else { - homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext())) - } - viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) - } - - private fun setupNotice() { - riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part) - - riotx_no_registration_notice.setOnClickListener { - openUrlInExternalBrowser(requireActivity(), "https://about.riot.im/downloads") - } } private fun authenticate() { @@ -93,23 +55,21 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { viewModel.handle(LoginAction.Login(login, password)) } - private fun setupAuthButton() { + private fun setupLoginButton() { Observable .combineLatest( loginField.textChanges().map { it.trim().isNotEmpty() }, passwordField.textChanges().map { it.trim().isNotEmpty() }, - homeServerField.textChanges().map { it.trim().isNotEmpty() }, - Function3 { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty -> - isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty + BiFunction { isLoginNotEmpty, isPasswordNotEmpty -> + isLoginNotEmpty && isPasswordNotEmpty } ) .subscribeBy { authenticateButton.isEnabled = it } .disposeOnDestroyView() authenticateButton.setOnClickListener { authenticate() } - - authenticateButtonSso.setOnClickListener { openSso() } } + // TODO Move to server selection screen private fun openSso() { loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) } @@ -148,7 +108,6 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginField.isVisible = false passwordContainer.isVisible = false authenticateButton.isVisible = false - authenticateButtonSso.isVisible = false passwordShown = false renderPasswordField() } @@ -158,7 +117,6 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginField.isVisible = false passwordContainer.isVisible = false authenticateButton.isVisible = false - authenticateButtonSso.isVisible = false Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show() } is Success -> { @@ -170,7 +128,6 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginField.isVisible = true passwordContainer.isVisible = true authenticateButton.isVisible = true - authenticateButtonSso.isVisible = false if (loginField.text.isNullOrBlank() && passwordField.text.isNullOrBlank()) { // Jump focus to login loginField.requestFocus() @@ -180,13 +137,11 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginField.isVisible = false passwordContainer.isVisible = false authenticateButton.isVisible = false - authenticateButtonSso.isVisible = true } LoginMode.Unsupported -> { loginField.isVisible = false passwordContainer.isVisible = false authenticateButton.isVisible = false - authenticateButtonSso.isVisible = false Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show() } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 011100a6f5..7be5223ac7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -21,6 +21,7 @@ import android.view.View import butterknife.OnClick import com.airbnb.mvrx.withState import im.vector.riotx.R +import im.vector.riotx.core.utils.openUrlInExternalBrowser import kotlinx.android.synthetic.main.fragment_login_server_selection.* import me.gujun.android.span.span import javax.inject.Inject @@ -50,6 +51,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment textDecorationLine = "underline" onClick = { // TODO + openUrlInExternalBrowser(requireActivity(), "https://example.org") } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt new file mode 100644 index 0000000000..bf5a6b9866 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.utils.openUrlInExternalBrowser +import kotlinx.android.synthetic.main.fragment_login_server_url_form.* +import javax.inject.Inject + +/** + * + */ +class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_server_url_form + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // TODO Import code from Riot to clear error on TIL + loginServerUrlFormHomeServerUrl.textChanges() + .subscribe( + { + loginServerUrlFormHomeServerUrlTil.error = null + }, + { + // Ignore error + }) + .disposeOnDestroy() + + loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + } + + @OnClick(R.id.loginServerUrlFormLearnMore) + fun learMore() { + // TODO + openUrlInExternalBrowser(requireActivity(), "https://example.org") + } + + @OnClick(R.id.loginServerUrlFormSubmit) + fun submit() { + // TODO Static check of homeserver url, empty, malformed, etc. + viewModel.handle(LoginAction.InitWith(LoginConfig(loginServerUrlFormHomeServerUrl.text.toString(), null))) + } + + override fun invalidate() = withState(viewModel) { state -> + when (state.serverType) { + ServerType.Modular -> { + loginServerUrlFormIcon.isVisible = true + loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular) + loginServerUrlFormText.text = getString(R.string.login_server_url_form_modular_text) + loginServerUrlFormLearnMore.isVisible = true + loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint) + loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_modular_notice) + } + ServerType.Other -> { + loginServerUrlFormIcon.isVisible = false + loginServerUrlFormTitle.text = getString(R.string.login_server_other_title) + loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server) + loginServerUrlFormLearnMore.isVisible = false + loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_other_hint) + loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_other_notice) + } + else -> error("This fragment should not be display in matrix.org mode") + } + } +} diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 3dde5e1748..94340cb97c 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -1,3 +1,4 @@ + - + style="@style/LoginTopIcon" + android:layout_gravity="center_horizontal" /> + android:layout_marginTop="84dp" + android:textAppearance="@style/TextAppearance.Vector.Login.Title" + tools:text="@string/login_signin_to" /> + + + android:layout_marginTop="32dp" + android:hint="@string/auth_user_name_placeholder" + app:errorEnabled="true"> + android:hint="@string/auth_password_placeholder" + app:errorEnabled="true"> - + android:layout_marginTop="22dp" + android:orientation="horizontal"> - + android:layout_gravity="start" + android:text="@string/auth_forgot_password" /> - + - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index 21835a864c..240e8866f3 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -1,3 +1,4 @@ + - - - https://matrix.org - https://matrix.org https://piwik.riot.im https://riot.im/bugreports/submit diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 36318ec52d..cfd1b6721c 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -36,10 +36,20 @@ Learn more Other Custom & advanced settings - Continue + Continue Connect to %1$s + Connect to Modular + Connect to a custom server + Sign in to %1$s Sign Up Sign In + Modular Address + Address + Premium hosting for organisations + + Enter the address of the Modular Riot or Server you want to use + Enter the address of a server or a Riot you want to connect to + diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml index cac4351c3f..81964377bd 100644 --- a/vector/src/main/res/values/styles_login.xml +++ b/vector/src/main/res/values/styles_login.xml @@ -8,10 +8,12 @@ From 7f1f98c2e53d176af2e5882a457f1bcb99b05e32 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 17:42:50 +0100 Subject: [PATCH 012/189] Login screens: reset state when navigating back --- .../im/vector/riotx/core/di/FragmentModule.kt | 7 +++ .../features/login/AbstractLoginFragment.kt | 11 +++- .../riotx/features/login/LoginAction.kt | 12 ++++- .../riotx/features/login/LoginActivity.kt | 12 +++-- .../riotx/features/login/LoginFragment.kt | 4 ++ .../riotx/features/login/LoginNavigation.kt | 2 +- .../login/LoginServerSelectionFragment.kt | 4 ++ .../login/LoginServerUrlFormFragment.kt | 51 +++++++++++++++++-- .../LoginSignUpSignInSelectionFragment.kt | 4 ++ .../features/login/LoginSplashFragment.kt | 4 ++ .../login/LoginSsoFallbackFragment.kt | 1 + .../riotx/features/login/LoginViewModel.kt | 42 ++++++++++++++- .../riotx/features/login/LoginViewState.kt | 2 +- .../vector/riotx/features/login/SignMode.kt | 1 + 14 files changed, 141 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 6ae4619033..20b53b40db 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -36,6 +36,7 @@ import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.login.LoginFragment +import im.vector.riotx.features.login.LoginServerUrlFormFragment import im.vector.riotx.features.login.LoginSsoFallbackFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment @@ -194,4 +195,10 @@ interface FragmentModule { @IntoMap @FragmentKey(PublicRoomsFragment::class) fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment + + // TODO Add all other LoginFragment + @Binds + @IntoMap + @FragmentKey(LoginServerUrlFormFragment::class) + fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 25472fa9a1..c0ff5103f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -20,12 +20,13 @@ import android.os.Bundle import android.view.View import androidx.annotation.CallSuper import com.airbnb.mvrx.activityViewModel +import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment /** * Parent Fragment for all the login/registration screens */ -abstract class AbstractLoginFragment() : VectorBaseFragment() { +abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { protected val viewModel: LoginViewModel by activityViewModel() protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @@ -37,4 +38,12 @@ abstract class AbstractLoginFragment() : VectorBaseFragment() { loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) } + override fun onBackPressed(): Boolean { + resetViewModel() + // Do not consume the Back event + return false + } + + // Reset any modification of the viewModel by the current fragment + abstract fun resetViewModel() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index d1690f46af..310d07b746 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -20,10 +20,18 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.riotx.core.platform.VectorViewModelAction sealed class LoginAction : VectorViewModelAction { + data class UpdateServerType(val serverType: ServerType) : LoginAction() data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() + data class UpdateSignMode(val signMode: SignMode) : LoginAction() data class Login(val login: String, val password: String) : LoginAction() data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() - data class UpdateServerType(val serverType: ServerType) : LoginAction() - data class UpdateSignMode(val signMode: SignMode) : LoginAction() + + // Reset actions + open class ResetAction : LoginAction() + + object ResetHomeServerType : ResetAction() + object ResetHomeServerUrl : ResetAction() + object ResetSignMode : ResetAction() + object ResetLogin : ResetAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index cf045b95e6..3c67971098 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -61,7 +61,7 @@ class LoginActivity : VectorBaseActivity() { when (it) { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerSelectionFragment::class.java) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } @@ -85,10 +85,12 @@ class LoginActivity : VectorBaseActivity() { } } - private fun onSignModeSelected() = withState(loginViewModel) { - when (it.signMode) { - SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, SignUpFragment::class.java) - SignMode.SignIn -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) + private fun onSignModeSelected(mode: LoginNavigation.OnSignModeSelected) { + // We cannot use the state, it is not ready... + when (mode.signMode) { + SignMode.Unknown -> error("Sign mode has to be set before calling this method") + SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, SignUpFragment::class.java) + SignMode.SignIn -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 582567aba5..166a733d8a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -98,6 +98,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { } } + override fun resetViewModel() { + viewModel.handle(LoginAction.ResetLogin) + } + override fun invalidate() = withState(viewModel) { state -> TransitionManager.beginDelayedTransition(login_fragment) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index 6f7fc174d4..e906bfeba8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -22,7 +22,7 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() - object OnSignModeSelected : LoginNavigation() + data class OnSignModeSelected(val signMode: SignMode) : LoginNavigation() object OpenSsoLoginFallback : LoginNavigation() object GoBack : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 7be5223ac7..0af00348ff 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -77,6 +77,10 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) } + override fun resetViewModel() { + viewModel.handle(LoginAction.ResetHomeServerType) + } + override fun invalidate() = withState(viewModel) { updateSelectedChoice(it.serverType) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index bf5a6b9866..7304f15cf6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -16,14 +16,16 @@ package im.vector.riotx.features.login +import android.annotation.SuppressLint import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.withState +import com.airbnb.mvrx.* import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.utils.openUrlInExternalBrowser import kotlinx.android.synthetic.main.fragment_login_server_url_form.* import javax.inject.Inject @@ -31,7 +33,9 @@ import javax.inject.Inject /** * */ -class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() { +class LoginServerUrlFormFragment @Inject constructor( + private val errorFormatter: ErrorFormatter +) : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_server_url_form @@ -64,10 +68,29 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() openUrlInExternalBrowser(requireActivity(), "https://example.org") } + override fun resetViewModel() { + viewModel.handle(LoginAction.ResetHomeServerUrl) + } + + @SuppressLint("SetTextI18n") @OnClick(R.id.loginServerUrlFormSubmit) fun submit() { - // TODO Static check of homeserver url, empty, malformed, etc. - viewModel.handle(LoginAction.InitWith(LoginConfig(loginServerUrlFormHomeServerUrl.text.toString(), null))) + // Static check of homeserver url, empty, malformed, etc. + var serverUrl = loginServerUrlFormHomeServerUrl.text.toString() + + when { + serverUrl.isBlank() -> { + loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) + } + else -> { + if (serverUrl.startsWith("http").not()) { + serverUrl = "https://$serverUrl" + loginServerUrlFormHomeServerUrl.setText(serverUrl) + + } + viewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) + } + } } override fun invalidate() = withState(viewModel) { state -> @@ -90,5 +113,25 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() } else -> error("This fragment should not be display in matrix.org mode") } + + when (state.asyncHomeServerLoginFlowRequest) { + is Uninitialized -> { + progressBar.isVisible = false + touchArea.isVisible = false + } + is Loading -> { + progressBar.isVisible = true + touchArea.isVisible = true + } + is Fail -> { + progressBar.isVisible = false + touchArea.isVisible = false + // TODO Error text is not correct + loginServerUrlFormHomeServerUrlTil.error = errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error) + } + is Success -> { + // The home server is valid, the next screen will be opened by the Activity + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index ad99037f53..eb855f93c1 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -64,6 +64,10 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } + override fun resetViewModel() { + viewModel.handle(LoginAction.ResetSignMode) + } + override fun invalidate() = withState(viewModel) { updateViews(it.serverType) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt index 672502a167..33db8fa81a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -31,4 +31,8 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { fun getStarted() { loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) } + + override fun resetViewModel() { + // Nothing to do + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index f77b36b1d9..74ff7bb4bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -44,6 +44,7 @@ import javax.inject.Inject /** * Only login is supported for the moment + * TODO Migrate to new flow */ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnBackPressed { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 0dad62a816..227ddf89da 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -68,6 +68,44 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) is LoginAction.SsoLoginSuccess -> handleSsoLoginSuccess(action) + is LoginAction.ResetAction -> handleResetAction(action) + } + } + + private fun handleResetAction(action: LoginAction.ResetAction) { + // Cancel any request + currentTask?.cancel() + currentTask = null + + when (action) { + LoginAction.ResetLogin -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + } + LoginAction.ResetHomeServerUrl -> { + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized + ) + } + } + LoginAction.ResetHomeServerType -> { + setState { + copy( + serverType = ServerType.MatrixOrg + ) + } + } + LoginAction.ResetSignMode -> { + setState { + copy( + signMode = SignMode.Unknown + ) + } + } } } @@ -107,7 +145,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } - authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback { + currentTask = authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback { override fun onSuccess(data: Session) { onSessionCreated(data) } @@ -153,7 +191,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun handleUpdateHomeserver(action: LoginAction.UpdateHomeServer) = withState { state -> - var newConfig: HomeServerConnectionConfig? = null Try { val homeServerUri = action.homeServerUrl @@ -167,6 +204,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi && state.asyncHomeServerLoginFlowRequest is Success) return@withState currentTask?.cancel() + currentTask = null homeServerConnectionConfig = newConfig val homeServerConnectionConfigFinal = homeServerConnectionConfig diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index bfa81a55ef..542bc0799d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -22,7 +22,7 @@ import com.airbnb.mvrx.Uninitialized data class LoginViewState( val serverType: ServerType = ServerType.MatrixOrg, - val signMode: SignMode = SignMode.SignUp, + val signMode: SignMode = SignMode.Unknown, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt index e8c9b9a734..b793a0fe1d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.login enum class SignMode { + Unknown, // Account creation SignUp, // Login From 3c93807fe63cead9e5aff7b5c68a8904d8c328ca Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 17:46:53 +0100 Subject: [PATCH 013/189] Login screens: add some doc --- .../java/im/vector/riotx/features/login/LoginFragment.kt | 5 +++-- .../riotx/features/login/LoginServerSelectionFragment.kt | 2 +- .../riotx/features/login/LoginServerUrlFormFragment.kt | 2 +- .../features/login/LoginSignUpSignInSelectionFragment.kt | 2 +- .../im/vector/riotx/features/login/LoginSplashFragment.kt | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 166a733d8a..4a4ca5fce0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -32,11 +32,12 @@ import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject /** - * What can be improved: - * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect + * In this screen, the user is asked for login and password to sign in to a homeserver. + * He also can reset his password */ class LoginFragment @Inject constructor() : AbstractLoginFragment() { + // TODO Move to viewState? private var passwordShown = false override fun getLayoutResId() = R.layout.fragment_login diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 0af00348ff..488bb340f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -27,7 +27,7 @@ import me.gujun.android.span.span import javax.inject.Inject /** - * + * In this screen, the user will choose between matrix.org, modular or other type of homeserver */ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 7304f15cf6..87043222f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -31,7 +31,7 @@ import kotlinx.android.synthetic.main.fragment_login_server_url_form.* import javax.inject.Inject /** - * + * In this screen, the user is prompted to enter a homeserver url */ class LoginServerUrlFormFragment @Inject constructor( private val errorFormatter: ErrorFormatter diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index eb855f93c1..dbf8abfe49 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -24,7 +24,7 @@ import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject /** - * + * In this screen, the user is asked to sign up or to sign in to the homeserver */ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt index 33db8fa81a..53de8c2c43 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -21,7 +21,7 @@ import im.vector.riotx.R import javax.inject.Inject /** - * + * In this screen, the user is viewing an introduction to what he can do with this application */ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { From c6b0ae63ea8c5f4f3c9d77fa8dfdf8ab153495fd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 21:20:45 +0100 Subject: [PATCH 014/189] Login screens: handle loading Views and global navigation - WIP --- .../im/vector/riotx/core/di/FragmentModule.kt | 12 +-- .../vector/riotx/core/error/ErrorFormatter.kt | 19 +++-- .../riotx/features/login/LoginActivity.kt | 71 +++++++++++++---- .../riotx/features/login/LoginFragment.kt | 77 ++++--------------- .../riotx/features/login/LoginNavigation.kt | 5 +- .../login/LoginServerSelectionFragment.kt | 21 ++++- .../login/LoginServerUrlFormFragment.kt | 21 ++--- .../LoginSignUpSignInSelectionFragment.kt | 4 +- .../login/LoginSsoFallbackFragment.kt | 20 ++--- .../riotx/features/login/LoginViewModel.kt | 9 ++- .../riotx/features/login/LoginViewState.kt | 10 ++- vector/src/main/res/layout/activity_login.xml | 47 +++++++++++ vector/src/main/res/layout/fragment_login.xml | 27 +------ .../layout/fragment_login_server_url_form.xml | 23 ------ vector/src/main/res/values/strings_riotX.xml | 2 + 15 files changed, 194 insertions(+), 174 deletions(-) create mode 100644 vector/src/main/res/layout/activity_login.xml diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 20b53b40db..1457519052 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -116,6 +116,12 @@ interface FragmentModule { @FragmentKey(LoginFragment::class) fun bindLoginFragment(fragment: LoginFragment): Fragment + // TODO Add all other Login Fragments + @Binds + @IntoMap + @FragmentKey(LoginServerUrlFormFragment::class) + fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginSsoFallbackFragment::class) @@ -195,10 +201,4 @@ interface FragmentModule { @IntoMap @FragmentKey(PublicRoomsFragment::class) fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment - - // TODO Add all other LoginFragment - @Binds - @IntoMap - @FragmentKey(LoginServerUrlFormFragment::class) - fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 10c4fe3354..d08675ea1b 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -41,12 +41,19 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi } } is Failure.ServerError -> { - if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) { - // Special case for terms and conditions - stringProvider.getString(R.string.error_terms_not_accepted) - } else { - throwable.error.message.takeIf { it.isNotEmpty() } - ?: throwable.error.code.takeIf { it.isNotEmpty() } + when { + throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { + // Special case for terms and conditions + stringProvider.getString(R.string.error_terms_not_accepted) + } + throwable.error.code == MatrixError.FORBIDDEN + && throwable.error.message == "Invalid password" -> { + stringProvider.getString(R.string.auth_invalid_login_param) + } + else -> { + throwable.error.message.takeIf { it.isNotEmpty() } + ?: throwable.error.code.takeIf { it.isNotEmpty() } + } } } else -> throwable.localizedMessage diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 3c67971098..0ab10134e9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -18,6 +18,8 @@ package im.vector.riotx.features.login import android.content.Context import android.content.Intent +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel @@ -29,8 +31,12 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity +import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject +/** + * The LoginActivity manages the fragment navigation and also display the loading View + */ class LoginActivity : VectorBaseActivity() { private val loginViewModel: LoginViewModel by viewModel() @@ -42,16 +48,17 @@ class LoginActivity : VectorBaseActivity() { injector.inject(this) } - override fun getLayoutRes() = R.layout.activity_simple + override fun getLayoutRes() = R.layout.activity_login override fun initUiAndData() { if (isFirstCreation()) { - addFragment(R.id.simpleFragmentContainer, LoginSplashFragment::class.java) + addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) } // Get config extra val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG) if (loginConfig != null && isFirstCreation()) { + // TODO Check this loginViewModel.handle(LoginAction.InitWith(loginConfig)) } @@ -59,29 +66,59 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerSelectionFragment::class.java) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) - is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) - is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved(it) + is LoginNavigation.OnSsoLoginFallbackError -> onSsoLoginFallbackError(it) } } .disposeOnDestroy() - loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) { - if (it is Success) { - val intent = HomeActivity.newIntent(this) - startActivity(intent) - finish() - } + loginViewModel + .subscribe(this) { + updateWithState(it) + } + .disposeOnDestroy() + } + + private fun onLoginFlowRetrieved(onLoginFlowRetrieved: LoginNavigation.OnLoginFlowRetrieved) { + when (onLoginFlowRetrieved.loginMode) { + LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSsoFallbackFragment::class.java) + LoginMode.Unsupported, + LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) } } + private fun updateWithState(loginViewState: LoginViewState) { + if (loginViewState.asyncLoginAction is Success) { + val intent = HomeActivity.newIntent(this) + startActivity(intent) + finish() + return + } + + // Loading + loginLoading.isVisible = loginViewState.isLoading() + } + + private fun onSsoLoginFallbackError(onSsoLoginFallbackError: LoginNavigation.OnSsoLoginFallbackError) { + // Pop the backstack + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + // And inform the user + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(getString(R.string.login_sso_error_message, onSsoLoginFallbackError.description, onSsoLoginFallbackError.errorCode)) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun onServerSelectionDone() = withState(loginViewModel) { when (it.serverType) { - ServerType.MatrixOrg -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) + ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.Modular, - ServerType.Other -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerUrlFormFragment::class.java) + ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerUrlFormFragment::class.java) } } @@ -89,8 +126,8 @@ class LoginActivity : VectorBaseActivity() { // We cannot use the state, it is not ready... when (mode.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, SignUpFragment::class.java) - SignMode.SignIn -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) + SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.loginFragmentContainer, SignUpFragment::class.java) + SignMode.SignIn -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 4a4ca5fce0..571305b722 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -18,12 +18,14 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View -import android.widget.Toast -import androidx.core.view.isVisible import androidx.transition.TransitionManager -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.showPassword import io.reactivex.Observable import io.reactivex.functions.BiFunction @@ -35,7 +37,9 @@ import javax.inject.Inject * In this screen, the user is asked for login and password to sign in to a homeserver. * He also can reset his password */ -class LoginFragment @Inject constructor() : AbstractLoginFragment() { +class LoginFragment @Inject constructor( + private val errorFormatter: ErrorFormatter +) : AbstractLoginFragment() { // TODO Move to viewState? private var passwordShown = false @@ -70,10 +74,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { authenticateButton.setOnClickListener { authenticate() } } - // TODO Move to server selection screen - private fun openSso() { - loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) - } +// // TODO Move to server selection screen +// private fun openSso() { +// loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) +// } private fun setupPasswordReveal() { passwordShown = false @@ -106,65 +110,16 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { override fun invalidate() = withState(viewModel) { state -> TransitionManager.beginDelayedTransition(login_fragment) - when (state.asyncHomeServerLoginFlowRequest) { - is Incomplete -> { - progressBar.isVisible = true - touchArea.isVisible = true - loginField.isVisible = false - passwordContainer.isVisible = false - authenticateButton.isVisible = false - passwordShown = false - renderPasswordField() - } - is Fail -> { - progressBar.isVisible = false - touchArea.isVisible = false - loginField.isVisible = false - passwordContainer.isVisible = false - authenticateButton.isVisible = false - Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show() - } - is Success -> { - progressBar.isVisible = false - touchArea.isVisible = false - - when (state.asyncHomeServerLoginFlowRequest()) { - LoginMode.Password -> { - loginField.isVisible = true - passwordContainer.isVisible = true - authenticateButton.isVisible = true - if (loginField.text.isNullOrBlank() && passwordField.text.isNullOrBlank()) { - // Jump focus to login - loginField.requestFocus() - } - } - LoginMode.Sso -> { - loginField.isVisible = false - passwordContainer.isVisible = false - authenticateButton.isVisible = false - } - LoginMode.Unsupported -> { - loginField.isVisible = false - passwordContainer.isVisible = false - authenticateButton.isVisible = false - Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show() - } - } - } - } - when (state.asyncLoginAction) { is Loading -> { - progressBar.isVisible = true - touchArea.isVisible = true - + // Ensure password is hidden passwordShown = false renderPasswordField() } is Fail -> { - progressBar.isVisible = false - touchArea.isVisible = false - Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show() + // TODO Handle error text properly + // TODO Reset error when text is changed + passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error) } // Success is handled by the LoginActivity is Success -> Unit diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index e906bfeba8..ba1e327e3a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -22,7 +22,8 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() + data class OnLoginFlowRetrieved(val loginMode: LoginMode) : LoginNavigation() data class OnSignModeSelected(val signMode: SignMode) : LoginNavigation() - object OpenSsoLoginFallback : LoginNavigation() - object GoBack : LoginNavigation() + //object OpenSsoLoginFallback : LoginNavigation() + data class OnSsoLoginFallbackError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 488bb340f5..59be9c7aa8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -19,6 +19,8 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View import butterknife.OnClick +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Success import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.utils.openUrlInExternalBrowser @@ -73,8 +75,13 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment } @OnClick(R.id.loginServerSubmit) - fun submit() { - loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + fun submit() = withState(viewModel) { + if (it.serverType == ServerType.MatrixOrg) { + // Request login flow here + viewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) + } else { + loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + } } override fun resetViewModel() { @@ -83,5 +90,15 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment override fun invalidate() = withState(viewModel) { updateSelectedChoice(it.serverType) + + when (it.asyncHomeServerLoginFlowRequest) { + is Fail -> { + // TODO Display error in a dialog? + } + is Success -> { + // The home server url is valid + loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved(it.asyncHomeServerLoginFlowRequest.invoke())) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 87043222f9..2ea5869448 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -22,7 +22,9 @@ import android.view.View import android.view.inputmethod.EditorInfo import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R import im.vector.riotx.core.error.ErrorFormatter @@ -115,22 +117,13 @@ class LoginServerUrlFormFragment @Inject constructor( } when (state.asyncHomeServerLoginFlowRequest) { - is Uninitialized -> { - progressBar.isVisible = false - touchArea.isVisible = false - } - is Loading -> { - progressBar.isVisible = true - touchArea.isVisible = true - } - is Fail -> { - progressBar.isVisible = false - touchArea.isVisible = false + is Fail -> { // TODO Error text is not correct loginServerUrlFormHomeServerUrlTil.error = errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error) } - is Success -> { - // The home server is valid, the next screen will be opened by the Activity + is Success -> { + // The home server url is valid + loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved(state.asyncHomeServerLoginFlowRequest.invoke())) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index dbf8abfe49..23c94425bc 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -55,13 +55,13 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignUp) fun signUp() { viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected(SignMode.SignUp)) } @OnClick(R.id.loginSignupSigninSignIn) fun signIn() { viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected(SignMode.SignIn)) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index 74ff7bb4bb..36ab47bb55 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -30,13 +30,10 @@ import android.webkit.SslErrorHandler import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog -import com.airbnb.mvrx.activityViewModel import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R -import im.vector.riotx.core.platform.OnBackPressed -import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_login_sso_fallback.* import timber.log.Timber import java.net.URLDecoder @@ -44,14 +41,10 @@ import javax.inject.Inject /** * Only login is supported for the moment - * TODO Migrate to new flow */ -class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnBackPressed { +class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { - private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel - private val viewModel: LoginViewModel by activityViewModel() - - var homeServerUrl: String = "" + private var homeServerUrl: String = "" enum class Mode { MODE_LOGIN, @@ -71,7 +64,6 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB login_sso_fallback_toolbar.title = getString(R.string.login) setupWebview() - loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) } @SuppressLint("SetJavaScriptEnabled") @@ -146,7 +138,7 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB super.onReceivedError(view, errorCode, description, failingUrl) // on error case, close this fragment - loginSharedActionViewModel.post(LoginNavigation.GoBack) + loginSharedActionViewModel.post(LoginNavigation.OnSsoLoginFallbackError(errorCode, description, failingUrl)) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { @@ -294,12 +286,16 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB } } + override fun resetViewModel() { + // Nothing to do + } + override fun onBackPressed(): Boolean { return if (login_sso_fallback_webview.canGoBack()) { login_sso_fallback_webview.goBack() true } else { - false + super.onBackPressed() } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 227ddf89da..ab50d6f036 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -78,14 +78,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi currentTask = null when (action) { - LoginAction.ResetLogin -> { + LoginAction.ResetLogin -> { setState { copy( asyncLoginAction = Uninitialized ) } } - LoginAction.ResetHomeServerUrl -> { + LoginAction.ResetHomeServerUrl -> { setState { copy( asyncHomeServerLoginFlowRequest = Uninitialized @@ -99,10 +99,11 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } } - LoginAction.ResetSignMode -> { + LoginAction.ResetSignMode -> { setState { copy( - signMode = SignMode.Unknown + signMode = SignMode.Unknown, + asyncHomeServerLoginFlowRequest = Uninitialized ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 542bc0799d..0d1b592611 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized @@ -25,7 +26,14 @@ data class LoginViewState( val signMode: SignMode = SignMode.Unknown, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized -) : MvRxState +) : MvRxState { + + fun isLoading(): Boolean { + // TODO Add other async here + return asyncLoginAction is Loading + || asyncHomeServerLoginFlowRequest is Loading + } +} enum class LoginMode { Password, diff --git a/vector/src/main/res/layout/activity_login.xml b/vector/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000000..0add6040a7 --- /dev/null +++ b/vector/src/main/res/layout/activity_login.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 94340cb97c..03efb60ab2 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -66,11 +66,13 @@ android:layout_marginTop="16dp"> + app:errorEnabled="true" + app:errorIconDrawable="@null"> - - - - diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml index f61ce48daf..5f6cc80072 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml @@ -98,28 +98,5 @@ - - - - diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index cfd1b6721c..c64b020eba 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -52,4 +52,6 @@ Enter the address of the Modular Riot or Server you want to use Enter the address of a server or a Riot you want to connect to + An error occurred when loading the page: %1$s (%2$d) + From d50b690523a68555b6dadc2d5539e2f4f173e21c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 21:48:06 +0100 Subject: [PATCH 015/189] Login screens: improve LoginFragment --- .../im/vector/riotx/core/utils/ViewUtils.kt | 55 +++++++++++++++++++ .../riotx/features/login/LoginFragment.kt | 39 +++++++++++-- .../riotx/features/login/LoginViewModel.kt | 1 + vector/src/main/res/layout/fragment_login.xml | 20 +++++-- 4 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt new file mode 100644 index 0000000000..335b9112ef --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.utils + +import android.text.Editable +import android.view.ViewGroup +import androidx.core.view.children +import com.google.android.material.textfield.TextInputLayout +import im.vector.riotx.core.platform.SimpleTextWatcher + +/** + * Find all TextInputLayout in a ViewGroup and in all its descendants + */ +fun ViewGroup.findAllTextInputLayout(): List { + val res = ArrayList() + + children.forEach { + if (it is TextInputLayout) { + res.add(it) + } else if (it is ViewGroup) { + // Recursive call + res.addAll(it.findAllTextInputLayout()) + } + } + + return res +} + +/** + * Add a text change listener to all TextInputEditText to reset error on its TextInputLayout when the text is changed + */ +fun autoResetTextInputLayoutErrors(textInputLayouts: List) { + textInputLayouts.forEach { + it.editText?.addTextChangedListener(object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + // Reset the error + it.error = null + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 571305b722..4cd25d7640 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -18,7 +18,7 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View -import androidx.transition.TransitionManager +import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success @@ -69,9 +69,14 @@ class LoginFragment @Inject constructor( isLoginNotEmpty && isPasswordNotEmpty } ) - .subscribeBy { authenticateButton.isEnabled = it } - .disposeOnDestroyView() - authenticateButton.setOnClickListener { authenticate() } + .subscribeBy { + loginFieldTil.error = null + passwordFieldTil.error = null + loginSubmit.isEnabled = it + } + .disposeOnDestroy() + + loginSubmit.setOnClickListener { authenticate() } } // // TODO Move to server selection screen @@ -108,7 +113,28 @@ class LoginFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { state -> - TransitionManager.beginDelayedTransition(login_fragment) + when (state.serverType) { + ServerType.MatrixOrg -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginNotice.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_modular) + // TODO + loginTitle.text = getString(R.string.login_connect_to, "TODO") + // TODO Remove https:// + loginNotice.text = viewModel.getHomeServerUrl() + } + ServerType.Other -> { + loginServerIcon.isVisible = false + loginTitle.text = getString(R.string.login_server_other_title) + // TODO Remove https:// + loginNotice.text = viewModel.getHomeServerUrl() + } + } when (state.asyncLoginAction) { is Loading -> { @@ -117,8 +143,9 @@ class LoginFragment @Inject constructor( renderPasswordField() } is Fail -> { + // TODO This does not work, we want the error to be on without text. Fix that + loginFieldTil.error = "" // TODO Handle error text properly - // TODO Reset error when text is changed passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error) } // Success is handled by the LoginActivity diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index ab50d6f036..96f8d5ea99 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -152,6 +152,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException setState { copy( asyncLoginAction = Fail(failure) diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 03efb60ab2..708afd6943 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -25,24 +25,32 @@ style="@style/LoginTopIcon" android:layout_gravity="center_horizontal" /> - + + + android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" + tools:text="@string/login_server_matrix_org_text" /> Date: Thu, 14 Nov 2019 21:54:03 +0100 Subject: [PATCH 016/189] Login screens: re-click on an item submit it --- .../login/LoginServerSelectionFragment.kt | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 59be9c7aa8..fb0ec25ab0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -52,7 +52,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment text = getString(R.string.login_server_modular_learn_more) textDecorationLine = "underline" onClick = { - // TODO + // TODO this does not work openUrlInExternalBrowser(requireActivity(), "https://example.org") } } @@ -61,17 +61,32 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment @OnClick(R.id.loginServerChoiceMatrixOrg) fun selectMatrixOrg() { - viewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + if (loginServerChoiceMatrixOrg.isChecked) { + // Consider this is a submit + submit() + } else { + viewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + } } @OnClick(R.id.loginServerChoiceModular) fun selectModular() { - viewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + if (loginServerChoiceModular.isChecked) { + // Consider this is a submit + submit() + } else { + viewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + } } @OnClick(R.id.loginServerChoiceOther) fun selectOther() { - viewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + if (loginServerChoiceOther.isChecked) { + // Consider this is a submit + submit() + } else { + viewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + } } @OnClick(R.id.loginServerSubmit) From 5b9876a20cc3f12c363e3603eccf41997023bd16 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 22:16:50 +0100 Subject: [PATCH 017/189] Login screens: Fix navigation issue --- .../vector/riotx/core/error/ErrorFormatter.kt | 4 +++ .../riotx/features/login/LoginActivity.kt | 27 +++++++++++-------- .../vector/riotx/features/login/LoginMode.kt | 23 ++++++++++++++++ .../riotx/features/login/LoginNavigation.kt | 2 +- .../login/LoginServerSelectionFragment.kt | 2 +- .../login/LoginServerUrlFormFragment.kt | 2 +- .../riotx/features/login/LoginViewState.kt | 15 +++++------ 7 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index d08675ea1b..65f48f5e7b 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import java.net.SocketTimeoutException +import java.net.UnknownHostException import javax.inject.Inject class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) { @@ -36,6 +37,9 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi is Failure.NetworkConnection -> { if (throwable.ioException is SocketTimeoutException) { stringProvider.getString(R.string.error_network_timeout) + } else if (throwable.ioException is UnknownHostException) { + // Invalid homeserver? + stringProvider.getString(R.string.login_error_unknown_host) } else { stringProvider.getString(R.string.error_no_network) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 0ab10134e9..0eaaea33d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -21,7 +21,6 @@ import android.content.Intent import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager -import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import im.vector.riotx.R @@ -69,7 +68,7 @@ class LoginActivity : VectorBaseActivity() { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) - is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved(it) + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() is LoginNavigation.OnSsoLoginFallbackError -> onSsoLoginFallbackError(it) } } @@ -82,16 +81,12 @@ class LoginActivity : VectorBaseActivity() { .disposeOnDestroy() } - private fun onLoginFlowRetrieved(onLoginFlowRetrieved: LoginNavigation.OnLoginFlowRetrieved) { - when (onLoginFlowRetrieved.loginMode) { - LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSsoFallbackFragment::class.java) - LoginMode.Unsupported, - LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) - } + private fun onLoginFlowRetrieved() { + addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) } private fun updateWithState(loginViewState: LoginViewState) { - if (loginViewState.asyncLoginAction is Success) { + if (loginViewState.isUserLogged()) { val intent = HomeActivity.newIntent(this) startActivity(intent) finish() @@ -123,11 +118,21 @@ class LoginActivity : VectorBaseActivity() { } private fun onSignModeSelected(mode: LoginNavigation.OnSignModeSelected) { - // We cannot use the state, it is not ready... + // We cannot use the state to get the SignMode, it is not ready... when (mode.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.loginFragmentContainer, SignUpFragment::class.java) - SignMode.SignIn -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + SignMode.SignIn -> { + // It depends on the LoginMode + withState(loginViewModel) { + when (it.asyncHomeServerLoginFlowRequest.invoke()) { + null -> error("Developer error") + LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSsoFallbackFragment::class.java) + LoginMode.Unsupported -> TODO() // TODO Import Fallback login fragment from Riot-Android + } + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt new file mode 100644 index 0000000000..ae40d3a95a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +enum class LoginMode { + Password, + Sso, + Unsupported +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index ba1e327e3a..a223660708 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -22,7 +22,7 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() - data class OnLoginFlowRetrieved(val loginMode: LoginMode) : LoginNavigation() + object OnLoginFlowRetrieved : LoginNavigation() data class OnSignModeSelected(val signMode: SignMode) : LoginNavigation() //object OpenSsoLoginFallback : LoginNavigation() data class OnSsoLoginFallbackError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index fb0ec25ab0..27ae99c20b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -112,7 +112,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment } is Success -> { // The home server url is valid - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved(it.asyncHomeServerLoginFlowRequest.invoke())) + loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 2ea5869448..b5f002b353 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -123,7 +123,7 @@ class LoginServerUrlFormFragment @Inject constructor( } is Success -> { // The home server url is valid - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved(state.asyncHomeServerLoginFlowRequest.invoke())) + loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 0d1b592611..4be96d20c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -16,10 +16,7 @@ package im.vector.riotx.features.login -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.* data class LoginViewState( val serverType: ServerType = ServerType.MatrixOrg, @@ -33,10 +30,10 @@ data class LoginViewState( return asyncLoginAction is Loading || asyncHomeServerLoginFlowRequest is Loading } + + fun isUserLogged(): Boolean { + // TODO Add other async here + return asyncLoginAction is Success + } } -enum class LoginMode { - Password, - Sso, - Unsupported -} From 2849e1f8465a874fa82bb8b0ffc959ba7517d2a5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 09:59:19 +0100 Subject: [PATCH 018/189] Login screens: Splash: update icons --- .../res/drawable/ic_login_splash_lock.xml | 22 ++++++++++++++++++ .../ic_login_splash_message_circle.xml | 14 +++++++++++ .../res/drawable/ic_login_splash_sliders.xml | 14 +++++++++++ .../main/res/layout/fragment_login_splash.xml | 23 +++++++++---------- 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_login_splash_lock.xml create mode 100644 vector/src/main/res/drawable/ic_login_splash_message_circle.xml create mode 100644 vector/src/main/res/drawable/ic_login_splash_sliders.xml diff --git a/vector/src/main/res/drawable/ic_login_splash_lock.xml b/vector/src/main/res/drawable/ic_login_splash_lock.xml new file mode 100644 index 0000000000..26470cefce --- /dev/null +++ b/vector/src/main/res/drawable/ic_login_splash_lock.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_login_splash_message_circle.xml b/vector/src/main/res/drawable/ic_login_splash_message_circle.xml new file mode 100644 index 0000000000..81b5e9476a --- /dev/null +++ b/vector/src/main/res/drawable/ic_login_splash_message_circle.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_login_splash_sliders.xml b/vector/src/main/res/drawable/ic_login_splash_sliders.xml new file mode 100644 index 0000000000..b7c850eea7 --- /dev/null +++ b/vector/src/main/res/drawable/ic_login_splash_sliders.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index e49270ea51..b77493f6d1 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -31,13 +31,13 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashLogo" /> - @@ -46,22 +46,22 @@ android:id="@+id/loginSplashText1" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="12dp" + android:layout_marginStart="36dp" android:layout_marginTop="32dp" android:gravity="start" android:text="@string/login_splash_text1" android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@+id/loginSplashText2" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/loginSplashPicto1" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle" /> - @@ -70,22 +70,21 @@ android:id="@+id/loginSplashText2" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="12dp" + android:layout_marginStart="36dp" android:layout_marginTop="16dp" android:gravity="start" android:text="@string/login_splash_text2" android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@id/loginSplashText3" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/loginSplashPicto2" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" /> - @@ -94,14 +93,14 @@ android:id="@+id/loginSplashText3" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="12dp" + android:layout_marginStart="36dp" android:layout_marginTop="16dp" android:gravity="start" android:text="@string/login_splash_text3" android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/loginSplashPicto3" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" /> Date: Fri, 15 Nov 2019 10:08:27 +0100 Subject: [PATCH 019/189] Login screens: Fix Other rendering issue --- .../res/layout/fragment_login_signup_signin_selection.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index 240e8866f3..3182818ca5 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -29,10 +29,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="172dp" + android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninLogo" - app:layout_goneMarginTop="172dp" - tools:src="@drawable/ic_logo_matrix_org" /> + tools:src="@drawable/ic_logo_matrix_org" + tools:visibility="visible" /> Date: Fri, 15 Nov 2019 11:47:49 +0100 Subject: [PATCH 020/189] Login screens: move elements from ViewState to ViewModel --- .../features/login/AbstractLoginFragment.kt | 4 +- .../riotx/features/login/LoginActivity.kt | 11 ++- .../riotx/features/login/LoginFragment.kt | 55 +++++++------- .../riotx/features/login/LoginNavigation.kt | 2 +- .../login/LoginServerSelectionFragment.kt | 35 +++++---- .../login/LoginServerUrlFormFragment.kt | 73 ++++++++++--------- .../LoginSignUpSignInSelectionFragment.kt | 27 ++++--- .../login/LoginSsoFallbackFragment.kt | 6 +- .../riotx/features/login/LoginViewModel.kt | 26 +++---- .../riotx/features/login/LoginViewState.kt | 2 - 10 files changed, 124 insertions(+), 117 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index c0ff5103f9..9e57350b79 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -28,7 +28,7 @@ import im.vector.riotx.core.platform.VectorBaseFragment */ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { - protected val viewModel: LoginViewModel by activityViewModel() + protected val loginViewModel: LoginViewModel by activityViewModel() protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @CallSuper @@ -44,6 +44,6 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { return false } - // Reset any modification of the viewModel by the current fragment + // Reset any modification on the loginViewModel by the current fragment abstract fun resetViewModel() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 0eaaea33d4..7e90931716 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -67,7 +67,7 @@ class LoginActivity : VectorBaseActivity() { when (it) { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() is LoginNavigation.OnSsoLoginFallbackError -> onSsoLoginFallbackError(it) } @@ -109,17 +109,16 @@ class LoginActivity : VectorBaseActivity() { .show() } - private fun onServerSelectionDone() = withState(loginViewModel) { - when (it.serverType) { + private fun onServerSelectionDone() { + when (loginViewModel.serverType) { ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.Modular, ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerUrlFormFragment::class.java) } } - private fun onSignModeSelected(mode: LoginNavigation.OnSignModeSelected) { - // We cannot use the state to get the SignMode, it is not ready... - when (mode.signMode) { + private fun onSignModeSelected() { + when (loginViewModel.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.loginFragmentContainer, SignUpFragment::class.java) SignMode.SignIn -> { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 4cd25d7640..2bc5a6171e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -49,6 +49,7 @@ class LoginFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupUi() setupLoginButton() setupPasswordReveal() } @@ -57,7 +58,32 @@ class LoginFragment @Inject constructor( val login = loginField.text?.trim().toString() val password = passwordField.text?.trim().toString() - viewModel.handle(LoginAction.Login(login, password)) + loginViewModel.handle(LoginAction.Login(login, password)) + } + + private fun setupUi() { + when (loginViewModel.serverType) { + ServerType.MatrixOrg -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginNotice.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_modular) + // TODO + loginTitle.text = getString(R.string.login_connect_to, "TODO") + // TODO Remove https:// + loginNotice.text = loginViewModel.getHomeServerUrl() + } + ServerType.Other -> { + loginServerIcon.isVisible = false + loginTitle.text = getString(R.string.login_server_other_title) + // TODO Remove https:// + loginNotice.text = loginViewModel.getHomeServerUrl() + } + } } private fun setupLoginButton() { @@ -109,33 +135,10 @@ class LoginFragment @Inject constructor( } override fun resetViewModel() { - viewModel.handle(LoginAction.ResetLogin) + loginViewModel.handle(LoginAction.ResetLogin) } - override fun invalidate() = withState(viewModel) { state -> - when (state.serverType) { - ServerType.MatrixOrg -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(R.string.login_connect_to, "matrix.org") - loginNotice.text = getString(R.string.login_server_matrix_org_text) - } - ServerType.Modular -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_modular) - // TODO - loginTitle.text = getString(R.string.login_connect_to, "TODO") - // TODO Remove https:// - loginNotice.text = viewModel.getHomeServerUrl() - } - ServerType.Other -> { - loginServerIcon.isVisible = false - loginTitle.text = getString(R.string.login_server_other_title) - // TODO Remove https:// - loginNotice.text = viewModel.getHomeServerUrl() - } - } - + override fun invalidate() = withState(loginViewModel) { state -> when (state.asyncLoginAction) { is Loading -> { // Ensure password is hidden diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index a223660708..2c366a0eb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -23,7 +23,7 @@ sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() object OnLoginFlowRetrieved : LoginNavigation() - data class OnSignModeSelected(val signMode: SignMode) : LoginNavigation() + object OnSignModeSelected : LoginNavigation() //object OpenSsoLoginFallback : LoginNavigation() data class OnSsoLoginFallbackError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 27ae99c20b..e819389b9c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -38,13 +38,16 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + updateSelectedChoice() initTextViews() } - private fun updateSelectedChoice(serverType: ServerType) { - loginServerChoiceMatrixOrg.isChecked = serverType == ServerType.MatrixOrg - loginServerChoiceModular.isChecked = serverType == ServerType.Modular - loginServerChoiceOther.isChecked = serverType == ServerType.Other + private fun updateSelectedChoice() { + loginViewModel.serverType.let { + loginServerChoiceMatrixOrg.isChecked = it == ServerType.MatrixOrg + loginServerChoiceModular.isChecked = it == ServerType.Modular + loginServerChoiceOther.isChecked = it == ServerType.Other + } } private fun initTextViews() { @@ -56,7 +59,6 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment openUrlInExternalBrowser(requireActivity(), "https://example.org") } } - } @OnClick(R.id.loginServerChoiceMatrixOrg) @@ -65,7 +67,8 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Consider this is a submit submit() } else { - viewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + updateSelectedChoice() } } @@ -75,7 +78,8 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Consider this is a submit submit() } else { - viewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + updateSelectedChoice() } } @@ -85,33 +89,32 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Consider this is a submit submit() } else { - viewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + updateSelectedChoice() } } @OnClick(R.id.loginServerSubmit) - fun submit() = withState(viewModel) { - if (it.serverType == ServerType.MatrixOrg) { + fun submit() { + if (loginViewModel.serverType == ServerType.MatrixOrg) { // Request login flow here - viewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) + loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) } else { loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) } } override fun resetViewModel() { - viewModel.handle(LoginAction.ResetHomeServerType) + loginViewModel.handle(LoginAction.ResetHomeServerType) } - override fun invalidate() = withState(viewModel) { - updateSelectedChoice(it.serverType) - + override fun invalidate() = withState(loginViewModel) { when (it.asyncHomeServerLoginFlowRequest) { is Fail -> { // TODO Display error in a dialog? } is Success -> { - // The home server url is valid + // LoginFlow for matrix.org has been retrieved loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index b5f002b353..c3d841369b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -44,6 +44,11 @@ class LoginServerUrlFormFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupUi() + setupHomeServerField() + } + + private fun setupHomeServerField() { // TODO Import code from Riot to clear error on TIL loginServerUrlFormHomeServerUrl.textChanges() .subscribe( @@ -64,39 +69,8 @@ class LoginServerUrlFormFragment @Inject constructor( } } - @OnClick(R.id.loginServerUrlFormLearnMore) - fun learMore() { - // TODO - openUrlInExternalBrowser(requireActivity(), "https://example.org") - } - - override fun resetViewModel() { - viewModel.handle(LoginAction.ResetHomeServerUrl) - } - - @SuppressLint("SetTextI18n") - @OnClick(R.id.loginServerUrlFormSubmit) - fun submit() { - // Static check of homeserver url, empty, malformed, etc. - var serverUrl = loginServerUrlFormHomeServerUrl.text.toString() - - when { - serverUrl.isBlank() -> { - loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) - } - else -> { - if (serverUrl.startsWith("http").not()) { - serverUrl = "https://$serverUrl" - loginServerUrlFormHomeServerUrl.setText(serverUrl) - - } - viewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) - } - } - } - - override fun invalidate() = withState(viewModel) { state -> - when (state.serverType) { + private fun setupUi() { + when (loginViewModel.serverType) { ServerType.Modular -> { loginServerUrlFormIcon.isVisible = true loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular) @@ -115,7 +89,40 @@ class LoginServerUrlFormFragment @Inject constructor( } else -> error("This fragment should not be display in matrix.org mode") } + } + @OnClick(R.id.loginServerUrlFormLearnMore) + fun learMore() { + // TODO + openUrlInExternalBrowser(requireActivity(), "https://example.org") + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetHomeServerUrl) + } + + @SuppressLint("SetTextI18n") + @OnClick(R.id.loginServerUrlFormSubmit) + fun submit() { + // Static check of homeserver url, empty, malformed, etc. + var serverUrl = loginServerUrlFormHomeServerUrl.text.toString() + + when { + serverUrl.isBlank() -> { + loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) + } + else -> { + if (serverUrl.startsWith("http").not()) { + serverUrl = "https://$serverUrl" + loginServerUrlFormHomeServerUrl.setText(serverUrl) + + } + loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) + } + } + } + + override fun invalidate() = withState(loginViewModel) { state -> when (state.asyncHomeServerLoginFlowRequest) { is Fail -> { // TODO Error text is not correct diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 23c94425bc..cdb9aa8b7a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -16,9 +16,10 @@ package im.vector.riotx.features.login +import android.os.Bundle +import android.view.View import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.withState import im.vector.riotx.R import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject @@ -30,8 +31,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection - private fun updateViews(serverType: ServerType) { - when (serverType) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + } + + private fun setupUi() { + when (loginViewModel.serverType) { ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) loginSignupSigninServerIcon.isVisible = true @@ -54,21 +61,17 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignUp) fun signUp() { - viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected(SignMode.SignUp)) + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } @OnClick(R.id.loginSignupSigninSignIn) fun signIn() { - viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected(SignMode.SignIn)) + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } override fun resetViewModel() { - viewModel.handle(LoginAction.ResetSignMode) - } - - override fun invalidate() = withState(viewModel) { - updateViews(it.serverType) + loginViewModel.handle(LoginAction.ResetSignMode) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index 36ab47bb55..fcf34bbdc4 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -74,7 +74,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { // the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK) login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google" - homeServerUrl = viewModel.getHomeServerUrl() + homeServerUrl = loginViewModel.getHomeServerUrl() if (!homeServerUrl.endsWith("/")) { homeServerUrl += "/" @@ -248,7 +248,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { refreshToken = null ) - viewModel.handle(LoginAction.SsoLoginSuccess(safeCredentials)) + loginViewModel.handle(LoginAction.SsoLoginSuccess(safeCredentials)) } } } catch (e: Exception) { @@ -273,7 +273,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { refreshToken = null ) - viewModel.handle(LoginAction.SsoLoginSuccess(credentials)) + loginViewModel.handle(LoginAction.SsoLoginSuccess(credentials)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 96f8d5ea99..f4a9b24812 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -55,6 +55,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + var serverType: ServerType = ServerType.MatrixOrg + private set + + var signMode: SignMode = SignMode.Unknown + private set + private var loginConfig: LoginConfig? = null private var homeServerConnectionConfig: HomeServerConnectionConfig? = null @@ -93,16 +99,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } LoginAction.ResetHomeServerType -> { - setState { - copy( - serverType = ServerType.MatrixOrg - ) - } + serverType = ServerType.MatrixOrg } LoginAction.ResetSignMode -> { + signMode = SignMode.Unknown setState { copy( - signMode = SignMode.Unknown, asyncHomeServerLoginFlowRequest = Uninitialized ) } @@ -111,19 +113,11 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun handleUpdateSignMode(action: LoginAction.UpdateSignMode) { - setState { - copy( - signMode = action.signMode - ) - } + signMode = action.signMode } private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { - setState { - copy( - serverType = action.serverType - ) - } + serverType = action.serverType } private fun handleInitWith(action: LoginAction.InitWith) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 4be96d20c2..0853765d63 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -19,8 +19,6 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.* data class LoginViewState( - val serverType: ServerType = ServerType.MatrixOrg, - val signMode: SignMode = SignMode.Unknown, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState { From b7bfb20a2e1ef2f0f0fa4dc8156154e865e02971 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 11:57:18 +0100 Subject: [PATCH 021/189] Login screens: login and registration fallback --- .../im/vector/riotx/core/di/FragmentModule.kt | 6 +- .../riotx/features/login/LoginAction.kt | 2 +- .../riotx/features/login/LoginActivity.kt | 35 ++++++---- .../riotx/features/login/LoginFragment.kt | 5 -- .../vector/riotx/features/login/LoginMode.kt | 8 +-- .../riotx/features/login/LoginNavigation.kt | 3 +- .../riotx/features/login/LoginViewModel.kt | 6 +- ...allbackFragment.kt => LoginWebFragment.kt} | 70 +++++++++---------- ...so_fallback.xml => fragment_login_web.xml} | 4 +- vector/src/main/res/values/strings_riotX.xml | 1 + 10 files changed, 73 insertions(+), 67 deletions(-) rename vector/src/main/java/im/vector/riotx/features/login/{LoginSsoFallbackFragment.kt => LoginWebFragment.kt} (86%) rename vector/src/main/res/layout/{fragment_login_sso_fallback.xml => fragment_login_web.xml} (86%) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 1457519052..d788f4c04c 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -37,7 +37,7 @@ import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.login.LoginFragment import im.vector.riotx.features.login.LoginServerUrlFormFragment -import im.vector.riotx.features.login.LoginSsoFallbackFragment +import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment @@ -124,8 +124,8 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(LoginSsoFallbackFragment::class) - fun bindLoginSsoFallbackFragment(fragment: LoginSsoFallbackFragment): Fragment + @FragmentKey(LoginWebFragment::class) + fun bindLoginWebFragment(fragment: LoginWebFragment): Fragment @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 310d07b746..754372e82c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -24,7 +24,7 @@ sealed class LoginAction : VectorViewModelAction { data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class UpdateSignMode(val signMode: SignMode) : LoginAction() data class Login(val login: String, val password: String) : LoginAction() - data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() + data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() // Reset actions diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 7e90931716..c3a748f442 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -65,11 +65,11 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() - is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() - is LoginNavigation.OnSsoLoginFallbackError -> onSsoLoginFallbackError(it) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() + is LoginNavigation.OnWebLoginError -> onWebLoginError(it) } } .disposeOnDestroy() @@ -97,14 +97,14 @@ class LoginActivity : VectorBaseActivity() { loginLoading.isVisible = loginViewState.isLoading() } - private fun onSsoLoginFallbackError(onSsoLoginFallbackError: LoginNavigation.OnSsoLoginFallbackError) { + private fun onWebLoginError(onWebLoginError: LoginNavigation.OnWebLoginError) { // Pop the backstack supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) // And inform the user AlertDialog.Builder(this) .setTitle(R.string.dialog_title_error) - .setMessage(getString(R.string.login_sso_error_message, onSsoLoginFallbackError.description, onSsoLoginFallbackError.errorCode)) + .setMessage(getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) .setPositiveButton(R.string.ok, null) .show() } @@ -124,17 +124,28 @@ class LoginActivity : VectorBaseActivity() { SignMode.SignIn -> { // It depends on the LoginMode withState(loginViewModel) { - when (it.asyncHomeServerLoginFlowRequest.invoke()) { - null -> error("Developer error") - LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) - LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSsoFallbackFragment::class.java) - LoginMode.Unsupported -> TODO() // TODO Import Fallback login fragment from Riot-Android + when (val loginMode = it.asyncHomeServerLoginFlowRequest.invoke()) { + null -> error("Developer error") + LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginWebFragment::class.java) + is LoginMode.Unsupported -> onLoginModeNotSupported(loginMode) } } } } } + private fun onLoginModeNotSupported(unsupportedLoginMode: LoginMode.Unsupported) { + AlertDialog.Builder(this) + .setTitle(R.string.app_name) + .setMessage(getString(R.string.login_mode_not_supported, unsupportedLoginMode.types.joinToString { "'$it'" })) + .setPositiveButton(R.string.yes) { _, _ -> + addFragmentToBackstack(R.id.loginFragmentContainer, LoginWebFragment::class.java) + } + .setNegativeButton(R.string.no, null) + .show() + } + override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 2bc5a6171e..58017d0f21 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -105,11 +105,6 @@ class LoginFragment @Inject constructor( loginSubmit.setOnClickListener { authenticate() } } -// // TODO Move to server selection screen -// private fun openSso() { -// loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) -// } - private fun setupPasswordReveal() { passwordShown = false diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt index ae40d3a95a..bea4c41cb8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt @@ -16,8 +16,8 @@ package im.vector.riotx.features.login -enum class LoginMode { - Password, - Sso, - Unsupported +sealed class LoginMode { + object Password : LoginMode() + object Sso : LoginMode() + data class Unsupported(val types: List) : LoginMode() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index 2c366a0eb7..f0ff456734 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -24,6 +24,5 @@ sealed class LoginNavigation : VectorSharedAction { object OnServerSelectionDone : LoginNavigation() object OnLoginFlowRetrieved : LoginNavigation() object OnSignModeSelected : LoginNavigation() - //object OpenSsoLoginFallback : LoginNavigation() - data class OnSsoLoginFallbackError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index f4a9b24812..9ba5fa739b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -73,7 +73,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) - is LoginAction.SsoLoginSuccess -> handleSsoLoginSuccess(action) + is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.ResetAction -> handleResetAction(action) } } @@ -167,7 +167,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } - private fun handleSsoLoginSuccess(action: LoginAction.SsoLoginSuccess) { + private fun handleWebLoginSuccess(action: LoginAction.WebLoginSuccess) { val homeServerConnectionConfigFinal = homeServerConnectionConfig if (homeServerConnectionConfigFinal == null) { @@ -233,7 +233,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi // SSO login is taken first data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_SSO } -> LoginMode.Sso data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_PASSWORD } -> LoginMode.Password - else -> LoginMode.Unsupported + else -> LoginMode.Unsupported(data.flows.mapNotNull { it.type }.toList()) } setState { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt similarity index 86% rename from vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt rename to vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index fcf34bbdc4..d7230f0075 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -34,47 +34,47 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R -import kotlinx.android.synthetic.main.fragment_login_sso_fallback.* +import kotlinx.android.synthetic.main.fragment_login_web.* import timber.log.Timber import java.net.URLDecoder import javax.inject.Inject /** - * Only login is supported for the moment + * This screen is displayed for SSO login and also when the application does not support login flow or registration flow + * of the homeserfver, as a fallback to login or to create an account */ -class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { +class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { - private var homeServerUrl: String = "" + private lateinit var homeServerUrl: String + private lateinit var signMode: SignMode - enum class Mode { - MODE_LOGIN, - // Not supported in RiotX for the moment - MODE_REGISTER - } - - // Mode (MODE_LOGIN or MODE_REGISTER) - private var mMode = Mode.MODE_LOGIN - - override fun getLayoutResId() = R.layout.fragment_login_sso_fallback + override fun getLayoutResId() = R.layout.fragment_login_web override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupToolbar(login_sso_fallback_toolbar) - login_sso_fallback_toolbar.title = getString(R.string.login) + homeServerUrl = loginViewModel.getHomeServerUrl() + signMode = loginViewModel.signMode.takeIf { it != SignMode.Unknown } ?: error("Developer error: Invalid sign mode") - setupWebview() + setupToolbar(loginWebToolbar) + setupTitle() + setupWebView() + } + + private fun setupTitle() { + loginWebToolbar.title = when (signMode) { + SignMode.SignIn -> getString(R.string.login_signin) + else -> getString(R.string.login_signup) + } } @SuppressLint("SetJavaScriptEnabled") - private fun setupWebview() { - login_sso_fallback_webview.settings.javaScriptEnabled = true + private fun setupWebView() { + loginWebWebView.settings.javaScriptEnabled = true // Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack // the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK) - login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google" - - homeServerUrl = loginViewModel.getHomeServerUrl() + loginWebWebView.settings.userAgentString = "Mozilla/5.0 Google" if (!homeServerUrl.endsWith("/")) { homeServerUrl += "/" @@ -109,14 +109,14 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { } private fun launchWebView() { - if (mMode == Mode.MODE_LOGIN) { - login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/login/") + if (signMode == SignMode.SignIn) { + loginWebWebView.loadUrl(homeServerUrl + "_matrix/static/client/login/") } else { // MODE_REGISTER - login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/register/") + loginWebWebView.loadUrl(homeServerUrl + "_matrix/static/client/register/") } - login_sso_fallback_webview.webViewClient = object : WebViewClient() { + loginWebWebView.webViewClient = object : WebViewClient() { override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { AlertDialog.Builder(requireActivity()) @@ -131,20 +131,20 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { } false }) + .setCancelable(false) .show() } override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) - // on error case, close this fragment - loginSharedActionViewModel.post(LoginNavigation.OnSsoLoginFallbackError(errorCode, description, failingUrl)) + loginSharedActionViewModel.post(LoginNavigation.OnWebLoginError(errorCode, description, failingUrl)) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - login_sso_fallback_toolbar.subtitle = url + loginWebToolbar.subtitle = url } override fun onPageFinished(view: WebView, url: String) { @@ -160,7 +160,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { view.loadUrl(mxcJavascriptSendObjectMessage) - if (mMode == Mode.MODE_LOGIN) { + if (signMode == SignMode.SignIn) { // The function the fallback page calls when the login is complete val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" + " sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" + @@ -227,7 +227,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { if (parameters != null) { val action = parameters["action"] as String - if (mMode == Mode.MODE_LOGIN) { + if (signMode == SignMode.SignIn) { try { if (action == "onLogin") { @Suppress("UNCHECKED_CAST") @@ -248,7 +248,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { refreshToken = null ) - loginViewModel.handle(LoginAction.SsoLoginSuccess(safeCredentials)) + loginViewModel.handle(LoginAction.WebLoginSuccess(safeCredentials)) } } } catch (e: Exception) { @@ -273,7 +273,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { refreshToken = null ) - loginViewModel.handle(LoginAction.SsoLoginSuccess(credentials)) + loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) } } } @@ -291,8 +291,8 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { } override fun onBackPressed(): Boolean { - return if (login_sso_fallback_webview.canGoBack()) { - login_sso_fallback_webview.goBack() + return if (loginWebWebView.canGoBack()) { + loginWebWebView.goBack() true } else { super.onBackPressed() diff --git a/vector/src/main/res/layout/fragment_login_sso_fallback.xml b/vector/src/main/res/layout/fragment_login_web.xml similarity index 86% rename from vector/src/main/res/layout/fragment_login_sso_fallback.xml rename to vector/src/main/res/layout/fragment_login_web.xml index e83680d2cd..6383b9f137 100644 --- a/vector/src/main/res/layout/fragment_login_sso_fallback.xml +++ b/vector/src/main/res/layout/fragment_login_web.xml @@ -6,7 +6,7 @@ android:orientation="vertical"> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index c64b020eba..17bb99ac4a 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -53,5 +53,6 @@ Enter the address of a server or a Riot you want to connect to An error occurred when loading the page: %1$s (%2$d) + The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? From 2871e4f5b196d34ec6fa5e014c126a0c2faa47c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 14:22:49 +0100 Subject: [PATCH 022/189] Login screens: forget password screens --- .../matrix/android/api/auth/Authenticator.kt | 5 + .../internal/auth/DefaultAuthenticator.kt | 19 +++ .../riotx/features/login/LoginAction.kt | 2 + .../riotx/features/login/LoginActivity.kt | 16 ++- .../riotx/features/login/LoginFragment.kt | 16 ++- .../riotx/features/login/LoginNavigation.kt | 4 + .../login/LoginResetPasswordFragment.kt | 136 ++++++++++++++++++ .../LoginResetPasswordSuccessFragment.kt | 51 +++++++ .../riotx/features/login/LoginViewModel.kt | 55 ++++++- .../riotx/features/login/LoginViewState.kt | 6 +- .../layout/fragment_login_reset_password.xml | 118 +++++++++++++++ .../fragment_login_reset_password_success.xml | 62 ++++++++ vector/src/main/res/values/strings_riotX.xml | 14 ++ 13 files changed, 491 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_reset_password.xml create mode 100644 vector/src/main/res/layout/fragment_login_reset_password_success.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt index c1dfa465fb..def8293798 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt @@ -68,4 +68,9 @@ interface Authenticator { * Create a session after a SSO successful login */ fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable + + /** + * Reset user password + */ + fun resetPassword(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index ff49d4308b..995ec0aedb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -131,6 +131,25 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated sessionManager.getOrCreateSession(sessionParams) } + override fun resetPassword(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String, callback: MatrixCallback): Cancelable { + val job = GlobalScope.launch(coroutineDispatchers.main) { + val result = runCatching { + resetPasswordInternal(/*homeServerConnectionConfig, email, newPassword*/) + } + result.foldToCallback(callback) + } + return CancelableCoroutine(job) + } + + private fun resetPasswordInternal(/*homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String*/) { + // TODO + error("Not implemented") + //val authAPI = buildAuthAPI(homeServerConnectionConfig) + //executeRequest { + // apiCall = authAPI.getLoginFlows() + //} + } + private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 754372e82c..a03af1376d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -26,6 +26,7 @@ sealed class LoginAction : VectorViewModelAction { data class Login(val login: String, val password: String) : LoginAction() data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() + data class ResetPassword(val email: String, val newPassword: String) : LoginAction() // Reset actions open class ResetAction : LoginAction() @@ -34,4 +35,5 @@ sealed class LoginAction : VectorViewModelAction { object ResetHomeServerUrl : ResetAction() object ResetSignMode : ResetAction() object ResetLogin : ResetAction() + object ResetResetPassword : ResetAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index c3a748f442..7bc713a4f2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -65,11 +65,17 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() - is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() - is LoginNavigation.OnWebLoginError -> onWebLoginError(it) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() + is LoginNavigation.OnWebLoginError -> onWebLoginError(it) + is LoginNavigation.OnForgetPasswordClicked -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordFragment::class.java) + is LoginNavigation.OnResetPasswordSuccess -> { + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordSuccessFragment::class.java) + } + is LoginNavigation.OnResetPasswordSuccessDone -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 58017d0f21..14465fa48b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View import androidx.core.view.isVisible +import butterknife.OnClick import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success @@ -50,7 +51,7 @@ class LoginFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupUi() - setupLoginButton() + setupSubmitButton() setupPasswordReveal() } @@ -74,19 +75,17 @@ class LoginFragment @Inject constructor( loginServerIcon.setImageResource(R.drawable.ic_logo_modular) // TODO loginTitle.text = getString(R.string.login_connect_to, "TODO") - // TODO Remove https:// - loginNotice.text = loginViewModel.getHomeServerUrl() + loginNotice.text = loginViewModel.getHomeServerUrlSimple() } ServerType.Other -> { loginServerIcon.isVisible = false loginTitle.text = getString(R.string.login_server_other_title) - // TODO Remove https:// - loginNotice.text = loginViewModel.getHomeServerUrl() + loginNotice.text = loginViewModel.getHomeServerUrlSimple() } } } - private fun setupLoginButton() { + private fun setupSubmitButton() { Observable .combineLatest( loginField.textChanges().map { it.trim().isNotEmpty() }, @@ -105,6 +104,11 @@ class LoginFragment @Inject constructor( loginSubmit.setOnClickListener { authenticate() } } + @OnClick(R.id.forgetPasswordButton) + fun forgetPasswordClicked() { + loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + } + private fun setupPasswordReveal() { passwordShown = false diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index f0ff456734..8eccaa0297 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -24,5 +24,9 @@ sealed class LoginNavigation : VectorSharedAction { object OnServerSelectionDone : LoginNavigation() object OnLoginFlowRetrieved : LoginNavigation() object OnSignModeSelected : LoginNavigation() + object OnForgetPasswordClicked : LoginNavigation() + object OnResetPasswordSuccess : LoginNavigation() + object OnResetPasswordSuccessDone : LoginNavigation() + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt new file mode 100644 index 0000000000..600013c5ba --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.showPassword +import io.reactivex.Observable +import io.reactivex.functions.BiFunction +import io.reactivex.rxkotlin.subscribeBy +import kotlinx.android.synthetic.main.fragment_login.* +import kotlinx.android.synthetic.main.fragment_login.passwordField +import kotlinx.android.synthetic.main.fragment_login.passwordFieldTil +import kotlinx.android.synthetic.main.fragment_login.passwordReveal +import kotlinx.android.synthetic.main.fragment_login_reset_password.* +import javax.inject.Inject + +/** + * In this screen, the user is asked for email and new password to reset his password + */ +class LoginResetPasswordFragment @Inject constructor( + private val errorFormatter: ErrorFormatter +) : AbstractLoginFragment() { + + private var passwordShown = false + + override fun getLayoutResId() = R.layout.fragment_login_reset_password + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + setupSubmitButton() + setupPasswordReveal() + } + + private fun setupUi() { + resetPasswordTitle.text = getString(R.string.login_reset_password_on, loginViewModel.getHomeServerUrlSimple()) + } + + private fun setupSubmitButton() { + Observable + .combineLatest( + resetPasswordEmail.textChanges().map { it.trim().isNotEmpty() }, + passwordField.textChanges().map { it.trim().isNotEmpty() }, + BiFunction { isEmailNotEmpty, isPasswordNotEmpty -> + isEmailNotEmpty && isPasswordNotEmpty + } + ) + .subscribeBy { + resetPasswordEmail.error = null + passwordFieldTil.error = null + loginSubmit.isEnabled = it + } + .disposeOnDestroy() + + resetPasswordSubmit.setOnClickListener { submit() } + } + + private fun submit() { + val email = resetPasswordEmail.text?.trim().toString() + val password = passwordField.text?.trim().toString() + + // TODO Add static check? + + loginViewModel.handle(LoginAction.ResetPassword(email, password)) + } + + private fun setupPasswordReveal() { + passwordShown = false + + passwordReveal.setOnClickListener { + passwordShown = !passwordShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + passwordField.showPassword(passwordShown) + + if (passwordShown) { + passwordReveal.setImageResource(R.drawable.ic_eye_closed_black) + passwordReveal.contentDescription = getString(R.string.a11y_hide_password) + } else { + passwordReveal.setImageResource(R.drawable.ic_eye_black) + passwordReveal.contentDescription = getString(R.string.a11y_show_password) + } + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetResetPassword) + } + + override fun invalidate() = withState(loginViewModel) { state -> + when (state.asyncResetPassword) { + is Loading -> { + // Ensure new password is hidden + passwordShown = false + renderPasswordField() + } + is Fail -> { + // TODO This does not work, we want the error to be on without text. Fix that + resetPasswordEmailTil.error = "" + // TODO Handle error text properly + passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) + } + is Success -> { + loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSuccess) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt new file mode 100644 index 0000000000..20e209573e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.os.Bundle +import android.view.View +import butterknife.OnClick +import im.vector.riotx.R +import kotlinx.android.synthetic.main.fragment_login_reset_password_success.* +import javax.inject.Inject + +/** + * In this screen, the user is asked for email and new password to reset his password + */ +class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_reset_password_success + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + } + + private fun setupUi() { + resetPasswordSuccessNotice.text = getString(R.string.login_reset_password_success_notice, loginViewModel.resetPasswordEmail) + } + + @OnClick(R.id.resetPasswordSuccessSubmit) + fun submit() { + loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSuccessDone) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 9ba5fa739b..ac8362eb31 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -57,9 +57,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi var serverType: ServerType = ServerType.MatrixOrg private set - var signMode: SignMode = SignMode.Unknown private set + var resetPasswordEmail: String? = null + private set private var loginConfig: LoginConfig? = null @@ -74,6 +75,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) + is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetAction -> handleResetAction(action) } } @@ -109,6 +111,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } } + LoginAction.ResetResetPassword -> { + resetPasswordEmail = null + setState { + copy( + asyncResetPassword = Uninitialized + ) + } + } } } @@ -124,6 +134,45 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi loginConfig = action.loginConfig } + private fun handleResetPassword(action: LoginAction.ResetPassword) { + val homeServerConnectionConfigFinal = homeServerConnectionConfig + + if (homeServerConnectionConfigFinal == null) { + setState { + copy( + asyncResetPassword = Fail(Throwable("Bad configuration")) + ) + } + } else { + resetPasswordEmail = action.email + + setState { + copy( + asyncResetPassword = Loading() + ) + } + + currentTask = authenticator.resetPassword(homeServerConnectionConfigFinal, action.email, action.newPassword, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + asyncResetPassword = Success(data) + ) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncResetPassword = Fail(failure) + ) + } + } + }) + } + } + private fun handleLogin(action: LoginAction.Login) { val homeServerConnectionConfigFinal = homeServerConnectionConfig @@ -259,4 +308,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi fun getHomeServerUrl(): String { return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" } + + fun getHomeServerUrlSimple(): String { + return getHomeServerUrl().substringAfter("://") + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 0853765d63..5ecfd7fad6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -16,17 +16,21 @@ package im.vector.riotx.features.login + import com.airbnb.mvrx.* data class LoginViewState( val asyncLoginAction: Async = Uninitialized, - val asyncHomeServerLoginFlowRequest: Async = Uninitialized + val asyncHomeServerLoginFlowRequest: Async = Uninitialized, + val asyncResetPassword: Async = Uninitialized ) : MvRxState { + fun isLoading(): Boolean { // TODO Add other async here return asyncLoginAction is Loading || asyncHomeServerLoginFlowRequest is Loading + || asyncResetPassword is Loading } fun isUserLogged(): Boolean { diff --git a/vector/src/main/res/layout/fragment_login_reset_password.xml b/vector/src/main/res/layout/fragment_login_reset_password.xml new file mode 100644 index 0000000000..8662b5b077 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_reset_password_success.xml b/vector/src/main/res/layout/fragment_login_reset_password_success.xml new file mode 100644 index 0000000000..b777250f8d --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password_success.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 17bb99ac4a..089ac19b7e 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -38,9 +38,11 @@ Custom & advanced settings Continue + Connect to %1$s Connect to Modular Connect to a custom server + Sign in to %1$s Sign Up Sign In @@ -55,4 +57,16 @@ An error occurred when loading the page: %1$s (%2$d) The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? + + Reset password on %1$s + A verification email will be sent to your inbox to confirm setting your new password. + Next + Email + New password + Check your inbox + + A verification email was sent to %1$s. + Tap on the link to confirm your new password. + Back to Sign In + From ca4e75a1a0b24765bd3fbc6d71d6092f92bce50c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 14:39:09 +0100 Subject: [PATCH 023/189] Login screens: Fix a few bugs --- .../im/vector/riotx/core/di/FragmentModule.kt | 30 ++++++++++++++++--- .../login/LoginResetPasswordFragment.kt | 2 +- .../LoginSignUpSignInSelectionFragment.kt | 5 ++-- .../riotx/features/login/LoginViewModel.kt | 7 ++++- vector/src/main/res/layout/fragment_login.xml | 5 +++- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index d788f4c04c..37ce9583b8 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -35,9 +35,7 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment -import im.vector.riotx.features.login.LoginFragment -import im.vector.riotx.features.login.LoginServerUrlFormFragment -import im.vector.riotx.features.login.LoginWebFragment +import im.vector.riotx.features.login.* import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment @@ -116,12 +114,36 @@ interface FragmentModule { @FragmentKey(LoginFragment::class) fun bindLoginFragment(fragment: LoginFragment): Fragment - // TODO Add all other Login Fragments @Binds @IntoMap @FragmentKey(LoginServerUrlFormFragment::class) fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LoginResetPasswordFragment::class) + fun bindLoginResetPasswordFragment(fragment: LoginResetPasswordFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginResetPasswordSuccessFragment::class) + fun bindLoginResetPasswordSuccessFragment(fragment: LoginResetPasswordSuccessFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginServerSelectionFragment::class) + fun bindLoginServerSelectionFragment(fragment: LoginServerSelectionFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginSignUpSignInSelectionFragment::class) + fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginSplashFragment::class) + fun bindLoginSplashFragment(fragment: LoginSplashFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginWebFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt index 600013c5ba..045e60e364 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -71,7 +71,7 @@ class LoginResetPasswordFragment @Inject constructor( .subscribeBy { resetPasswordEmail.error = null passwordFieldTil.error = null - loginSubmit.isEnabled = it + resetPasswordSubmit.isEnabled = it } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index cdb9aa8b7a..ec59f95874 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -48,13 +48,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr ServerType.Modular -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_modular) loginSignupSigninServerIcon.isVisible = true + // TODO loginSignupSigninTitle.text = getString(R.string.login_connect_to, "TODO MODULAR NAME") - loginSignupSigninText.text = "TODO MODULAR URL" + loginSignupSigninText.text = loginViewModel.getHomeServerUrlSimple() } ServerType.Other -> { loginSignupSigninServerIcon.isVisible = false loginSignupSigninTitle.text = getString(R.string.login_server_other_title) - loginSignupSigninText.text = "TODO SERVER URL" + loginSignupSigninText.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index ac8362eb31..abaa00283f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -309,7 +309,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" } + /** + * Ex: "https://matrix.org/" -> "matrix.org" + */ fun getHomeServerUrlSimple(): String { - return getHomeServerUrl().substringAfter("://") + return getHomeServerUrl() + .substringAfter("://") + .trim { it == '/' } } } diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 708afd6943..7eef66272f 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -25,11 +25,14 @@ style="@style/LoginTopIcon" android:layout_gravity="center_horizontal" /> + + Date: Fri, 15 Nov 2019 14:44:32 +0100 Subject: [PATCH 024/189] Fix compilation issue after rebase --- .../auth/registration/DefaultRegistrationService.kt | 5 +++-- .../auth/registration/DefaultRegistrationWizard.kt | 8 +++----- .../java/im/vector/riotx/features/login/LoginFragment.kt | 2 +- .../riotx/features/login/LoginResetPasswordFragment.kt | 2 +- .../riotx/features/login/LoginServerUrlFormFragment.kt | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt index 89dbda077b..fadbfe39cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.auth.registration +import dagger.Lazy import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.api.auth.registration.RegistrationWizard @@ -25,10 +26,10 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import okhttp3.OkHttpClient -import javax.inject.Provider +// TODO Add @Inject internal class DefaultRegistrationService(@Unauthenticated - private val okHttpClient: Provider, + private val okHttpClient: Lazy, private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index d856d9211a..cda3240cb1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.auth.registration +import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams @@ -27,18 +28,15 @@ import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthAPI import im.vector.matrix.android.internal.auth.SessionParamsStore -import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import okhttp3.OkHttpClient -import javax.inject.Provider internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: HomeServerConnectionConfig, - @Unauthenticated - private val okHttpClient: Provider, + private val okHttpClient: Lazy, private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, @@ -100,7 +98,7 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: } private fun buildAuthAPI(): AuthAPI { - val retrofit = retrofitFactory.create(okHttpClient.get(), homeServerConnectionConfig.homeServerUri.toString()) + val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 14465fa48b..3a59ea51b5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -99,7 +99,7 @@ class LoginFragment @Inject constructor( passwordFieldTil.error = null loginSubmit.isEnabled = it } - .disposeOnDestroy() + .disposeOnDestroyView() loginSubmit.setOnClickListener { authenticate() } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt index 045e60e364..d90bfc4f84 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -73,7 +73,7 @@ class LoginResetPasswordFragment @Inject constructor( passwordFieldTil.error = null resetPasswordSubmit.isEnabled = it } - .disposeOnDestroy() + .disposeOnDestroyView() resetPasswordSubmit.setOnClickListener { submit() } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index c3d841369b..e4dd5ad7e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -58,7 +58,7 @@ class LoginServerUrlFormFragment @Inject constructor( { // Ignore error }) - .disposeOnDestroy() + .disposeOnDestroyView() loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { From 9a628c7b5dcd51f0e07190ffcf1c51554623d378 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 14:53:02 +0100 Subject: [PATCH 025/189] ktlint --- .../android/api/auth/registration/RegistrationService.kt | 1 - .../android/api/auth/registration/RegistrationWizard.kt | 1 - .../im/vector/matrix/android/api/auth/registration/Stage.kt | 6 +----- .../java/im/vector/matrix/android/api/util/Cancelable.kt | 1 - .../matrix/android/internal/auth/DefaultAuthenticator.kt | 6 +++--- .../internal/auth/registration/DefaultRegistrationWizard.kt | 1 - .../auth/registration/LocalizedFlowDataLoginTerms.kt | 2 +- .../internal/auth/registration/RegistrationParams.kt | 2 +- .../riotx/features/login/LoginServerUrlFormFragment.kt | 1 - .../java/im/vector/riotx/features/login/LoginViewState.kt | 3 --- 10 files changed, 6 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt index 7b131b922d..b314f92b9b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt @@ -21,5 +21,4 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig interface RegistrationService { fun getOrCreateRegistrationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): RegistrationWizard - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 879bd5d74b..332c1ef781 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -27,5 +27,4 @@ interface RegistrationWizard { fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable // TODO Add other method here - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt index f302b953c2..283a79348b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.api.auth.registration import im.vector.matrix.android.api.util.JsonDict - sealed class Stage(open val mandatory: Boolean) { // m.login.password @@ -45,7 +44,4 @@ sealed class Stage(open val mandatory: Boolean) { data class Other(override val mandatory: Boolean, val type: String, val params: JsonDict?) : Stage(mandatory) } - -class TermPolicies { - -} +class TermPolicies diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt index 7ec01cca10..8473f50796 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt @@ -30,5 +30,4 @@ interface Cancelable { } } - object NoOpCancellable : Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index 995ec0aedb..3a6a833dcd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -144,10 +144,10 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated private fun resetPasswordInternal(/*homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String*/) { // TODO error("Not implemented") - //val authAPI = buildAuthAPI(homeServerConnectionConfig) - //executeRequest { + // val authAPI = buildAuthAPI(homeServerConnectionConfig) + // executeRequest { // apiCall = authAPI.getLoginFlows() - //} + // } } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index cda3240cb1..f37f2b97be 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -72,7 +72,6 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } - private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val result = runCatching { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt index dd125e3c74..2cd52f702e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -28,4 +28,4 @@ data class LocalizedFlowDataLoginTerms( var version: String? = null, var localizedUrl: String? = null, var localizedName: String? = null -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt index db8475e06c..8d668f7f11 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt @@ -44,4 +44,4 @@ data class RegistrationParams( // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app // versions to end up in fallback because the HS returns the msisdn flow which they don't support val x_show_msisdn: Boolean? = null -) \ No newline at end of file +) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index e4dd5ad7e2..e53159da70 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -115,7 +115,6 @@ class LoginServerUrlFormFragment @Inject constructor( if (serverUrl.startsWith("http").not()) { serverUrl = "https://$serverUrl" loginServerUrlFormHomeServerUrl.setText(serverUrl) - } loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 5ecfd7fad6..1f01e2348d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.login - import com.airbnb.mvrx.* data class LoginViewState( @@ -25,7 +24,6 @@ data class LoginViewState( val asyncResetPassword: Async = Uninitialized ) : MvRxState { - fun isLoading(): Boolean { // TODO Add other async here return asyncLoginAction is Loading @@ -38,4 +36,3 @@ data class LoginViewState( return asyncLoginAction is Success } } - From 3e91125872d57ea63e740f7b66e4a2c5bcde8002 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 15:02:09 +0100 Subject: [PATCH 026/189] Fix issues --- .../main/java/im/vector/riotx/features/login/LoginFragment.kt | 4 ++-- .../features/login/LoginSignUpSignInSelectionFragment.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 3a59ea51b5..4161673a52 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -67,7 +67,7 @@ class LoginFragment @Inject constructor( ServerType.MatrixOrg -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginTitle.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) loginNotice.text = getString(R.string.login_server_matrix_org_text) } ServerType.Modular -> { @@ -80,7 +80,7 @@ class LoginFragment @Inject constructor( ServerType.Other -> { loginServerIcon.isVisible = false loginTitle.text = getString(R.string.login_server_other_title) - loginNotice.text = loginViewModel.getHomeServerUrlSimple() + loginNotice.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index ec59f95874..c1023f3356 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -42,7 +42,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) loginSignupSigninServerIcon.isVisible = true - loginSignupSigninTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginSignupSigninTitle.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text) } ServerType.Modular -> { From 823acebf7879cecb7eea127f0d66eee6cacec8ed Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 09:43:38 +0100 Subject: [PATCH 027/189] Login screens: harmonize styles for containers --- vector/src/main/res/layout/fragment_login.xml | 4 ++-- .../main/res/layout/fragment_login_reset_password.xml | 4 ++-- .../res/layout/fragment_login_reset_password_success.xml | 4 ++-- .../main/res/layout/fragment_login_server_selection.xml | 7 ++----- .../main/res/layout/fragment_login_server_url_form.xml | 4 ++-- .../layout/fragment_login_signup_signin_selection.xml | 7 ++----- vector/src/main/res/layout/fragment_login_splash.xml | 4 ++-- vector/src/main/res/values/styles_login.xml | 9 ++++++++- 8 files changed, 22 insertions(+), 21 deletions(-) diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 7eef66272f..4c92a94b88 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -15,10 +15,10 @@ app:layout_constraintTop_toTopOf="parent"> + android:orientation="vertical"> + android:orientation="vertical"> + android:orientation="vertical"> + android:layout_height="match_parent"> + android:orientation="vertical"> + android:layout_height="match_parent"> + android:layout_height="match_parent"> + + - \ No newline at end of file + From 416bef790303bb1d035b6747b3aed57a88a6a8e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 09:59:45 +0100 Subject: [PATCH 028/189] Login screens: button theme --- vector/src/main/res/layout/fragment_create_direct_room.xml | 2 +- .../main/res/layout/fragment_keys_backup_setup_step1.xml | 2 +- .../main/res/layout/fragment_keys_backup_setup_step2.xml | 2 +- .../main/res/layout/fragment_keys_backup_setup_step3.xml | 2 +- vector/src/main/res/layout/fragment_login.xml | 2 +- .../res/layout/fragment_login_signup_signin_selection.xml | 2 +- vector/src/main/res/layout/fragment_public_rooms.xml | 2 +- .../res/layout/fragment_sas_verification_display_code.xml | 2 +- .../layout/fragment_sas_verification_incoming_request.xml | 2 +- .../main/res/layout/fragment_sas_verification_start.xml | 4 ++-- vector/src/main/res/layout/item_room_filter_footer.xml | 6 +++--- vector/src/main/res/layout/vector_invite_view.xml | 2 +- vector/src/main/res/layout/view_button_state.xml | 4 ++-- vector/src/main/res/values/styles_login.xml | 7 ++++++- vector/src/main/res/values/styles_riot.xml | 4 +++- 15 files changed, 26 insertions(+), 19 deletions(-) diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index 66a040b935..f8450d1e6e 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -107,7 +107,7 @@ diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 4c92a94b88..7e6e24a48c 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -120,7 +120,7 @@ @@ -68,7 +68,7 @@ false - + + diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index c5b04de730..07091dedc0 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -136,7 +136,7 @@ - From f24889230c76baf14d97daec4265554a03b81820 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 10:27:00 +0100 Subject: [PATCH 031/189] Login screens: Captch screen (UI) --- .../features/login/LoginCaptchaFragment.kt | 34 +++++++++++++++++++ .../res/layout/fragment_login_captcha.xml | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_captcha.xml diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt new file mode 100644 index 0000000000..1490f3d5e0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import im.vector.riotx.R +import javax.inject.Inject + +/** + * In this screen, the user is asked to confirm he is not a robot + */ +class LoginCaptchaFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_captcha + + // TODO + + override fun resetViewModel() { + // Nothing to do + } +} diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml new file mode 100644 index 0000000000..e3ba0ade22 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_captcha.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + From 08ea3d049e8ca723d8597a8cc129e05a427cdd03 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 15:03:07 +0100 Subject: [PATCH 032/189] Login screens: Simple Input form (UI) --- .../LoginGenericTextInputFormFragment.kt | 140 ++++++++++++++++++ ...fragment_login_generic_text_input_form.xml | 98 ++++++++++++ vector/src/main/res/values/strings_riotX.xml | 19 +++ 3 files changed, 257 insertions(+) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_generic_text_input_form.xml diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt new file mode 100644 index 0000000000..98c30c685f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.os.Bundle +import android.os.Parcelable +import android.text.InputType +import android.view.View +import androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.args +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.* +import javax.inject.Inject + +enum class TextInputFormFragmentMode { + SetEmailMandatory, + SetEmailOptional, + SetMsisdnMandatory, + SetMsisdnOptional, + ConfirmMsisdn +} + +@Parcelize +data class LoginGenericTextInputFormFragmentArgument( + val mode: TextInputFormFragmentMode +) : Parcelable + +/** + * In this screen, the user is asked for a text input + */ +class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFragment() { + + private val params: LoginGenericTextInputFormFragmentArgument by args() + + override fun getLayoutResId() = R.layout.fragment_login_generic_text_input_form + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + setupSubmitButton() + } + + private fun setupUi() { + when (params.mode) { + TextInputFormFragmentMode.SetEmailMandatory -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_set_email_mandatory_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + loginGenericTextInputFormOtherButton.isVisible = false + loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) + } + TextInputFormFragmentMode.SetEmailOptional -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_set_email_optional_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + loginGenericTextInputFormOtherButton.isVisible = false + loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) + } + TextInputFormFragmentMode.SetMsisdnMandatory -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_set_msisdn_mandatory_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE + loginGenericTextInputFormOtherButton.isVisible = false + loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) + } + TextInputFormFragmentMode.SetMsisdnOptional -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_set_msisdn_optional_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE + loginGenericTextInputFormOtherButton.isVisible = false + loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_msisdn_confirm_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER + loginGenericTextInputFormOtherButton.isVisible = true + loginGenericTextInputFormOtherButton.text = getString(R.string.login_msisdn_confirm_send_again) + loginGenericTextInputFormSubmit.text = getString(R.string.login_msisdn_confirm_submit) + } + } + } + + @OnClick(R.id.loginGenericTextInputFormOtherButton) + fun onOtherButtonClicked() { + // TODO + } + + @OnClick(R.id.loginGenericTextInputFormSubmit) + fun onSubmitClicked() { + // TODO + } + + private fun setupSubmitButton() { + when (params.mode) { + TextInputFormFragmentMode.SetEmailMandatory, + TextInputFormFragmentMode.SetMsisdnMandatory, + TextInputFormFragmentMode.ConfirmMsisdn -> { + loginGenericTextInputFormSubmit.isEnabled = false + loginGenericTextInputFormTextInput.textChanges() + .subscribe { + // TODO Better check for email format, etc? + loginGenericTextInputFormSubmit.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + } + TextInputFormFragmentMode.SetEmailOptional, + TextInputFormFragmentMode.SetMsisdnOptional -> { + loginGenericTextInputFormSubmit.isEnabled = true + } + } + } + + override fun resetViewModel() { + // Nothing to do + } +} diff --git a/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml new file mode 100644 index 0000000000..4edd635515 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 089ac19b7e..db6d9e673f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -69,4 +69,23 @@ Tap on the link to confirm your new password. Back to Sign In + Set email address + Set an email to recover your account. Later, you can optionally allow people you know to discover you by your email. + Email + Email (optional) + Next + + Set phone number + Set a phone number to optionally allow people you know to discover you. + Phone number + Phone number (optional) + Next + + Confirm phone number + + We just sent a code to %1$s. Enter it below to verify it’s you. + Enter code + Send again + Next + From 41ac2c6d7035a9130afc868e5ceaf0d764a83a36 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 17:39:51 +0100 Subject: [PATCH 033/189] Login screens: Registration WIP --- .../im/vector/matrix/android/api/Matrix.kt | 6 ++ .../auth/registration/RegistrationResult.kt | 31 +++++++++ .../auth/registration/RegistrationWizard.kt | 7 ++- .../android/api/auth/registration/Stage.kt | 17 +++-- .../matrix/android/internal/auth/AuthAPI.kt | 4 +- .../android/internal/auth/AuthModule.kt | 5 ++ .../internal/auth/data/LoginFlowTypes.kt | 2 + .../DefaultRegistrationService.kt | 14 ++--- .../registration/DefaultRegistrationWizard.kt | 18 ++++-- .../registration/RegistrationFlowResponse.kt | 40 ++++++++++++ .../android/internal/di/MatrixComponent.kt | 3 + .../vector/riotx/core/di/ScreenComponent.kt | 4 +- .../vector/riotx/core/di/VectorComponent.kt | 3 + .../im/vector/riotx/core/di/VectorModule.kt | 7 +++ .../riotx/features/login/LoginAction.kt | 9 +++ .../riotx/features/login/LoginActivity.kt | 2 +- .../riotx/features/login/LoginFragment.kt | 42 ++++++++++--- .../LoginSignUpSignInSelectionFragment.kt | 38 ++++++++++- .../riotx/features/login/LoginViewModel.kt | 63 ++++++++++++++++++- .../riotx/features/login/LoginViewState.kt | 6 +- vector/src/main/res/layout/fragment_login.xml | 4 +- vector/src/main/res/values/strings_riotX.xml | 6 ++ 22 files changed, 284 insertions(+), 47 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 1bfa871a42..3c4e9b23e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -23,6 +23,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.di.DaggerMatrixComponent import im.vector.matrix.android.internal.network.UserAgentHolder @@ -47,6 +48,7 @@ data class MatrixConfiguration( class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { @Inject internal lateinit var authenticator: Authenticator + @Inject internal lateinit var registrationService: RegistrationService @Inject internal lateinit var userAgentHolder: UserAgentHolder @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var olmManager: OlmManager @@ -68,6 +70,10 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo return authenticator } + fun registrationService(): RegistrationService { + return registrationService + } + companion object { private lateinit var instance: Matrix diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt new file mode 100644 index 0000000000..ddc231f186 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.registration + +import im.vector.matrix.android.api.session.Session + +// Either a session or an object containing data about registration stages +sealed class RegistrationResult { + data class Success(val session: Session) : RegistrationResult() + data class FlowResponse(val flowResult: FlowResult) : RegistrationResult() +} + + +data class FlowResult( + val missingStages: List, + val completedStages: List +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 332c1ef781..7144ce389f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -17,14 +17,15 @@ package im.vector.matrix.android.api.auth.registration import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable interface RegistrationWizard { - fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback): Cancelable + fun getRegistrationFlow(callback: MatrixCallback): Cancelable - fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable + fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback): Cancelable + + fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable // TODO Add other method here } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt index 283a79348b..9f1883e4b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -16,32 +16,29 @@ package im.vector.matrix.android.api.auth.registration -import im.vector.matrix.android.api.util.JsonDict - sealed class Stage(open val mandatory: Boolean) { - // m.login.password - data class Password(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) - // m.login.recaptcha data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) // m.login.oauth2 // m.login.email.identity - data class Email(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + data class Email(override val mandatory: Boolean) : Stage(mandatory) // m.login.msisdn - data class Msisdn(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) + // m.login.token + // m.login.dummy + object Dummy : Stage(false) // Undocumented yet: m.login.terms data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) - // TODO SSO - // For unknown stages - data class Other(override val mandatory: Boolean, val type: String, val params: JsonDict?) : Stage(mandatory) + data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory) } +//TODO class TermPolicies diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index 8316589ad4..6e16393723 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -36,8 +36,8 @@ internal interface AuthAPI { * Register to the homeserver * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") - fun register(registrationParams: RegistrationParams): Call + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") + fun register(@Body registrationParams: RegistrationParams): Call /** * Get the supported login flow diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 31a85afbfb..e54073ac08 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -21,8 +21,10 @@ import dagger.Binds import dagger.Module import dagger.Provides import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore +import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationService import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.RealmConfiguration @@ -60,4 +62,7 @@ internal abstract class AuthModule { @Binds abstract fun bindAuthenticator(authenticator: DefaultAuthenticator): Authenticator + + @Binds + abstract fun bindRegistrationService(service: DefaultRegistrationService): RegistrationService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt index 81196c7414..59c962c0ba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.auth.data +// TODO Move to [InteractiveAuthenticationFlow] object LoginFlowTypes { const val PASSWORD = "m.login.password" const val OAUTH2 = "m.login.oauth2" @@ -25,4 +26,5 @@ object LoginFlowTypes { const val MSISDN = "m.login.msisdn" const val RECAPTCHA = "m.login.recaptcha" const val DUMMY = "m.login.dummy" + const val TERMS = "m.login.terms" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt index fadbfe39cd..68915fd990 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt @@ -26,14 +26,14 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import okhttp3.OkHttpClient +import javax.inject.Inject -// TODO Add @Inject -internal class DefaultRegistrationService(@Unauthenticated - private val okHttpClient: Lazy, - private val retrofitFactory: RetrofitFactory, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val sessionParamsStore: SessionParamsStore, - private val sessionManager: SessionManager) : RegistrationService { +internal class DefaultRegistrationService @Inject constructor(@Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager) : RegistrationService { override fun getOrCreateRegistrationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): RegistrationWizard { // TODO Persist the wizard? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index f37f2b97be..1d234eaf8d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -20,9 +20,9 @@ import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager @@ -47,10 +47,14 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: private val authAPI = buildAuthAPI() private val registerTask = DefaultRegisterTask(authAPI) + override fun getRegistrationFlow(callback: MatrixCallback): Cancelable { + return performRegistrationRequest(RegistrationParams(), callback) + } + override fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, - callback: MatrixCallback): Cancelable { + callback: MatrixCallback): Cancelable { return performRegistrationRequest(RegistrationParams( username = userName, password = password, @@ -58,7 +62,7 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } - override fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable { + override fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable { val safeSession = currentSession ?: run { callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable @@ -72,7 +76,7 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } - private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { + private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val result = runCatching { registerTask.execute(RegisterTask.Params(registrationParams)) @@ -83,13 +87,15 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: sessionParamsStore.save(sessionParams) val session = sessionManager.getOrCreateSession(sessionParams) - callback.onSuccess(session) + callback.onSuccess(RegistrationResult.Success(session)) }, { if (it is Failure.RegistrationFlowError) { currentSession = it.registrationFlowResponse.session + callback.onSuccess(RegistrationResult.FlowResponse(it.registrationFlowResponse.toFlowResult())) + } else { + callback.onFailure(it) } - callback.onFailure(it) } ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index 218251cfe5..aa9fae3362 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.registration.FlowResult +import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.auth.registration.TermPolicies import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes @JsonClass(generateAdapter = true) data class RegistrationFlowResponse( @@ -51,3 +55,39 @@ data class RegistrationFlowResponse( @Json(name = "params") var params: JsonDict? = null ) + +/** + * Convert to something easier to exploit on client side + */ +fun RegistrationFlowResponse.toFlowResult(): FlowResult { + // Get all the returned stages + val allFlowTypes = mutableSetOf() + + val missingStage = mutableListOf() + val completedStage = mutableListOf() + + this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } } + + allFlowTypes.forEach { type -> + val isMandatory = flows?.all { type in it.stages ?: emptyList() } == true + + val stage = when (type) { + LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) + ?: "") + LoginFlowTypes.DUMMY -> Stage.Dummy + LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, TermPolicies()) + LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory) + LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory) + else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) + } + + if (type in completedStages ?: emptyList()) { + completedStage.add(stage) + } else { + missingStage.add(stage) + } + } + + return FlowResult(missingStage, completedStage) +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index f7314fe6b4..97285bc75d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -23,6 +23,7 @@ import dagger.BindsInstance import dagger.Component import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthModule import im.vector.matrix.android.internal.auth.SessionParamsStore @@ -46,6 +47,8 @@ internal interface MatrixComponent { fun authenticator(): Authenticator + fun registrationService(): RegistrationService + fun context(): Context fun resources(): Resources diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 17622020d0..9f0f83a41f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -32,8 +32,8 @@ import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsB import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity -import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.home.room.list.RoomListModule +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.login.LoginActivity @@ -47,7 +47,7 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity -import im.vector.riotx.features.settings.* +import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.ui.UiStateRepository diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index d31955ce8e..2106ebf750 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -22,6 +22,7 @@ import dagger.BindsInstance import dagger.Component import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.api.session.Session import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.EmojiCompatFontProvider @@ -99,6 +100,8 @@ interface VectorComponent { fun authenticator(): Authenticator + fun registrationService(): RegistrationService + fun bugReporter(): BugReporter fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt index e3df0eb635..3206c441e2 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt @@ -25,6 +25,7 @@ import dagger.Module import dagger.Provides import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.api.session.Session import im.vector.riotx.features.navigation.DefaultNavigator import im.vector.riotx.features.navigation.Navigator @@ -67,6 +68,12 @@ abstract class VectorModule { fun providesAuthenticator(matrix: Matrix): Authenticator { return matrix.authenticator() } + + @Provides + @JvmStatic + fun providesRegistrationService(matrix: Matrix): RegistrationService { + return matrix.registrationService() + } } @Binds diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index a03af1376d..be76013ca3 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -28,6 +28,15 @@ sealed class LoginAction : VectorViewModelAction { data class InitWith(val loginConfig: LoginConfig) : LoginAction() data class ResetPassword(val email: String, val newPassword: String) : LoginAction() + // Register actions + open class RegisterAction : LoginAction() + + data class RegisterWith(val username: String, val password: String) : RegisterAction() + data class AddEmail(val email: String) : RegisterAction() + data class AddMsisdn(val msisdn: String) : RegisterAction() + data class ConfirmMsisdn(val code: String) : RegisterAction() + data class PerformCaptcha(val captcha: String /* TODO Add other params */) : RegisterAction() + // Reset actions open class ResetAction : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 7bc713a4f2..f934ebf27f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -126,7 +126,7 @@ class LoginActivity : VectorBaseActivity() { private fun onSignModeSelected() { when (loginViewModel.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.loginFragmentContainer, SignUpFragment::class.java) + SignMode.SignUp -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) SignMode.SignIn -> { // It depends on the LoginMode withState(loginViewModel) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 4161673a52..f21ce74d1a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -35,8 +35,11 @@ import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject /** - * In this screen, the user is asked for login and password to sign in to a homeserver. - * He also can reset his password + * In this screen, in signin mode: + * - the user is asked for login and password to sign in to a homeserver. + * - He also can reset his password + * In signup mode: + * - the user is asked for login and password */ class LoginFragment @Inject constructor( private val errorFormatter: ErrorFormatter @@ -53,38 +56,61 @@ class LoginFragment @Inject constructor( setupUi() setupSubmitButton() setupPasswordReveal() + setupButtons() } - private fun authenticate() { + @OnClick(R.id.loginSubmit) + fun submit() { val login = loginField.text?.trim().toString() val password = passwordField.text?.trim().toString() - loginViewModel.handle(LoginAction.Login(login, password)) + when (loginViewModel.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> loginViewModel.handle(LoginAction.RegisterWith(login, password)) + SignMode.SignIn -> loginViewModel.handle(LoginAction.Login(login, password)) + } } private fun setupUi() { + val resId = when (loginViewModel.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_to + SignMode.SignIn -> R.string.login_connect_to + } + when (loginViewModel.serverType) { ServerType.MatrixOrg -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) + loginTitle.text = getString(resId, loginViewModel.getHomeServerUrlSimple()) loginNotice.text = getString(R.string.login_server_matrix_org_text) } ServerType.Modular -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_modular) // TODO - loginTitle.text = getString(R.string.login_connect_to, "TODO") + loginTitle.text = getString(resId, "TODO") loginNotice.text = loginViewModel.getHomeServerUrlSimple() } ServerType.Other -> { loginServerIcon.isVisible = false loginTitle.text = getString(R.string.login_server_other_title) - loginNotice.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) + loginNotice.text = getString(resId, loginViewModel.getHomeServerUrlSimple()) } } } + private fun setupButtons() { + forgetPasswordButton.isVisible = loginViewModel.signMode == SignMode.SignIn + + loginSubmit.text = getString(when (loginViewModel.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_submit + SignMode.SignIn -> R.string.login_signin + }) + } + + private fun setupSubmitButton() { Observable .combineLatest( @@ -100,8 +126,6 @@ class LoginFragment @Inject constructor( loginSubmit.isEnabled = it } .disposeOnDestroyView() - - loginSubmit.setOnClickListener { authenticate() } } @OnClick(R.id.forgetPasswordButton) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index c1023f3356..08872606bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -20,6 +20,12 @@ import android.os.Bundle import android.view.View import androidx.core.view.isVisible import butterknife.OnClick +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.auth.registration.FlowResult +import im.vector.matrix.android.api.auth.registration.RegistrationResult +import im.vector.matrix.android.api.auth.registration.Stage import im.vector.riotx.R import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject @@ -63,7 +69,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignUp) fun signUp() { loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } @OnClick(R.id.loginSignupSigninSignIn) @@ -75,4 +80,35 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetSignMode) } + + override fun invalidate() = withState(loginViewModel) { + when (it.asyncRegistration) { + is Success -> { + when (val res = it.asyncRegistration()) { + is RegistrationResult.Success -> + // Should not happen + Unit + is RegistrationResult.FlowResponse -> handleFlowResult(res.flowResult) + } + } + is Fail -> { + // TODO Registration disabled, etc + when (it.asyncRegistration.error) { + + } + } + } + } + + private fun handleFlowResult(flowResult: FlowResult) { + // Check that all flows are supported by the application + if (flowResult.missingStages.any { it is Stage.Other }) { + // Display a popup to propose use web fallback + // TODO + } else { + // Go on with registration flow + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + } + } + } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index abaa00283f..bfb6be6733 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -23,6 +23,10 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.registration.FlowResult +import im.vector.matrix.android.api.auth.registration.RegistrationResult +import im.vector.matrix.android.api.auth.registration.RegistrationService +import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow @@ -36,6 +40,7 @@ import timber.log.Timber class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, private val authenticator: Authenticator, + private val registrationService: RegistrationService, private val activeSessionHolder: ActiveSessionHolder, private val pushRuleTriggerListener: PushRuleTriggerListener, private val sessionListener: SessionListener) @@ -55,6 +60,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private var registrationWizard: RegistrationWizard? = null + var serverType: ServerType = ServerType.MatrixOrg private set var signMode: SignMode = SignMode.Unknown @@ -89,7 +96,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi LoginAction.ResetLogin -> { setState { copy( - asyncLoginAction = Uninitialized + asyncLoginAction = Uninitialized, + asyncRegistration = Uninitialized ) } } @@ -124,6 +132,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleUpdateSignMode(action: LoginAction.UpdateSignMode) { signMode = action.signMode + + if (signMode == SignMode.SignUp) { + startRegistrationFlow() + } } private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { @@ -206,6 +218,53 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun startRegistrationFlow() { + val homeServerConnectionConfigFinal = homeServerConnectionConfig + + if (homeServerConnectionConfigFinal == null) { + setState { + copy( + asyncRegistration = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncRegistration = Loading() + ) + } + + registrationWizard = registrationService.getOrCreateRegistrationWizard(homeServerConnectionConfigFinal) + + currentTask = registrationWizard?.getRegistrationFlow(object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + } + + private fun onFlowResponse(flowResult: FlowResult) { + setState { + copy( + asyncRegistration = Success(RegistrationResult.FlowResponse(flowResult)) + ) + } + } + + private fun onSessionCreated(session: Session) { activeSessionHolder.setActiveSession(session) session.configureAndStart(pushRuleTriggerListener, sessionListener) @@ -246,7 +305,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi // Do not retry if we already have flows for this config -> causes infinite focus loop if (newConfig?.homeServerUri?.toString() == homeServerConnectionConfig?.homeServerUri?.toString() - && state.asyncHomeServerLoginFlowRequest is Success) return@withState + && state.asyncHomeServerLoginFlowRequest is Success) return@withState currentTask?.cancel() currentTask = null diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 1f01e2348d..f42d46e22c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -17,11 +17,13 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.* +import im.vector.matrix.android.api.auth.registration.RegistrationResult data class LoginViewState( val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized, - val asyncResetPassword: Async = Uninitialized + val asyncResetPassword: Async = Uninitialized, + val asyncRegistration: Async = Uninitialized ) : MvRxState { fun isLoading(): Boolean { @@ -29,10 +31,10 @@ data class LoginViewState( return asyncLoginAction is Loading || asyncHomeServerLoginFlowRequest is Loading || asyncResetPassword is Loading + || asyncRegistration is Loading } fun isUserLogged(): Boolean { - // TODO Add other async here return asyncLoginAction is Success } } diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 990ccb3db3..1f9859f74b 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -58,7 +58,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="32dp" - android:hint="@string/auth_user_name_placeholder" + android:hint="@string/login_signup_username_hint" app:errorEnabled="true"> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index db6d9e673f..42667ce5fa 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -88,4 +88,10 @@ Send again Next + + Sign up to %1$s + Username + Password + Next + From 381084b2abb3ce4d70c89d1755a6fff33de19a89 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 18:04:14 +0100 Subject: [PATCH 034/189] Login screens: USER_IN_USE error --- .../vector/riotx/core/error/ErrorFormatter.kt | 3 ++ .../riotx/features/login/LoginFragment.kt | 13 +++++++ .../riotx/features/login/LoginViewModel.kt | 35 +++++++++++++++++++ vector/src/main/res/values/strings_riotX.xml | 1 + 4 files changed, 52 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 65f48f5e7b..29506cf880 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -54,6 +54,9 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi && throwable.error.message == "Invalid password" -> { stringProvider.getString(R.string.auth_invalid_login_param) } + throwable.error.code == MatrixError.USER_IN_USE -> { + stringProvider.getString(R.string.login_signup_error_user_in_use) + } else -> { throwable.error.message.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index f21ce74d1a..88c1917616 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -177,5 +177,18 @@ class LoginFragment @Inject constructor( // Success is handled by the LoginActivity is Success -> Unit } + + when (state.asyncRegistration) { + is Loading -> { + // Ensure password is hidden + passwordShown = false + renderPasswordField() + } + is Fail -> { + loginFieldTil.error = errorFormatter.toHumanReadable(state.asyncRegistration.error) + } + // Success is handled by the LoginActivity + is Success -> Unit + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index bfb6be6733..f0d872ce48 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -74,6 +74,24 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null + private val registrationCallback = object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + } + override fun handle(action: LoginAction) { when (action) { is LoginAction.UpdateServerType -> handleUpdateServerType(action) @@ -83,10 +101,27 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.Login -> handleLogin(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.ResetPassword -> handleResetPassword(action) + is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.ResetAction -> handleResetAction(action) } } + private fun handleRegisterAction(action: LoginAction.RegisterAction) { + when (action) { + is LoginAction.RegisterWith -> handleRegisterWith(action) + } + } + + private fun handleRegisterWith(action: LoginAction.RegisterWith) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.createAccount(action.username, action.password, null /* TODO InitialDisplayName */, registrationCallback) + } + private fun handleResetAction(action: LoginAction.ResetAction) { // Cancel any request currentTask?.cancel() diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 42667ce5fa..abd44dc54c 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -93,5 +93,6 @@ Username Password Next + That username is taken From 95fc20dca019410cd8661925295659075b1a22e9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 19:08:10 +0100 Subject: [PATCH 035/189] Login screens: Registration: login/password step --- .../riotx/features/login/LoginActivity.kt | 62 +++++++++++++++++++ .../LoginGenericTextInputFormFragment.kt | 61 ++++++------------ .../LoginSignUpSignInSelectionFragment.kt | 28 +-------- .../riotx/features/login/LoginViewEvents.kt | 27 ++++++++ .../riotx/features/login/LoginViewModel.kt | 52 ++++++++++------ 5 files changed, 143 insertions(+), 87 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index f934ebf27f..e05a8d2d5e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -23,6 +23,8 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.auth.registration.FlowResult +import im.vector.matrix.android.api.auth.registration.Stage import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment @@ -85,6 +87,34 @@ class LoginActivity : VectorBaseActivity() { updateWithState(it) } .disposeOnDestroy() + + loginViewModel.viewEvents + .observe() + .subscribe { + handleLoginViewEvents(it) + } + .disposeOnDestroy() + } + + private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { + when (loginViewEvents) { + is LoginViewEvents.RegistrationFlowResult -> { + // Check that all flows are supported by the application + if (loginViewEvents.flowResult.missingStages.any { it is Stage.Other }) { + // Display a popup to propose use web fallback + // TODO + } else { + // Go on with registration flow + // loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + if (loginViewModel.isPasswordSent) { + handleRegistrationNavigation(loginViewEvents.flowResult) + } else { + // First ask for login and password + addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + } + } + } + } } private fun onLoginFlowRetrieved() { @@ -152,6 +182,38 @@ class LoginActivity : VectorBaseActivity() { .show() } + private fun handleRegistrationNavigation(flowResult: FlowResult) { + // Complete all mandatory stage first + val mandatoryStages = flowResult.missingStages.filter { it.mandatory } + + if (mandatoryStages.isEmpty()) { + // Consider optional stages + val optionalStages = flowResult.missingStages.filter { !it.mandatory } + if (optionalStages.isEmpty()) { + // Should not happen... + } else { + doStage(optionalStages.first()) + } + } else { + doStage(mandatoryStages.first()) + } + } + + private fun doStage(stage: Stage) { + when (stage) { + is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginCaptchaFragment::class.java) + is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory)) + is Stage.Msisdn + -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory)) + is Stage.Terms + -> TODO() + } + } + + override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 98c30c685f..4e25769740 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -30,16 +30,15 @@ import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.* import javax.inject.Inject enum class TextInputFormFragmentMode { - SetEmailMandatory, - SetEmailOptional, - SetMsisdnMandatory, - SetMsisdnOptional, + SetEmail, + SetMsisdn, ConfirmMsisdn } @Parcelize data class LoginGenericTextInputFormFragmentArgument( - val mode: TextInputFormFragmentMode + val mode: TextInputFormFragmentMode, + val mandatory: Boolean ) : Parcelable /** @@ -60,39 +59,23 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra private fun setupUi() { when (params.mode) { - TextInputFormFragmentMode.SetEmailMandatory -> { + TextInputFormFragmentMode.SetEmail -> { loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) - loginGenericTextInputFormTil.hint = getString(R.string.login_set_email_mandatory_hint) + loginGenericTextInputFormTil.hint = getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint) loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS loginGenericTextInputFormOtherButton.isVisible = false loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) } - TextInputFormFragmentMode.SetEmailOptional -> { - loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) - loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) - loginGenericTextInputFormTil.hint = getString(R.string.login_set_email_optional_hint) - loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS - loginGenericTextInputFormOtherButton.isVisible = false - loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) - } - TextInputFormFragmentMode.SetMsisdnMandatory -> { + TextInputFormFragmentMode.SetMsisdn -> { loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) - loginGenericTextInputFormTil.hint = getString(R.string.login_set_msisdn_mandatory_hint) + loginGenericTextInputFormTil.hint = getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint) loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE loginGenericTextInputFormOtherButton.isVisible = false loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) } - TextInputFormFragmentMode.SetMsisdnOptional -> { - loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) - loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) - loginGenericTextInputFormTil.hint = getString(R.string.login_set_msisdn_optional_hint) - loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE - loginGenericTextInputFormOtherButton.isVisible = false - loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) - } - TextInputFormFragmentMode.ConfirmMsisdn -> { + TextInputFormFragmentMode.ConfirmMsisdn -> { loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title) loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice) loginGenericTextInputFormTil.hint = getString(R.string.login_msisdn_confirm_hint) @@ -115,22 +98,16 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra } private fun setupSubmitButton() { - when (params.mode) { - TextInputFormFragmentMode.SetEmailMandatory, - TextInputFormFragmentMode.SetMsisdnMandatory, - TextInputFormFragmentMode.ConfirmMsisdn -> { - loginGenericTextInputFormSubmit.isEnabled = false - loginGenericTextInputFormTextInput.textChanges() - .subscribe { - // TODO Better check for email format, etc? - loginGenericTextInputFormSubmit.isEnabled = it.isNotBlank() - } - .disposeOnDestroyView() - } - TextInputFormFragmentMode.SetEmailOptional, - TextInputFormFragmentMode.SetMsisdnOptional -> { - loginGenericTextInputFormSubmit.isEnabled = true - } + if (params.mandatory) { + loginGenericTextInputFormSubmit.isEnabled = false + loginGenericTextInputFormTextInput.textChanges() + .subscribe { + // TODO Better check for email format, etc? + loginGenericTextInputFormSubmit.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + } else { + loginGenericTextInputFormSubmit.isEnabled = true } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 08872606bb..4425632fd1 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -21,11 +21,7 @@ import android.view.View import androidx.core.view.isVisible import butterknife.OnClick import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Success import com.airbnb.mvrx.withState -import im.vector.matrix.android.api.auth.registration.FlowResult -import im.vector.matrix.android.api.auth.registration.RegistrationResult -import im.vector.matrix.android.api.auth.registration.Stage import im.vector.riotx.R import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject @@ -83,32 +79,12 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun invalidate() = withState(loginViewModel) { when (it.asyncRegistration) { - is Success -> { - when (val res = it.asyncRegistration()) { - is RegistrationResult.Success -> - // Should not happen - Unit - is RegistrationResult.FlowResponse -> handleFlowResult(res.flowResult) - } - } - is Fail -> { - // TODO Registration disabled, etc + is Fail -> { + // TODO Registration disabled, (move to Activity?) when (it.asyncRegistration.error) { } } } } - - private fun handleFlowResult(flowResult: FlowResult) { - // Check that all flows are supported by the application - if (flowResult.missingStages.any { it is Stage.Other }) { - // Display a popup to propose use web fallback - // TODO - } else { - // Go on with registration flow - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) - } - } - } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt new file mode 100644 index 0000000000..b8b7965c77 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.features.login + +import im.vector.matrix.android.api.auth.registration.FlowResult + +/** + * Transient events for Login + */ +sealed class LoginViewEvents { + data class RegistrationFlowResult(val flowResult: FlowResult) : LoginViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index f0d872ce48..362a126e8c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -34,6 +34,8 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.utils.DataSource +import im.vector.riotx.core.utils.PublishDataSource import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import timber.log.Timber @@ -60,6 +62,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + var isPasswordSent: Boolean = false + private set + private var registrationWizard: RegistrationWizard? = null var serverType: ServerType = ServerType.MatrixOrg @@ -74,23 +79,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null - private val registrationCallback = object : MatrixCallback { - override fun onSuccess(data: RegistrationResult) { - when (data) { - is RegistrationResult.Success -> onSessionCreated(data.session) - is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) - } - } - - override fun onFailure(failure: Throwable) { - // TODO Handled JobCancellationException - setState { - copy( - asyncRegistration = Fail(failure) - ) - } - } - } + private val _viewEvents = PublishDataSource() + val viewEvents: DataSource = _viewEvents override fun handle(action: LoginAction) { when (action) { @@ -109,6 +99,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleRegisterAction(action: LoginAction.RegisterAction) { when (action) { is LoginAction.RegisterWith -> handleRegisterWith(action) + // TODO Add other actions here } } @@ -119,7 +110,25 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } - currentTask = registrationWizard?.createAccount(action.username, action.password, null /* TODO InitialDisplayName */, registrationCallback) + currentTask = registrationWizard?.createAccount(action.username, action.password, null /* TODO InitialDisplayName */, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + isPasswordSent = true + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) } private fun handleResetAction(action: LoginAction.ResetAction) { @@ -129,6 +138,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi when (action) { LoginAction.ResetLogin -> { + isPasswordSent = false + setState { copy( asyncLoginAction = Uninitialized, @@ -292,9 +303,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun onFlowResponse(flowResult: FlowResult) { + // Notify the user + _viewEvents.post(LoginViewEvents.RegistrationFlowResult(flowResult)) + setState { copy( - asyncRegistration = Success(RegistrationResult.FlowResponse(flowResult)) + asyncRegistration = Uninitialized ) } } From dfbf448bb7be08523f6c959b8caf2a7fce0edba8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 13:30:15 +0100 Subject: [PATCH 036/189] Login screens: Captcha step for registration --- .../internal/auth/registration/AuthParams.kt | 47 +++++- .../auth/registration/AuthParamsCaptcha.kt | 30 ---- .../registration/AuthParamsEmailIdentity.kt | 40 ----- .../registration/DefaultRegistrationWizard.kt | 4 +- .../registration/RegistrationFlowResponse.kt | 11 +- .../auth/registration/RegistrationParams.kt | 2 +- vector/src/main/assets/reCaptchaPage.html | 22 +++ .../im/vector/riotx/core/di/FragmentModule.kt | 5 + .../vector/riotx/core/di/VectorComponent.kt | 3 + .../im/vector/riotx/core/utils/AssetReader.kt | 62 +++++++ .../riotx/features/login/LoginAction.kt | 2 +- .../riotx/features/login/LoginActivity.kt | 6 +- .../features/login/LoginCaptchaFragment.kt | 152 +++++++++++++++++- .../riotx/features/login/LoginViewModel.kt | 51 +++++- .../res/layout/fragment_login_captcha.xml | 10 +- 15 files changed, 358 insertions(+), 89 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt create mode 100644 vector/src/main/assets/reCaptchaPage.html create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt index 69ef4e2238..f0314b6c25 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt @@ -18,15 +18,58 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes /** * Open class, parent to all possible authentication parameters */ @JsonClass(generateAdapter = true) -open class AuthParams( +internal data class AuthParams( @Json(name = "type") val type: String, @Json(name = "session") - val session: String + val session: String, + + /** + * parameter for "m.login.recaptcha" type + */ + @Json(name = "response") + val captchaResponse: String? = null, + + /** + * parameter for "m.login.email.identity" type + */ + @Json(name = "threepid_creds") + val threePidCredentials: ThreePidCredentials? = null +) { + + companion object { + fun createForCaptcha(session: String, captchaResponse: String): AuthParams { + return AuthParams( + type = LoginFlowTypes.RECAPTCHA, + session = session, + captchaResponse = captchaResponse + ) + } + + fun createForEmailIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams { + return AuthParams( + type = LoginFlowTypes.EMAIL_IDENTITY, + session = session, + threePidCredentials = threePidCredentials + ) + } + } +} + + +data class ThreePidCredentials( + @Json(name = "client_secret") + val clientSecret: String? = null, + + @Json(name = "id_server") + val idServer: String? = null, + + val sid: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt deleted file mode 100644 index daf9f911c0..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2018 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.matrix.android.internal.auth.registration - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes - -/** - * Class to define the authentication parameters for "m.login.recaptcha" type - */ -@JsonClass(generateAdapter = true) -class AuthParamsCaptcha(session: String, - - @Json(name = "response") - val response: String) - : AuthParams(LoginFlowTypes.RECAPTCHA, session) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt deleted file mode 100644 index 981b8682f9..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2018 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.matrix.android.internal.auth.registration - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes - -/** - * Class to define the authentication parameters for "m.login.email.identity" type - */ -@JsonClass(generateAdapter = true) -class AuthParamsEmailIdentity(session: String, - - @Json(name = "threepid_creds") - val threePidCredentials: ThreePidCredentials) - : AuthParams(LoginFlowTypes.EMAIL_IDENTITY, session) - -data class ThreePidCredentials( - @Json(name = "client_secret") - val clientSecret: String? = null, - - @Json(name = "id_server") - val idServer: String? = null, - - val sid: String? = null -) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index 1d234eaf8d..a25071aa3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -70,9 +70,7 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: return performRegistrationRequest( RegistrationParams( - auth = AuthParamsCaptcha( - session = safeSession, - response = response) + auth = AuthParams.createForCaptcha(safeSession, response) ), callback) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index aa9fae3362..17850397b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -53,7 +53,16 @@ data class RegistrationFlowResponse( * For example, the public key of reCAPTCHA stage could be given here. */ @Json(name = "params") - var params: JsonDict? = null + var params: JsonDict? = null, + + /** + * The two MatrixError fields can also be present here in case of error when validating a stage + */ + @Json(name = "errcode") + var code: String? = null, + + @Json(name = "error") + var message: String? = null ) /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt index 8d668f7f11..6a874c7387 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt @@ -24,7 +24,7 @@ import com.squareup.moshi.JsonClass * Class to pass parameters to the different registration types for /register. */ @JsonClass(generateAdapter = true) -data class RegistrationParams( +internal data class RegistrationParams( // authentication parameters @Json(name = "auth") val auth: AuthParams? = null, diff --git a/vector/src/main/assets/reCaptchaPage.html b/vector/src/main/assets/reCaptchaPage.html new file mode 100644 index 0000000000..8029acf28c --- /dev/null +++ b/vector/src/main/assets/reCaptchaPage.html @@ -0,0 +1,22 @@ + + + + + + +
+ + + diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 37ce9583b8..19dfe75336 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -114,6 +114,11 @@ interface FragmentModule { @FragmentKey(LoginFragment::class) fun bindLoginFragment(fragment: LoginFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LoginCaptchaFragment::class) + fun bindLoginCaptchaFragment(fragment: LoginCaptchaFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginServerUrlFormFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 2106ebf750..e06093a5c8 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -29,6 +29,7 @@ import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication import im.vector.riotx.core.pushers.PushersManager +import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler @@ -70,6 +71,8 @@ interface VectorComponent { fun resources(): Resources + fun assetReader(): AssetReader + fun dimensionConverter(): DimensionConverter fun vectorConfiguration(): VectorConfiguration diff --git a/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt b/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt new file mode 100644 index 0000000000..908f0e68b6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.utils + +import android.content.Context +import timber.log.Timber +import javax.inject.Inject + +/** + * Read asset files + */ +class AssetReader @Inject constructor(private val context: Context) { + + /* ========================================================================================== + * CACHE + * ========================================================================================== */ + private val cache = mutableMapOf() + + /** + * Read an asset from resource and return a String or null in case of error. + * + * @param assetFilename Asset filename + * @return the content of the asset file, or null in case of error + */ + fun readAssetFile(assetFilename: String): String? { + return cache.getOrPut(assetFilename, { + return try { + context.assets.open(assetFilename) + .use { asset -> + buildString { + var ch = asset.read() + while (ch != -1) { + append(ch.toChar()) + ch = asset.read() + } + } + } + } catch (e: Exception) { + Timber.e(e, "## readAssetFile() failed") + null + } + }) + } + + fun clearCache() { + cache.clear() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index be76013ca3..b13305dc9a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -35,7 +35,7 @@ sealed class LoginAction : VectorViewModelAction { data class AddEmail(val email: String) : RegisterAction() data class AddMsisdn(val msisdn: String) : RegisterAction() data class ConfirmMsisdn(val code: String) : RegisterAction() - data class PerformCaptcha(val captcha: String /* TODO Add other params */) : RegisterAction() + data class CaptchaDone(val captchaResponse: String) : RegisterAction() // Reset actions open class ResetAction : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index e05a8d2d5e..4f558a10ae 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -201,12 +201,14 @@ class LoginActivity : VectorBaseActivity() { private fun doStage(stage: Stage) { when (stage) { - is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginCaptchaFragment::class.java) + is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginCaptchaFragment::class.java, LoginCaptchaFragmentArgument(stage.publicKey)) is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory)) is Stage.Msisdn - -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, + -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory)) is Stage.Terms -> TODO() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index 1490f3d5e0..b7a4ced8a3 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -16,17 +16,165 @@ package im.vector.riotx.features.login +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.net.http.SslError +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import android.view.KeyEvent +import android.view.View +import android.webkit.* +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.args +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R +import im.vector.riotx.core.utils.AssetReader +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_login_captcha.* +import timber.log.Timber +import java.net.URLDecoder +import java.util.* import javax.inject.Inject +@Parcelize +data class LoginCaptchaFragmentArgument( + val siteKey: String +) : Parcelable + +@JsonClass(generateAdapter = true) +data class JavascriptResponse( + @Json(name = "action") + val action: String? = null, + @Json(name = "response") + val response: String? = null +) + /** * In this screen, the user is asked to confirm he is not a robot */ -class LoginCaptchaFragment @Inject constructor() : AbstractLoginFragment() { +class LoginCaptchaFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_captcha - // TODO + private val params: LoginCaptchaFragmentArgument by args() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupWebView() + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebView() { + loginCaptchaWevView.settings.javaScriptEnabled = true + + val reCaptchaPage = assetReader.readAssetFile("reCaptchaPage.html") ?: error("missing asset reCaptchaPage.html") + + val html = Formatter().format(reCaptchaPage, params.siteKey).toString() + val mime = "text/html" + val encoding = "utf-8" + + val homeServerUrl = loginViewModel.getHomeServerUrl() + loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null) + loginCaptchaWevView.requestLayout() + + loginCaptchaWevView.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + + // TODO Hide loader + } + + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + Timber.d("## onReceivedSslError() : " + error.certificate) + + if (!isAdded) { + return + } + + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.ssl_could_not_verify) + .setPositiveButton(R.string.ssl_trust) { _, _ -> + Timber.d("## onReceivedSslError() : the user trusted") + handler.proceed() + } + .setNegativeButton(R.string.ssl_do_not_trust) { _, _ -> + Timber.d("## onReceivedSslError() : the user did not trust") + handler.cancel() + } + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + handler.cancel() + Timber.d("## onReceivedSslError() : the user dismisses the trust dialog.") + dialog.dismiss() + return@OnKeyListener true + } + false + }) + .setCancelable(false) + .show() + } + + // common error message + private fun onError(errorMessage: String) { + Timber.e("## onError() : $errorMessage") + + // TODO + // Toast.makeText(this@AccountCreationCaptchaActivity, errorMessage, Toast.LENGTH_LONG).show() + + // on error case, close this activity + // runOnUiThread(Runnable { finish() }) + } + + @SuppressLint("NewApi") + override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { + super.onReceivedHttpError(view, request, errorResponse) + + if (request.url.toString().endsWith("favicon.ico")) { + // Ignore this error + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + onError(errorResponse.reasonPhrase) + } else { + onError(errorResponse.toString()) + } + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + @Suppress("DEPRECATION") + super.onReceivedError(view, errorCode, description, failingUrl) + onError(description) + } + + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + if (url?.startsWith("js:") == true) { + var json = url.substring(3) + var parameters: JavascriptResponse? = null + + try { + // URL decode + json = URLDecoder.decode(json, "UTF-8") + parameters = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java).fromJson(json) + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading(): failed") + } + + val response = parameters?.response + if (parameters?.action == "verifyCallback" && response != null) { + loginViewModel.handle(LoginAction.CaptchaDone(response)) + } + } + return true + } + } + + } + override fun resetViewModel() { // Nothing to do diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 362a126e8c..b7fb27f565 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -99,6 +99,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleRegisterAction(action: LoginAction.RegisterAction) { when (action) { is LoginAction.RegisterWith -> handleRegisterWith(action) + is LoginAction.CaptchaDone -> handleCaptchaDone(action) // TODO Add other actions here } } @@ -114,6 +115,44 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi override fun onSuccess(data: RegistrationResult) { isPasswordSent = true + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + + private fun handleCaptchaDone(action: LoginAction.CaptchaDone) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.performReCaptcha(action.captchaResponse, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + when (data) { is RegistrationResult.Success -> onSessionCreated(data.session) is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) @@ -284,6 +323,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi currentTask = registrationWizard?.getRegistrationFlow(object : MatrixCallback { override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + when (data) { is RegistrationResult.Success -> onSessionCreated(data.session) is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) @@ -305,12 +350,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun onFlowResponse(flowResult: FlowResult) { // Notify the user _viewEvents.post(LoginViewEvents.RegistrationFlowResult(flowResult)) - - setState { - copy( - asyncRegistration = Uninitialized - ) - } } diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml index e3ba0ade22..8dec490a14 100644 --- a/vector/src/main/res/layout/fragment_login_captcha.xml +++ b/vector/src/main/res/layout/fragment_login_captcha.xml @@ -22,11 +22,19 @@ style="@style/LoginTopIcon" android:layout_gravity="center_horizontal" /> + + + android:layout_marginTop="8dp" />
From 3f80076fb1e5909e5bffe4d521ce95f7583104d7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 14:37:36 +0100 Subject: [PATCH 037/189] Login screens: Terms step for registration --- .../auth/registration/RegistrationWizard.kt | 2 + .../android/api/auth/registration/Stage.kt | 3 +- .../registration/DefaultRegistrationWizard.kt | 16 +++ .../registration/RegistrationFlowResponse.kt | 2 +- .../im/vector/riotx/core/di/FragmentModule.kt | 6 + .../riotx/features/login/LoginAction.kt | 1 + .../riotx/features/login/LoginActivity.kt | 25 ++-- .../riotx/features/login/LoginViewModel.kt | 36 ++++++ .../LocalizedFlowDataLoginTermsChecked.kt | 22 ++++ .../login/terms/LoginTermsFragment.kt | 104 +++++++++++++++ .../login/terms/LoginTermsViewState.kt | 36 ++++++ .../features/login/terms/PolicyController.kt | 47 +++++++ .../riotx/features/login/terms/PolicyModel.kt | 62 +++++++++ .../riotx/features/login/terms/UrlAndName.kt | 22 ++++ .../riotx/features/login/terms/converter.kt | 120 ++++++++++++++++++ .../main/res/layout/fragment_login_terms.xml | 43 +++++++ vector/src/main/res/layout/item_policy.xml | 48 +++++++ 17 files changed, 582 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt create mode 100755 vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt create mode 100644 vector/src/main/res/layout/fragment_login_terms.xml create mode 100644 vector/src/main/res/layout/item_policy.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 7144ce389f..85ac0d0aae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -27,5 +27,7 @@ interface RegistrationWizard { fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable + fun acceptTerms(callback: MatrixCallback): Cancelable + // TODO Add other method here } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt index 9f1883e4b1..e6d6f87869 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -40,5 +40,4 @@ sealed class Stage(open val mandatory: Boolean) { data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory) } -//TODO -class TermPolicies +typealias TermPolicies = Map<*, *> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index a25071aa3c..c81c6221ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthAPI import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -74,6 +75,21 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } + override fun acceptTerms(callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + return performRegistrationRequest( + RegistrationParams( + auth = AuthParams( + type = LoginFlowTypes.TERMS, + session = safeSession + ) + ), callback) + } + private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val result = runCatching { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index 17850397b8..1d67369afc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -84,7 +84,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult { LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) ?: "") LoginFlowTypes.DUMMY -> Stage.Dummy - LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, TermPolicies()) + LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap()) LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory) LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory) else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 19dfe75336..71588575db 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -36,6 +36,7 @@ import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.login.* +import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment @@ -119,6 +120,11 @@ interface FragmentModule { @FragmentKey(LoginCaptchaFragment::class) fun bindLoginCaptchaFragment(fragment: LoginCaptchaFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LoginTermsFragment::class) + fun bindLoginTermsFragment(fragment: LoginTermsFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginServerUrlFormFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index b13305dc9a..ec6ae9ea88 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -36,6 +36,7 @@ sealed class LoginAction : VectorViewModelAction { data class AddMsisdn(val msisdn: String) : RegisterAction() data class ConfirmMsisdn(val code: String) : RegisterAction() data class CaptchaDone(val captchaResponse: String) : RegisterAction() + object AcceptTerms : RegisterAction() // Reset actions open class ResetAction : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 4f558a10ae..6644b2c193 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -32,6 +32,9 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity +import im.vector.riotx.features.login.terms.LoginTermsFragment +import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument +import im.vector.riotx.features.login.terms.toLocalizedLoginTerms import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject @@ -184,21 +187,22 @@ class LoginActivity : VectorBaseActivity() { private fun handleRegistrationNavigation(flowResult: FlowResult) { // Complete all mandatory stage first - val mandatoryStages = flowResult.missingStages.filter { it.mandatory } + val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } - if (mandatoryStages.isEmpty()) { + if (mandatoryStage != null) { + doStage(mandatoryStage) + } else { // Consider optional stages - val optionalStages = flowResult.missingStages.filter { !it.mandatory } - if (optionalStages.isEmpty()) { + val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } + if (optionalStage == null) { // Should not happen... } else { - doStage(optionalStages.first()) + doStage(optionalStage) } - } else { - doStage(mandatoryStages.first()) } } + // TODO Unstack fragment when stage is complete private fun doStage(stage: Stage) { when (stage) { is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, @@ -210,12 +214,13 @@ class LoginActivity : VectorBaseActivity() { -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory)) - is Stage.Terms - -> TODO() + is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginTermsFragment::class.java, + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language)))) + else -> TODO() } } - override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index b7fb27f565..8fa78da496 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -40,6 +40,9 @@ import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import timber.log.Timber +/** + * TODO To speed up registration, consider fetching registration flow instead of login flow at startup + */ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, private val authenticator: Authenticator, private val registrationService: RegistrationService, @@ -100,10 +103,43 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi when (action) { is LoginAction.RegisterWith -> handleRegisterWith(action) is LoginAction.CaptchaDone -> handleCaptchaDone(action) + is LoginAction.AcceptTerms -> handleAcceptTerms() // TODO Add other actions here } } + private fun handleAcceptTerms() { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.acceptTerms(object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + private fun handleRegisterWith(action: LoginAction.RegisterWith) { setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt new file mode 100644 index 0000000000..52aaa9d4a4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login.terms + +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms + +data class LocalizedFlowDataLoginTermsChecked(val localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, + var checked: Boolean = false) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt new file mode 100755 index 0000000000..3e202b5245 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login.terms + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import butterknife.OnClick +import com.airbnb.mvrx.args +import im.vector.riotx.R +import im.vector.riotx.core.utils.openUrlInExternalBrowser +import im.vector.riotx.features.login.AbstractLoginFragment +import im.vector.riotx.features.login.LoginAction +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_login_terms.* +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import javax.inject.Inject + +@Parcelize +data class LoginTermsFragmentArgument( + val localizedFlowDataLoginTerms: List +) : Parcelable + +/** + * LoginTermsFragment displays the list of policies the user has to accept + */ +class LoginTermsFragment @Inject constructor(private val policyController: PolicyController) : AbstractLoginFragment(), + PolicyController.PolicyControllerListener { + + private val params: LoginTermsFragmentArgument by args() + + override fun getLayoutResId() = R.layout.fragment_login_terms + + private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList()) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + loginTermsPolicyList.setController(policyController) + policyController.listener = this + + val list = ArrayList() + + params.localizedFlowDataLoginTerms + .forEach { + list.add(LocalizedFlowDataLoginTermsChecked(it)) + } + + loginTermsViewState = LoginTermsViewState(list) + + renderState() + } + + private fun renderState() { + policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked) + + // Button is enabled only if all checkboxes are checked + loginTermsSubmit.isEnabled = loginTermsViewState.allChecked() + } + + override fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) { + if (isChecked) { + loginTermsViewState.check(localizedFlowDataLoginTerms) + } else { + loginTermsViewState.uncheck(localizedFlowDataLoginTerms) + } + + renderState() + } + + override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) { + openUrlInExternalBrowser(requireContext(), localizedFlowDataLoginTerms.localizedUrl!!) + + // This code crashed, because user is not authenticated yet + //val intent = VectorWebViewActivity.getIntent(requireContext(), + // localizedFlowDataLoginTerms.localizedUrl!!, + // localizedFlowDataLoginTerms.localizedName!!, + // WebViewMode.DEFAULT) + //startActivity(intent) + } + + @OnClick(R.id.loginTermsSubmit) + internal fun submit() { + loginViewModel.handle(LoginAction.AcceptTerms) + } + + override fun resetViewModel() { + // No op + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt new file mode 100644 index 0000000000..104ea88daa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login.terms + +import com.airbnb.mvrx.MvRxState +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms + +data class LoginTermsViewState( + val localizedFlowDataLoginTermsChecked: List +) : MvRxState { + fun check(data: LocalizedFlowDataLoginTerms) { + localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = true + } + + fun uncheck(data: LocalizedFlowDataLoginTerms) { + localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = false + } + + fun allChecked(): Boolean { + return localizedFlowDataLoginTermsChecked.all { it.checked } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt new file mode 100644 index 0000000000..6e86d40e3d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login.terms + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import javax.inject.Inject + +class PolicyController @Inject constructor() : TypedEpoxyController>() { + + var listener: PolicyControllerListener? = null + + override fun buildModels(data: List) { + data.forEach { entry -> + policy { + id(entry.localizedFlowDataLoginTerms.policyName) + checked(entry.checked) + title(entry.localizedFlowDataLoginTerms.localizedName!!) + + clickListener(View.OnClickListener { listener?.openPolicy(entry.localizedFlowDataLoginTerms) }) + checkChangeListener { _, isChecked -> + listener?.setChecked(entry.localizedFlowDataLoginTerms, isChecked) + } + } + } + } + + interface PolicyControllerListener { + fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) + fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt new file mode 100644 index 0000000000..85b7c80dd0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login.terms + +import android.view.View +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_policy) +abstract class PolicyModel : EpoxyModelWithHolder() { + @EpoxyAttribute + var checked: Boolean = false + + @EpoxyAttribute + var title: String? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + holder.let { + it.checkbox.isChecked = checked + it.checkbox.setOnCheckedChangeListener(checkChangeListener) + it.title.text = title + it.view.setOnClickListener(clickListener) + } + } + + // Ensure checkbox behaves as expected (remove the listener) + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.checkbox.setOnCheckedChangeListener(null) + } + + class Holder : VectorEpoxyHolder() { + val checkbox by bind(R.id.adapter_item_policy_checkbox) + val title by bind(R.id.adapter_item_policy_title) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt new file mode 100644 index 0000000000..1ccb7cac49 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login.terms + +data class UrlAndName( + val url: String, + val name: String +) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt new file mode 100644 index 0000000000..c9e6dcf3fd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login.terms + +import im.vector.matrix.android.api.auth.registration.TermPolicies +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms + +/** + * This method extract the policies from the login terms parameter, regarding the user language. + * For each policy, if user language is not found, the default language is used and if not found, the first url and name are used (not predictable) + * + * Example of Data: + *
+ * "m.login.terms": {
+ *       "policies": {
+ *         "privacy_policy": {
+ *           "version": "1.0",
+ *           "en": {
+ *             "url": "http:\/\/matrix.org\/_matrix\/consent?v=1.0",
+ *             "name": "Terms and Conditions"
+ *           }
+ *         }
+ *       }
+ *     }
+ *
+ * + * @param userLanguage the user language + * @param defaultLanguage the default language to use if the user language is not found for a policy in registrationFlowResponse + */ +fun TermPolicies.toLocalizedLoginTerms(userLanguage: String, + defaultLanguage: String = "en"): List { + val result = ArrayList() + + val policies = get("policies") + if (policies is Map<*, *>) { + policies.keys.forEach { policyName -> + val localizedFlowDataLoginTerms = LocalizedFlowDataLoginTerms() + localizedFlowDataLoginTerms.policyName = policyName as String + + val policy = policies[policyName] + + // Enter this policy + if (policy is Map<*, *>) { + // Version + localizedFlowDataLoginTerms.version = policy["version"] as String? + + var userLanguageUrlAndName: UrlAndName? = null + var defaultLanguageUrlAndName: UrlAndName? = null + var firstUrlAndName: UrlAndName? = null + + // Search for language + policy.keys.forEach { policyKey -> + when (policyKey) { + "version" -> Unit // Ignore + userLanguage -> { + // We found the data for the user language + userLanguageUrlAndName = extractUrlAndName(policy[policyKey]) + } + defaultLanguage -> { + // We found default language + defaultLanguageUrlAndName = extractUrlAndName(policy[policyKey]) + } + else -> { + if (firstUrlAndName == null) { + // Get at least some data + firstUrlAndName = extractUrlAndName(policy[policyKey]) + } + } + } + } + + // Copy found language data by priority + when { + userLanguageUrlAndName != null -> { + localizedFlowDataLoginTerms.localizedUrl = userLanguageUrlAndName!!.url + localizedFlowDataLoginTerms.localizedName = userLanguageUrlAndName!!.name + } + defaultLanguageUrlAndName != null -> { + localizedFlowDataLoginTerms.localizedUrl = defaultLanguageUrlAndName!!.url + localizedFlowDataLoginTerms.localizedName = defaultLanguageUrlAndName!!.name + } + firstUrlAndName != null -> { + localizedFlowDataLoginTerms.localizedUrl = firstUrlAndName!!.url + localizedFlowDataLoginTerms.localizedName = firstUrlAndName!!.name + } + } + } + + result.add(localizedFlowDataLoginTerms) + } + } + + return result +} + +private fun extractUrlAndName(policyData: Any?): UrlAndName? { + if (policyData is Map<*, *>) { + val url = policyData["url"] as String? + val name = policyData["name"] as String? + + if (url != null && name != null) { + return UrlAndName(url, name) + } + } + return null +} diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml new file mode 100644 index 0000000000..98f773e284 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_terms.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_policy.xml b/vector/src/main/res/layout/item_policy.xml new file mode 100644 index 0000000000..f28beef73d --- /dev/null +++ b/vector/src/main/res/layout/item_policy.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + \ No newline at end of file From 9aa270c7ad8aa97f1a2d72e232b1d633b8d88ea8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 14:51:51 +0100 Subject: [PATCH 038/189] Login screens: Perform dummy action when user does not want to enter an email -> account created! --- .../auth/registration/RegistrationWizard.kt | 2 + .../registration/DefaultRegistrationWizard.kt | 15 +++++++ .../riotx/features/login/LoginAction.kt | 1 + .../LoginGenericTextInputFormFragment.kt | 19 ++++++++- .../riotx/features/login/LoginViewModel.kt | 39 +++++++++++++++++-- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 85ac0d0aae..6e967fc235 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -29,5 +29,7 @@ interface RegistrationWizard { fun acceptTerms(callback: MatrixCallback): Cancelable + fun dummy(callback: MatrixCallback): Cancelable + // TODO Add other method here } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index c81c6221ea..ef3e8d1c30 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -90,6 +90,21 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } + override fun dummy(callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + return performRegistrationRequest( + RegistrationParams( + auth = AuthParams( + type = LoginFlowTypes.DUMMY, + session = safeSession + ) + ), callback) + } + private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val result = runCatching { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index ec6ae9ea88..2367b273ea 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -37,6 +37,7 @@ sealed class LoginAction : VectorViewModelAction { data class ConfirmMsisdn(val code: String) : RegisterAction() data class CaptchaDone(val captchaResponse: String) : RegisterAction() object AcceptTerms : RegisterAction() + object RegisterDummy : RegisterAction() // Reset actions open class ResetAction : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 4e25769740..4ffd149620 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -94,7 +94,24 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra @OnClick(R.id.loginGenericTextInputFormSubmit) fun onSubmitClicked() { - // TODO + val text = loginGenericTextInputFormTextInput.text.toString() + + if (text.isEmpty()) { + // Perform dummy action + loginViewModel.handle(LoginAction.RegisterDummy) + } else { + when (params.mode) { + TextInputFormFragmentMode.SetEmail -> { + // TODO + } + TextInputFormFragmentMode.SetMsisdn -> { + // TODO + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + // TODO + } + } + } } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 8fa78da496..1dd445ca20 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -101,9 +101,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleRegisterAction(action: LoginAction.RegisterAction) { when (action) { - is LoginAction.RegisterWith -> handleRegisterWith(action) - is LoginAction.CaptchaDone -> handleCaptchaDone(action) - is LoginAction.AcceptTerms -> handleAcceptTerms() + is LoginAction.RegisterWith -> handleRegisterWith(action) + is LoginAction.CaptchaDone -> handleCaptchaDone(action) + is LoginAction.AcceptTerms -> handleAcceptTerms() + is LoginAction.RegisterDummy -> handleRegisterDummy() // TODO Add other actions here } } @@ -140,6 +141,38 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi }) } + private fun handleRegisterDummy() { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.dummy(object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + private fun handleRegisterWith(action: LoginAction.RegisterWith) { setState { copy( From 1c03163a33b3911409c93e9efb6ec6491dff0775 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 15:13:50 +0100 Subject: [PATCH 039/189] Login screens: prepare email and msisdn --- .../auth/registration/RegistrationWizard.kt | 7 +- .../registration/DefaultRegistrationWizard.kt | 43 +++++++- .../LoginGenericTextInputFormFragment.kt | 6 +- .../riotx/features/login/LoginViewModel.kt | 100 +++++++++++++++++- 4 files changed, 150 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 6e967fc235..b17c8c9dfa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -31,5 +31,10 @@ interface RegistrationWizard { fun dummy(callback: MatrixCallback): Cancelable - // TODO Add other method here + fun addEmail(email: String, callback: MatrixCallback): Cancelable + + fun addMsisdn(msisdn: String, callback: MatrixCallback): Cancelable + + fun confirmMsisdn(code: String, callback: MatrixCallback): Cancelable + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index ef3e8d1c30..8d1bd8d393 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -42,7 +42,6 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, private val sessionManager: SessionManager) : RegistrationWizard { - private var currentSession: String? = null private val authAPI = buildAuthAPI() @@ -90,6 +89,48 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } + override fun addEmail(email: String, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + // TODO + return performRegistrationRequest( + RegistrationParams( + // TODO + auth = AuthParams.createForEmailIdentity(safeSession, ThreePidCredentials(email)) + ), callback) + } + + override fun addMsisdn(msisdn: String, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + // TODO + return performRegistrationRequest( + RegistrationParams( + // TODO + auth = AuthParams.createForEmailIdentity(safeSession, ThreePidCredentials(msisdn)) + ), callback) + } + + override fun confirmMsisdn(code: String, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + // TODO + return performRegistrationRequest( + RegistrationParams( + // TODO + auth = AuthParams.createForEmailIdentity(safeSession, ThreePidCredentials(code)) + ), callback) + } + override fun dummy(callback: MatrixCallback): Cancelable { val safeSession = currentSession ?: run { callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 4ffd149620..a2b5feb1b1 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -102,13 +102,13 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra } else { when (params.mode) { TextInputFormFragmentMode.SetEmail -> { - // TODO + loginViewModel.handle(LoginAction.AddEmail(text)) } TextInputFormFragmentMode.SetMsisdn -> { - // TODO + loginViewModel.handle(LoginAction.AddMsisdn(text)) } TextInputFormFragmentMode.ConfirmMsisdn -> { - // TODO + loginViewModel.handle(LoginAction.ConfirmMsisdn(text)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 1dd445ca20..6cc73233a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -105,10 +105,108 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.CaptchaDone -> handleCaptchaDone(action) is LoginAction.AcceptTerms -> handleAcceptTerms() is LoginAction.RegisterDummy -> handleRegisterDummy() - // TODO Add other actions here + is LoginAction.AddEmail -> handleAddEmail(action) + is LoginAction.AddMsisdn -> handleAddMsisdn(action) + is LoginAction.ConfirmMsisdn -> handleConfirmMsisdn(action) } } + private fun handleConfirmMsisdn(action: LoginAction.ConfirmMsisdn) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.confirmMsisdn(action.code, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + + private fun handleAddMsisdn(action: LoginAction.AddMsisdn) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.addMsisdn(action.msisdn, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + + private fun handleAddEmail(action: LoginAction.AddEmail) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.addEmail(action.email, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + private fun handleAcceptTerms() { setState { copy( From 1dc7dfc896e5b917084abf3d91535db525ffa9bc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 16:04:57 +0100 Subject: [PATCH 040/189] Login screens: registration fallback --- .../android/api/auth/data/Credentials.kt | 1 + vector/src/main/assets/onLogin.js | 1 + vector/src/main/assets/onRegistered.js | 1 + vector/src/main/assets/sendObject.js | 1 + .../features/login/JavascriptResponse.kt | 39 +++++++++ .../riotx/features/login/LoginActivity.kt | 19 ++++- .../features/login/LoginCaptchaFragment.kt | 10 --- .../riotx/features/login/LoginWebFragment.kt | 79 ++++--------------- .../riotx/features/login/SupportedStage.kt | 30 +++++++ vector/src/main/res/values/strings_riotX.xml | 1 + 10 files changed, 105 insertions(+), 77 deletions(-) create mode 100644 vector/src/main/assets/onLogin.js create mode 100644 vector/src/main/assets/onRegistered.js create mode 100644 vector/src/main/assets/sendObject.js create mode 100644 vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index 089129967b..082ffe8f1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -31,4 +31,5 @@ data class Credentials( @Json(name = "access_token") val accessToken: String, @Json(name = "refresh_token") val refreshToken: String?, @Json(name = "device_id") val deviceId: String? + // TODO Add Wellknown ) diff --git a/vector/src/main/assets/onLogin.js b/vector/src/main/assets/onLogin.js new file mode 100644 index 0000000000..dd33227e40 --- /dev/null +++ b/vector/src/main/assets/onLogin.js @@ -0,0 +1 @@ +javascript:window.matrixLogin.onLogin = function(response) { sendObjectMessage({ 'action': 'onLogin', 'credentials': response }); }; \ No newline at end of file diff --git a/vector/src/main/assets/onRegistered.js b/vector/src/main/assets/onRegistered.js new file mode 100644 index 0000000000..67aca7b79e --- /dev/null +++ b/vector/src/main/assets/onRegistered.js @@ -0,0 +1 @@ +javascript:window.matrixRegistration.onRegistered = function(homeserverUrl, userId, accessToken) { sendObjectMessage({ 'action': 'onRegistered', 'homeServer': homeserverUrl, 'userId': userId, 'accessToken': accessToken }); } \ No newline at end of file diff --git a/vector/src/main/assets/sendObject.js b/vector/src/main/assets/sendObject.js new file mode 100644 index 0000000000..ebde72b58d --- /dev/null +++ b/vector/src/main/assets/sendObject.js @@ -0,0 +1 @@ +javascript:window.sendObjectMessage = function(parameters) { var iframe = document.createElement('iframe'); iframe.setAttribute('src', 'js:' + JSON.stringify(parameters)); document.documentElement.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null;}; \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt b/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt new file mode 100644 index 0000000000..4d88cf6097 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.Credentials + +@JsonClass(generateAdapter = true) +data class JavascriptResponse( + @Json(name = "action") + val action: String? = null, + + /** + * Use for captcha result + */ + @Json(name = "response") + val response: String? = null, + + /** + * Used for login/registration result + */ + @Json(name = "credentials") + val credentials: Credentials? = null +) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 6644b2c193..95206afefe 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -103,9 +103,9 @@ class LoginActivity : VectorBaseActivity() { when (loginViewEvents) { is LoginViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application - if (loginViewEvents.flowResult.missingStages.any { it is Stage.Other }) { + if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback - // TODO + onRegistrationStageNotSupported() } else { // Go on with registration flow // loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) @@ -159,7 +159,9 @@ class LoginActivity : VectorBaseActivity() { private fun onSignModeSelected() { when (loginViewModel.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + SignMode.SignUp -> { + // This is managed by the LoginViewEvents + } SignMode.SignIn -> { // It depends on the LoginMode withState(loginViewModel) { @@ -174,6 +176,17 @@ class LoginActivity : VectorBaseActivity() { } } + private fun onRegistrationStageNotSupported() { + AlertDialog.Builder(this) + .setTitle(R.string.app_name) + .setMessage(getString(R.string.login_registration_not_supported)) + .setPositiveButton(R.string.yes) { _, _ -> + addFragmentToBackstack(R.id.loginFragmentContainer, LoginWebFragment::class.java) + } + .setNegativeButton(R.string.no, null) + .show() + } + private fun onLoginModeNotSupported(unsupportedLoginMode: LoginMode.Unsupported) { AlertDialog.Builder(this) .setTitle(R.string.app_name) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index b7a4ced8a3..a3dc9b4d9a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -27,8 +27,6 @@ import android.view.View import android.webkit.* import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.args -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R import im.vector.riotx.core.utils.AssetReader @@ -44,14 +42,6 @@ data class LoginCaptchaFragmentArgument( val siteKey: String ) : Parcelable -@JsonClass(generateAdapter = true) -data class JavascriptResponse( - @Json(name = "action") - val action: String? = null, - @Json(name = "response") - val response: String? = null -) - /** * In this screen, the user is asked to confirm he is not a robot */ diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index d7230f0075..5b0fe743cf 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -30,10 +30,9 @@ import android.webkit.SslErrorHandler import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog -import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R +import im.vector.riotx.core.utils.AssetReader import kotlinx.android.synthetic.main.fragment_login_web.* import timber.log.Timber import java.net.URLDecoder @@ -43,7 +42,7 @@ import javax.inject.Inject * This screen is displayed for SSO login and also when the application does not support login flow or registration flow * of the homeserfver, as a fallback to login or to create an account */ -class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { +class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() { private lateinit var homeServerUrl: String private lateinit var signMode: SignMode @@ -151,33 +150,17 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { // avoid infinite onPageFinished call if (url.startsWith("http")) { // Generic method to make a bridge between JS and the UIWebView - val mxcJavascriptSendObjectMessage = "javascript:window.sendObjectMessage = function(parameters) {" + - " var iframe = document.createElement('iframe');" + - " iframe.setAttribute('src', 'js:' + JSON.stringify(parameters));" + - " document.documentElement.appendChild(iframe);" + - " iframe.parentNode.removeChild(iframe); iframe = null;" + - " };" - + val mxcJavascriptSendObjectMessage = assetReader.readAssetFile("sendObject.js") view.loadUrl(mxcJavascriptSendObjectMessage) if (signMode == SignMode.SignIn) { // The function the fallback page calls when the login is complete - val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" + - " sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" + - " };" - - view.loadUrl(mxcJavascriptOnRegistered) + val mxcJavascriptOnLogin = assetReader.readAssetFile("onLogin.js") + view.loadUrl(mxcJavascriptOnLogin) } else { // MODE_REGISTER // The function the fallback page calls when the registration is complete - val mxcJavascriptOnRegistered = "javascript:window.matrixRegistration.onRegistered" + - " = function(homeserverUrl, userId, accessToken) {" + - " sendObjectMessage({ 'action': 'onRegistered'," + - " 'homeServer': homeserverUrl," + - " 'userId': userId," + - " 'accessToken': accessToken });" + - " };" - + val mxcJavascriptOnRegistered = assetReader.readAssetFile("onRegistered.js") view.loadUrl(mxcJavascriptOnRegistered) } } @@ -209,46 +192,27 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { if (null != url && url.startsWith("js:")) { var json = url.substring(3) - var parameters: Map? = null + var parameters: JavascriptResponse? = null try { // URL decode json = URLDecoder.decode(json, "UTF-8") - - val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) - - @Suppress("UNCHECKED_CAST") - parameters = adapter.fromJson(json) as JsonDict? + val adapter = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java) + parameters = adapter.fromJson(json) } catch (e: Exception) { Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed") } // succeeds to parse parameters if (parameters != null) { - val action = parameters["action"] as String + val action = parameters.action if (signMode == SignMode.SignIn) { try { if (action == "onLogin") { - @Suppress("UNCHECKED_CAST") - val credentials = parameters["credentials"] as Map - - val userId = credentials["user_id"] - val accessToken = credentials["access_token"] - val homeServer = credentials["home_server"] - val deviceId = credentials["device_id"] - - // check if the parameters are defined - if (null != homeServer && null != userId && null != accessToken) { - val safeCredentials = Credentials( - userId = userId, - accessToken = accessToken, - homeServer = homeServer, - deviceId = deviceId, - refreshToken = null - ) - - loginViewModel.handle(LoginAction.WebLoginSuccess(safeCredentials)) + val credentials = parameters.credentials + if (credentials != null) { + loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) } } } catch (e: Exception) { @@ -258,21 +222,8 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { // MODE_REGISTER // check the required parameters if (action == "onRegistered") { - // TODO The keys are very strange, this code comes from Riot-Android... - if (parameters.containsKey("homeServer") - && parameters.containsKey("userId") - && parameters.containsKey("accessToken")) { - // We cannot parse Credentials here because of https://github.com/matrix-org/synapse/issues/4756 - // Build on object manually - val credentials = Credentials( - userId = parameters["userId"] as String, - accessToken = parameters["accessToken"] as String, - homeServer = parameters["homeServer"] as String, - // TODO We need deviceId on RiotX... - deviceId = "TODO", - refreshToken = null - ) - + val credentials = parameters.credentials + if (credentials != null) { loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt b/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt new file mode 100644 index 0000000000..5fd4505b00 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import im.vector.matrix.android.api.auth.registration.Stage + +/** + * Stage.Other is not supported, as well as any other new stage added to the SDK before it is added to the list below + */ +fun Stage.isSupported(): Boolean { + return this is Stage.ReCaptcha + || this is Stage.Dummy + || this is Stage.Msisdn + || this is Stage.Terms + || this is Stage.Email +} diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index abd44dc54c..6db6c1a2d1 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -56,6 +56,7 @@ An error occurred when loading the page: %1$s (%2$d) The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? + The application is not able to create an account on this homeserver.\n\nDo you want to signup using a web client? Reset password on %1$s From c18c140ec9a00ecaaa1a308ece7312ace971d47c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 17:25:33 +0100 Subject: [PATCH 041/189] Login screens: Animate the logo in screen transition --- .../java/im/vector/riotx/core/extensions/Activity.kt | 8 +++++++- .../im/vector/riotx/features/login/LoginActivity.kt | 10 +++++++++- .../features/login/LoginServerSelectionFragment.kt | 10 ++++++++++ .../res/layout/fragment_login_server_selection.xml | 1 + vector/src/main/res/layout/fragment_login_splash.xml | 1 + 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt index 6d7c3d39e6..f9f5d3b3d2 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt @@ -18,6 +18,7 @@ package im.vector.riotx.core.extensions import android.os.Parcelable import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentTransaction import im.vector.riotx.core.platform.VectorBaseActivity fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) { @@ -44,8 +45,13 @@ fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragment: Fragment, supportFragmentManager.commitTransaction { replace(frameId, fragment).addToBackStack(tag) } } -fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { +fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, + fragmentClass: Class, + params: Parcelable? = null, + tag: String? = null, + option: ((FragmentTransaction) -> Unit)? = null) { supportFragmentManager.commitTransaction { + option?.invoke(this) replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 95206afefe..79d6eb874d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -18,7 +18,9 @@ package im.vector.riotx.features.login import android.content.Context import android.content.Intent +import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.viewModel @@ -70,7 +72,13 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java, + option = { ft -> + val view = findViewById(R.id.loginSplashLogo) + if (view != null) { + ft.addSharedElement(view, ViewCompat.getTransitionName(view) ?: "") + } + }) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() is LoginNavigation.OnSignModeSelected -> onSignModeSelected() is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index e819389b9c..b08c46e335 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -16,8 +16,10 @@ package im.vector.riotx.features.login +import android.os.Build import android.os.Bundle import android.view.View +import androidx.transition.TransitionInflater import butterknife.OnClick import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Success @@ -35,6 +37,14 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment override fun getLayoutResId() = R.layout.fragment_login_server_selection + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index 570272e392..d380cc39c9 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -16,6 +16,7 @@ diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index f19e2e6603..c0d9a7c2c6 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -14,6 +14,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/riotx_logo" + android:transitionName="loginLogoTransition" app:layout_constraintBottom_toTopOf="@+id/loginSplashTitle" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" From 6d8e5b892e56d4c96da251e3ca323cdb1d25af08 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 17:28:39 +0100 Subject: [PATCH 042/189] Login screens: Show disclaimer dialog only in HomeActivity, now that RiotX supports registration --- .../java/im/vector/riotx/features/login/LoginActivity.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 79d6eb874d..3c7148b802 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -32,7 +32,6 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.VectorBaseActivity -import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument @@ -242,12 +241,6 @@ class LoginActivity : VectorBaseActivity() { } } - override fun onResume() { - super.onResume() - - showDisclaimerDialog(this) - } - companion object { private const val EXTRA_CONFIG = "EXTRA_CONFIG" From 375833482445f494a39ac41d5eabb964daecedaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 18:09:05 +0100 Subject: [PATCH 043/189] Login screens: cleanup the Fragment stack after completing stage --- .../riotx/features/login/LoginActivity.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 3c7148b802..a0b718f134 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -222,26 +222,34 @@ class LoginActivity : VectorBaseActivity() { } } - // TODO Unstack fragment when stage is complete private fun doStage(stage: Stage) { + // Ensure there is no fragment for registration stage in the backstack + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + when (stage) { is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, - LoginCaptchaFragment::class.java, LoginCaptchaFragmentArgument(stage.publicKey)) + LoginCaptchaFragment::class.java, + LoginCaptchaFragmentArgument(stage.publicKey), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory)) - is Stage.Msisdn - -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) + is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory)) + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginTermsFragment::class.java, - LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language)))) - else -> TODO() + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) + else -> Unit // Should not happen } } companion object { + private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" + private const val EXTRA_CONFIG = "EXTRA_CONFIG" fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { From 8ae9544b48a792a13412240186d1589b66aab305 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 18:23:33 +0100 Subject: [PATCH 044/189] Login screens: Loading on Captcha step --- .../features/login/LoginCaptchaFragment.kt | 12 +++- .../res/layout/fragment_login_captcha.xml | 62 ++++++++++--------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index a3dc9b4d9a..7728d4eaac 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.login import android.annotation.SuppressLint import android.content.DialogInterface +import android.graphics.Bitmap import android.net.http.SslError import android.os.Build import android.os.Bundle @@ -26,6 +27,7 @@ import android.view.KeyEvent import android.view.View import android.webkit.* import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import com.airbnb.mvrx.args import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R @@ -72,10 +74,18 @@ class LoginCaptchaFragment @Inject constructor(private val assetReader: AssetRea loginCaptchaWevView.requestLayout() loginCaptchaWevView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + // Show loader + loginCaptchaProgress.isVisible = true + } + override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) - // TODO Hide loader + // Hide loader + loginCaptchaProgress.isVisible = false } override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml index 8dec490a14..2dfc37871a 100644 --- a/vector/src/main/res/layout/fragment_login_captcha.xml +++ b/vector/src/main/res/layout/fragment_login_captcha.xml @@ -2,41 +2,47 @@ + android:layout_height="match_parent" + android:paddingStart="16dp" + android:paddingTop="32dp" + android:paddingEnd="16dp" + android:paddingBottom="16dp"> - + app:layout_constraintTop_toTopOf="parent" /> - + - + - - + From a8f24e5c39b7a6388295ae3bf2a676412c4c76b7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 18:44:11 +0100 Subject: [PATCH 045/189] Login screens: a11y --- vector/src/main/res/layout/fragment_loading.xml | 3 ++- vector/src/main/res/layout/fragment_login.xml | 3 ++- vector/src/main/res/layout/fragment_login_captcha.xml | 4 +++- .../res/layout/fragment_login_generic_text_input_form.xml | 3 ++- .../src/main/res/layout/fragment_login_reset_password.xml | 3 ++- .../res/layout/fragment_login_reset_password_success.xml | 3 ++- .../src/main/res/layout/fragment_login_server_selection.xml | 6 ++++++ .../src/main/res/layout/fragment_login_server_url_form.xml | 4 +++- .../res/layout/fragment_login_signup_signin_selection.xml | 1 + vector/src/main/res/layout/fragment_login_splash.xml | 3 +++ vector/src/main/res/layout/fragment_login_terms.xml | 1 + vector/src/main/res/layout/item_policy.xml | 5 +---- vector/src/main/res/values/strings_riotX.xml | 5 +++++ 13 files changed, 33 insertions(+), 11 deletions(-) diff --git a/vector/src/main/res/layout/fragment_loading.xml b/vector/src/main/res/layout/fragment_loading.xml index 96bafda319..ae605097cd 100644 --- a/vector/src/main/res/layout/fragment_loading.xml +++ b/vector/src/main/res/layout/fragment_loading.xml @@ -4,12 +4,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no" /> @@ -26,15 +27,16 @@ android:text="@string/auth_recaptcha_message" app:layout_constraintTop_toBottomOf="@+id/logoImageView" /> + - + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no" /> + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no" /> + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no" /> + android:layout_marginBottom="130dp" + android:importantForAccessibility="no" /> diff --git a/vector/src/main/res/layout/item_policy.xml b/vector/src/main/res/layout/item_policy.xml index f28beef73d..ae68e33598 100644 --- a/vector/src/main/res/layout/item_policy.xml +++ b/vector/src/main/res/layout/item_policy.xml @@ -10,8 +10,6 @@ android:id="@+id/adapter_item_policy_checkbox" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -37,8 +35,7 @@ android:id="@+id/adapter_item_policy_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="16dp" - android:layout_marginRight="16dp" + android:importantForAccessibility="no" android:rotationY="@integer/rtl_mirror_flip" android:src="@drawable/ic_material_chevron_right_black" app:layout_constraintBottom_toBottomOf="parent" diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 6db6c1a2d1..bf8f22e59a 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -96,4 +96,9 @@ Next That username is taken + Select matrix.org + Select modular + Select a custom homeserver + Please perform the captcha challenge + From 20f969d563f66ac43fef5645fa945adc20f71d78 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 19:16:34 +0100 Subject: [PATCH 046/189] Login screens: fix issue on terms --- .../login/terms/LoginTermsFragment.kt | 19 +++++++----- .../features/login/terms/PolicyController.kt | 7 +++-- .../terms/{PolicyModel.kt => PolicyItem.kt} | 7 ++++- .../src/main/res/color/login_button_tint.xml | 7 +++++ .../main/res/layout/fragment_login_terms.xml | 13 ++++++-- vector/src/main/res/layout/item_policy.xml | 31 ++++++++++++++----- vector/src/main/res/values/strings_riotX.xml | 1 + 7 files changed, 65 insertions(+), 20 deletions(-) rename vector/src/main/java/im/vector/riotx/features/login/terms/{PolicyModel.kt => PolicyItem.kt} (89%) create mode 100644 vector/src/main/res/color/login_button_tint.xml diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt index 3e202b5245..4b98563503 100755 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -51,6 +51,7 @@ class LoginTermsFragment @Inject constructor(private val policyController: Polic super.onViewCreated(view, savedInstanceState) loginTermsPolicyList.setController(policyController) + policyController.homeServer = loginViewModel.getHomeServerUrlSimple() policyController.listener = this val list = ArrayList() @@ -83,14 +84,18 @@ class LoginTermsFragment @Inject constructor(private val policyController: Polic } override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) { - openUrlInExternalBrowser(requireContext(), localizedFlowDataLoginTerms.localizedUrl!!) + localizedFlowDataLoginTerms.localizedUrl + ?.takeIf { it.isNotBlank() } + ?.let { + openUrlInExternalBrowser(requireContext(), it) - // This code crashed, because user is not authenticated yet - //val intent = VectorWebViewActivity.getIntent(requireContext(), - // localizedFlowDataLoginTerms.localizedUrl!!, - // localizedFlowDataLoginTerms.localizedName!!, - // WebViewMode.DEFAULT) - //startActivity(intent) + // This code crashed, because user is not authenticated yet + //val intent = VectorWebViewActivity.getIntent(requireContext(), + // localizedFlowDataLoginTerms.localizedUrl!!, + // localizedFlowDataLoginTerms.localizedName!!, + // WebViewMode.DEFAULT) + //startActivity(intent) + } } @OnClick(R.id.loginTermsSubmit) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt index 6e86d40e3d..c301463c2a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt @@ -25,12 +25,15 @@ class PolicyController @Inject constructor() : TypedEpoxyController) { data.forEach { entry -> - policy { + policyItem { id(entry.localizedFlowDataLoginTerms.policyName) checked(entry.checked) - title(entry.localizedFlowDataLoginTerms.localizedName!!) + title(entry.localizedFlowDataLoginTerms.localizedName) + subtitle(homeServer) clickListener(View.OnClickListener { listener?.openPolicy(entry.localizedFlowDataLoginTerms) }) checkChangeListener { _, isChecked -> diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt similarity index 89% rename from vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt rename to vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt index 85b7c80dd0..9931d33068 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt @@ -27,13 +27,16 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder @EpoxyModelClass(layout = R.layout.item_policy) -abstract class PolicyModel : EpoxyModelWithHolder() { +abstract class PolicyItem : EpoxyModelWithHolder() { @EpoxyAttribute var checked: Boolean = false @EpoxyAttribute var title: String? = null + @EpoxyAttribute + var subtitle: String? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null @@ -45,6 +48,7 @@ abstract class PolicyModel : EpoxyModelWithHolder() { it.checkbox.isChecked = checked it.checkbox.setOnCheckedChangeListener(checkChangeListener) it.title.text = title + it.subtitle.text = subtitle it.view.setOnClickListener(clickListener) } } @@ -58,5 +62,6 @@ abstract class PolicyModel : EpoxyModelWithHolder() { class Holder : VectorEpoxyHolder() { val checkbox by bind(R.id.adapter_item_policy_checkbox) val title by bind(R.id.adapter_item_policy_title) + val subtitle by bind(R.id.adapter_item_policy_subtitle) } } diff --git a/vector/src/main/res/color/login_button_tint.xml b/vector/src/main/res/color/login_button_tint.xml new file mode 100644 index 0000000000..719335766c --- /dev/null +++ b/vector/src/main/res/color/login_button_tint.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml index c595587a5d..e44139e18b 100644 --- a/vector/src/main/res/layout/fragment_login_terms.xml +++ b/vector/src/main/res/layout/fragment_login_terms.xml @@ -14,14 +14,23 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintTop_toBottomOf="@+id/loginTermsTitle" /> + android:foreground="?attr/selectableItemBackground" + android:minHeight="72dp"> - + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index bf8f22e59a..8a013a255b 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -100,5 +100,6 @@ Select modular Select a custom homeserver Please perform the captcha challenge + Accept terms to continue From 7caa8ce3bc4c34d887979f2281cd0dde75b83ed0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 10:18:14 +0100 Subject: [PATCH 047/189] Login screens: disabled registration --- .../vector/riotx/core/error/ErrorFormatter.kt | 24 ++++++++++++----- .../riotx/features/login/LoginActivity.kt | 27 +++++++++++++++++++ .../riotx/features/login/LoginViewEvents.kt | 1 + .../riotx/features/login/LoginViewModel.kt | 12 +++++++++ vector/src/main/res/values/strings_riotX.xml | 2 ++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 29506cf880..c943a86fbc 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -35,13 +35,14 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi return when (throwable) { null -> null is Failure.NetworkConnection -> { - if (throwable.ioException is SocketTimeoutException) { - stringProvider.getString(R.string.error_network_timeout) - } else if (throwable.ioException is UnknownHostException) { - // Invalid homeserver? - stringProvider.getString(R.string.login_error_unknown_host) - } else { - stringProvider.getString(R.string.error_no_network) + when { + throwable.ioException is SocketTimeoutException -> + stringProvider.getString(R.string.error_network_timeout) + throwable.ioException is UnknownHostException -> + // Invalid homeserver? + stringProvider.getString(R.string.login_error_unknown_host) + else -> + stringProvider.getString(R.string.error_no_network) } } is Failure.ServerError -> { @@ -57,6 +58,15 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi throwable.error.code == MatrixError.USER_IN_USE -> { stringProvider.getString(R.string.login_signup_error_user_in_use) } + throwable.error.code == MatrixError.BAD_JSON -> { + stringProvider.getString(R.string.login_error_bad_json) + } + throwable.error.code == MatrixError.NOT_JSON -> { + stringProvider.getString(R.string.login_error_not_json) + } + throwable.error.code == MatrixError.LIMIT_EXCEEDED -> { + stringProvider.getString(R.string.login_error_limit_exceeded) + } else -> { throwable.error.message.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index a0b718f134..e7cb90f5df 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -27,8 +27,11 @@ import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.VectorBaseActivity @@ -38,6 +41,7 @@ import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument import im.vector.riotx.features.login.terms.toLocalizedLoginTerms import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection /** * The LoginActivity manages the fragment navigation and also display the loading View @@ -48,6 +52,7 @@ class LoginActivity : VectorBaseActivity() { private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -124,9 +129,31 @@ class LoginActivity : VectorBaseActivity() { } } } + is LoginViewEvents.RegistrationError -> displayRegistrationError(loginViewEvents.throwable) } } + private fun displayRegistrationError(throwable: Throwable) { + val message = when(throwable) { + is Failure.ServerError -> { + if(throwable.error.code == MatrixError.FORBIDDEN + && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { + getString(R.string.login_registration_disabled) + } else { + null + } + } + else -> null + } + ?: errorFormatter.toHumanReadable(throwable) + + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun onLoginFlowRetrieved() { addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt index b8b7965c77..b7351feead 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt @@ -24,4 +24,5 @@ import im.vector.matrix.android.api.auth.registration.FlowResult */ sealed class LoginViewEvents { data class RegistrationFlowResult(val flowResult: FlowResult) : LoginViewEvents() + data class RegistrationError(val throwable: Throwable) : LoginViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 6cc73233a7..49676b4a35 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -489,7 +489,16 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi registrationWizard = registrationService.getOrCreateRegistrationWizard(homeServerConnectionConfigFinal) currentTask = registrationWizard?.getRegistrationFlow(object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + /* + // Simulate registration disabled + onFailure(Failure.ServerError(MatrixError( + code = MatrixError.FORBIDDEN, + message = "Registration is disabled" + ), 403)) + */ + setState { copy( asyncRegistration = Success(data) @@ -503,6 +512,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } override fun onFailure(failure: Throwable) { + // Notify the user + _viewEvents.post(LoginViewEvents.RegistrationError(failure)) + // TODO Handled JobCancellationException setState { copy( diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8a013a255b..958ebec6cc 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -46,6 +46,7 @@ Sign in to %1$s Sign Up Sign In + Sign In with SSO Modular Address Address @@ -56,6 +57,7 @@ An error occurred when loading the page: %1$s (%2$d) The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? + Sorry, the homeserver does not accept new account creation. The application is not able to create an account on this homeserver.\n\nDo you want to signup using a web client? From f12e6c941d9d8f5b3d30d080400b6ad1fe514154 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 10:38:27 +0100 Subject: [PATCH 048/189] Login screens: sigin button for SSO --- .../LoginSignUpSignInSelectionFragment.kt | 36 +++++++++++-------- ...fragment_login_signup_signin_selection.xml | 12 ++++--- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 4425632fd1..bcbef9e807 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -20,7 +20,6 @@ import android.os.Bundle import android.view.View import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.withState import im.vector.riotx.R import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* @@ -33,10 +32,15 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection + private var isSsoSignIn: Boolean = false + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + isSsoSignIn = withState(loginViewModel) { it.asyncHomeServerLoginFlowRequest.invoke() } == LoginMode.Sso + setupUi() + setupButtons() } private fun setupUi() { @@ -62,9 +66,24 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr } } - @OnClick(R.id.loginSignupSigninSignUp) + private fun setupButtons() { + if (isSsoSignIn) { + loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) + loginSignupSigninSignIn.isVisible = false + } else { + loginSignupSigninSubmit.text = getString(R.string.login_signup) + loginSignupSigninSignIn.isVisible = true + } + } + + + @OnClick(R.id.loginSignupSigninSubmit) fun signUp() { - loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + if (isSsoSignIn) { + signIn() + } else { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + } } @OnClick(R.id.loginSignupSigninSignIn) @@ -76,15 +95,4 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetSignMode) } - - override fun invalidate() = withState(loginViewModel) { - when (it.asyncRegistration) { - is Fail -> { - // TODO Registration disabled, (move to Activity?) - when (it.asyncRegistration.error) { - - } - } - } - } } diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index e58a986b14..de9a7e403c 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -56,17 +56,19 @@ app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninTitle" tools:text="@string/login_server_matrix_org_text" /> + + app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninText" + tools:text="@string/login_signup" /> + + app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninSubmit" + tools:visibility="visible" />
From 62d5aba796f3fa324feb33907e00623d898fde9e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 11:12:08 +0100 Subject: [PATCH 049/189] Login screens: back button management for SSO --- .../vector/riotx/core/platform/OnBackPressed.kt | 3 ++- .../riotx/core/platform/VectorBaseActivity.kt | 16 ++++++++++------ .../features/home/room/list/RoomListFragment.kt | 2 +- .../features/login/AbstractLoginFragment.kt | 2 +- .../vector/riotx/features/login/LoginActivity.kt | 14 ++++++++++---- .../riotx/features/login/LoginWebFragment.kt | 13 ++++++------- 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt b/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt index 17f7730f86..c8a58997a1 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt @@ -21,6 +21,7 @@ interface OnBackPressed { /** * Returns true, if the on back pressed event has been handled by this Fragment. * Otherwise return false + * @param toolbarButton true if this is the back button from the toolbar */ - fun onBackPressed(): Boolean + fun onBackPressed(toolbarButton: Boolean): Boolean } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 4a3056657f..79b040cd41 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -278,7 +278,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressed(true) return true } @@ -286,20 +286,24 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } override fun onBackPressed() { - val handled = recursivelyDispatchOnBackPressed(supportFragmentManager) + onBackPressed(false) + } + + private fun onBackPressed(fromToolbar: Boolean) { + val handled = recursivelyDispatchOnBackPressed(supportFragmentManager, fromToolbar) if (!handled) { super.onBackPressed() } } - private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { - val reverseOrder = fm.fragments.filter { it is VectorBaseFragment }.reversed() + private fun recursivelyDispatchOnBackPressed(fm: FragmentManager, fromToolbar: Boolean): Boolean { + val reverseOrder = fm.fragments.filterIsInstance().reversed() for (f in reverseOrder) { - val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) + val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager, fromToolbar) if (handledByChildFragments) { return true } - if (f is OnBackPressed && f.onBackPressed()) { + if (f is OnBackPressed && f.onBackPressed(fromToolbar)) { return true } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index a5e9a7b4bf..04d1802264 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -329,7 +329,7 @@ class RoomListFragment @Inject constructor( stateView.state = StateView.State.Error(message) } - override fun onBackPressed(): Boolean { + override fun onBackPressed(toolbarButton: Boolean): Boolean { if (createChatFabMenu.onBackPressed()) { return true } diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 9e57350b79..1d8ad9b05f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -38,7 +38,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) } - override fun onBackPressed(): Boolean { + override fun onBackPressed(toolbarButton: Boolean): Boolean { resetViewModel() // Do not consume the Back event return false diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index e7cb90f5df..f273316c69 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.Intent import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager @@ -34,6 +35,7 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.login.terms.LoginTermsFragment @@ -46,7 +48,7 @@ import javax.net.ssl.HttpsURLConnection /** * The LoginActivity manages the fragment navigation and also display the loading View */ -class LoginActivity : VectorBaseActivity() { +class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private val loginViewModel: LoginViewModel by viewModel() private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @@ -134,16 +136,16 @@ class LoginActivity : VectorBaseActivity() { } private fun displayRegistrationError(throwable: Throwable) { - val message = when(throwable) { + val message = when (throwable) { is Failure.ServerError -> { - if(throwable.error.code == MatrixError.FORBIDDEN + if (throwable.error.code == MatrixError.FORBIDDEN && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { getString(R.string.login_registration_disabled) } else { null } } - else -> null + else -> null } ?: errorFormatter.toHumanReadable(throwable) @@ -274,6 +276,10 @@ class LoginActivity : VectorBaseActivity() { } } + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } + companion object { private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index 5b0fe743cf..4416a9261f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -40,7 +40,7 @@ import javax.inject.Inject /** * This screen is displayed for SSO login and also when the application does not support login flow or registration flow - * of the homeserfver, as a fallback to login or to create an account + * of the homeserver, as a fallback to login or to create an account */ class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() { @@ -241,12 +241,11 @@ class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) // Nothing to do } - override fun onBackPressed(): Boolean { - return if (loginWebWebView.canGoBack()) { - loginWebWebView.goBack() - true - } else { - super.onBackPressed() + override fun onBackPressed(toolbarButton: Boolean): Boolean { + return when { + toolbarButton -> super.onBackPressed(toolbarButton) + loginWebWebView.canGoBack() -> loginWebWebView.goBack().run { true } + else -> super.onBackPressed(toolbarButton) } } } From 0e2237226fa914569d82d583ebf023c010848779 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 11:45:13 +0100 Subject: [PATCH 050/189] Login screens: back button management for registration --- .../features/login/AbstractLoginFragment.kt | 23 ++++++++++++++++--- .../riotx/features/login/LoginActivity.kt | 8 +++++-- .../features/login/LoginCaptchaFragment.kt | 4 +--- .../LoginGenericTextInputFormFragment.kt | 2 +- .../riotx/features/login/LoginWebFragment.kt | 2 +- .../login/terms/LoginTermsFragment.kt | 2 +- vector/src/main/res/values/strings_riotX.xml | 4 ++++ 7 files changed, 34 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 1d8ad9b05f..0752d50e79 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -16,10 +16,12 @@ package im.vector.riotx.features.login +import android.app.AlertDialog import android.os.Bundle import android.view.View import androidx.annotation.CallSuper import com.airbnb.mvrx.activityViewModel +import im.vector.riotx.R import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment @@ -39,9 +41,24 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { } override fun onBackPressed(toolbarButton: Boolean): Boolean { - resetViewModel() - // Do not consume the Back event - return false + if (loginViewModel.isPasswordSent) { + // Ask for confirmation before cancelling the registration + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.login_signup_cancel_confirmation_title) + .setMessage(R.string.login_signup_cancel_confirmation_content) + .setPositiveButton(R.string.login_signup_cancel_confirmation_yes) { _, _ -> + resetViewModel() + vectorBaseActivity.onBackPressed() + } + .setNegativeButton(R.string.login_signup_cancel_confirmation_no, null) + .show() + + return true + } else { + resetViewModel() + // Do not consume the Back event + return false + } } // Reset any modification on the loginViewModel by the current fragment diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index f273316c69..09a5777108 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -127,7 +127,11 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { handleRegistrationNavigation(loginViewEvents.flowResult) } else { // First ask for login and password - addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + // I add a tag to indicate that this fragment is a registration stage. + // This way it will be automatically popped in when starting the next registration stage + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_REGISTRATION_STAGE_TAG) } } } @@ -235,7 +239,7 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { } private fun handleRegistrationNavigation(flowResult: FlowResult) { - // Complete all mandatory stage first + // Complete all mandatory stages first val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } if (mandatoryStage != null) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index 7728d4eaac..3795c9ddb1 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -172,11 +172,9 @@ class LoginCaptchaFragment @Inject constructor(private val assetReader: AssetRea return true } } - } - override fun resetViewModel() { - // Nothing to do + loginViewModel.handle(LoginAction.ResetLogin) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index a2b5feb1b1..70af1af484 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -129,6 +129,6 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra } override fun resetViewModel() { - // Nothing to do + loginViewModel.handle(LoginAction.ResetLogin) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index 4416a9261f..2aaf734ca6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -238,7 +238,7 @@ class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) } override fun resetViewModel() { - // Nothing to do + loginViewModel.handle(LoginAction.ResetLogin) } override fun onBackPressed(toolbarButton: Boolean): Boolean { diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt index 4b98563503..6c8f86c1e2 100755 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -104,6 +104,6 @@ class LoginTermsFragment @Inject constructor(private val policyController: Polic } override fun resetViewModel() { - // No op + loginViewModel.handle(LoginAction.ResetLogin) } } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 958ebec6cc..ff74833104 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -97,6 +97,10 @@ Password Next That username is taken + Warning + Your account is not created yet.\n\nStop the registration process? + Yes + No Select matrix.org Select modular From f74cabd1455974731cd552c838895dc124a4f90c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 12:55:20 +0100 Subject: [PATCH 051/189] Login screens: UI: style to prepare for landscape --- vector/src/main/res/layout/fragment_login.xml | 206 +++++------ .../res/layout/fragment_login_captcha.xml | 68 ++-- ...fragment_login_generic_text_input_form.xml | 133 +++---- .../layout/fragment_login_reset_password.xml | 159 ++++---- .../fragment_login_reset_password_success.xml | 78 ++-- .../fragment_login_server_selection.xml | 342 +++++++++--------- .../layout/fragment_login_server_url_form.xml | 151 ++++---- ...fragment_login_signup_signin_selection.xml | 138 ++++--- .../main/res/layout/fragment_login_terms.xml | 75 ++-- .../src/main/res/values-land/styles_login.xml | 19 + vector/src/main/res/values/styles_login.xml | 31 +- 11 files changed, 665 insertions(+), 735 deletions(-) create mode 100644 vector/src/main/res/values-land/styles_login.xml diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 28eeee1549..977c694952 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -6,143 +6,123 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + + + + + + + + + android:layout_marginTop="32dp" + android:hint="@string/login_signup_username_hint" + app:errorEnabled="true"> - - - - - + android:inputType="textEmailAddress" + android:maxLines="1" /> - + - + + android:hint="@string/login_signup_password_hint" + app:errorEnabled="true" + app:errorIconDrawable="@null"> + android:ems="10" + android:inputType="textPassword" + android:maxLines="1" + android:paddingEnd="48dp" + android:paddingRight="48dp" + tools:ignore="RtlSymmetry" /> - + + + + + + + android:layout_gravity="start" + android:text="@string/auth_forgot_password" /> - - - - - - - - - - - + android:layout_alignParentEnd="true" + android:layout_gravity="end" + android:text="@string/auth_login" + tools:enabled="false" + tools:ignore="RelativeOverlap" /> - + - - - - - - - +
diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml index 762b6af4c0..31e1e882b1 100644 --- a/vector/src/main/res/layout/fragment_login_captcha.xml +++ b/vector/src/main/res/layout/fragment_login_captcha.xml @@ -2,49 +2,47 @@ + android:layout_height="match_parent"> - + - - - - + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="16dp"> + + + + + + +
+ app:layout_constraintBottom_toBottomOf="@id/loginFormContainer" + app:layout_constraintEnd_toEndOf="@id/loginFormContainer" + app:layout_constraintStart_toStartOf="@id/loginFormContainer" + app:layout_constraintTop_toTopOf="@id/loginFormContainer" /> diff --git a/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml index c61babea95..20e050fe9e 100644 --- a/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml +++ b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml @@ -6,94 +6,77 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + + + + + + + android:layout_marginTop="26dp" + app:errorEnabled="true" + tools:hint="@string/login_set_email_optional_hint"> - - - - - - - + android:imeOptions="actionDone" + android:maxLines="1" + tools:inputType="textEmailAddress" /> - + - + - + android:layout_gravity="start" + android:visibility="gone" + tools:text="@string/login_msisdn_confirm_send_again" + tools:visibility="visible" /> - + - + - - - - - +
diff --git a/vector/src/main/res/layout/fragment_login_reset_password.xml b/vector/src/main/res/layout/fragment_login_reset_password.xml index 7f09f0444b..b1dd495417 100644 --- a/vector/src/main/res/layout/fragment_login_reset_password.xml +++ b/vector/src/main/res/layout/fragment_login_reset_password.xml @@ -5,115 +5,98 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + + + + + + + android:layout_marginTop="32dp" + android:hint="@string/login_reset_password_email_hint" + app:errorEnabled="true"> - - - + android:inputType="textEmailAddress" + android:maxLines="1" /> - + + + + android:hint="@string/login_reset_password_password_hint" + app:errorEnabled="true" + app:errorIconDrawable="@null"> + android:ems="10" + android:inputType="textPassword" + android:maxLines="1" + android:paddingEnd="48dp" + android:paddingRight="48dp" + tools:ignore="RtlSymmetry" /> - - - - - - - - - - - - - + android:layout_marginTop="8dp" + android:background="?attr/selectableItemBackground" + android:scaleType="center" + android:src="@drawable/ic_eye_black" + android:tint="?attr/colorAccent" + tools:contentDescription="@string/a11y_show_password" /> - + - + + + diff --git a/vector/src/main/res/layout/fragment_login_reset_password_success.xml b/vector/src/main/res/layout/fragment_login_reset_password_success.xml index aaa54937a2..bffb9c11ef 100644 --- a/vector/src/main/res/layout/fragment_login_reset_password_success.xml +++ b/vector/src/main/res/layout/fragment_login_reset_password_success.xml @@ -1,63 +1,45 @@ - + - + + + android:text="@string/login_reset_password_success_title" + android:textAppearance="@style/TextAppearance.Vector.Login.Title" /> - + - + - + - - - - - - - + diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index a5b2c32084..d120aa3a75 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -1,200 +1,188 @@ - - + - + + + + + + + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:background="@drawable/bg_login_server_selector" + android:contentDescription="@string/login_a11y_choose_matrix_org" + android:minHeight="80dp" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/loginServerText"> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceMatrixOrgIcon" /> - + - + - + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml index a6bf98bbe4..a0476673e3 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml @@ -6,99 +6,82 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + + + + + + + + + + + android:layout_marginTop="26dp" + app:errorEnabled="true" + tools:hint="@string/login_server_url_form_modular_hint"> - - - - - - - - - - - + android:imeOptions="actionDone" + android:inputType="textUri" + android:maxLines="1" /> - + - + - - - - - - + + diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index de9a7e403c..32bc445884 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -1,91 +1,79 @@ - - + - + - + - + - + - + + - - + + - - + - - - - + diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml index e44139e18b..ecad15079c 100644 --- a/vector/src/main/res/layout/fragment_login_terms.xml +++ b/vector/src/main/res/layout/fragment_login_terms.xml @@ -2,52 +2,49 @@ - + - + - + - + - + + + + diff --git a/vector/src/main/res/values-land/styles_login.xml b/vector/src/main/res/values-land/styles_login.xml new file mode 100644 index 0000000000..29ddebedd2 --- /dev/null +++ b/vector/src/main/res/values-land/styles_login.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml index 9e74e8abec..989b1615d9 100644 --- a/vector/src/main/res/values/styles_login.xml +++ b/vector/src/main/res/values/styles_login.xml @@ -8,10 +8,39 @@ 36dp - + + + + + + + - + + - From 11bc7051fd45b5a5bbd01650d4d4287195e181a1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Nov 2019 11:29:12 +0100 Subject: [PATCH 093/189] Login screens: splash scrollable --- .../main/res/layout/fragment_login_splash.xml | 223 +++++++++--------- 1 file changed, 116 insertions(+), 107 deletions(-) diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index c237cb8aba..90f29764ea 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -1,125 +1,134 @@ - - + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + - + - - + - + + - + - + - + - + - + - + - + + + + From af45c554fde3f69fcc815d796e5d76e8373d4bf7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Nov 2019 11:41:01 +0100 Subject: [PATCH 094/189] Login screens: fix scroll issue --- vector/src/main/res/layout/fragment_login_terms.xml | 2 -- vector/src/main/res/values/styles_login.xml | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml index 10bf9683b4..10830daaea 100644 --- a/vector/src/main/res/layout/fragment_login_terms.xml +++ b/vector/src/main/res/layout/fragment_login_terms.xml @@ -14,8 +14,6 @@ @id/loginFormContainer - 32dp - 32dp 36dp 36dp match_parent @@ -43,6 +41,9 @@ parent 0dp wrap_content + false + 32dp + 32dp + + \ No newline at end of file diff --git a/vector/src/main/res/values/integers.xml b/vector/src/main/res/values/integers.xml index 59c1327f30..75e8bb6f9a 100644 --- a/vector/src/main/res/values/integers.xml +++ b/vector/src/main/res/values/integers.xml @@ -1,7 +1,11 @@ - 500 + 200 + + 400 + + 200 0 diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml index c753e13aff..3bcda048dc 100644 --- a/vector/src/main/res/values/styles_login.xml +++ b/vector/src/main/res/values/styles_login.xml @@ -22,7 +22,9 @@ parent - From 6bd7257cf24c24da3443a69e3ed5aa3dc8dfb6dd Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 15 Nov 2019 00:14:28 +0100 Subject: [PATCH 137/189] Send mention pills from composer --- .../room/model/relation/RelationService.kt | 6 +- .../api/session/room/send/SendService.kt | 6 +- .../api/session/room/send/UserMentionSpan.kt | 25 ++++++++ .../room/relation/DefaultRelationService.kt | 4 +- .../session/room/send/DefaultSendService.kt | 6 +- .../room/send/LocalEchoEventFactory.kt | 60 ++++++++++++++----- .../riotx/features/command/CommandParser.kt | 4 +- .../riotx/features/command/ParsedCommand.kt | 2 +- .../home/room/detail/RoomDetailAction.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 41 +++++++++++-- .../home/room/detail/RoomDetailViewModel.kt | 12 +++- .../room/detail/composer/TextComposerView.kt | 7 ++- .../riotx/features/html/PillImageSpan.kt | 8 ++- 13 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 5af5183dfa..385699b4db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -72,7 +72,7 @@ interface RelationService { */ fun editTextMessage(targetEventId: String, msgType: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable @@ -97,12 +97,14 @@ interface RelationService { /** * Reply to an event in the timeline (must be in same room) * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 + * The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated + * by the sdk into pills. * @param eventReplied the event referenced by the reply * @param replyText the reply text * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present */ fun replyToMessage(eventReplied: TimelineEvent, - replyText: String, + replyText: CharSequence, autoMarkdown: Boolean = false): Cancelable? fun getEventSummaryLive(eventId: String): LiveData> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 8c783837a2..e45069bcff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -29,12 +29,14 @@ interface SendService { /** * Method to send a text message asynchronously. + * The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated + * by the sdk into pills. * @param text the text message to send * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @return a [Cancelable] */ - fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable + fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable /** * Method to send a text message with a formatted body. @@ -42,7 +44,7 @@ interface SendService { * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @return a [Cancelable] */ - fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable + fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable /** * Method to send a media asynchronously. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt new file mode 100644 index 0000000000..0899e4f27e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.send + +/** + * Tag class for spans that should mention a user. + * These Spans will be transformed into pills when detected in message to send + */ +interface UserMentionSpan { + abstract val displayName: String + abstract val userId: String +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 11be821d7e..db3b6100a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv override fun editTextMessage(targetEventId: String, msgType: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { val event = eventFactory @@ -164,7 +164,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv .executeBy(taskExecutor) } - override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? { + override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? { val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown) ?.also { saveLocalEcho(it) } ?: return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 7c720e56a7..8fad03b588 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -68,7 +68,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() - override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { + override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { saveLocalEcho(it) } @@ -76,8 +76,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private return sendEvent(event) } - override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { - val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also { + override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { + val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { saveLocalEcho(it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 3fa0dcdca1..4b099a25be 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever +import android.text.SpannableString import androidx.exifinterface.media.ExifInterface import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R @@ -28,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent +import im.vector.matrix.android.api.session.room.send.UserMentionSpan import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.internal.database.helper.addSendingEvent @@ -58,37 +60,67 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use // TODO Inject private val renderer = HtmlRenderer.builder().build() - fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { - if (msgType == MessageType.MSGTYPE_TEXT) { - return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown)) + fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { + if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { + return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType) } - val content = MessageTextContent(type = msgType, body = text) + val content = MessageTextContent(type = msgType, body = text.toString()) return createEvent(roomId, content) } - private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent { + private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { if (autoMarkdown) { - val document = parser.parse(text) + val source = transformPills(text,"[%2\$s](https://matrix.to/#/%1\$s)") ?: text.toString() + val document = parser.parse(source) val htmlText = renderer.render(document) - if (isFormattedTextPertinent(text, htmlText)) { - return TextContent(text, htmlText) + if (isFormattedTextPertinent(source, htmlText)) { + return TextContent(source, htmlText) + } + } else { + //Try to detect pills + transformPills(text, "%2\$s")?.let { + return TextContent(text.toString(),it) } } - return TextContent(text) + return TextContent(text.toString()) + } + + private fun transformPills(text: CharSequence, + template : String) + : String? { + val bufSB = StringBuffer() + var currIndex = 0 + SpannableString.valueOf(text).let { + val pills = it.getSpans(0, text.length, UserMentionSpan::class.java) + if (pills.isNotEmpty()) { + pills.forEachIndexed { _, urlSpan -> + val start = it.getSpanStart(urlSpan) + val end = it.getSpanEnd(urlSpan) + //We want to replace with the pill with a html link + bufSB.append(text, currIndex, start) + bufSB.append(String.format(template,urlSpan.userId,urlSpan.displayName)) + currIndex = end + } + bufSB.append(text, currIndex, text.length) + return bufSB.toString() + } else { + return null + } + } } private fun isFormattedTextPertinent(text: String, htmlText: String?) = text != htmlText && htmlText != "

${text.trim()}

\n" - fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event { - return createEvent(roomId, textContent.toMessageTextContent()) + fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { + return createEvent(roomId, textContent.toMessageTextContent(msgType)) } fun createReplaceTextEvent(roomId: String, targetEventId: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, msgType: String, compatibilityText: String): Event { @@ -279,7 +311,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use return System.currentTimeMillis() } - fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Event? { + fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Event? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null @@ -298,7 +330,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use // // > <@alice:example.org> This is the original body // - val replyFallback = buildReplyFallback(body, userId, replyText) + val replyFallback = buildReplyFallback(body, userId, replyText.toString()) val eventId = eventReplied.root.eventId ?: return null val content = MessageTextContent( diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index 3f5808949b..bc451f8e84 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -27,7 +27,7 @@ object CommandParser { * @param textMessage the text message * @return a parsed slash command (ok or error) */ - fun parseSplashCommand(textMessage: String): ParsedCommand { + fun parseSplashCommand(textMessage: CharSequence): ParsedCommand { // check if it has the Slash marker if (!textMessage.startsWith("/")) { return ParsedCommand.ErrorNotACommand @@ -76,7 +76,7 @@ object CommandParser { } } Command.EMOTE.command -> { - val message = textMessage.substring(Command.EMOTE.command.length).trim() + val message = textMessage.subSequence(Command.EMOTE.command.length, textMessage.length).trim() ParsedCommand.SendEmote(message) } diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt index 02f5abe540..89438c8a9d 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt @@ -33,7 +33,7 @@ sealed class ParsedCommand { // Valid commands: - class SendEmote(val message: String) : ParsedCommand() + class SendEmote(val message: CharSequence) : ParsedCommand() class BanUser(val userId: String, val reason: String) : ParsedCommand() class UnbanUser(val userId: String) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 2e59e70d08..0a6321dd57 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -25,7 +25,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class RoomDetailAction : VectorViewModelAction { data class SaveDraft(val draft: String) : RoomDetailAction() - data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailAction() + data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction() data class SendMedia(val attachments: List) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 6fdbf94590..6186bd1ac1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -28,6 +28,7 @@ import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable +import android.text.SpannableStringBuilder import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -609,7 +610,7 @@ class RoomDetailFragment @Inject constructor( attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing) } - override fun onSendMessage(text: String) { + override fun onSendMessage(text: CharSequence) { if (lockSendButton) { Timber.w("Send button is locked") return @@ -977,7 +978,9 @@ class RoomDetailFragment @Inject constructor( @SuppressLint("SetTextI18n") override fun onMemberNameClicked(informationData: MessageInformationData) { - insertUserDisplayNameInTextEditor(informationData.memberName?.toString()) + session.getUser(informationData.senderId)?.let { + insertUserDisplayNameInTextEditor(it) + } } override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { @@ -1166,8 +1169,9 @@ class RoomDetailFragment @Inject constructor( * @param text the text to insert. */ // TODO legacy, refactor - private fun insertUserDisplayNameInTextEditor(text: String?) { + private fun insertUserDisplayNameInTextEditor(member: User) { // TODO move logic outside of fragment + val text = member.displayName if (null != text) { // var vibrate = false @@ -1176,19 +1180,44 @@ class RoomDetailFragment @Inject constructor( // current user if (composerLayout.composerEditText.text.isNullOrBlank()) { composerLayout.composerEditText.append(Command.EMOTE.command + " ") - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length + ?: 0) // vibrate = true } } else { // another user + val sanitizeDisplayName = sanitizeDisplayName(text) if (composerLayout.composerEditText.text.isNullOrBlank()) { // Ensure displayName will not be interpreted as a Slash command if (text.startsWith("/")) { composerLayout.composerEditText.append("\\") } - composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ") + SpannableStringBuilder().apply { + append(sanitizeDisplayName) + setSpan( + PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member), + 0, + sanitizeDisplayName.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + append(": ") + }.let { + composerLayout.composerEditText.append(it) + } } else { - composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ") + SpannableStringBuilder().apply { + append(sanitizeDisplayName) + setSpan( + PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member), + 0, + sanitizeDisplayName.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + append(" ") + }.let { + composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, it) + } +// composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName + " ") } // vibrate = true diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index d2c2c7fdde..b8d4ccb7c6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl @@ -165,6 +166,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro invisibleEventsObservable.accept(action) } + fun getMember(userId: String) : RoomMember? { + return room.getRoomMember(userId) + } /** * Convert a send mode to a draft and save the draft */ @@ -355,7 +359,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro if (inReplyTo != null) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { - room.editReply(state.sendMode.timelineEvent, it, action.text) + room.editReply(state.sendMode.timelineEvent, it, action.text.toString()) } } else { val messageContent: MessageContent? = @@ -380,7 +384,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body - val finalText = legacyRiotQuoteText(textMsg, action.text) + val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) + + //TODO check for pills? // TODO Refactor this, just temporary for quotes val parser = Parser.builder().build() @@ -397,7 +403,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } is SendMode.REPLY -> { state.sendMode.timelineEvent.let { - room.replyToMessage(it, action.text, action.autoMarkdown) + room.replyToMessage(it, action.text.toString(), action.autoMarkdown) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) popDraft() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 32307dc3d4..63e74d6f32 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -26,6 +26,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.text.toSpannable import androidx.transition.AutoTransition import androidx.transition.Transition import androidx.transition.TransitionManager @@ -43,7 +44,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib interface Callback : ComposerEditText.Callback { fun onCloseRelatedMessage() - fun onSendMessage(text: String) + fun onSendMessage(text: CharSequence) fun onAddAttachment() } @@ -86,8 +87,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib } sendButton.setOnClickListener { - val textMessage = text?.toString() ?: "" - callback?.onSendMessage(textMessage) + val textMessage = text?.toSpannable() + callback?.onSendMessage(textMessage ?: "") } attachmentButton.setOnClickListener { diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index bc954204c0..414cd71de7 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -28,6 +28,7 @@ import androidx.annotation.UiThread import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.google.android.material.chip.ChipDrawable +import im.vector.matrix.android.api.session.room.send.UserMentionSpan import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.glide.GlideRequests @@ -37,14 +38,15 @@ import java.lang.ref.WeakReference /** * This span is able to replace a text by a [ChipDrawable] * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached. + * Implements UserMentionSpan so that it could be automatically transformed in matrix links and displayed as pills. */ class PillImageSpan(private val glideRequests: GlideRequests, private val avatarRenderer: AvatarRenderer, private val context: Context, - private val userId: String, - private val user: User?) : ReplacementSpan() { + override val userId: String, + private val user: User?) : ReplacementSpan(), UserMentionSpan { - private val displayName by lazy { + override val displayName by lazy { if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! } From 2a4cdec0201dd6aa14198bdd40ea3e4dcdf2985f Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 15 Nov 2019 11:40:05 +0100 Subject: [PATCH 138/189] klint cleaning --- .../session/room/send/LocalEchoEventFactory.kt | 13 +++++++------ .../home/room/detail/RoomDetailViewModel.kt | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 4b099a25be..3f47b8fff3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -70,7 +70,8 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { if (autoMarkdown) { - val source = transformPills(text,"[%2\$s](https://matrix.to/#/%1\$s)") ?: text.toString() + val source = transformPills(text, "[%2\$s](https://matrix.to/#/%1\$s)") + ?: text.toString() val document = parser.parse(source) val htmlText = renderer.render(document) @@ -78,9 +79,9 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use return TextContent(source, htmlText) } } else { - //Try to detect pills + // Try to detect pills transformPills(text, "%2\$s")?.let { - return TextContent(text.toString(),it) + return TextContent(text.toString(), it) } } @@ -88,7 +89,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use } private fun transformPills(text: CharSequence, - template : String) + template: String) : String? { val bufSB = StringBuffer() var currIndex = 0 @@ -98,9 +99,9 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use pills.forEachIndexed { _, urlSpan -> val start = it.getSpanStart(urlSpan) val end = it.getSpanEnd(urlSpan) - //We want to replace with the pill with a html link + // We want to replace with the pill with a html link bufSB.append(text, currIndex, start) - bufSB.append(String.format(template,urlSpan.userId,urlSpan.displayName)) + bufSB.append(String.format(template, urlSpan.userId, urlSpan.displayName)) currIndex = end } bufSB.append(text, currIndex, text.length) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index b8d4ccb7c6..a264e0d06c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -386,7 +386,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) - //TODO check for pills? + // TODO check for pills? // TODO Refactor this, just temporary for quotes val parser = Parser.builder().build() From 62bae6708071b9fcb84d23838d1b74657478f296 Mon Sep 17 00:00:00 2001 From: Valere Date: Sat, 16 Nov 2019 12:32:50 +0100 Subject: [PATCH 139/189] Code review --- .../api/session/room/send/SendService.kt | 1 + .../api/session/room/send/TextPillsUtils.kt | 67 +++++++++++++++++++ .../room/send/LocalEchoEventFactory.kt | 31 +-------- .../home/room/detail/RoomDetailFragment.kt | 27 +++++--- .../room/detail/composer/TextComposerView.kt | 5 +- .../riotx/features/html/MxLinkTagHandler.kt | 3 +- .../riotx/features/html/PillImageSpan.kt | 10 +-- 7 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index e45069bcff..bdae5eaaa6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -42,6 +42,7 @@ interface SendService { * Method to send a text message with a formatted body. * @param text the text message to send * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML + * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @return a [Cancelable] */ fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt new file mode 100644 index 0000000000..b50d5dd4a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.send + +import android.text.SpannableString + +/** + * Utility class to detect special span in CharSequence and turn them into + * formatted text to send them as a Matrix messages. + * + * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) + */ +object TextPillsUtils { + + private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" + + private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" + + /** + * Detects if transformable spans are present in the text. + * @return the transformed String or null if no Span found + */ + fun processSpecialSpansToHtml(text: CharSequence): String? { + return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE) + } + + /** + * Detects if transformable spans are present in the text. + * @return the transformed String or null if no Span found + */ + fun processSpecialSpansToMarkdown(text: CharSequence): String? { + return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE) + } + + private fun transformPills(text: CharSequence, template: String): String? { + val spannableString = SpannableString.valueOf(text) + val pills = spannableString + ?.getSpans(0, text.length, UserMentionSpan::class.java) + ?.takeIf { it.isNotEmpty() } + ?: return null + + return buildString { + var currIndex = 0 + pills.forEachIndexed { _, urlSpan -> + val start = spannableString.getSpanStart(urlSpan) + val end = spannableString.getSpanEnd(urlSpan) + // We want to replace with the pill with a html link + append(text, currIndex, start) + append(String.format(template, urlSpan.userId, urlSpan.displayName)) + currIndex = end + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 3f47b8fff3..becba6bffe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever -import android.text.SpannableString import androidx.exifinterface.media.ExifInterface import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R @@ -29,7 +28,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent -import im.vector.matrix.android.api.session.room.send.UserMentionSpan +import im.vector.matrix.android.api.session.room.send.TextPillsUtils import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.internal.database.helper.addSendingEvent @@ -70,7 +69,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { if (autoMarkdown) { - val source = transformPills(text, "[%2\$s](https://matrix.to/#/%1\$s)") + val source = TextPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() val document = parser.parse(source) val htmlText = renderer.render(document) @@ -80,7 +79,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use } } else { // Try to detect pills - transformPills(text, "%2\$s")?.let { + TextPillsUtils.processSpecialSpansToHtml(text)?.let { return TextContent(text.toString(), it) } } @@ -88,30 +87,6 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use return TextContent(text.toString()) } - private fun transformPills(text: CharSequence, - template: String) - : String? { - val bufSB = StringBuffer() - var currIndex = 0 - SpannableString.valueOf(text).let { - val pills = it.getSpans(0, text.length, UserMentionSpan::class.java) - if (pills.isNotEmpty()) { - pills.forEachIndexed { _, urlSpan -> - val start = it.getSpanStart(urlSpan) - val end = it.getSpanEnd(urlSpan) - // We want to replace with the pill with a html link - bufSB.append(text, currIndex, start) - bufSB.append(String.format(template, urlSpan.userId, urlSpan.displayName)) - currIndex = end - } - bufSB.append(text, currIndex, text.length) - return bufSB.toString() - } else { - return null - } - } - } - private fun isFormattedTextPertinent(text: String, htmlText: String?) = text != htmlText && htmlText != "

${text.trim()}

\n" diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 6186bd1ac1..b6b6270602 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail -import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Context import android.content.DialogInterface @@ -61,6 +60,7 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -406,7 +406,8 @@ class RoomDetailFragment @Inject constructor( composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.sendButton.setContentDescription(getString(descriptionRes)) - avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar) + avatarRenderer.render(event.senderAvatar, event.root.senderId + ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar) composerLayout.expand { // need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -419,7 +420,8 @@ class RoomDetailFragment @Inject constructor( if (text != composerLayout.composerEditText.text.toString()) { // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length + ?: 0) } } @@ -589,7 +591,8 @@ class RoomDetailFragment @Inject constructor( // Add the span val user = session.getUser(item.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, requireContext(), item.userId, user) + val span = PillImageSpan(glideRequests, avatarRenderer, requireContext(), item.userId, user?.displayName + ?: item.userId, user?.avatarUrl) span.bind(composerLayout.composerEditText) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -976,10 +979,10 @@ class RoomDetailFragment @Inject constructor( vectorBaseActivity.notImplemented("Click on user avatar") } - @SuppressLint("SetTextI18n") override fun onMemberNameClicked(informationData: MessageInformationData) { - session.getUser(informationData.senderId)?.let { - insertUserDisplayNameInTextEditor(it) + val userId = informationData.senderId + roomDetailViewModel.getMember(userId)?.let { + insertUserDisplayNameInTextEditor(userId, it) } } @@ -1169,9 +1172,9 @@ class RoomDetailFragment @Inject constructor( * @param text the text to insert. */ // TODO legacy, refactor - private fun insertUserDisplayNameInTextEditor(member: User) { + private fun insertUserDisplayNameInTextEditor(userId: String, memberInfo: RoomMember) { // TODO move logic outside of fragment - val text = member.displayName + val text = memberInfo.displayName if (null != text) { // var vibrate = false @@ -1195,7 +1198,8 @@ class RoomDetailFragment @Inject constructor( SpannableStringBuilder().apply { append(sanitizeDisplayName) setSpan( - PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member), + PillImageSpan(glideRequests, avatarRenderer, requireContext(), userId, memberInfo.displayName + ?: userId, memberInfo.avatarUrl), 0, sanitizeDisplayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE @@ -1208,7 +1212,8 @@ class RoomDetailFragment @Inject constructor( SpannableStringBuilder().apply { append(sanitizeDisplayName) setSpan( - PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member), + PillImageSpan(glideRequests, avatarRenderer, requireContext(), userId, memberInfo.displayName + ?: userId, memberInfo.avatarUrl), 0, sanitizeDisplayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 63e74d6f32..f3a29b8dc0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -88,7 +88,10 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib sendButton.setOnClickListener { val textMessage = text?.toSpannable() - callback?.onSendMessage(textMessage ?: "") + callback?.onSendMessage( + textMessage + ?: "" + ) } attachmentButton.setOnClickListener { diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index fdcbb12cd7..ecbf0da415 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -41,7 +41,8 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, when (permalinkData) { is PermalinkData.UserLink -> { val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user) + val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user?.displayName + ?: permalinkData.userId, user?.avatarUrl) SpannableBuilder.setSpans( visitor.builder(), span, diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index 414cd71de7..a192c71961 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -29,7 +29,6 @@ import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.google.android.material.chip.ChipDrawable import im.vector.matrix.android.api.session.room.send.UserMentionSpan -import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.features.home.AvatarRenderer @@ -44,11 +43,8 @@ class PillImageSpan(private val glideRequests: GlideRequests, private val avatarRenderer: AvatarRenderer, private val context: Context, override val userId: String, - private val user: User?) : ReplacementSpan(), UserMentionSpan { - - override val displayName by lazy { - if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! - } + override val displayName: String, + private val avatarUrl: String?) : ReplacementSpan(), UserMentionSpan { private val pillDrawable = createChipDrawable() private val target = PillImageSpanTarget(this) @@ -57,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, @UiThread fun bind(textView: TextView) { tv = WeakReference(textView) - avatarRenderer.render(context, glideRequests, user?.avatarUrl, userId, displayName, target) + avatarRenderer.render(context, glideRequests, avatarUrl, userId, displayName, target) } // ReplacementSpan ***************************************************************************** From 38b93c527b8a2c64df0516970dd03cc0616b100f Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 20 Nov 2019 11:14:47 +0100 Subject: [PATCH 140/189] Ensure received pills spans do not overlap --- .../api/session/room/send/TextPillsUtils.kt | 68 ++++++++++++++++++- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt index b50d5dd4a6..941861f2ed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.send import android.text.SpannableString +import java.util.* /** * Utility class to detect special span in CharSequence and turn them into @@ -23,12 +24,17 @@ import android.text.SpannableString * * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ + + object TextPillsUtils { + private data class MentionLinkSpec(val span: UserMentionSpan, val start: Int, val end: Int) + private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" + /** * Detects if transformable spans are present in the text. * @return the transformed String or null if no Span found @@ -49,14 +55,17 @@ object TextPillsUtils { val spannableString = SpannableString.valueOf(text) val pills = spannableString ?.getSpans(0, text.length, UserMentionSpan::class.java) + ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) } + ?.toMutableList() ?.takeIf { it.isNotEmpty() } ?: return null + //we need to prune overlaps! + pruneOverlaps(pills) + return buildString { var currIndex = 0 - pills.forEachIndexed { _, urlSpan -> - val start = spannableString.getSpanStart(urlSpan) - val end = spannableString.getSpanEnd(urlSpan) + pills.forEachIndexed { _, (urlSpan, start, end) -> // We want to replace with the pill with a html link append(text, currIndex, start) append(String.format(template, urlSpan.userId, urlSpan.displayName)) @@ -64,4 +73,57 @@ object TextPillsUtils { } } } + + private fun pruneOverlaps(links: MutableList) { + Collections.sort(links, COMPARATOR) + var len = links.size + var i = 0 + while (i < len - 1) { + val a = links[i] + val b = links[i + 1] + var remove = -1 + + //test if there is an overlap + if (b.start in a.start until a.end) { + + when { + b.end <= a.end -> + //b is inside a -> b should be removed + remove = i + 1 + a.end - a.start > b.end - b.start -> + //overlap and a is bigger -> b should be removed + remove = i + 1 + a.end - a.start < b.end - b.start -> + //overlap and a is smaller -> a should be removed + remove = i + } + + + if (remove != -1) { + links.removeAt(remove) + len-- + continue + } + } + i++ + } + } + + private val COMPARATOR = Comparator { (_, startA, endA), (_, startB, endB) -> + if (startA < startB) { + return@Comparator -1 + } + + if (startA > startB) { + return@Comparator 1 + } + + if (endA < endB) { + return@Comparator 1 + } + + if (endA > endB) { + -1 + } else 0 + } } From 97766404d639d9dce42da317970301009b3a321f Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 20 Nov 2019 17:32:36 +0100 Subject: [PATCH 141/189] klint --- .../api/session/room/send/TextPillsUtils.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt index 941861f2ed..936b1d18c2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt @@ -24,8 +24,6 @@ import java.util.* * * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ - - object TextPillsUtils { private data class MentionLinkSpec(val span: UserMentionSpan, val start: Int, val end: Int) @@ -34,7 +32,6 @@ object TextPillsUtils { private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" - /** * Detects if transformable spans are present in the text. * @return the transformed String or null if no Span found @@ -60,7 +57,7 @@ object TextPillsUtils { ?.takeIf { it.isNotEmpty() } ?: return null - //we need to prune overlaps! + // we need to prune overlaps! pruneOverlaps(pills) return buildString { @@ -83,22 +80,20 @@ object TextPillsUtils { val b = links[i + 1] var remove = -1 - //test if there is an overlap + // test if there is an overlap if (b.start in a.start until a.end) { - when { b.end <= a.end -> - //b is inside a -> b should be removed + // b is inside a -> b should be removed remove = i + 1 a.end - a.start > b.end - b.start -> - //overlap and a is bigger -> b should be removed + // overlap and a is bigger -> b should be removed remove = i + 1 a.end - a.start < b.end - b.start -> - //overlap and a is smaller -> a should be removed + // overlap and a is smaller -> a should be removed remove = i } - if (remove != -1) { links.removeAt(remove) len-- From f984758d37f15ef4688be29b887e86a02a9df1ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 28 Nov 2019 21:40:02 +0100 Subject: [PATCH 142/189] Pills: Daggerization --- .../room/send/LocalEchoEventFactory.kt | 15 ++++---- .../room/send/pills/MentionLinkSpec.kt | 25 ++++++++++++++ .../send/pills/MentionLinkSpecComparator.kt | 32 +++++++++++++++++ .../room/send/pills}/TextPillsUtils.kt | 34 ++++++------------- 4 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/{api/session/room/send => internal/session/room/send/pills}/TextPillsUtils.kt (81%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index becba6bffe..b773d1f892 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -28,7 +28,6 @@ import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent -import im.vector.matrix.android.api.session.room.send.TextPillsUtils import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.internal.database.helper.addSendingEvent @@ -37,6 +36,7 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater +import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.util.StringProvider import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer @@ -51,9 +51,12 @@ import javax.inject.Inject * * The transactionID is used as loc */ -internal class LocalEchoEventFactory @Inject constructor(@UserId private val userId: String, - private val stringProvider: StringProvider, - private val roomSummaryUpdater: RoomSummaryUpdater) { +internal class LocalEchoEventFactory @Inject constructor( + @UserId private val userId: String, + private val stringProvider: StringProvider, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val textPillsUtils: TextPillsUtils +) { // TODO Inject private val parser = Parser.builder().build() // TODO Inject @@ -69,7 +72,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { if (autoMarkdown) { - val source = TextPillsUtils.processSpecialSpansToMarkdown(text) + val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() val document = parser.parse(source) val htmlText = renderer.render(document) @@ -79,7 +82,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use } } else { // Try to detect pills - TextPillsUtils.processSpecialSpansToHtml(text)?.let { + textPillsUtils.processSpecialSpansToHtml(text)?.let { return TextContent(text.toString(), it) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt new file mode 100644 index 0000000000..5ad61b5441 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.send.pills + +import im.vector.matrix.android.api.session.room.send.UserMentionSpan + +internal data class MentionLinkSpec( + val span: UserMentionSpan, + val start: Int, + val end: Int +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt new file mode 100644 index 0000000000..76fd8336cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.send.pills + +import javax.inject.Inject + +internal class MentionLinkSpecComparator @Inject constructor() : Comparator { + + override fun compare(o1: MentionLinkSpec, o2: MentionLinkSpec): Int { + return when { + o1.start < o2.start -> -1 + o1.start > o2.start -> 1 + o1.end < o2.end -> 1 + o1.end > o2.end -> -1 + else -> 0 + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt similarity index 81% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index 936b1d18c2..02f48e5800 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.api.session.room.send +package im.vector.matrix.android.internal.session.room.send.pills import android.text.SpannableString +import im.vector.matrix.android.api.session.room.send.UserMentionSpan import java.util.* +import javax.inject.Inject /** * Utility class to detect special span in CharSequence and turn them into @@ -24,13 +26,9 @@ import java.util.* * * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ -object TextPillsUtils { - - private data class MentionLinkSpec(val span: UserMentionSpan, val start: Int, val end: Int) - - private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" - - private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" +internal class TextPillsUtils @Inject constructor( + private val mentionLinkSpecComparator: MentionLinkSpecComparator +) { /** * Detects if transformable spans are present in the text. @@ -72,7 +70,7 @@ object TextPillsUtils { } private fun pruneOverlaps(links: MutableList) { - Collections.sort(links, COMPARATOR) + Collections.sort(links, mentionLinkSpecComparator) var len = links.size var i = 0 while (i < len - 1) { @@ -104,21 +102,9 @@ object TextPillsUtils { } } - private val COMPARATOR = Comparator { (_, startA, endA), (_, startB, endB) -> - if (startA < startB) { - return@Comparator -1 - } + companion object { + private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" - if (startA > startB) { - return@Comparator 1 - } - - if (endA < endB) { - return@Comparator 1 - } - - if (endA > endB) { - -1 - } else 0 + private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" } } From f11cd47df3e036e5969e01ac27595b482a951727 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 28 Nov 2019 21:40:44 +0100 Subject: [PATCH 143/189] Pills: cleanup --- .../android/api/session/room/send/UserMentionSpan.kt | 5 +++-- .../features/home/room/detail/composer/TextComposerView.kt | 7 ++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt index 0899e4f27e..4cd8080dc3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package im.vector.matrix.android.api.session.room.send /** @@ -20,6 +21,6 @@ package im.vector.matrix.android.api.session.room.send * These Spans will be transformed into pills when detected in message to send */ interface UserMentionSpan { - abstract val displayName: String - abstract val userId: String + val displayName: String + val userId: String } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index f3a29b8dc0..593ce1a8f6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -87,11 +87,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib } sendButton.setOnClickListener { - val textMessage = text?.toSpannable() - callback?.onSendMessage( - textMessage - ?: "" - ) + val textMessage = text?.toSpannable() ?: "" + callback?.onSendMessage(textMessage) } attachmentButton.setOnClickListener { From 4b273e8746500010fec7f730a811ad6a68b8526a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 28 Nov 2019 21:54:37 +0100 Subject: [PATCH 144/189] Pills: simplify and improve the algorithm --- .../home/room/detail/RoomDetailFragment.kt | 140 ++++++++---------- 1 file changed, 62 insertions(+), 78 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index b6b6270602..baf24a51a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail +import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Context import android.content.DialogInterface @@ -27,7 +28,6 @@ import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable -import android.text.SpannableStringBuilder import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -37,6 +37,7 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat +import androidx.core.text.buildSpannedString import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach @@ -60,7 +61,6 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -159,7 +159,7 @@ class RoomDetailFragment @Inject constructor( companion object { - /**x + /** * Sanitize the display name. * * @param displayName the display name to sanitize @@ -406,8 +406,12 @@ class RoomDetailFragment @Inject constructor( composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.sendButton.setContentDescription(getString(descriptionRes)) - avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar) + avatarRenderer.render( + event.senderAvatar, + event.root.senderId ?: "", + event.getDisambiguatedDisplayName(), + composerLayout.composerRelatedMessageAvatar + ) composerLayout.expand { // need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -420,8 +424,7 @@ class RoomDetailFragment @Inject constructor( if (text != composerLayout.composerEditText.text.toString()) { // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length - ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) } } @@ -591,8 +594,13 @@ class RoomDetailFragment @Inject constructor( // Add the span val user = session.getUser(item.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, requireContext(), item.userId, user?.displayName - ?: item.userId, user?.avatarUrl) + val span = PillImageSpan( + glideRequests, + avatarRenderer, + requireContext(), + item.userId, + user?.displayName ?: item.userId, + user?.avatarUrl) span.bind(composerLayout.composerEditText) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -980,10 +988,7 @@ class RoomDetailFragment @Inject constructor( } override fun onMemberNameClicked(informationData: MessageInformationData) { - val userId = informationData.senderId - roomDetailViewModel.getMember(userId)?.let { - insertUserDisplayNameInTextEditor(userId, it) - } + insertUserDisplayNameInTextEditor(informationData.senderId) } override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { @@ -1165,77 +1170,56 @@ class RoomDetailFragment @Inject constructor( } } -// utils /** - * Insert an user displayname in the message editor. + * Insert a user displayName in the message editor. * - * @param text the text to insert. + * @param userId the userId. */ -// TODO legacy, refactor - private fun insertUserDisplayNameInTextEditor(userId: String, memberInfo: RoomMember) { - // TODO move logic outside of fragment - val text = memberInfo.displayName - if (null != text) { -// var vibrate = false + @SuppressLint("SetTextI18n") + private fun insertUserDisplayNameInTextEditor(userId: String) { + val startToCompose = composerLayout.composerEditText.text.isNullOrBlank() - val myDisplayName = session.getUser(session.myUserId)?.displayName - if (myDisplayName == text) { - // current user - if (composerLayout.composerEditText.text.isNullOrBlank()) { - composerLayout.composerEditText.append(Command.EMOTE.command + " ") - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length - ?: 0) -// vibrate = true - } - } else { - // another user - val sanitizeDisplayName = sanitizeDisplayName(text) - if (composerLayout.composerEditText.text.isNullOrBlank()) { - // Ensure displayName will not be interpreted as a Slash command - if (text.startsWith("/")) { - composerLayout.composerEditText.append("\\") + if (startToCompose + && userId == session.myUserId) { + // Empty composer, current user: start an emote + composerLayout.composerEditText.setText(Command.EMOTE.command + " ") + composerLayout.composerEditText.setSelection(Command.EMOTE.command.length + 1) + } else { + val roomMember = roomDetailViewModel.getMember(userId) + // TODO move logic outside of fragment + (roomMember?.displayName ?: userId) + .let { sanitizeDisplayName(it) } + .let { displayName -> + buildSpannedString { + append(displayName) + setSpan( + PillImageSpan( + glideRequests, + avatarRenderer, + requireContext(), + userId, + displayName, + roomMember?.avatarUrl), + 0, + displayName.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + append(if (startToCompose) ": " else " ") + }.let { pill -> + if (startToCompose) { + if (displayName.startsWith("/")) { + // Ensure displayName will not be interpreted as a Slash command + composerLayout.composerEditText.append("\\") + } + composerLayout.composerEditText.append(pill) + } else { + composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, pill) + } + } } - SpannableStringBuilder().apply { - append(sanitizeDisplayName) - setSpan( - PillImageSpan(glideRequests, avatarRenderer, requireContext(), userId, memberInfo.displayName - ?: userId, memberInfo.avatarUrl), - 0, - sanitizeDisplayName.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - append(": ") - }.let { - composerLayout.composerEditText.append(it) - } - } else { - SpannableStringBuilder().apply { - append(sanitizeDisplayName) - setSpan( - PillImageSpan(glideRequests, avatarRenderer, requireContext(), userId, memberInfo.displayName - ?: userId, memberInfo.avatarUrl), - 0, - sanitizeDisplayName.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - append(" ") - }.let { - composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, it) - } -// composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName + " ") - } - -// vibrate = true - } - -// if (vibrate && vectorPreferences.vibrateWhenMentioning()) { -// val v= context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator -// if (v?.hasVibrator() == true) { -// v.vibrate(100) -// } -// } - focusComposerAndShowKeyboard() } + + focusComposerAndShowKeyboard() } private fun focusComposerAndShowKeyboard() { From a3f8f138a61f3c37a72afcddaec918ba715e83a7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 28 Nov 2019 22:05:59 +0100 Subject: [PATCH 145/189] Create showKeyBoard() extension --- .../java/im/vector/riotx/core/extensions/View.kt | 12 ++++++++++-- .../CreateDirectRoomDirectoryUsersFragment.kt | 5 ++--- .../features/home/room/detail/RoomDetailFragment.kt | 5 ++--- .../settings/VectorSettingsGeneralFragment.kt | 7 +++---- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/View.kt b/vector/src/main/java/im/vector/riotx/core/extensions/View.kt index bcbab97360..41f98ed264 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/View.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/View.kt @@ -21,6 +21,14 @@ import android.view.View import android.view.inputmethod.InputMethodManager fun View.hideKeyboard() { - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(windowToken, 0) + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.hideSoftInputFromWindow(windowToken, 0) +} + +fun View.showKeyboard(andRequestFocus: Boolean = false) { + if (andRequestFocus) { + requestFocus() + } + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index cf6abf12e9..17eef126d8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setupAsSearch +import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import javax.inject.Inject @@ -63,9 +64,7 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor( viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString())) } .disposeOnDestroyView() - createDirectRoomSearchById.requestFocus() - val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(createDirectRoomSearchById, InputMethodManager.SHOW_IMPLICIT) + createDirectRoomSearchById.showKeyboard(andRequestFocus = true) } private fun setupCloseView() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index baf24a51a6..fc7613c530 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -74,6 +74,7 @@ import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseFragment @@ -1223,9 +1224,7 @@ class RoomDetailFragment @Inject constructor( } private fun focusComposerAndShowKeyboard() { - composerLayout.composerEditText.requestFocus() - val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT) + composerLayout.composerEditText.showKeyboard(andRequestFocus = true) } private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index ff76c61754..cbf2f0eec1 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -38,6 +38,7 @@ import com.bumptech.glide.load.engine.cache.DiskCache import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.preference.UserAvatarPreference @@ -696,8 +697,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { .setPositiveButton(R.string.settings_change_password_submit, null) .setNegativeButton(R.string.cancel, null) .setOnDismissListener { - val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.applicationWindowToken, 0) + view.hideKeyboard() } .create() @@ -762,8 +762,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { showPassword.performClick() } - val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.applicationWindowToken, 0) + view.hideKeyboard() val oldPwd = oldPasswordText.text.toString().trim() val newPwd = newPasswordText.text.toString().trim() From 5d3c376267702b2fbacc77ad8ec11e5823a627fb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 10:29:40 +0100 Subject: [PATCH 146/189] Pills: remove pills when a char is deleted --- .../room/detail/composer/ComposerEditText.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt index 273aeecbfa..093792ca17 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt @@ -20,12 +20,16 @@ package im.vector.riotx.features.home.room.detail.composer import android.content.Context import android.net.Uri import android.os.Build +import android.text.Editable import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat +import im.vector.riotx.core.platform.SimpleTextWatcher +import im.vector.riotx.features.html.PillImageSpan +import timber.log.Timber class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) : AppCompatEditText(context, attrs, defStyleAttr) { @@ -55,4 +59,38 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib } return InputConnectionCompat.createWrapper(ic, editorInfo, callback) } + + init { + addTextChangedListener( + object : SimpleTextWatcher() { + var spanToRemove: PillImageSpan? = null + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + Timber.v("beforeTextChanged: start:$start count:$count after:$after") + + if (count > after) { + // A char has been deleted + val deleteCharPosition = start + count + Timber.v("beforeTextChanged: deleted char at $deleteCharPosition") + + // Get span at this position + val spans = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) + spanToRemove = spans.firstOrNull() + } + } + + override fun afterTextChanged(s: Editable) { + if (spanToRemove != null) { + Timber.v("Removing the span") + val start = editableText.getSpanStart(spanToRemove) + val end = editableText.getSpanEnd(spanToRemove) + // Must be done before text replacement + editableText.removeSpan(spanToRemove) + editableText.replace(start, end, "") + spanToRemove = null + } + } + } + ) + } } From c412006f0eb430f61c8f6714e0586fe1d52d74d1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 11:13:23 +0100 Subject: [PATCH 147/189] Pills: render the avatar --- .../riotx/features/home/room/detail/RoomDetailFragment.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index fc7613c530..59437273d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail import android.annotation.SuppressLint import android.app.Activity.RESULT_OK -import android.content.Context import android.content.DialogInterface import android.content.Intent import android.graphics.drawable.ColorDrawable @@ -29,7 +28,6 @@ import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.view.* -import android.view.inputmethod.InputMethodManager import android.widget.TextView import android.widget.Toast import androidx.annotation.DrawableRes @@ -1200,7 +1198,8 @@ class RoomDetailFragment @Inject constructor( requireContext(), userId, displayName, - roomMember?.avatarUrl), + roomMember?.avatarUrl) + .also { it.bind(composerLayout.composerEditText) }, 0, displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE From 9f9c41808504cfecfec8caef4b21e67564ff4acf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 11:17:11 +0100 Subject: [PATCH 148/189] Pills: cleanup and robustness --- .../room/detail/composer/ComposerEditText.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt index 093792ca17..ce27b1c098 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt @@ -66,27 +66,30 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib var spanToRemove: PillImageSpan? = null override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - Timber.v("beforeTextChanged: start:$start count:$count after:$after") + Timber.v("Pills: beforeTextChanged: start:$start count:$count after:$after") if (count > after) { // A char has been deleted val deleteCharPosition = start + count - Timber.v("beforeTextChanged: deleted char at $deleteCharPosition") + Timber.v("Pills: beforeTextChanged: deleted char at $deleteCharPosition") - // Get span at this position - val spans = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) - spanToRemove = spans.firstOrNull() + // Get the first span at this position + spanToRemove = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) + .also { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") } + .firstOrNull() } } override fun afterTextChanged(s: Editable) { if (spanToRemove != null) { - Timber.v("Removing the span") val start = editableText.getSpanStart(spanToRemove) val end = editableText.getSpanEnd(spanToRemove) + Timber.v("Pills: afterTextChanged Removing the span start:$start end:$end") // Must be done before text replacement editableText.removeSpan(spanToRemove) - editableText.replace(start, end, "") + if (start != -1 && end != -1) { + editableText.replace(start, end, "") + } spanToRemove = null } } From 46d96429e08c53b6c5d6fbf3c7475831fe3fd3ca Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 11:20:46 +0100 Subject: [PATCH 149/189] Create ooi extension --- .../java/im/vector/riotx/core/extensions/BasicExtensions.kt | 2 ++ .../features/home/room/detail/composer/ComposerEditText.kt | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt index 2dc75c5fa2..1e3da7f878 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt @@ -21,6 +21,8 @@ import androidx.fragment.app.Fragment fun Boolean.toOnOff() = if (this) "ON" else "OFF" +inline fun T.ooi(block: (T) -> Unit): T = also(block) + /** * Apply argument to a Fragment */ diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt index ce27b1c098..ab37431103 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt @@ -27,6 +27,7 @@ import android.view.inputmethod.InputConnection import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat +import im.vector.riotx.core.extensions.ooi import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.features.html.PillImageSpan import timber.log.Timber @@ -75,7 +76,7 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib // Get the first span at this position spanToRemove = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) - .also { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") } + .ooi { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") } .firstOrNull() } } From 10cc270273381b7df7d7bc0c85e0f4f1813e7569 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 11:26:19 +0100 Subject: [PATCH 150/189] ktlint --- .../home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt | 2 -- .../riotx/features/settings/VectorSettingsGeneralFragment.kt | 2 -- 2 files changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index 17eef126d8..59f31ec2ee 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -16,10 +16,8 @@ package im.vector.riotx.features.home.createdirect -import android.content.Context import android.os.Bundle import android.view.View -import android.view.inputmethod.InputMethodManager import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index cbf2f0eec1..ca994db62c 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -19,13 +19,11 @@ package im.vector.riotx.features.settings import android.app.Activity -import android.content.Context import android.content.Intent import android.text.Editable import android.util.Patterns import android.view.View import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager import android.widget.ImageView import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat From 67fe776d910d035c5e9939f44961bf0c6e7f4379 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 15 Nov 2019 10:12:23 +0100 Subject: [PATCH 151/189] Update Changes --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0f1c76203d..27619f7d28 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features ✨: - Improvements 🙌: - - + - Send mention Pills from composer Other changes: - Fix a small grammatical error when an empty room list is shown. From c06b8486eac9befcaec6eaac0d7110a52d5d095d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 15:36:54 +0100 Subject: [PATCH 152/189] Update wording --- vector/src/main/res/values/strings_riotX.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 9bc13cdec2..5299d0db1f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -46,7 +46,7 @@ Sign in to %1$s Sign Up Sign In - Sign In with SSO + Continue with SSO Modular Address Address From e23763e6db176b48f91e9d75a8bbf58b852dd993 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 15:43:39 +0100 Subject: [PATCH 153/189] Update password from email twice --- .../java/im/vector/riotx/features/login/LoginViewModel.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 7dc8a1119b..de76f6b416 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -322,13 +322,15 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi if (safeLoginWizard == null) { setState { copy( - asyncResetPassword = Fail(Throwable("Bad configuration")) + asyncResetPassword = Fail(Throwable("Bad configuration")), + asyncResetMailConfirmed = Uninitialized ) } } else { setState { copy( - asyncResetPassword = Loading() + asyncResetPassword = Loading(), + asyncResetMailConfirmed = Uninitialized ) } @@ -360,12 +362,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi if (safeLoginWizard == null) { setState { copy( + asyncResetPassword = Uninitialized, asyncResetMailConfirmed = Fail(Throwable("Bad configuration")) ) } } else { setState { copy( + asyncResetPassword = Uninitialized, asyncResetMailConfirmed = Loading() ) } From 938289e8eba424c175de80b7f8bc265fc7105eb7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 15:44:08 +0100 Subject: [PATCH 154/189] ktlint --- .../main/java/im/vector/riotx/features/login/LoginActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index eb1dc4a0e6..2dec402f85 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -128,7 +128,7 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // TODO Disabled because it provokes a flickering - //ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() is LoginNavigation.OnSignModeSelected -> onSignModeSelected() From a343da594f218dddfb1e6c91ef49a9b95ad9f221 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 16:22:04 +0100 Subject: [PATCH 155/189] Import Strings from Riot --- vector/src/main/res/values-bg/strings.xml | 149 +++++++++++++++++++++- vector/src/main/res/values-de/strings.xml | 4 +- vector/src/main/res/values-fi/strings.xml | 12 +- vector/src/main/res/values-ko/strings.xml | 8 +- vector/src/main/res/values-sq/strings.xml | 67 ++++++++++ vector/src/main/res/values-tr/strings.xml | 35 +++-- vector/src/main/res/values/strings.xml | 33 ++++- 7 files changed, 282 insertions(+), 26 deletions(-) diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml index e03e174e54..e98bcd1a5f 100644 --- a/vector/src/main/res/values-bg/strings.xml +++ b/vector/src/main/res/values-bg/strings.xml @@ -448,8 +448,8 @@ Информация за устройството ID - Име - Име на устройството + Публично име + Обнови публичното име Последно видян %1$s @ %2$s Тази операция изискра допълнителна автентикация. @@ -582,9 +582,9 @@ Грешка при разшифроване Информация за устройството на подателя - Име на устройство - Име - ID на устройство + Публично име + Публично име + ID Ключ на устройство Потвърждение Ed25519 отпечатък @@ -745,7 +745,7 @@ Фонова синхронизация Включване на фонова синхронизация Времето за синхронизация изтече - Време за чакане между всяка заявка + Време за чакане между синхронизации Разрешение за достъп до контакти Начало Вибрация при споменаване на потребител @@ -1622,4 +1622,141 @@ Прочетете на + Cyrl + + Нищо + Оттегли + Прекъсни + Не е настроен сървър за самоличност. + + Обаждането се провали поради грешно настроен сървър + Попитайте администратора на сървъра (%1$s) да конфигурира TURN сървър за да може разговорите да работят надеждно. +\n +\nКато алтернатива, също може да използвате публичния сървър %2$s, но това няма да е толкова надеждно, а и ще сподели IP адреса ви със сървъра. Може да управлявате това в Настройки. + Опитай с %s + Не питай пак + + Настройте имейл за възстановяване на профила, а после по желание и за да бъдете откриваеми от познати. + Настройте телефон за възстановяване на профила, а после по желание и за да бъдете откриваеми от познати. + Настройте имейл за възстановяване на профила. По-късно използвайте имейл или телефонен номер за да бъдете откривани от хора, които ви познават. + Настройте имейл за възстановяване на профила. По-късно използвайте имейл или телефонен номер за да бъдете откривани от хора, които ви познават. + Неуспешна връзка със сървъра на този адрес, моля проверете + Позволи използването на резервен сървър за свързване на обаждания + Ще използва %s за асистиращ сървър, когато вашия сървър не предлага такъв (IP адресът ви ще бъде споделен по време на обаждане) + Добавете сървър за самоличност в настройки за да извършите това действие. + Режим на фонова синхронизация (експериментално) + Пестящ батерия + Riot ще синхронизира във фонов режим по начин, който пести ограничените ресурси на устройството (батерия). +\nВ зависимост от състоянието на ресурсите, синхронизацията може да бъде отложена от операционната система. + Целящ висока интерактивност + Riot ще синхронизира във фонов режим на определен интервал (конфигурируемо). +\nТова ще повлияе на използването на антената и батерията. Ще се показва перманентна нотификация, че Riot слуша за събития. + Без фонова синхронизация + Няма да бъдете уведомени за входящи съобщения, когато приложението е във фонов режим. + Неуспешно обновяване на настройките. + + + Предпочитан интервал за синхронизация + %s +\nСинхронизацията може да бъде отложена, в зависимост от ресурсите (батерия) или състоянието на устройството (заспиване). + Откриване + Управлявайте настройките на откриваемостта. + Публично име (видимо за хора, с които общувате) + Публичното име на устройството е видимо за хората, с които общувате + Не използвате сървър за самоличност + Не е настроен сървър за самоличност. Необходим е за възстановяване на паролата. + + Изглежда се опитвате да се свържете с друг сървър. Искате ли да излезете от профила\? + + Сървър за самоличност + Прекъсни сървъра за самоличност + Настрой сървър за самоличност + Промени сървъра за самоличност + В момента използвате %1$s за да откривате и да бъдете открити от съществуващи ваши контакти. + В момента не използвате сървър за самоличност. Настройте такъв по-долу, за да откривате и да бъдете открити от съществуващи ваши контакти. + Откриваеми имейл адреси + Ще се появят настройки за откриваемост след като добавите имейл. + Ще се появят настройки за откриваемост след като добавите телефонен номер. + Прекъсването на връзката със сървъра за самоличност означава, че няма да бъдете откриваеми от други потребители и няма да можете да каните други по имейл или телефон. + Откриваеми телефонни номера + Изпратихме имейл за потвърждение на %s. Проверете имейла и кликнете връзката за потвърждение + Изчакване + + Въведете нов сървър за самоличност + Неуспешна връзка със сървъра за самоличност + Въведете адреса на сървъра за самоличност + Сървъра за самоличност няма условия за ползване + Избрания сървър за самоличност няма условия за ползване на услугата. Продължете само ако вярвате на собственика на услугата + Беше изпратено текстово съобщение на %s. Въведете съдържащият се код за потвърждение. + + В момента споделяте имейл адреси или телефонни номера със сървър за самоличност %1$s. Ще трябва да се свържете наново с %2$s за да спрете да ги споделяте. + Съгласете се с условията за ползване на услугата на сървъра за самоличност (%s) за да бъдете откриваеми по имейл адрес или телефонен номер. + + Включи подробни логове. + Подробните логове помагат на разработчиците, понеже предоставят повече логове когато изпращате RageShake. Дори включени, приложението не записва съдържанието на съобщенията или други лични данни. + + + Опитайте пак след като приемете условията за ползване на сървъра. + + Изглежда сървъра отнема прекалено дълго за да отговори, поради лоша връзка или проблем със сървърите. Опитайте пак по-късно. + + Изпрати прикачен файл + + Отвори навигационния панел + Отвори менюто за създаване на стая + Затвори менюто за създаване на стая… + Създай нова директна кореспонденция + Създай нова стая + Затвори съобщението за резервно копие на ключовете + Покажи паролата + Скрий паролата + Отиди най-отдолу + + %1$s, %2$s и %3$d други прочетоха + %1$s, %2$s и %3$s прочетоха + %1$s и %2$s прочетоха + %s прочете + + 1 потребител прочете + %d потребителя прочетоха + + + Файлът \'%1$s\' (%2$s) е прекалено голям за да се качи. Ограничението е %3$s. + + Възникна грешка при извличане на прикачения файл. + Файл + Контакт + Камера + Аудио + Галерия + Стикер + Неуспешна обработка на споделени данни + + Това е спам + Това е неподходящо + Собствен доклад + Докладване на това съдържание + Причина за докладване на това съдържание + ДОКЛАДВАЙ + БЛОКИРАЙ ПОТРЕБИТЕЛЯ + + Съдържанието беше докладвано + Съдържанието беше докладвано. +\n +\nАко не искате да виждате повече съдържание от този потребител, може да го блокирате за да скриете съобщенията му + Докладвано като спам + Съдържанието беше докладвано като спам. +\n +\nАко не искате да виждате повече съдържание от този потребител, може да го блокирате за да скриете съобщенията му + Докладвано като неподходящо + Съдържанието беше докладвано като неподходящо. +\n +\nАко не искате да виждате повече съдържание от този потребител, може да го блокирате за да скриете съобщенията му + + Riot се нуждае от привилегии за да запази E2E ключовете върху диска. +\n +\nПозволете достъп на следващия екран, за да може в бъдеще да експортирате ключовете си ръчно. + + В момента няма връзка с мрежата +
diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 251c42c00c..47dc512cb4 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -473,7 +473,7 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.
Räume mit ungelesenen Benachrichtigungen anheften Räume mit ungelesenen Nachrichten anheften Geräte - Geräte Information + Geräteinformationen ID Öffentlicher Name Öffentlichen Namen aktualisieren @@ -1707,5 +1707,5 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A ausstehend Gib einen neuen Identitätsserver ein - Konnte keine Verbindung zum Heimserver herstellen. + Konnte keine Verbindung zum Heimserver herstellen diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 651978f92f..fc9546d21d 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -121,8 +121,8 @@ Luo tili Kirjaudu sisään Kirjaudu ulos - Kotipalvelimen URL - Identiteettipalvelimen URL + Kotipalvelimen URL-osoite + Identiteettipalvelimen URL-osoite Etsi @@ -142,7 +142,7 @@ Luo tili Lähetä Ohita - Lähetä nollaussähköposti + Lähetä palautussähköposti Palaa kirjautumiseen Sähköposti tai käyttäjätunnus Salasana @@ -621,7 +621,7 @@ Lähettävän laitteen tiedot Julkinen nimi Julkinen nimi - Laitteen ID + Tunnus Laitteen avain Vahvistus Ed25519-sormenjälki @@ -672,7 +672,7 @@ Valitse huoneluettelo Palvelin saattaa olla tavoittamattomissa tai ylikuormitettu Syötä kotipalvelin, jolta julkiset huoneet listataan - Kotipalvelimen URL + Kotipalvelimen URL-osoite Kaikki huoneet palvelimella %s Kaikki alkuperäiset %s huoneet @@ -1707,4 +1707,6 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Ole löydettävissä Tekstiviesti on lähetetty numeroon %s. Syötä sen sisältämä varmistuskoodi. + Push-sääntöjä ei ole määritetty + Luo uusi yksityiskeskustelu diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml index 3f2469d766..76dafef323 100644 --- a/vector/src/main/res/values-ko/strings.xml +++ b/vector/src/main/res/values-ko/strings.xml @@ -32,7 +32,7 @@ 공유 고유 주소 소스 보기 - 해독된 소스 보기 + 복호화된 소스 보기 삭제 다시 이름 짓기 내용 신고하기 @@ -904,7 +904,7 @@ Ed25519 핑거프린트 키가 필요함 알고리즘 세션 ID - 암호 해독 오류 + 암호 복호화 오류 발신자 기기 정보 공개 이름 @@ -1264,7 +1264,7 @@ 메시지 복구 복구 키를 잃어버렸나요\? 설정에서 새로운 키를 만들 수 있습니다. - 이 암호로 백업을 해독할 수 없습니다: 올바른 복구 암호를 입력해서 확인해주세요. + 이 암호로 백업을 복호화할 수 없습니다: 올바른 복구 암호를 입력해서 확인해주세요. 네트워크 오류: 인터넷 연결 상태를 확인하고 다시 시도해주세요. 백업 복구: @@ -1273,7 +1273,7 @@ 키 가져오는 중… 기록 풀기 복구 키를 입력하세요 - 이 복구 키로 백업을 해독할 수 없습니다: 올바른 복구 키를 입력해서 확인해주세요. + 이 복구 키로 백업을 복호화할 수 없습니다: 올바른 복구 키를 입력해서 확인해주세요. 백업이 복구되었습니다 %s ! %1$d개의 세션 키를 복구했고, 이 기기에서 알려지지 않은 %2$d개의 새 키를 추가함 diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index f299ad4385..10338ddde9 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -1654,4 +1654,71 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Hëpërhë, ndani me të tjerë adresa email ose numra telefoni te shërbyesi i identiteteve %1$s. Do të duhet të rilidheni me %2$s që të ndalni ndarjen e tyre. Që të lejoni veten të jetë e zbulueshme nga adresë email apo numër telefoni, pajtohuni me Kushtet e Shërbimit të shërbyesit të identiteteve (%s). + Latn + + Zbulim + Aktivizo regjistra fjalamanë. + Regjistrat fjalamanë do t’i ndihmojnë zhvilluesit duke furnizuar më tepër regjistrim kur dërgoni një RageShake. Edhe kur është e aktivizuar kjo, aplikimi nuk regjistron lëndë mesazhesh apo çfarëdo të dhëne tjetër private. + + + Ju lutemi, riprovoni sapo të keni pranuar termat dhe kushtet e shërbyesit tuaj Home. + + Duket sikur shërbyesit po i duhet shumë kohë për t’u përgjigjur,kjo mund të shkaktohet ose nga lidhje e dobët, ose nga një gabim me shërbyesit tanë. Ju lutemi, riprovoni pas pak. + + Dërgo bashkëngjitje + + Hapni zonën e lëvizjeve + Hapni menunë e krijimit të dhomave + Mbylleni menunë e krijmit të dhomave… + Krijoni një bisedë të re të drejtpërdrejtë + Krijoni një dhomë të re + Shfaq fjalëkalim + Fshihe fjalëkalimin + Hidhu në fund + + %1$s, %2$s dhe %3$d të tjerë të lexuar + %1$s, %2$s dhe %3$s të lexuar + %1$s dhe %2$s të lexuar + %s i lexuar + + 1 përdorues lexoi + %d përdorues lexuan + + + "Kartela \'%1$s\' (%2$s) është shumë e madhe për ngarkim. Caku është %3$s." + + Ndodhi një gabim gjatë marrjes së bashkëngjitjes. + Kartelë + Kontakt + Kamerë + Audio + Galeri + Ngjitës + Është e padëshiruar + Është e papërshtatshme + Raport vetjak + Raportojeni këtë lëndë + Arsye për raportimin e kësaj lënde + RAPORTOJENI + BLLOKOJENI PËRDORUESIN + + Lënda u raportua + Kjo lëndë është raportuar. +\n +\nNëse s’doni të shihni më lëndë nga ky përdorues, mund ta bllokoni, që të fshihen mesazhet e tij + E raportuar si e padëshiruar + Kjo lëndë është raportuar si e padëshiruar. +\n +\nNëse s’doni të shihni më lëndë nga ky përdorues, mund ta bllokoni, që të fshihen mesazhet e tij + E raportuar si e papërshtatshme + Kjo lëndë është raportuar si e papërshtatshme. +\n +\nNëse s’doni të shihni më lëndë nga ky përdorues, mund ta bllokoni, që të fshihen mesazhet e tij + + Riot-i lyp leje për të ruajtur kyçet tuaj E2E në disk. +\n +\nJu lutemi, lejoni, te flluska pasuese, hyrje për të qenë e mundur të eksportohen kyçet tuaj dorazi. + + Tani për tani s’la lidhje rrjeti + diff --git a/vector/src/main/res/values-tr/strings.xml b/vector/src/main/res/values-tr/strings.xml index 58256833c9..2bee7a14f9 100644 --- a/vector/src/main/res/values-tr/strings.xml +++ b/vector/src/main/res/values-tr/strings.xml @@ -50,9 +50,10 @@ Yeniden Adlandır İçeriği bildir Şu anki görüşme - Devam eden konferans görüşmesi.\n%1$s veya %2$s katıl. + Devam eden konferans görüşmesi. +\n%1$s veya %2$s olarak katıl. Sesli - görüntülü + Görüntülü Görüşme başlatılamıyor, lütfen sonra tekrar deneyin Eksik izinler nedeni ile bazı özellikler eksik olabilir… Bu odada bir konferans başlatma davetiyesi göndermek için izniniz olması gerekmektedir @@ -88,10 +89,10 @@ Topluluklar Odaları ara - Favorileri ara - Kişileri ara - Odaları ara - Toplulukları ara + Favorileri filtrele + Kişileri filtrele + Oda adlarını filtrele + Topluluk adlarını filtrele Davetler Düşük öncelik @@ -137,7 +138,7 @@ Odaya Katıl Kullanıcı adı - Kayıt + Hesap oluştur Giriş yap Çıkış yap Ev Sunucusu URL\'si @@ -276,7 +277,7 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Kayıt olunamadı: Ağ hatası Kayıt olunamadı Kayıt olunamadı: eposta sahiplik hatası - Geçerli bir URL girin + Lütfen geçerli bir URL girin Mobil Geçersiz kullanıcıadı/şifre @@ -1306,4 +1307,22 @@ Neden Riot.im’i seçmeliyim? Algoritma İmza + İşlem başlatılıyor + Cihazı doğrula + + Hiç (Yok) + Geri Al + Bağlantıyı Kes + Önemseme + Gözden Geçir + Reddet + + Okunmuş olarak işaretle + Arama yanlış yapılandırılmış odadan dolayı başarısız oldu + %s kullanmayı deneyin + Yeniden sorma + + Tek oturum açma ile giriş yap + Hesap kurtarması için email ayarla, ve sonradan da isteğe bağlı olarak başklarının seni tanıyan kişilerin bulması için kullan. + Bu adrese erişilemiyor, lütfen kontrol et diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 231575ff2b..4c328cb15e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -630,6 +630,9 @@ Add phone number Application info Show the application info in the system settings. + Confirm your password + You can\'t do this from Riot mobile + Authentication is required Advanced Notification Settings @@ -770,6 +773,8 @@ Ignored users Other Advanced + Integrations + Use an Integration Manager to manage bots, bridges, widgets and sticker packs.\nIntegration Managers receive configuration data, and can modify widgets, send room invites and set power levels on your behalf. Cryptography Cryptography Keys Management Notification Targets @@ -836,6 +841,7 @@ Logged in as Home Server Identity Server + Allow integrations Integration Manager User interface @@ -1108,7 +1114,32 @@ %d active widgets + + Widget + Load Widget + This widget was added by: + Using it may set cookies and share data with %s: + Using it may share data with %s: + Failed to load widget.\n%s + Reload widget + Open in browser + Revoke access for me + + Your display name + Your avatar URL + Your user ID + Your theme + Widget ID + Room ID + + Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 5.0) + This widget wants to use the following resources: + Allow + Block All + Use the camera + Use the microphone + Read DRM protected Media Unable to create widget. @@ -1715,7 +1746,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Please retry once you have accepted the terms and conditions of your homeserver. - Looks like the server is taking to long to respond, this can be caused by either poor connectivity or an error with our servers. Please try again in a while. + Looks like the server is taking too long to respond, this can be caused by either poor connectivity or an error with the server. Please try again in a while. Send attachment From 1cadbb8eedf7b085f916e68c9dbc694f68b3f978 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 2 Dec 2019 10:57:19 +0100 Subject: [PATCH 156/189] Ensure credentials can be stored, even if they already exist --- .../android/internal/auth/db/RealmSessionParamsStore.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt index dfe35c363b..1b15995ae6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt @@ -22,6 +22,8 @@ import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.Realm import io.realm.RealmConfiguration +import io.realm.exceptions.RealmPrimaryKeyConstraintException +import timber.log.Timber import javax.inject.Inject internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper, @@ -63,7 +65,12 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S awaitTransaction(realmConfiguration) { val entity = mapper.map(sessionParams) if (entity != null) { - it.insert(entity) + try { + it.insert(entity) + } catch (e: RealmPrimaryKeyConstraintException) { + Timber.e(e, "Something wrong happened during previous session creation. Override with new credentials") + it.insertOrUpdate(entity) + } } } } From 538c4d1a6446c5e4eb73ecf771df2577c5796de7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 2 Dec 2019 18:15:21 +0100 Subject: [PATCH 157/189] typo --- .../internal/database/SessionRealmConfigurationFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt index 881d7ce2c5..bc806a56a4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -83,7 +83,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val try { File(directory, file).deleteRecursively() } catch (e: Exception) { - Timber.e(e, "Unable to move files") + Timber.e(e, "Unable to delete files") } } } From 5b63856d9670ca6b01a8b093db60063d8c96b308 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 2 Dec 2019 18:33:31 +0100 Subject: [PATCH 158/189] Add log to detect if a realm instance is not properly closed --- .../internal/session/signout/SignOutTask.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index fbeabff0b5..7bff2936fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.signout import android.content.Context +import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.CryptoModule @@ -27,6 +28,8 @@ import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.worker.WorkManagerUtil +import io.realm.Realm +import io.realm.RealmConfiguration import timber.log.Timber import java.io.File import javax.inject.Inject @@ -42,6 +45,8 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, @UserCacheDirectory private val userFile: File, private val realmKeysUtils: RealmKeysUtils, + @SessionDatabase private val realmSessionConfiguration: RealmConfiguration, + @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, @UserMd5 private val userMd5: String) : SignOutTask { override suspend fun execute(params: Unit) { @@ -71,5 +76,15 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte Timber.d("SignOut: clear the database keys") realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5) realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5) + + // Sanity check + if (BuildConfig.DEBUG) { + Realm.getGlobalInstanceCount(realmSessionConfiguration) + .takeIf { it > 0 } + ?.let { Timber.e("All realm instance for session has not been closed ($it)") } + Realm.getGlobalInstanceCount(realmCryptoConfiguration) + .takeIf { it > 0 } + ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") } + } } } From 490ce4b51dabbdf556595f9db2aba1f66c928cb2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 10:05:10 +0100 Subject: [PATCH 159/189] Fix issue of closing Realm in another thread (#725) --- CHANGES.md | 1 + .../session/filter/DefaultFilterRepository.kt | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 64de4ac68d..5e9f9ce671 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Other changes: Bugfix 🐛: - Do not show long click help if only invitation are displayed - Fix emoji filtering not working + - Fix issue of closing Realm in another thread (#725) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt index 53967784a1..33cd245a42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt @@ -30,25 +30,26 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy: override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val filter = FilterEntity.getFilter(realm) - val result = if (filter.filterBodyJson != filterBody.toJSONString()) { - // Filter has changed, store it and reset the filter Id - monarchy.awaitTransaction { + // Filter has changed, or no filter Id yet + filter.filterBodyJson != filterBody.toJSONString() + || filter.filterId.isBlank() + }.also { hasChanged -> + if (hasChanged) { + // Filter is new or has changed, store it and reset the filter Id. + // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread + monarchy.awaitTransaction { realm -> // We manage only one filter for now val filterBodyJson = filterBody.toJSONString() val roomEventFilterJson = roomEventFilter.toJSONString() - val filterEntity = FilterEntity.getFilter(it) + val filterEntity = FilterEntity.getFilter(realm) filterEntity.filterBodyJson = filterBodyJson filterEntity.roomEventFilterJson = roomEventFilterJson // Reset filterId filterEntity.filterId = "" } - true - } else { - filter.filterId.isBlank() } - result } } From 49178dc6332fad02a996b858496830d0b985f6e0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 11:09:14 +0100 Subject: [PATCH 160/189] Reduce some log level --- .../internal/crypto/IncomingRoomKeyRequestManager.kt | 6 +++--- .../im/vector/matrix/android/internal/crypto/MXOlmDevice.kt | 2 +- .../internal/crypto/OutgoingRoomKeyRequestManager.kt | 2 +- .../internal/session/DefaultInitialSyncProgressService.kt | 2 +- .../main/java/im/vector/riotx/features/home/HomeActivity.kt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt index 3c8d70f2f1..aeee025fdc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt @@ -78,7 +78,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") if (userId == null || credentials.userId != userId) { // TODO: determine if we sent this device the keys already: in - Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now") + Timber.w("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now") return } // TODO: should we queue up requests we don't yet have keys for, in case they turn up later? @@ -86,11 +86,11 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( // the keys for the requested events, and can drop the requests. val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) if (null == decryptor) { - Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId") + Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId") continue } if (!decryptor.hasKeysForKeyRequest(request)) { - Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}") + Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}") cryptoStore.deleteIncomingRoomKeyRequest(request) continue } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 68aaaf3831..6171b32811 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -764,7 +764,7 @@ internal class MXOlmDevice @Inject constructor( return session } } else { - Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt index 86e8a1825c..433dee4fb9 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt @@ -187,7 +187,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor( OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND)) if (null == outgoingRoomKeyRequest) { - Timber.e("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests") + Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests") sendOutgoingRoomKeyRequestsRunning.set(false) return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultInitialSyncProgressService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultInitialSyncProgressService.kt index c8bd5154a2..3f653571b7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultInitialSyncProgressService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultInitialSyncProgressService.kt @@ -101,7 +101,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr val parentProgress = (currentProgress * parentWeight).toInt() it.setProgress(offset + parentProgress) } ?: run { - Timber.e("--- ${leaf().nameRes}: $currentProgress") + Timber.v("--- ${leaf().nameRes}: $currentProgress") status.postValue( InitialSyncProgressService.Status(leaf().nameRes, currentProgress) ) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 104aa301cb..1102c67e16 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -97,7 +97,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { if (status == null) { waiting_view.isVisible = false } else { - Timber.e("${getString(status.statusText)} ${status.percentProgress}") + Timber.v("${getString(status.statusText)} ${status.percentProgress}") waiting_view.setOnClickListener { // block interactions } From 9b882978ed00391d1a68976444fc55193ea98be7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 10:14:57 +0100 Subject: [PATCH 161/189] Update modular link --- vector/src/main/java/im/vector/riotx/features/login/Config.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/Config.kt b/vector/src/main/java/im/vector/riotx/features/login/Config.kt index 964e3fa0a1..e35923f5b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/Config.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/Config.kt @@ -16,5 +16,4 @@ package im.vector.riotx.features.login -// TODO Check the link with Nad -const val MODULAR_LINK = "https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication" +const val MODULAR_LINK = "https://modular.im/?utm_source=riot-x-android&utm_medium=native&utm_campaign=riot-x-android-authentication" From 7b4398404b5d9117a38d7b1175ee92221c04d9c5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 10:17:29 +0100 Subject: [PATCH 162/189] Update wording for modular screens --- .../main/java/im/vector/riotx/features/login/LoginFragment.kt | 3 +-- .../riotx/features/login/LoginSignUpSignInSelectionFragment.kt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index cc4f25141e..67935c1ae8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -109,8 +109,7 @@ class LoginFragment @Inject constructor( ServerType.Modular -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_modular) - // TODO - loginTitle.text = getString(resId, "TODO") + loginTitle.text = getString(resId, "Modular") loginNotice.text = getString(R.string.login_server_modular_text) } ServerType.Other -> { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 0473f3d91c..0484357ae2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -46,8 +46,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor( ServerType.Modular -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_modular) loginSignupSigninServerIcon.isVisible = true - // TODO - loginSignupSigninTitle.text = getString(R.string.login_connect_to, "TODO MODULAR NAME") + loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular) loginSignupSigninText.text = state.homeServerUrlSimple } ServerType.Other -> { From 3c6eb4bccfadc5bfab31a60bebc2706cbfbd8e70 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 11:10:11 +0100 Subject: [PATCH 163/189] Rework FilterEntityQueries to fix issue of ghost Realm reference --- .../database/query/FilterEntityQueries.kt | 29 ++++++++++--------- .../session/filter/DefaultFilterRepository.kt | 14 +++++---- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt index 4f64f2896f..6902d39a82 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt @@ -16,26 +16,27 @@ package im.vector.matrix.android.internal.database.query -import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.model.FilterEntity import im.vector.matrix.android.internal.session.filter.FilterFactory import io.realm.Realm +import io.realm.kotlin.createObject import io.realm.kotlin.where +/** + * Get the current filter + */ +internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? { + return realm.where().findFirst() +} + /** * Get the current filter, create one if it does not exist */ -internal suspend fun FilterEntity.Companion.getFilter(realm: Realm): FilterEntity { - var filter = realm.where().findFirst() - if (filter == null) { - filter = FilterEntity().apply { - filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString() - roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() - filterId = "" - } - awaitTransaction(realm.configuration) { - it.insert(filter) - } - } - return filter +internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity { + return get(realm) ?: realm.createObject() + .apply { + filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString() + roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() + filterId = "" + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt index 33cd245a42..ae8e8ce891 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt @@ -19,7 +19,8 @@ package im.vector.matrix.android.internal.session.filter import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.model.FilterEntity import im.vector.matrix.android.internal.database.model.FilterEntityFields -import im.vector.matrix.android.internal.database.query.getFilter +import im.vector.matrix.android.internal.database.query.get +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import io.realm.kotlin.where @@ -29,9 +30,10 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy: override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val filter = FilterEntity.getFilter(realm) + val filter = FilterEntity.get(realm) // Filter has changed, or no filter Id yet - filter.filterBodyJson != filterBody.toJSONString() + filter == null + || filter.filterBodyJson != filterBody.toJSONString() || filter.filterId.isBlank() }.also { hasChanged -> if (hasChanged) { @@ -42,7 +44,7 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy: val filterBodyJson = filterBody.toJSONString() val roomEventFilterJson = roomEventFilter.toJSONString() - val filterEntity = FilterEntity.getFilter(realm) + val filterEntity = FilterEntity.getOrCreate(realm) filterEntity.filterBodyJson = filterBodyJson filterEntity.roomEventFilterJson = roomEventFilterJson @@ -68,7 +70,7 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy: override suspend fun getFilter(): String { return Realm.getInstance(monarchy.realmConfiguration).use { - val filter = FilterEntity.getFilter(it) + val filter = FilterEntity.getOrCreate(it) if (filter.filterId.isBlank()) { // Use the Json format filter.filterBodyJson @@ -81,7 +83,7 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy: override suspend fun getRoomFilter(): String { return Realm.getInstance(monarchy.realmConfiguration).use { - FilterEntity.getFilter(it).roomEventFilterJson + FilterEntity.getOrCreate(it).roomEventFilterJson } } } From 4154cb2b852dcde8fc9d36eca16c8620794fda79 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 13:21:42 +0100 Subject: [PATCH 164/189] Improve wording of the title of read receipt list --- .../room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt | 2 +- vector/src/main/res/values/strings_riotX.xml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt index 50ade56474..f220570e69 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -66,7 +66,7 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { super.onActivityCreated(savedInstanceState) recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) recyclerView.adapter = epoxyController.adapter - bottomSheetTitle.text = getString(R.string.read_at) + bottomSheetTitle.text = getString(R.string.seen_by) epoxyController.setData(displayReadReceiptArgs.readReceipts) } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 5299d0db1f..dd8dd52dc6 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -137,4 +137,6 @@ Too many requests have been sent. You can retry in %1$d seconds… + Seen by + From 4f3da353e411e32db9f8a4419e89d810a1a4d469 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 13:23:44 +0100 Subject: [PATCH 165/189] Add ellipsis char for action with another step --- vector/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 4c328cb15e..57e9ca35d6 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1783,7 +1783,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming "It's spam" "It's inappropriate" - "Custom report" + "Custom report…" "Report this content" "Reason for reporting this content" "REPORT" From 998d9f2c5996f6abc85bb8c260fe718f60fe2e53 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 14:48:41 +0100 Subject: [PATCH 166/189] Bugfix: Text after the last pill was not send --- .../internal/session/room/send/pills/TextPillsUtils.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index 02f48e5800..580e49b2ce 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -62,10 +62,14 @@ internal class TextPillsUtils @Inject constructor( var currIndex = 0 pills.forEachIndexed { _, (urlSpan, start, end) -> // We want to replace with the pill with a html link + // append text before pill append(text, currIndex, start) + // append the pill append(String.format(template, urlSpan.userId, urlSpan.displayName)) currIndex = end } + // append text after the last pill + append(text, currIndex, text.length) } } From 71de8fdad3cfeedec0c74ace7b4ca2769338aaf8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 15:05:41 +0100 Subject: [PATCH 167/189] Display pills Avatar in the message preview --- .../BottomSheetItemMessagePreview.kt | 2 ++ .../detail/timeline/item/MessageTextItem.kt | 20 ++--------- .../timeline/tools/EventRenderingTools.kt | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt index 999068b289..3e3f1d3cf2 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt @@ -25,6 +25,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess /** * A message preview for bottom sheet. @@ -49,6 +50,7 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() { holder.messageView.setOnClickListener(attributes.itemClickListener) holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) if (searchForPills) { - findPillsAndProcess { it.bind(holder.messageView) } + message?.findPillsAndProcess { it.bind(holder.messageView) } } val textFuture = PrecomputedTextCompat.getTextFuture( message ?: "", @@ -85,17 +80,6 @@ abstract class MessageTextItem : AbsMessageItem() { holder.messageView.setTextFuture(textFuture) } - private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) { - GlobalScope.launch(Dispatchers.Main) { - val pillImageSpans: Array? = withContext(Dispatchers.IO) { - message?.toSpannable()?.let { spannable -> - spannable.getSpans(0, spannable.length, PillImageSpan::class.java) - } - } - pillImageSpans?.forEach { processBlock(it) } - } - } - override fun getViewType() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt new file mode 100644 index 0000000000..685799cd32 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.tools + +import androidx.core.text.toSpannable +import im.vector.riotx.features.html.PillImageSpan +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) { + GlobalScope.launch(Dispatchers.Main) { + withContext(Dispatchers.IO) { + toSpannable().let { spannable -> + spannable.getSpans(0, spannable.length, PillImageSpan::class.java) + } + }.forEach { processBlock(it) } + } +} From 6d7f2670df15ef32baa00497f122b46f2945273e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 16:02:07 +0100 Subject: [PATCH 168/189] Make url clickable on the preview of event in the bottom sheet --- .../BottomSheetItemMessagePreview.kt | 5 +++ .../home/room/detail/RoomDetailFragment.kt | 6 +++ .../timeline/action/EventSharedAction.kt | 8 ++++ .../action/MessageActionsBottomSheet.kt | 12 ++++++ .../action/MessageActionsEpoxyController.kt | 7 +++- .../timeline/factory/MessageItemFactory.kt | 34 +++++---------- .../detail/timeline/item/MessageTextItem.kt | 25 +---------- .../timeline/tools/EventRenderingTools.kt | 42 +++++++++++++++++++ vector/src/main/res/values/styles_riot.xml | 3 ++ 9 files changed, 93 insertions(+), 49 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt index 3e3f1d3cf2..7f6ff6fc78 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt @@ -25,6 +25,8 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess /** @@ -45,10 +47,13 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel { roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId)) } + is EventSharedAction.OnUrlClicked -> { + onUrlClicked(action.url) + } + is EventSharedAction.OnUrlLongClicked -> { + onUrlLongClicked(action.url) + } else -> { Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt index 37d96ad62c..8077786d06 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -88,4 +88,12 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, @DrawableRes val ic data class ViewEditHistory(val messageInformationData: MessageInformationData) : EventSharedAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) + + // An url in the event preview has been clicked + data class OnUrlClicked(val url: String) : + EventSharedAction(0, 0) + + // An url in the event preview has been long clicked + data class OnUrlLongClicked(val url: String) : + EventSharedAction(0, 0) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 3f4171f733..a5bf6f8558 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -68,6 +68,18 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message messageActionsEpoxyController.listener = this } + override fun onUrlClicked(url: String): Boolean { + sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url)) + // Always consume + return true + } + + override fun onUrlLongClicked(url: String): Boolean { + sharedActionViewModel.post(EventSharedAction.OnUrlLongClicked(url)) + // Always consume + return true + } + override fun didSelectMenuAction(eventAction: EventSharedAction) { if (eventAction is EventSharedAction.ReportContent) { // Toggle report menu diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index b561a6df3c..53a7ce0354 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -23,6 +23,8 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.bottomsheet.* import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import javax.inject.Inject /** @@ -44,7 +46,8 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid avatarUrl(state.informationData.avatarUrl ?: "") senderId(state.informationData.senderId) senderName(state.senderName()) - body(body) + urlClickCallback(listener) + body(body.linkify(listener)) time(state.time()) } } @@ -127,7 +130,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid } } - interface MessageActionsEpoxyControllerListener { + interface MessageActionsEpoxyControllerListener : TimelineEventController.UrlClickCallback { fun didSelectMenuAction(eventAction: EventSharedAction) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index ac6c563099..417f8d2f9a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -24,8 +24,6 @@ import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.view.View import dagger.Lazy -import im.vector.matrix.android.api.permalinks.MatrixLinkify -import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.* @@ -35,7 +33,6 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel -import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DebouncedClickListener @@ -45,8 +42,9 @@ import im.vector.riotx.core.utils.isLocalFile import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.* import im.vector.riotx.features.home.room.detail.timeline.item.* -import im.vector.riotx.features.html.EventHtmlRenderer +import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import im.vector.riotx.features.html.CodeVisitor +import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import me.gujun.android.span.span @@ -89,7 +87,7 @@ class MessageItemFactory @Inject constructor( return defaultItemFactory.create(malformedText, informationData, highlight, callback) } if (messageContent.relatesTo?.type == RelationType.REPLACE - || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event return noticeItemFactory.create(event, highlight, readMarkerVisible, callback) @@ -195,8 +193,7 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.videoInfo?.thumbnailFile?.url - ?: messageContent.videoInfo?.thumbnailUrl, + url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, @@ -258,7 +255,7 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { - val linkifiedBody = linkifyBody(body, callback) + val linkifiedBody = body.linkify(callback) return MessageTextItem_().apply { if (informationData.hasBeenEdited) { @@ -326,9 +323,9 @@ class MessageItemFactory @Inject constructor( // nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } @@ -344,7 +341,7 @@ class MessageItemFactory @Inject constructor( textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) textStyle = "italic" } - linkifyBody(formattedBody, callback) + formattedBody.linkify(callback) } return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) @@ -361,7 +358,7 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes): MessageTextItem? { val message = messageContent.body.let { val formattedBody = "* ${informationData.memberName} $it" - linkifyBody(formattedBody, callback) + formattedBody.linkify(callback) } return MessageTextItem_() .apply { @@ -386,17 +383,6 @@ class MessageItemFactory @Inject constructor( .highlighted(highlight) } - private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { - val spannable = SpannableStringBuilder(body) - MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { - override fun onUrlClicked(url: String) { - callback?.onUrlClicked(url) - } - }) - VectorLinkify.addLinks(spannable, true) - return spannable - } - companion object { private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5 } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt index cbdc192425..15aa5aa4cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -16,17 +16,15 @@ package im.vector.riotx.features.home.room.detail.timeline.item -import android.view.MotionEvent import androidx.appcompat.widget.AppCompatTextView import androidx.core.text.PrecomputedTextCompat import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.utils.isValidUrl import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess -import me.saket.bettermovementmethod.BetterLinkMovementMethod @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageTextItem : AbsMessageItem() { @@ -40,28 +38,9 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var urlClickCallback: TimelineEventController.UrlClickCallback? = null - // Better link movement methods fixes the issue when - // long pressing to open the context menu on a TextView also triggers an autoLink click. - private val mvmtMethod = BetterLinkMovementMethod.newInstance().also { - it.setOnLinkClickListener { _, url -> - // Return false to let android manage the click on the link, or true if the link is handled by the application - url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true - } - // We need also to fix the case when long click on link will trigger long click on cell - it.setOnLinkLongClickListener { tv, url -> - // Long clicks are handled by parent, return true to block android to do something with url - if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) { - tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)) - true - } else { - false - } - } - } - override fun bind(holder: Holder) { super.bind(holder) - holder.messageView.movementMethod = mvmtMethod + holder.messageView.movementMethod = createLinkMovementMethod(urlClickCallback) if (useBigFont) { holder.messageView.textSize = 44F } else { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt index 685799cd32..492248985e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt @@ -16,12 +16,20 @@ package im.vector.riotx.features.home.room.detail.timeline.tools +import android.text.SpannableStringBuilder +import android.view.MotionEvent import androidx.core.text.toSpannable +import im.vector.matrix.android.api.permalinks.MatrixLinkify +import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan +import im.vector.riotx.core.linkify.VectorLinkify +import im.vector.riotx.core.utils.isValidUrl +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.html.PillImageSpan import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import me.saket.bettermovementmethod.BetterLinkMovementMethod fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) { GlobalScope.launch(Dispatchers.Main) { @@ -32,3 +40,37 @@ fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) { }.forEach { processBlock(it) } } } + +fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence { + val spannable = SpannableStringBuilder(this) + MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { + override fun onUrlClicked(url: String) { + callback?.onUrlClicked(url) + } + }) + VectorLinkify.addLinks(spannable, true) + return spannable +} + +// Better link movement methods fixes the issue when +// long pressing to open the context menu on a TextView also triggers an autoLink click. +fun createLinkMovementMethod(urlClickCallback: TimelineEventController.UrlClickCallback?): BetterLinkMovementMethod { + return BetterLinkMovementMethod.newInstance() + .apply { + setOnLinkClickListener { _, url -> + // Return false to let android manage the click on the link, or true if the link is handled by the application + url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true + } + + // We need also to fix the case when long click on link will trigger long click on cell + setOnLinkLongClickListener { tv, url -> + // Long clicks are handled by parent, return true to block android to do something with url + if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) { + tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)) + true + } else { + false + } + } + } +} diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 798c7ced87..ea41a3c7ca 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -266,6 +266,7 @@ @color/riot_secondary_text_color_dark @color/riot_tertiary_text_color_dark + @color/riotx_links From c69852c849f548b7f7bf7f9ce94e35fb6ece4fab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 16:17:49 +0100 Subject: [PATCH 169/189] Make url clickable on the preview of event in the bottom sheet - avoid instantiating objects in the bind() method --- .../epoxy/bottomsheet/BottomSheetItemMessagePreview.kt | 7 +++---- .../timeline/action/MessageActionsEpoxyController.kt | 3 ++- .../room/detail/timeline/factory/EncryptedItemFactory.kt | 7 ++++--- .../room/detail/timeline/factory/MessageItemFactory.kt | 7 ++++--- .../home/room/detail/timeline/item/MessageTextItem.kt | 7 +++---- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt index 7f6ff6fc78..fdad24c34e 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt @@ -16,6 +16,7 @@ */ package im.vector.riotx.core.epoxy.bottomsheet +import android.text.method.MovementMethod import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute @@ -25,8 +26,6 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess /** @@ -48,12 +47,12 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 417f8d2f9a..de2686de04 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -42,6 +42,7 @@ import im.vector.riotx.core.utils.isLocalFile import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.* import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import im.vector.riotx.features.html.CodeVisitor import im.vector.riotx.features.html.EventHtmlRenderer @@ -270,7 +271,7 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) - .urlClickCallback(callback) + .movementMethod(createLinkMovementMethod(callback)) } private fun buildCodeBlockItem(formattedBody: CharSequence, @@ -348,7 +349,7 @@ class MessageItemFactory @Inject constructor( .attributes(attributes) .message(message) .highlighted(highlight) - .urlClickCallback(callback) + .movementMethod(createLinkMovementMethod(callback)) } private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, @@ -372,7 +373,7 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) - .urlClickCallback(callback) + .movementMethod(createLinkMovementMethod(callback)) } private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt index 15aa5aa4cb..5ee0576be7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -16,14 +16,13 @@ package im.vector.riotx.features.home.room.detail.timeline.item +import android.text.method.MovementMethod import androidx.appcompat.widget.AppCompatTextView import androidx.core.text.PrecomputedTextCompat import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess @EpoxyModelClass(layout = R.layout.item_timeline_event_base) @@ -36,11 +35,11 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var useBigFont: Boolean = false @EpoxyAttribute - var urlClickCallback: TimelineEventController.UrlClickCallback? = null + var movementMethod: MovementMethod? = null override fun bind(holder: Holder) { super.bind(holder) - holder.messageView.movementMethod = createLinkMovementMethod(urlClickCallback) + holder.messageView.movementMethod = movementMethod if (useBigFont) { holder.messageView.textSize = 44F } else { From 69f923383cfffd85cbeb84ded6676138e0cced6d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 16:32:25 +0100 Subject: [PATCH 170/189] Rename some classes with "Item" suffix, as a convention (ooi) --- .../features/debug/sas/SasEmojiController.kt | 2 +- .../sas/{ItemSasEmoji.kt => SasEmojiItem.kt} | 2 +- ...ItemAction.kt => BottomSheetActionItem.kt} | 2 +- ...ew.kt => BottomSheetMessagePreviewItem.kt} | 2 +- ...ns.kt => BottomSheetQuickReactionsItem.kt} | 2 +- ...eview.kt => BottomSheetRoomPreviewItem.kt} | 2 +- ...ndState.kt => BottomSheetSendStateItem.kt} | 2 +- ...parator.kt => BottomSheetSeparatorItem.kt} | 2 +- .../action/MessageActionsEpoxyController.kt | 20 +++++++++---------- .../RoomListQuickActionsEpoxyController.kt | 14 ++++++------- 10 files changed, 25 insertions(+), 25 deletions(-) rename vector/src/debug/java/im/vector/riotx/features/debug/sas/{ItemSasEmoji.kt => SasEmojiItem.kt} (96%) rename vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/{BottomSheetItemAction.kt => BottomSheetActionItem.kt} (97%) rename vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/{BottomSheetItemMessagePreview.kt => BottomSheetMessagePreviewItem.kt} (95%) rename vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/{BottomSheetItemQuickReactions.kt => BottomSheetQuickReactionsItem.kt} (96%) rename vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/{BottomSheetItemRoomPreview.kt => BottomSheetRoomPreviewItem.kt} (95%) rename vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/{BottomSheetItemSendState.kt => BottomSheetSendStateItem.kt} (94%) rename vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/{BottomSheetItemSeparator.kt => BottomSheetSeparatorItem.kt} (90%) diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt index daf432fb45..6804828b20 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt @@ -29,7 +29,7 @@ class SasEmojiController : TypedEpoxyController() { if (data == null) return data.emojiList.forEachIndexed { idx, emojiRepresentation -> - itemSasEmoji { + sasEmojiItem { id(idx) index(idx) emojiRepresentation(emojiRepresentation) diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt similarity index 96% rename from vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt rename to vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt index 92d9bc0b11..cf35873f6b 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt @@ -25,7 +25,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @EpoxyModelClass(layout = im.vector.riotx.R.layout.item_sas_emoji) -abstract class ItemSasEmoji : VectorEpoxyModel() { +abstract class SasEmojiItem : VectorEpoxyModel() { @EpoxyAttribute var index: Int = 0 diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt similarity index 97% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt index 483650a434..c55dbdde8a 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt @@ -37,7 +37,7 @@ import im.vector.riotx.features.themes.ThemeUtils * A action for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_action) -abstract class BottomSheetItemAction : VectorEpoxyModel() { +abstract class BottomSheetActionItem : VectorEpoxyModel() { @EpoxyAttribute @DrawableRes diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt similarity index 95% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index fdad24c34e..8105d7a7c0 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -32,7 +32,7 @@ import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProc * A message preview for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview) -abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() { +abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemQuickReactions.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetQuickReactionsItem.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemQuickReactions.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetQuickReactionsItem.kt index 4483340d02..aad033601d 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemQuickReactions.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetQuickReactionsItem.kt @@ -29,7 +29,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel * A quick reaction list for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction) -abstract class BottomSheetItemQuickReactions : VectorEpoxyModel() { +abstract class BottomSheetQuickReactionsItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var fontProvider: EmojiCompatFontProvider diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt similarity index 95% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt index 9b9d0fc380..1a5b4e2f66 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt @@ -31,7 +31,7 @@ import im.vector.riotx.features.home.AvatarRenderer * A room preview for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_room_preview) -abstract class BottomSheetItemRoomPreview : VectorEpoxyModel() { +abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt similarity index 94% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt index 08d727cfa9..8f830ba706 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt @@ -30,7 +30,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel * A send state for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status) -abstract class BottomSheetItemSendState : VectorEpoxyModel() { +abstract class BottomSheetSendStateItem : VectorEpoxyModel() { @EpoxyAttribute var showProgress: Boolean = false diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt similarity index 90% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt index fddf507bf9..dd41d5dd66 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt @@ -22,7 +22,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider) -abstract class BottomSheetItemSeparator : VectorEpoxyModel() { +abstract class BottomSheetSeparatorItem : VectorEpoxyModel() { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 59ee21adfe..efbfd3434c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -41,7 +41,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid // Message preview val body = state.messageBody if (body != null) { - bottomSheetItemMessagePreview { + bottomSheetMessagePreviewItem { id("preview") avatarRenderer(avatarRenderer) avatarUrl(state.informationData.avatarUrl ?: "") @@ -55,13 +55,13 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid // Send state if (state.informationData.sendState.isSending()) { - bottomSheetItemSendState { + bottomSheetSendStateItem { id("send_state") showProgress(true) text(stringProvider.getString(R.string.event_status_sending_message)) } } else if (state.informationData.sendState.hasFailed()) { - bottomSheetItemSendState { + bottomSheetSendStateItem { id("send_state") showProgress(false) text(stringProvider.getString(R.string.unable_to_send_message)) @@ -72,16 +72,16 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid // Quick reactions if (state.canReact() && state.quickStates is Success) { // Separator - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("reaction_separator") } - bottomSheetItemQuickReactions { + bottomSheetQuickReactionsItem { id("quick_reaction") fontProvider(fontProvider) texts(state.quickStates()?.map { it.reaction }.orEmpty()) selecteds(state.quickStates.invoke().map { it.isSelected }) - listener(object : BottomSheetItemQuickReactions.Listener { + listener(object : BottomSheetQuickReactionsItem.Listener { override fun didSelect(emoji: String, selected: Boolean) { listener?.didSelectMenuAction(EventSharedAction.QuickReact(state.eventId, emoji, selected)) } @@ -90,18 +90,18 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid } // Separator - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("actions_separator") } // Action state.actions()?.forEachIndexed { index, action -> if (action is EventSharedAction.Separator) { - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("separator_$index") } } else { - bottomSheetItemAction { + bottomSheetActionItem { id("action_$index") iconRes(action.iconResId) textRes(action.titleRes) @@ -118,7 +118,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid EventSharedAction.ReportContentInappropriate(action.eventId, action.senderId), EventSharedAction.ReportContentCustom(action.eventId, action.senderId) ).forEachIndexed { indexReport, actionReport -> - bottomSheetItemAction { + bottomSheetActionItem { id("actionReport_$indexReport") subMenuItem(true) iconRes(actionReport.iconResId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt index 2e17464cc6..84fd5bc6f2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt @@ -18,9 +18,9 @@ package im.vector.riotx.features.home.room.list.actions import android.view.View import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.room.notification.RoomNotificationState -import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemAction -import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemRoomPreview -import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemSeparator +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetActionItem +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetSeparatorItem import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject @@ -36,7 +36,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar val roomSummary = state.roomSummary() ?: return // Preview - bottomSheetItemRoomPreview { + bottomSheetRoomPreviewItem { id("preview") avatarRenderer(avatarRenderer) roomName(roomSummary.displayName) @@ -46,7 +46,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar } // Notifications - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("notifications_separator") } @@ -57,7 +57,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState) // Leave - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("leave_separator") } RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5) @@ -72,7 +72,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar is RoomListQuickActionsSharedAction.Settings, is RoomListQuickActionsSharedAction.Leave -> false } - return bottomSheetItemAction { + return bottomSheetActionItem { id("action_$index") selected(selected) iconRes(iconResId) From ff267ba9bc6d63f87fe712bb25cbd28658e7e419 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 16:36:44 +0100 Subject: [PATCH 171/189] Update changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 64de4ac68d..3ead09faac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features ✨: Improvements 🙌: - Send mention Pills from composer + - Links in message preview in the bottom sheet are now active. Other changes: - Fix a small grammatical error when an empty room list is shown. From 5e07e96bdb8857de41b2bbfc99cc8dde4b39e2ed Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 19 Nov 2019 19:49:48 +0100 Subject: [PATCH 172/189] Read marker: start reworking how we manage it [WIP] --- .../session/room/timeline/TimelineEvent.kt | 3 +- .../database/helper/ChunkEntityHelper.kt | 16 +-- .../database/mapper/TimelineEventMapper.kt | 5 +- .../database/model/ReadMarkerEntity.kt | 5 - .../database/model/TimelineEventEntity.kt | 3 +- .../session/room/timeline/DefaultTimeline.kt | 23 +-- .../room/timeline/DefaultTimelineService.kt | 21 ++- .../room/timeline/TimelineHiddenReadMarker.kt | 133 ------------------ .../session/sync/RoomFullyReadHandler.kt | 18 +-- .../riotx/core/extensions/TimelineEvent.kt | 4 - .../riotx/core/ui/views/ReadMarkerView.kt | 89 ------------ .../home/room/detail/ReadMarkerHelper.kt | 35 ----- .../home/room/detail/RoomDetailAction.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 36 +---- .../home/room/detail/RoomDetailViewModel.kt | 111 +++++++++------ .../home/room/detail/RoomDetailViewState.kt | 3 +- .../timeline/TimelineEventController.kt | 87 ++++++------ .../timeline/factory/DefaultItemFactory.kt | 3 +- .../timeline/factory/EncryptedItemFactory.kt | 3 +- .../factory/MergedHeaderItemFactory.kt | 11 -- .../timeline/factory/MessageItemFactory.kt | 5 +- .../timeline/factory/NoticeItemFactory.kt | 3 +- .../timeline/factory/TimelineItemFactory.kt | 13 +- .../helper/MessageInformationDataFactory.kt | 8 +- ...lineEventVisibilityStateChangedListener.kt | 13 ++ .../detail/timeline/item/AbsMessageItem.kt | 15 -- .../detail/timeline/item/BaseEventItem.kt | 2 - .../detail/timeline/item/MergedHeaderItem.kt | 20 --- .../timeline/item/MessageInformationData.kt | 4 +- .../room/detail/timeline/item/NoticeItem.kt | 18 --- .../timeline/item/TimelineReadMarkerItem.kt | 31 ++++ .../res/layout/item_timeline_event_base.xml | 13 +- .../item_timeline_event_base_noinfo.xml | 30 +--- .../res/layout/item_timeline_read_marker.xml | 18 +++ 34 files changed, 236 insertions(+), 570 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt delete mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt create mode 100644 vector/src/main/res/layout/item_timeline_read_marker.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index ad747efee9..ed7f49aa46 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -41,8 +41,7 @@ data class TimelineEvent( val isUniqueDisplayName: Boolean, val senderAvatar: String?, val annotations: EventAnnotationsSummary? = null, - val readReceipts: List = emptyList(), - val hasReadMarker: Boolean = false + val readReceipts: List = emptyList() ) { val metadata = HashMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index e9ffa140c9..826b35254e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -23,7 +23,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -140,7 +139,7 @@ internal fun ChunkEntity.add(roomId: String, val senderId = event.senderId ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId, roomId) + ?: ReadReceiptsSummaryEntity(eventId, roomId) // Update RR for the sender of a new message with a dummy one @@ -168,7 +167,6 @@ internal fun ChunkEntity.add(roomId: String, it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.readReceipts = readReceiptsSummaryEntity - it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst() } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) @@ -176,14 +174,14 @@ internal fun ChunkEntity.add(roomId: String, internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsDisplayIndex - PaginationDirection.BACKWARDS -> backwardsDisplayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 8046ecbff0..9959f940b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -36,7 +36,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS } return TimelineEvent( root = timelineEventEntity.root?.asDomain() - ?: Event("", timelineEventEntity.eventId), + ?: Event("", timelineEventEntity.eventId), annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, displayIndex = timelineEventEntity.root?.displayIndex ?: 0, @@ -45,8 +45,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderAvatar = timelineEventEntity.senderAvatar, readReceipts = readReceipts?.sortedByDescending { it.originServerTs - } ?: emptyList(), - hasReadMarker = timelineEventEntity.readMarker?.eventId?.isNotEmpty() == true + } ?: emptyList() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt index 9e78c94f88..4d16d120d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt @@ -17,8 +17,6 @@ package im.vector.matrix.android.internal.database.model import io.realm.RealmObject -import io.realm.RealmResults -import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey internal open class ReadMarkerEntity( @@ -27,8 +25,5 @@ internal open class ReadMarkerEntity( var eventId: String = "" ) : RealmObject() { - @LinkingObjects("readMarker") - val timelineEvent: RealmResults? = null - companion object } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt index fd3a427781..235910b1ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt @@ -30,8 +30,7 @@ internal open class TimelineEventEntity(var localId: Long = 0, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, var senderMembershipEvent: EventEntity? = null, - var readReceipts: ReadReceiptsSummaryEntity? = null, - var readMarker: ReadMarkerEntity? = null + var readReceipts: ReadReceiptsSummaryEntity? = null ) : RealmObject() { @LinkingObjects("timelineEvents") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 4127e43540..aa4bd42bf7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -74,9 +74,8 @@ internal class DefaultTimeline( private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, - private val hiddenReadReceipts: TimelineHiddenReadReceipts, - private val hiddenReadMarker: TimelineHiddenReadMarker -) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate { + private val hiddenReadReceipts: TimelineHiddenReadReceipts +) : Timeline, TimelineHiddenReadReceipts.Delegate { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -197,7 +196,6 @@ internal class DefaultTimeline( if (settings.buildReadReceipts) { hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } - hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this) isReady.set(true) } } @@ -217,7 +215,6 @@ internal class DefaultTimeline( if (this::filteredEvents.isInitialized) { filteredEvents.removeAllChangeListeners() } - hiddenReadMarker.dispose() if (settings.buildReadReceipts) { hiddenReadReceipts.dispose() } @@ -298,7 +295,7 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } -// TimelineHiddenReadReceipts.Delegate + // TimelineHiddenReadReceipts.Delegate override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { return rebuildEvent(eventId) { te -> @@ -310,19 +307,7 @@ internal class DefaultTimeline( postSnapshot() } -// TimelineHiddenReadMarker.Delegate - - override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean { - return rebuildEvent(eventId) { te -> - te.copy(hasReadMarker = hasReadMarker) - } - } - - override fun onReadMarkerUpdated() { - postSnapshot() - } - -// Private methods ***************************************************************************** + // Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { return builtEventsIdMap[eventId]?.let { builtIndex -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 3bd67d38c3..d92dbd66be 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -53,17 +53,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { return DefaultTimeline(roomId, - eventId, - monarchy.realmConfiguration, - taskExecutor, - contextOfEventTask, - clearUnlinkedEventsTask, - paginationTask, - cryptoService, - timelineEventMapper, - settings, - TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), - TimelineHiddenReadMarker(roomId, settings) + eventId, + monarchy.realmConfiguration, + taskExecutor, + contextOfEventTask, + clearUnlinkedEventsTask, + paginationTask, + cryptoService, + timelineEventMapper, + settings, + TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt deleted file mode 100644 index 4f80883bf9..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - - */ - -package im.vector.matrix.android.internal.session.room.timeline - -import im.vector.matrix.android.api.session.room.timeline.TimelineSettings -import im.vector.matrix.android.internal.database.model.ReadMarkerEntity -import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields -import im.vector.matrix.android.internal.database.query.FilterContent -import im.vector.matrix.android.internal.database.query.where -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.RealmResults - -/** - * This class is responsible for handling the read marker for hidden events. - * When an hidden event has read marker, we want to transfer it on the first older displayed event. - * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. - */ -internal class TimelineHiddenReadMarker constructor(private val roomId: String, - private val settings: TimelineSettings) { - - interface Delegate { - fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean - fun onReadMarkerUpdated() - } - - private var previousDisplayedEventId: String? = null - private var hiddenReadMarker: RealmResults? = null - - private lateinit var filteredEvents: RealmResults - private lateinit var nonFilteredEvents: RealmResults - private lateinit var delegate: Delegate - - private val readMarkerListener = OrderedRealmCollectionChangeListener> { readMarkers, changeSet -> - if (!readMarkers.isLoaded || !readMarkers.isValid) { - return@OrderedRealmCollectionChangeListener - } - var hasChange = false - if (changeSet.deletions.isNotEmpty()) { - previousDisplayedEventId?.also { - hasChange = delegate.rebuildEvent(it, false) - previousDisplayedEventId = null - } - } - val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener - val hiddenEvent = readMarker.timelineEvent?.firstOrNull() - ?: return@OrderedRealmCollectionChangeListener - - val isLoaded = nonFilteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId) - .findFirst() != null - - val displayIndex = hiddenEvent.root?.displayIndex - if (isLoaded && displayIndex != null) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = filteredEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) - .findFirst() - - // If we find one, we should rebuild this one with marker - if (firstDisplayedEvent != null) { - previousDisplayedEventId = firstDisplayedEvent.eventId - hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true) - } - } - if (hasChange) { - delegate.onReadMarkerUpdated() - } - } - - /** - * Start the realm query subscription. Has to be called on an HandlerThread - */ - fun start(realm: Realm, - filteredEvents: RealmResults, - nonFilteredEvents: RealmResults, - delegate: Delegate) { - this.filteredEvents = filteredEvents - this.nonFilteredEvents = nonFilteredEvents - this.delegate = delegate - // We are looking for read receipts set on hidden events. - // We only accept those with a timelineEvent (so coming from pagination/sync). - hiddenReadMarker = ReadMarkerEntity.where(realm, roomId = roomId) - .isNotEmpty(ReadMarkerEntityFields.TIMELINE_EVENT) - .filterReceiptsWithSettings() - .findAllAsync() - .also { it.addChangeListener(readMarkerListener) } - } - - /** - * Dispose the realm query subscription. Has to be called on an HandlerThread - */ - fun dispose() { - this.hiddenReadMarker?.removeAllChangeListeners() - } - - /** - * We are looking for readMarker related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. - */ - private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { - beginGroup() - if (settings.filterTypes) { - not().`in`("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) - } - if (settings.filterTypes && settings.filterEdits) { - or() - } - if (settings.filterEdits) { - like("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) - } - endGroup() - return this - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index 853774460f..61ae8b9925 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -16,14 +16,10 @@ package im.vector.matrix.android.internal.session.sync -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.getOrCreate -import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.read.FullyReadContent import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -39,18 +35,8 @@ internal class RoomFullyReadHandler @Inject constructor() { RoomSummaryEntity.getOrCreate(realm, roomId).apply { readMarkerId = content.eventId } - // Remove the old markers if any - val oldReadMarkerEvents = TimelineEventEntity - .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) - .isNotNull(TimelineEventEntityFields.READ_MARKER.`$`) - .findAll() - - oldReadMarkerEvents.forEach { it.readMarker = null } - val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { + ReadMarkerEntity.getOrCreate(realm, roomId).apply { this.eventId = content.eventId } - // Attach to timelineEvent if known - val timelineEventEntities = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findAll() - timelineEventEntities.forEach { it.readMarker = readMarkerEntity } } } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt index 387105c480..388ec9bebe 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt @@ -24,7 +24,3 @@ fun TimelineEvent.canReact(): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment return root.getClearType() == EventType.MESSAGE && root.sendState == SendState.SYNCED && !root.isRedacted() } - -fun TimelineEvent.displayReadMarker(myUserId: String): Boolean { - return hasReadMarker && readReceipts.find { it.user.userId == myUserId } == null -} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt deleted file mode 100644 index 0fb8b55250..0000000000 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - - */ - -package im.vector.riotx.core.ui.views - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.view.animation.Animation -import android.view.animation.AnimationUtils -import im.vector.riotx.R -import kotlinx.coroutines.* - -private const val DELAY_IN_MS = 1_000L - -class ReadMarkerView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { - - interface Callback { - fun onReadMarkerLongBound(isDisplayed: Boolean) - } - - private var eventId: String? = null - private var callback: Callback? = null - private var callbackDispatcherJob: Job? = null - - fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) { - this.eventId = eventId - this.callback = readMarkerCallback - if (displayReadMarker) { - startAnimation() - } else { - this.animation?.cancel() - this.visibility = INVISIBLE - } - if (hasReadMarker) { - callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { - delay(DELAY_IN_MS) - callback?.onReadMarkerLongBound(displayReadMarker) - } - } - } - - fun unbind() { - this.callbackDispatcherJob?.cancel() - this.callback = null - this.eventId = null - this.animation?.cancel() - this.visibility = INVISIBLE - } - - private fun startAnimation() { - if (animation == null) { - animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim) - animation.startOffset = DELAY_IN_MS / 2 - animation.duration = DELAY_IN_MS / 2 - animation.setAnimationListener(object : Animation.AnimationListener { - override fun onAnimationStart(animation: Animation) { - } - - override fun onAnimationEnd(animation: Animation) { - visibility = INVISIBLE - } - - override fun onAnimationRepeat(animation: Animation) {} - }) - } - visibility = VISIBLE - animation.start() - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt index 7b3ebeb71c..98556cc7fa 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt @@ -28,49 +28,14 @@ class ReadMarkerHelper @Inject constructor() { lateinit var timelineEventController: TimelineEventController lateinit var layoutManager: LinearLayoutManager var callback: Callback? = null - - private var onReadMarkerLongDisplayed = false private var jumpToReadMarkerVisible = false - private var readMarkerVisible: Boolean = true private var state: RoomDetailViewState? = null - fun readMarkerVisible(): Boolean { - return readMarkerVisible - } - - fun onResume() { - onReadMarkerLongDisplayed = false - } - - fun onReadMarkerLongDisplayed() { - onReadMarkerLongDisplayed = true - } - fun updateWith(newState: RoomDetailViewState) { state = newState - checkReadMarkerVisibility() checkJumpToReadMarkerVisibility() } - fun onTimelineScrolled() { - checkJumpToReadMarkerVisibility() - } - - private fun checkReadMarkerVisibility() { - val nonNullState = this.state ?: return - val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - readMarkerVisible = if (!onReadMarkerLongDisplayed) { - true - } else { - if (nonNullState.timeline?.isLive == false) { - true - } else { - !(firstVisibleItem == 0 && lastVisibleItem > 0) - } - } - } - private fun checkJumpToReadMarkerVisibility() { val nonNullState = this.state ?: return val lastVisibleItem = layoutManager.findLastVisibleItemPosition() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 0a6321dd57..c1743ae3fc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -35,13 +35,15 @@ sealed class RoomDetailAction : VectorViewModelAction { data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailAction() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction() data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction() - data class SetReadMarkerAction(val eventId: String) : RoomDetailAction() object MarkAllAsRead : RoomDetailAction() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailAction() data class HandleTombstoneEvent(val event: Event) : RoomDetailAction() object AcceptInvite : RoomDetailAction() object RejectInvite : RoomDetailAction() + object EnterTrackingUnreadMessagesState : RoomDetailAction() + object ExitTrackingUnreadMessagesState : RoomDetailAction() + data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 31278a1fff..e0c43b9e74 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -292,6 +292,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroy() { + roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) debouncer.cancelAll() super.onDestroy() } @@ -299,6 +300,7 @@ class RoomDetailFragment @Inject constructor( private fun setupJumpToBottomView() { jumpToBottomView.visibility = View.INVISIBLE jumpToBottomView.setOnClickListener { + roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) jumpToBottomView.visibility = View.INVISIBLE withState(roomDetailViewModel) { state -> if (state.timeline?.isLive == false) { @@ -428,7 +430,6 @@ class RoomDetailFragment @Inject constructor( } override fun onResume() { - readMarkerHelper.onResume() super.onResume() notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) } @@ -484,13 +485,6 @@ class RoomDetailFragment @Inject constructor( recyclerView.adapter = timelineEventController.adapter recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { - updateJumpToBottomViewVisibility() - } - readMarkerHelper.onTimelineScrolled() - } - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { when (newState) { RecyclerView.SCROLL_STATE_IDLE -> { @@ -668,7 +662,7 @@ class RoomDetailFragment @Inject constructor( val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { scrollOnHighlightedEventCallback.timeline = state.timeline - timelineEventController.update(state, readMarkerHelper.readMarkerVisible()) + timelineEventController.update(state) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) @@ -1024,28 +1018,8 @@ class RoomDetailFragment @Inject constructor( .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } - override fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) { - readMarkerHelper.onReadMarkerLongDisplayed() - val readMarkerIndex = timelineEventController.searchPositionOfEvent(readMarkerId) ?: return - val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() - if (readMarkerIndex > lastVisibleItemPosition) { - return - } - val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() - var nextReadMarkerId: String? = null - for (itemPosition in firstVisibleItemPosition until lastVisibleItemPosition) { - val timelineItem = timelineEventController.adapter.getModelAtPosition(itemPosition) - if (timelineItem is BaseEventItem) { - val eventId = timelineItem.getEventIds().firstOrNull() ?: continue - if (!LocalEcho.isLocalEchoId(eventId)) { - nextReadMarkerId = eventId - break - } - } - } - if (nextReadMarkerId != null) { - roomDetailViewModel.handle(RoomDetailAction.SetReadMarkerAction(nextReadMarkerId)) - } + override fun onReadMarkerDisplayed() { + roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } // AutocompleteUserPresenter.Callback diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a264e0d06c..e1ff991797 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt @@ -64,6 +65,7 @@ import org.commonmark.renderer.html.HtmlRenderer import timber.log.Timber import java.io.File import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, userPreferencesProvider: UserPreferencesProvider, @@ -102,6 +104,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Slot to keep a pending uri during permission request var pendingUri: Uri? = null + private var trackUnreadMessages = AtomicBoolean(false) + private var mostRecentDisplayedEvent: TimelineEvent? = null + @AssistedInject.Factory interface Factory { fun create(initialState: RoomDetailViewState): RoomDetailViewModel @@ -120,6 +125,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } init { + getSnapshotOfReadMarkerId() observeSyncState() observeRoomSummary() observeEventDisplayedActions() @@ -132,33 +138,47 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) - is RoomDetailAction.SendMedia -> handleSendMedia(action) - is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) - is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) - is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailAction.SendReaction -> handleSendReaction(action) - is RoomDetailAction.AcceptInvite -> handleAcceptInvite() - is RoomDetailAction.RejectInvite -> handleRejectInvite() - is RoomDetailAction.RedactAction -> handleRedactEvent(action) - is RoomDetailAction.UndoReaction -> handleUndoReact(action) - is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) - is RoomDetailAction.DownloadFile -> handleDownloadFile(action) - is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailAction.ResendMessage -> handleResendEvent(action) - is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.ClearSendQueue -> handleClearSendQueue() - is RoomDetailAction.ResendAll -> handleResendAll() - is RoomDetailAction.SetReadMarkerAction -> handleSetReadMarkerAction(action) - is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailAction.ReportContent -> handleReportContent(action) - is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.SaveDraft -> handleSaveDraft(action) + is RoomDetailAction.SendMessage -> handleSendMessage(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailAction.SendReaction -> handleSendReaction(action) + is RoomDetailAction.AcceptInvite -> handleAcceptInvite() + is RoomDetailAction.RejectInvite -> handleRejectInvite() + is RoomDetailAction.RedactAction -> handleRedactEvent(action) + is RoomDetailAction.UndoReaction -> handleUndoReact(action) + is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action) + is RoomDetailAction.EnterEditMode -> handleEditAction(action) + is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) + is RoomDetailAction.DownloadFile -> handleDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.ClearSendQueue -> handleClearSendQueue() + is RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.EnterTrackingUnreadMessagesState -> handleEnterTrackingUnreadMessages() + is RoomDetailAction.ExitTrackingUnreadMessagesState -> handleExitTrackingUnreadMessages() + } + } + + private fun handleEnterTrackingUnreadMessages() { + trackUnreadMessages.set(true) + } + + private fun handleExitTrackingUnreadMessages() { + if (trackUnreadMessages.getAndSet(false)) { + mostRecentDisplayedEvent?.root?.eventId?.also { + room.setReadMarker(it, callback = object : MatrixCallback {}) + } + mostRecentDisplayedEvent = null } } @@ -685,26 +705,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val mostRecentEvent = actions.maxBy { it.event.displayIndex } - mostRecentEvent?.event?.root?.eventId?.let { eventId -> + val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event ?: return@subscribeBy + val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent + if (trackUnreadMessages.get()) { + if (globalMostRecentDisplayedEvent == null) { + mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent + } else if (bufferedMostRecentDisplayedEvent.displayIndex > globalMostRecentDisplayedEvent.displayIndex) { + mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent + } + } + bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) } }) .disposeOnClear() } - private fun handleSetReadMarkerAction(action: RoomDetailAction.SetReadMarkerAction) = withState { - var readMarkerId = action.eventId - val indexOfEvent = timeline.getIndexOfEvent(readMarkerId) - // force to set the read marker on the next event - if (indexOfEvent != null) { - timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext -> - readMarkerId = eventIdOfNext - } - } - room.setReadMarker(readMarkerId, callback = object : MatrixCallback {}) - } - private fun handleMarkAllAsRead() { room.markAllAsRead(object : MatrixCallback {}) } @@ -759,6 +775,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + private fun getSnapshotOfReadMarkerId() { + room.rx().liveRoomSummary() + .unwrap() + .filter { it.readMarkerId != null } + .take(1) + .subscribe { roomSummary -> + setState { + copy(readMarkerIdSnapshot = roomSummary.readMarkerId) + } + } + .disposeOnClear() + } + private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 03110858a1..e476545aa8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -52,7 +52,8 @@ data class RoomDetailViewState( val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, - val highlightedEventId: String? = null + val highlightedEventId: String? = null, + val readMarkerIdSnapshot: String? = null ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index be2f1dd7e4..9614d2aba7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,15 +31,10 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime -import im.vector.riotx.core.utils.DimensionConverter -import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull +import im.vector.riotx.features.home.room.detail.timeline.helper.* import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer @@ -50,8 +45,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, - private val avatarRenderer: AvatarRenderer, - private val dimensionConverter: DimensionConverter, @TimelineEventControllerHandler private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { @@ -86,7 +79,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) + fun onReadMarkerDisplayed() } interface UrlClickCallback { @@ -101,6 +94,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null + private var readMarkerIdSnapshot: String? = null var callback: Callback? = null @@ -163,7 +157,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) { + fun update(viewState: RoomDetailViewState) { if (timeline != viewState.timeline) { timeline = viewState.timeline timeline?.listener = this @@ -188,8 +182,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec eventIdToHighlight = viewState.highlightedEventId requestModelBuild = true } - if (this.readMarkerVisible != readMarkerVisible) { - this.readMarkerVisible = readMarkerVisible + if (this.readMarkerIdSnapshot != viewState.readMarkerIdSnapshot) { + this.readMarkerIdSnapshot = viewState.readMarkerIdSnapshot requestModelBuild = true } if (requestModelBuild) { @@ -197,7 +191,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - private var readMarkerVisible: Boolean = false private var eventIdToHighlight: String? = null override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { @@ -247,42 +240,40 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun getModels(): List> { synchronized(modelCache) { + val readMarkerIdSnapshot = this.readMarkerIdSnapshot + val displayableReadMarkerId = if (readMarkerIdSnapshot != null) { + timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) + } else { + null + } (0 until modelCache.size).forEach { position -> - // Should be build if not cached or if cached but contains mergedHeader or formattedDay + // Should be build if not cached or if cached but contains additional models // We then are sure we always have items up to date. - if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { - modelCache[position] = buildItemModels(position, currentSnapshot) + if (modelCache[position] == null || modelCache[position]?.hasAdditionalModel() == true) { + modelCache[position] = buildItemModels(position, currentSnapshot, displayableReadMarkerId) } } - return modelCache - .map { - val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { - null - } else { - it.eventModel - } - listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) - } - .flatten() - .filterNotNull() } + return modelCache + .map { + val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { + null + } else { + it.eventModel + } + listOf(it?.readMarkerModel, eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + } + .flatten() + .filterNotNull() } - private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { + private fun buildItemModels(currentPosition: Int, items: List, displayableReadMarkerId: String?): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - // Don't show read marker if it's on first item - val showReadMarker = if (currentPosition == 0 && event.hasReadMarker) { - false - } else { - readMarkerVisible - } - val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, showReadMarker, callback).also { + val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } @@ -290,7 +281,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec nextEvent = nextEvent, items = items, addDaySeparator = addDaySeparator, - readMarkerVisible = readMarkerVisible, currentPosition = currentPosition, eventIdToHighlight = eventIdToHighlight, callback = callback @@ -298,8 +288,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) + val readMarkerItem = buildReadMarkerItem(currentPosition, event, displayableReadMarkerId) + return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem) + } - return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) + private fun buildReadMarkerItem(currentPosition: Int, event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? { + return if (currentPosition != 0 && event.root.eventId == displayableReadMarkerId) { + TimelineReadMarkerItem_() + .also { + it.id(event.localId) + it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) + } + } else { + null + } } private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { @@ -342,6 +344,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val eventId: String?, val eventModel: EpoxyModel<*>? = null, val mergedHeaderModel: MergedHeaderItem? = null, - val formattedDayModel: DaySeparatorItem? = null - ) + val formattedDayModel: DaySeparatorItem? = null, + val readMarkerModel: TimelineReadMarkerItem? = null + ) { + fun hasAdditionalModel() = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index 1ae47f9c22..94d7812512 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -46,7 +46,6 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava fun create(event: TimelineEvent, highlight: Boolean, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback?, exception: Exception? = null): DefaultItem { val text = if (exception == null) { @@ -54,7 +53,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava } else { "an exception occurred when rendering the event ${event.root.eventId}" } - val informationData = informationDataFactory.create(event, null, readMarkerVisible) + val informationData = informationDataFactory.create(event, null) return create(text, informationData, highlight, callback) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index c7aca768dc..512fffa29e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -42,7 +42,6 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { event.root.eventId ?: return null @@ -66,7 +65,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat // TODO This is not correct format for error, change it - val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible) + val informationData = messageInformationDataFactory.create(event, nextEvent) val attributes = attributesFactory.create(null, informationData, callback) return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 51364e24c9..a2e979a08d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -36,7 +36,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act nextEvent: TimelineEvent?, items: List, addDaySeparator: Boolean, - readMarkerVisible: Boolean, currentPosition: Int, eventIdToHighlight: String?, callback: TimelineEventController.Callback?, @@ -50,20 +49,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act null } else { var highlighted = false - var readMarkerId: String? = null - var showReadMarker = false val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() val mergedData = ArrayList(mergedEvents.size) mergedEvents.forEach { mergedEvent -> if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { highlighted = true } - if (readMarkerId == null && mergedEvent.hasReadMarker) { - readMarkerId = mergedEvent.root.eventId - } - if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) { - showReadMarker = true - } val senderAvatar = mergedEvent.senderAvatar val senderName = mergedEvent.getDisambiguatedDisplayName() val data = MergedHeaderItem.Data( @@ -96,8 +87,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act mergeItemCollapseStates[event.localId] = it requestModelBuild() }, - readMarkerId = readMarkerId, - showReadMarker = isCollapsed && showReadMarker, readReceiptsCallback = callback ) MergedHeaderItem_() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index de2686de04..9c96f17022 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -69,12 +69,11 @@ class MessageItemFactory @Inject constructor( fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null - val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible) + val informationData = messageInformationDataFactory.create(event, nextEvent) if (event.root.isRedacted()) { // message is redacted @@ -91,7 +90,7 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event - return noticeItemFactory.create(event, highlight, readMarkerVisible, callback) + return noticeItemFactory.create(event, highlight, callback) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 8768da26cf..4ee90f82a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -34,10 +34,9 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv fun create(event: TimelineEvent, highlight: Boolean, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { val formattedText = eventFormatter.format(event) ?: return null - val informationData = informationDataFactory.create(event, null, readMarkerVisible) + val informationData = informationDataFactory.create(event, null) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, informationData = informationData, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 618ca121c2..5b6dec9900 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -33,14 +33,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me fun create(event: TimelineEvent, nextEvent: TimelineEvent?, eventIdToHighlight: String?, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { val highlight = event.root.eventId == eventIdToHighlight val computedModel = try { when (event.root.getClearType()) { EventType.STICKER, - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) // State and call EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, @@ -53,21 +52,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_ANSWER, EventType.REACTION, EventType.REDACTION, - EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, readMarkerVisible, callback) + EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) + messageItemFactory.create(event, nextEvent, highlight, callback) } else { - encryptedItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) + encryptedItemFactory.create(event, nextEvent, highlight, callback) } } // Unhandled event types (yet) - EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback) + EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback) else -> { Timber.v("Type ${event.root.getClearType()} not handled") null @@ -75,7 +74,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me } } catch (e: Exception) { Timber.e(e, "failed to create message item") - defaultItemFactory.create(event, highlight, readMarkerVisible, callback, e) + defaultItemFactory.create(event, highlight, callback, e) } return (computedModel ?: EmptyItem_()) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index e44e657733..34e34fc7de 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -39,7 +39,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider) { - fun create(event: TimelineEvent, nextEvent: TimelineEvent?, readMarkerVisible: Boolean): MessageInformationData { + fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { // Non nullability has been tested before val eventId = event.root.eventId!! @@ -63,8 +63,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } - val displayReadMarker = readMarkerVisible && event.hasReadMarker - return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -88,9 +86,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses .map { ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } - .toList(), - hasReadMarker = event.hasReadMarker, - displayReadMarker = displayReadMarker + .toList() ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt index c2aaf482ae..7efbce0073 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -21,6 +21,19 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?) + : VectorEpoxyModel.OnVisibilityStateChangedListener { + + private var dispatched: Boolean = false + + override fun onVisibilityStateChanged(visibilityState: Int) { + if (visibilityState == VisibilityState.VISIBLE && !dispatched) { + dispatched = true + callback?.onReadMarkerDisplayed() + } + } +} + class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, private val event: TimelineEvent) : VectorEpoxyModel.OnVisibilityStateChangedListener { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 2ca6bbfd37..713b60d4d8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -27,7 +27,6 @@ import com.airbnb.epoxy.EpoxyAttribute import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -50,13 +49,6 @@ abstract class AbsMessageItem : BaseEventItem() { attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) - private val _readMarkerCallback = object : ReadMarkerView.Callback { - - override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed) - } - } - var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true) @@ -110,12 +102,6 @@ abstract class AbsMessageItem : BaseEventItem() { attributes.avatarRenderer, _readReceiptsClickListener ) - holder.readMarkerView.bindView( - attributes.informationData.eventId, - attributes.informationData.hasReadMarker, - attributes.informationData.displayReadMarker, - _readMarkerCallback - ) val reactions = attributes.informationData.orderedReactionList if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) { @@ -138,7 +124,6 @@ abstract class AbsMessageItem : BaseEventItem() { } override fun unbind(holder: H) { - holder.readMarkerView.unbind() holder.readReceiptsView.unbind() super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 8543484b00..576e596f90 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -26,7 +26,6 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.platform.CheckableView -import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DimensionConverter @@ -62,7 +61,6 @@ abstract class BaseEventItem : VectorEpoxyModel val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) val readReceiptsView by bind(R.id.readReceiptsView) - val readMarkerView by bind(R.id.readMarkerView) override fun bindView(itemView: View) { super.bindView(itemView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 01e82ddf6b..bbccb71ffd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -25,7 +25,6 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -39,13 +38,6 @@ abstract class MergedHeaderItem : BaseEventItem() { attributes.mergeData.distinctBy { it.userId } } - private val _readMarkerCallback = object : ReadMarkerView.Callback { - - override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.readMarkerId ?: "", isDisplayed) - } - } - override fun getViewType() = STUB_ID override fun bind(holder: Holder) { @@ -77,16 +69,6 @@ abstract class MergedHeaderItem : BaseEventItem() { } // No read receipt for this item holder.readReceiptsView.isVisible = false - holder.readMarkerView.bindView( - attributes.readMarkerId, - !attributes.readMarkerId.isNullOrEmpty(), - attributes.showReadMarker, - _readMarkerCallback) - } - - override fun unbind(holder: Holder) { - holder.readMarkerView.unbind() - super.unbind(holder) } override fun getEventIds(): List { @@ -102,9 +84,7 @@ abstract class MergedHeaderItem : BaseEventItem() { ) data class Attributes( - val readMarkerId: String?, val isCollapsed: Boolean, - val showReadMarker: Boolean, val mergeData: List, val avatarRenderer: AvatarRenderer, val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 96c74ccb88..2dd581ce6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -33,9 +33,7 @@ data class MessageInformationData( val orderedReactionList: List? = null, val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, - val readReceipts: List = emptyList(), - val hasReadMarker: Boolean = false, - val displayReadMarker: Boolean = false + val readReceipts: List = emptyList() ) : Parcelable @Parcelize diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 1f39ae3ca4..804990cc5c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -22,7 +22,6 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -37,12 +36,6 @@ abstract class NoticeItem : BaseEventItem() { attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) - private val _readMarkerCallback = object : ReadMarkerView.Callback { - - override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed) - } - } override fun bind(holder: Holder) { super.bind(holder) @@ -56,17 +49,6 @@ abstract class NoticeItem : BaseEventItem() { ) holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView( - attributes.informationData.eventId, - attributes.informationData.hasReadMarker, - attributes.informationData.displayReadMarker, - _readMarkerCallback - ) - } - - override fun unbind(holder: Holder) { - holder.readMarkerView.unbind() - super.unbind(holder) } override fun getEventIds(): List { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt new file mode 100644 index 0000000000..4d867156d3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.item + +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_timeline_read_marker) +abstract class TimelineReadMarkerItem : VectorEpoxyModel() { + + override fun bind(holder: Holder) { + } + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 50ed0aae23..ce47847550 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -11,7 +11,7 @@ android:id="@+id/messageSelectedBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_alignBottom="@+id/readMarkerView" + android:layout_alignBottom="@+id/informationBottom" android:layout_alignParentTop="true" android:background="?riotx_highlighted_message_background" /> @@ -145,15 +145,4 @@ - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 583997577a..b72933b94f 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -53,31 +53,13 @@ - - - - - - - + android:layout_alignParentEnd="true" + android:layout_marginEnd="8dp" + android:layout_marginBottom="4dp" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml new file mode 100644 index 0000000000..93150b45ea --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file From ab489df83dd0d90936473cc889455a45e02062ea Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 19 Nov 2019 22:23:27 +0100 Subject: [PATCH 173/189] Read marker: don't show unread on events we own --- .../timeline/TimelineEventController.kt | 61 ++++++++++++------- .../res/layout/item_timeline_read_marker.xml | 4 +- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 9614d2aba7..180deb998b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState +import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -39,9 +40,11 @@ import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime +import timber.log.Timber import javax.inject.Inject class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, + private val session: Session, private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, @@ -239,21 +242,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } private fun getModels(): List> { - synchronized(modelCache) { - val readMarkerIdSnapshot = this.readMarkerIdSnapshot - val displayableReadMarkerId = if (readMarkerIdSnapshot != null) { - timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) - } else { - null - } - (0 until modelCache.size).forEach { position -> - // Should be build if not cached or if cached but contains additional models - // We then are sure we always have items up to date. - if (modelCache[position] == null || modelCache[position]?.hasAdditionalModel() == true) { - modelCache[position] = buildItemModels(position, currentSnapshot, displayableReadMarkerId) - } - } - } + buildCacheItemsIfNeeded() return modelCache .map { val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { @@ -261,13 +250,41 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } else { it.eventModel } - listOf(it?.readMarkerModel, eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel, it?.readMarkerModel) } .flatten() .filterNotNull() } - private fun buildItemModels(currentPosition: Int, items: List, displayableReadMarkerId: String?): CacheItemData { + private fun buildCacheItemsIfNeeded() = synchronized(modelCache) { + if (modelCache.isEmpty()) { + return + } + val displayableReadMarkerId = computeDisplayableReadMarkerId() + (0 until modelCache.size).forEach { position -> + // Should be build if not cached or if cached but contains additional models + // We then are sure we always have items up to date. + if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { + modelCache[position] = buildCacheItem(position, currentSnapshot, displayableReadMarkerId) + } + } + } + + private fun computeDisplayableReadMarkerId(): String? { + val readMarkerIdSnapshot = this.readMarkerIdSnapshot ?: return null + val firstDisplayableEventId = timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) ?: return null + val firstDisplayableEventIndex = timeline?.getIndexOfEvent(firstDisplayableEventId) ?: return null + for (i in (firstDisplayableEventIndex - 1) downTo 0) { + val timelineEvent = currentSnapshot.getOrNull(i) ?: return null + val isFromMe = timelineEvent.root.senderId == session.myUserId + if (!isFromMe) { + return timelineEvent.root.eventId + } + } + return null + } + + private fun buildCacheItem(currentPosition: Int, items: List, displayableReadMarkerId: String?): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() @@ -288,15 +305,15 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) - val readMarkerItem = buildReadMarkerItem(currentPosition, event, displayableReadMarkerId) + val readMarkerItem = buildReadMarkerItem(event, displayableReadMarkerId) return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem) } - private fun buildReadMarkerItem(currentPosition: Int, event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? { - return if (currentPosition != 0 && event.root.eventId == displayableReadMarkerId) { + private fun buildReadMarkerItem(event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? { + return if (event.root.eventId == displayableReadMarkerId) { TimelineReadMarkerItem_() .also { - it.id(event.localId) + it.id("read_marker") it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) } } else { @@ -347,6 +364,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val formattedDayModel: DaySeparatorItem? = null, val readMarkerModel: TimelineReadMarkerItem? = null ) { - fun hasAdditionalModel() = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null + fun shouldTriggerBuild() = mergedHeaderModel != null || formattedDayModel != null } } diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml index 93150b45ea..8ee56045bf 100644 --- a/vector/src/main/res/layout/item_timeline_read_marker.xml +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -7,8 +7,8 @@ Date: Wed, 20 Nov 2019 16:13:11 +0100 Subject: [PATCH 174/189] Read marker: continue rework [WIP] --- .../api/session/room/timeline/Timeline.kt | 9 +- .../session/room/timeline/DefaultTimeline.kt | 41 +++-- .../core/ui/views/JumpToReadMarkerView.kt | 13 +- .../home/room/detail/ReadMarkerHelper.kt | 64 -------- .../home/room/detail/RoomDetailFragment.kt | 49 ++++-- .../home/room/detail/RoomDetailViewModel.kt | 144 ++++++++++++------ .../home/room/detail/RoomDetailViewState.kt | 11 +- .../timeline/TimelineEventController.kt | 99 ++++++------ ...imelineVisibilityStateChangedListeners.kt} | 9 +- 9 files changed, 229 insertions(+), 210 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/{TimelineEventVisibilityStateChangedListener.kt => TimelineVisibilityStateChangedListeners.kt} (91%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index c03effd7ad..a71f5b7479 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -30,10 +30,16 @@ package im.vector.matrix.android.api.session.room.timeline */ interface Timeline { - var listener: Listener? + val timelineID: String val isLive: Boolean + fun addListener(listener: Listener): Boolean + + fun removeListener(listener: Listener): Boolean + + fun removeAllListeners() + /** * This should be called before any other method after creating the timeline. It ensures the underlying database is open */ @@ -116,4 +122,5 @@ interface Timeline { */ BACKWARDS } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index aa4bd42bf7..4411a039a5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -81,14 +81,7 @@ internal class DefaultTimeline( val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") } - override var listener: Timeline.Listener? = null - set(value) { - field = value - BACKGROUND_HANDLER.post { - postSnapshot() - } - } - + private val listeners = ArrayList() private val isStarted = AtomicBoolean(false) private val isReady = AtomicBoolean(false) private val mainHandler = createUIHandler() @@ -109,7 +102,7 @@ internal class DefaultTimeline( private val backwardsState = AtomicReference(State()) private val forwardsState = AtomicReference(State()) - private val timelineID = UUID.randomUUID().toString() + override val timelineID = UUID.randomUUID().toString() override val isLive get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) @@ -295,6 +288,20 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } + override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { + listeners.add(listener).also { + postSnapshot() + } + } + + override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) { + listeners.remove(listener) + } + + override fun removeAllListeners() = synchronized(listeners) { + listeners.clear() + } + // TimelineHiddenReadReceipts.Delegate override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { @@ -487,9 +494,9 @@ internal class DefaultTimeline( return } val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -564,7 +571,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } @@ -637,7 +644,13 @@ internal class DefaultTimeline( } updateLoadingStates(filteredEvents) val snapshot = createSnapshot() - val runnable = Runnable { listener?.onUpdated(snapshot) } + val runnable = Runnable { + synchronized(listeners) { + listeners.forEach { + it.onUpdated(snapshot) + } + } + } debouncer.debounce("post_snapshot", runnable, 50) } } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt index abc2dd98f8..5ba482837e 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -34,7 +34,7 @@ class JumpToReadMarkerView @JvmOverloads constructor( ) : RelativeLayout(context, attrs, defStyleAttr) { interface Callback { - fun onJumpToReadMarkerClicked(readMarkerId: String) + fun onJumpToReadMarkerClicked() fun onClearReadMarkerClicked() } @@ -44,24 +44,15 @@ class JumpToReadMarkerView @JvmOverloads constructor( setupView() } - private var readMarkerId: String? = null - private fun setupView() { inflate(context, R.layout.view_jump_to_read_marker, this) setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color)) jumpToReadMarkerLabelView.setOnClickListener { - readMarkerId?.also { - callback?.onJumpToReadMarkerClicked(it) - } + callback?.onJumpToReadMarkerClicked() } closeJumpToReadMarkerView.setOnClickListener { visibility = View.INVISIBLE callback?.onClearReadMarkerClicked() } } - - fun render(show: Boolean, readMarkerId: String?) { - this.readMarkerId = readMarkerId - isInvisible = !show - } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt deleted file mode 100644 index 98556cc7fa..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - - */ -package im.vector.riotx.features.home.room.detail - -import androidx.recyclerview.widget.LinearLayoutManager -import im.vector.riotx.core.di.ScreenScope -import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import javax.inject.Inject - -@ScreenScope -class ReadMarkerHelper @Inject constructor() { - - lateinit var timelineEventController: TimelineEventController - lateinit var layoutManager: LinearLayoutManager - var callback: Callback? = null - private var jumpToReadMarkerVisible = false - private var state: RoomDetailViewState? = null - - fun updateWith(newState: RoomDetailViewState) { - state = newState - checkJumpToReadMarkerVisibility() - } - - private fun checkJumpToReadMarkerVisibility() { - val nonNullState = this.state ?: return - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId - val newJumpToReadMarkerVisible = if (readMarkerId == null) { - false - } else { - val correctedReadMarkerId = nonNullState.timeline?.getFirstDisplayableEventId(readMarkerId) - ?: readMarkerId - val positionOfReadMarker = timelineEventController.searchPositionOfEvent(correctedReadMarkerId) - if (positionOfReadMarker == null) { - nonNullState.timeline?.isLive == true && lastVisibleItem > 0 - } else { - positionOfReadMarker > lastVisibleItem - } - } - if (newJumpToReadMarkerVisible != jumpToReadMarkerVisible) { - jumpToReadMarkerVisible = newJumpToReadMarkerVisible - callback?.onJumpToReadMarkerVisibilityUpdate(jumpToReadMarkerVisible, readMarkerId) - } - } - - interface Callback { - fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index e0c43b9e74..8fb4d30b9f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -39,6 +39,7 @@ import androidx.core.text.buildSpannedString import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach +import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -57,7 +58,6 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState @@ -145,8 +145,7 @@ class RoomDetailFragment @Inject constructor( val textComposerViewModelFactory: TextComposerViewModel.Factory, private val errorFormatter: ErrorFormatter, private val eventHtmlRenderer: EventHtmlRenderer, - private val vectorPreferences: VectorPreferences, - private val readMarkerHelper: ReadMarkerHelper + private val vectorPreferences: VectorPreferences ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -425,7 +424,8 @@ class RoomDetailFragment @Inject constructor( if (text != composerLayout.composerEditText.text.toString()) { // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length + ?: 0) } } @@ -474,13 +474,7 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) - } - readMarkerHelper.timelineEventController = timelineEventController - readMarkerHelper.layoutManager = layoutManager - readMarkerHelper.callback = object : ReadMarkerHelper.Callback { - override fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) { - jumpToReadMarkerView.render(show, readMarkerId) - } + checkJumpToUnreadBanner() } recyclerView.adapter = timelineEventController.adapter @@ -526,6 +520,25 @@ class RoomDetailFragment @Inject constructor( } } + private fun checkJumpToUnreadBanner() = jumpToReadMarkerView.post { + withState(roomDetailViewModel) { + val showJumpToUnreadBanner = when (it.unreadState) { + UnreadState.Unknown, + UnreadState.HasNoUnread -> false + is UnreadState.HasUnread -> { + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() + if (positionOfReadMarker == null) { + it.timeline?.isLive == true && lastVisibleItem > 0 + } else { + positionOfReadMarker > lastVisibleItem + } + } + } + jumpToReadMarkerView.isVisible = showJumpToUnreadBanner + } + } + private fun updateJumpToBottomViewVisibility() { debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") @@ -656,7 +669,6 @@ class RoomDetailFragment @Inject constructor( } private fun renderState(state: RoomDetailViewState) { - readMarkerHelper.updateWith(state) renderRoomSummary(state) val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() @@ -1018,10 +1030,15 @@ class RoomDetailFragment @Inject constructor( .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } - override fun onReadMarkerDisplayed() { + override fun onReadMarkerVisible() { + checkJumpToUnreadBanner() roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } + override fun onReadMarkerInvisible() { + checkJumpToUnreadBanner() + } + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { @@ -1226,8 +1243,10 @@ class RoomDetailFragment @Inject constructor( // JumpToReadMarkerView.Callback - override fun onJumpToReadMarkerClicked(readMarkerId: String) { - roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(readMarkerId, false)) + override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) { + if (it.unreadState is UnreadState.HasUnread) { + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.eventId, false)) + } } override fun onClearReadMarkerClicked() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index e1ff991797..ccce70f33c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.* import com.jakewharton.rxrelay2.BehaviorRelay +import com.jakewharton.rxrelay2.PublishRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback @@ -35,11 +36,13 @@ import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent @@ -59,6 +62,8 @@ import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.settings.VectorPreferences +import io.reactivex.Observable +import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer @@ -72,7 +77,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val vectorPreferences: VectorPreferences, private val stringProvider: StringProvider, private val session: Session -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState), Timeline.Listener { private val room = session.getRoom(initialState.roomId)!! private val eventId = initialState.eventId @@ -80,18 +85,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val visibleEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, - filterEdits = false, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = false, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { TimelineSettings(30, - filterEdits = true, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = true, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } + private var timelineEvents = PublishRelay.create>() private var timeline = room.createTimeline(eventId, timelineSettings) // Can be used for several actions, for a one shot result @@ -125,13 +131,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } init { - getSnapshotOfReadMarkerId() + getUnreadState() observeSyncState() observeRoomSummary() observeEventDisplayedActions() observeSummaryState() observeDrafts() + observeUnreadState() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() + timeline.addListener(this) timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } } @@ -164,16 +172,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() is RoomDetailAction.ReportContent -> handleReportContent(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) - is RoomDetailAction.EnterTrackingUnreadMessagesState -> handleEnterTrackingUnreadMessages() - is RoomDetailAction.ExitTrackingUnreadMessagesState -> handleExitTrackingUnreadMessages() + is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() } } - private fun handleEnterTrackingUnreadMessages() { + private fun startTrackingUnreadMessages() { trackUnreadMessages.set(true) } - private fun handleExitTrackingUnreadMessages() { + private fun stopTrackingUnreadMessages() { if (trackUnreadMessages.getAndSet(false)) { mostRecentDisplayedEvent?.root?.eventId?.also { room.setReadMarker(it, callback = object : MatrixCallback {}) @@ -212,23 +220,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") ) } } @@ -237,7 +245,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -375,7 +383,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -384,13 +392,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -401,7 +409,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) @@ -517,7 +525,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { null -> room.sendMedias(attachments) else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name - ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) + ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) } } } @@ -647,6 +655,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { + stopTrackingUnreadMessages() val targetEventId: String = action.eventId val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId val indexOfEvent = timeline.getIndexOfEvent(correctedEventId) @@ -705,7 +714,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event ?: return@subscribeBy + val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event + ?: return@subscribeBy val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent if (trackUnreadMessages.get()) { if (globalMostRecentDisplayedEvent == null) { @@ -775,19 +785,53 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun getSnapshotOfReadMarkerId() { - room.rx().liveRoomSummary() - .unwrap() - .filter { it.readMarkerId != null } - .take(1) - .subscribe { roomSummary -> + private fun getUnreadState() { + Observable + .combineLatest, RoomSummary, UnreadState>( + timelineEvents, + room.rx().liveRoomSummary().unwrap(), + BiFunction { timelineEvents, roomSummary -> + computeUnreadState(timelineEvents, roomSummary) + } + ) + .takeUntil { + it != UnreadState.Unknown + } + .subscribe { unreadState -> setState { - copy(readMarkerIdSnapshot = roomSummary.readMarkerId) + copy(unreadState = unreadState) } } .disposeOnClear() } + private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { + val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown + val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) + ?: return UnreadState.Unknown + val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) + ?: return UnreadState.Unknown + for (i in (firstDisplayableEventIndex - 1) downTo 0) { + val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown + val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown + val isFromMe = timelineEvent.root.senderId == session.myUserId + if (!isFromMe) { + return UnreadState.HasUnread(eventId) + } + } + return UnreadState.HasNoUnread + } + + + private fun observeUnreadState() { + selectSubscribe(RoomDetailViewState::unreadState) { + Timber.v("Unread state: $it") + if (it is UnreadState.HasNoUnread) { + startTrackingUnreadMessages() + } + } + } + private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { @@ -803,8 +847,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + override fun onUpdated(snapshot: List) { + timelineEvents.accept(snapshot) + setState { copy(currentSnapshot = snapshot) } + } + override fun onCleared() { timeline.dispose() + timeline.removeAllListeners() super.onCleared() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index e476545aa8..23971a93cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -41,6 +41,12 @@ sealed class SendMode(open val text: String) { data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) } +sealed class UnreadState { + object Unknown : UnreadState() + object HasNoUnread : UnreadState() + data class HasUnread(val eventId: String) : UnreadState() +} + data class RoomDetailViewState( val roomId: String, val eventId: String?, @@ -53,7 +59,10 @@ data class RoomDetailViewState( val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, val highlightedEventId: String? = null, - val readMarkerIdSnapshot: String? = null + val currentSnapshot: List = emptyList(), + val hasMoreToLoadForward: Boolean = false, + val hasMoreToLoadBackward: Boolean = false, + val unreadState: UnreadState = UnreadState.Unknown ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 180deb998b..d4447f5b05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -33,6 +33,7 @@ import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.features.home.room.detail.RoomDetailViewState +import im.vector.riotx.features.home.room.detail.UnreadState import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotx.features.home.room.detail.timeline.helper.* @@ -40,7 +41,6 @@ import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime -import timber.log.Timber import javax.inject.Inject class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, @@ -82,7 +82,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerDisplayed() + fun onReadMarkerVisible() + fun onReadMarkerInvisible() } interface UrlClickCallback { @@ -97,7 +98,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null - private var readMarkerIdSnapshot: String? = null + private var unreadState: UnreadState = UnreadState.Unknown + private var positionOfReadMarker: Int? = null + private var eventIdToHighlight: String? = null var callback: Callback? = null @@ -150,6 +153,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Update position when we are building new items override fun intercept(models: MutableList>) { + positionOfReadMarker = null adapterPositionMapping.clear() models.forEachIndexed { index, epoxyModel -> if (epoxyModel is BaseEventItem) { @@ -157,19 +161,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec adapterPositionMapping[it] = index } } + if (epoxyModel is TimelineReadMarkerItem) { + positionOfReadMarker = index + } } } fun update(viewState: RoomDetailViewState) { - if (timeline != viewState.timeline) { + if (timeline?.timelineID != viewState.timeline?.timelineID) { timeline = viewState.timeline - timeline?.listener = this - // Clear cache - synchronized(modelCache) { - for (i in 0 until modelCache.size) { - modelCache[i] = null - } - } + timeline?.addListener(this) } var requestModelBuild = false if (eventIdToHighlight != viewState.highlightedEventId) { @@ -177,7 +178,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == eventIdToHighlight) { + || modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null } } @@ -185,8 +186,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec eventIdToHighlight = viewState.highlightedEventId requestModelBuild = true } - if (this.readMarkerIdSnapshot != viewState.readMarkerIdSnapshot) { - this.readMarkerIdSnapshot = viewState.readMarkerIdSnapshot + if (this.unreadState != viewState.unreadState) { + this.unreadState = viewState.unreadState requestModelBuild = true } if (requestModelBuild) { @@ -194,8 +195,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - private var eventIdToHighlight: String? = null - override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) timelineMediaSizeProvider.recyclerView = recyclerView @@ -250,7 +249,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } else { it.eventModel } - listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel, it?.readMarkerModel) + listOf(eventModel, it?.mergedHeaderModel, it?.readMarkerModel, it?.formattedDayModel) } .flatten() .filterNotNull() @@ -260,31 +259,17 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (modelCache.isEmpty()) { return } - val displayableReadMarkerId = computeDisplayableReadMarkerId() + val currentUnreadState = this.unreadState (0 until modelCache.size).forEach { position -> // Should be build if not cached or if cached but contains additional models // We then are sure we always have items up to date. - if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { - modelCache[position] = buildCacheItem(position, currentSnapshot, displayableReadMarkerId) + if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild(currentUnreadState) == true) { + modelCache[position] = buildCacheItem(position, currentSnapshot, currentUnreadState) } } } - private fun computeDisplayableReadMarkerId(): String? { - val readMarkerIdSnapshot = this.readMarkerIdSnapshot ?: return null - val firstDisplayableEventId = timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) ?: return null - val firstDisplayableEventIndex = timeline?.getIndexOfEvent(firstDisplayableEventId) ?: return null - for (i in (firstDisplayableEventIndex - 1) downTo 0) { - val timelineEvent = currentSnapshot.getOrNull(i) ?: return null - val isFromMe = timelineEvent.root.senderId == session.myUserId - if (!isFromMe) { - return timelineEvent.root.eventId - } - } - return null - } - - private fun buildCacheItem(currentPosition: Int, items: List, displayableReadMarkerId: String?): CacheItemData { + private fun buildCacheItem(currentPosition: Int, items: List, currentUnreadState: UnreadState): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() @@ -295,29 +280,35 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } val mergedHeaderModel = mergedHeaderItemFactory.create(event, - nextEvent = nextEvent, - items = items, - addDaySeparator = addDaySeparator, - currentPosition = currentPosition, - eventIdToHighlight = eventIdToHighlight, - callback = callback + nextEvent = nextEvent, + items = items, + addDaySeparator = addDaySeparator, + currentPosition = currentPosition, + eventIdToHighlight = eventIdToHighlight, + callback = callback ) { requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) - val readMarkerItem = buildReadMarkerItem(event, displayableReadMarkerId) + val readMarkerItem = buildReadMarkerItem(event, currentUnreadState) return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem) } - private fun buildReadMarkerItem(event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? { - return if (event.root.eventId == displayableReadMarkerId) { - TimelineReadMarkerItem_() - .also { - it.id("read_marker") - it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) - } - } else { - null + private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? { + return when (currentUnreadState) { + is UnreadState.HasUnread -> { + if (event.root.eventId == currentUnreadState.eventId) { + TimelineReadMarkerItem_() + .also { + it.id("read_marker") + it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) + } + } else { + null + } + } + UnreadState.Unknown, + UnreadState.HasNoUnread -> null } } @@ -354,6 +345,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return adapterPositionMapping[eventId] } + fun getPositionOfReadMarker(): Int? = synchronized(modelCache) { + return positionOfReadMarker + } + fun isLoadingForward() = showingForwardLoader private data class CacheItemData( @@ -364,6 +359,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val formattedDayModel: DaySeparatorItem? = null, val readMarkerModel: TimelineReadMarkerItem? = null ) { - fun shouldTriggerBuild() = mergedHeaderModel != null || formattedDayModel != null + fun shouldTriggerBuild(unreadState: UnreadState) = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null || (unreadState is UnreadState.HasUnread && unreadState.eventId == eventId) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt similarity index 91% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt index 7efbce0073..11b7d68923 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt @@ -24,12 +24,11 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?) : VectorEpoxyModel.OnVisibilityStateChangedListener { - private var dispatched: Boolean = false - override fun onVisibilityStateChanged(visibilityState: Int) { - if (visibilityState == VisibilityState.VISIBLE && !dispatched) { - dispatched = true - callback?.onReadMarkerDisplayed() + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onReadMarkerVisible() + } else if (visibilityState == VisibilityState.INVISIBLE) { + callback?.onReadMarkerInvisible() } } } From 64d73ae8e60be613ca48c568d89b579360bf37a7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 21 Nov 2019 14:42:16 +0100 Subject: [PATCH 175/189] Read marker: handle the jump to read marker --- .../timeline/DefaultGetContextOfEventTask.kt | 5 +- .../session/room/timeline/DefaultTimeline.kt | 2 +- .../room/timeline/EventContextResponse.kt | 5 +- .../home/room/detail/RoomDetailFragment.kt | 36 ++++--- .../home/room/detail/RoomDetailViewModel.kt | 94 ++++++++++--------- .../home/room/detail/RoomDetailViewState.kt | 9 +- .../timeline/TimelineEventController.kt | 27 +++--- .../helper/MessageInformationDataFactory.kt | 2 +- ...TimelineVisibilityStateChangedListeners.kt | 2 - 9 files changed, 98 insertions(+), 84 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index 96e1caf71b..08d34d3056 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -26,7 +26,8 @@ internal interface GetContextOfEventTask : Task { - apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) + apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter) } return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 4411a039a5..b83240a681 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -633,7 +633,7 @@ internal class DefaultTimeline( } private fun fetchEvent(eventId: String) { - val params = GetContextOfEventTask.Params(roomId, eventId) + val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize) cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt index 4dfe3e5c45..f06697351e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt @@ -30,6 +30,7 @@ data class EventContextResponse( @Json(name = "state") override val stateEvents: List = emptyList() ) : TokenChunkEvent { - override val events: List - get() = listOf(event) + override val events: List by lazy { + eventsAfter.reversed() + listOf(event) + eventsBefore + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 8fb4d30b9f..d50b0c9f68 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -474,7 +474,8 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) - checkJumpToUnreadBanner() + updateJumpToReadMarkerViewVisibility() + updateJumpToBottomViewVisibility() } recyclerView.adapter = timelineEventController.adapter @@ -520,18 +521,23 @@ class RoomDetailFragment @Inject constructor( } } - private fun checkJumpToUnreadBanner() = jumpToReadMarkerView.post { + private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post { withState(roomDetailViewModel) { val showJumpToUnreadBanner = when (it.unreadState) { UnreadState.Unknown, - UnreadState.HasNoUnread -> false - is UnreadState.HasUnread -> { - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() - if (positionOfReadMarker == null) { - it.timeline?.isLive == true && lastVisibleItem > 0 + UnreadState.HasNoUnread -> false + is UnreadState.ReadMarkerNotLoaded -> true + is UnreadState.HasUnread -> { + if (it.canShowJumpToReadMarker) { + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() + if (positionOfReadMarker == null) { + false + } else { + positionOfReadMarker > lastVisibleItem + } } else { - positionOfReadMarker > lastVisibleItem + false } } } @@ -1031,14 +1037,10 @@ class RoomDetailFragment @Inject constructor( } override fun onReadMarkerVisible() { - checkJumpToUnreadBanner() + updateJumpToReadMarkerViewVisibility() roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } - override fun onReadMarkerInvisible() { - checkJumpToUnreadBanner() - } - // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { @@ -1244,8 +1246,12 @@ class RoomDetailFragment @Inject constructor( // JumpToReadMarkerView.Callback override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) { + jumpToReadMarkerView.isVisible = false if (it.unreadState is UnreadState.HasUnread) { - roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.eventId, false)) + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false)) + } + if (it.unreadState is UnreadState.ReadMarkerNotLoaded) { + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.readMarkerId, false)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index ccce70f33c..2dab9264e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -65,6 +65,7 @@ import im.vector.riotx.features.settings.VectorPreferences import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import timber.log.Timber @@ -85,16 +86,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val visibleEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, - filterEdits = false, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = false, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { TimelineSettings(30, - filterEdits = true, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = true, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } private var timelineEvents = PublishRelay.create>() @@ -179,6 +180,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun startTrackingUnreadMessages() { trackUnreadMessages.set(true) + setState { copy(canShowJumpToReadMarker = false) } } private fun stopTrackingUnreadMessages() { @@ -188,6 +190,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } mostRecentDisplayedEvent = null } + setState { copy(canShowJumpToReadMarker = true) } } private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) { @@ -220,23 +223,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") ) } } @@ -245,7 +248,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -383,7 +386,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -392,13 +395,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -409,7 +412,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) @@ -525,7 +528,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { null -> room.sendMedias(attachments) else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name - ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) + ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) } } } @@ -715,7 +718,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event - ?: return@subscribeBy + ?: return@subscribeBy val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent if (trackUnreadMessages.get()) { if (globalMostRecentDisplayedEvent == null) { @@ -788,29 +791,33 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun getUnreadState() { Observable .combineLatest, RoomSummary, UnreadState>( - timelineEvents, + timelineEvents.observeOn(Schedulers.computation()), room.rx().liveRoomSummary().unwrap(), BiFunction { timelineEvents, roomSummary -> computeUnreadState(timelineEvents, roomSummary) } ) - .takeUntil { - it != UnreadState.Unknown - } - .subscribe { unreadState -> - setState { - copy(unreadState = unreadState) + // We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread + .distinctUntilChanged { previous, current -> + when { + previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false + current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true + else -> false } } + .subscribe { + setState { copy(unreadState = it) } + } .disposeOnClear() } private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { + if (events.isEmpty()) return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) - ?: return UnreadState.Unknown + ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) - ?: return UnreadState.Unknown + ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) for (i in (firstDisplayableEventIndex - 1) downTo 0) { val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown @@ -849,7 +856,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro override fun onUpdated(snapshot: List) { timelineEvents.accept(snapshot) - setState { copy(currentSnapshot = snapshot) } } override fun onCleared() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 23971a93cd..a0be8fc9dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -44,7 +44,8 @@ sealed class SendMode(open val text: String) { sealed class UnreadState { object Unknown : UnreadState() object HasNoUnread : UnreadState() - data class HasUnread(val eventId: String) : UnreadState() + data class ReadMarkerNotLoaded(val readMarkerId: String): UnreadState() + data class HasUnread(val firstUnreadEventId: String) : UnreadState() } data class RoomDetailViewState( @@ -59,10 +60,8 @@ data class RoomDetailViewState( val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, val highlightedEventId: String? = null, - val currentSnapshot: List = emptyList(), - val hasMoreToLoadForward: Boolean = false, - val hasMoreToLoadBackward: Boolean = false, - val unreadState: UnreadState = UnreadState.Unknown + val unreadState: UnreadState = UnreadState.Unknown, + val canShowJumpToReadMarker: Boolean = true ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index d4447f5b05..e3e9cf7378 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -83,7 +83,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) fun onReadMarkerVisible() - fun onReadMarkerInvisible() } interface UrlClickCallback { @@ -178,7 +177,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == eventIdToHighlight) { + || modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null } } @@ -280,12 +279,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } val mergedHeaderModel = mergedHeaderItemFactory.create(event, - nextEvent = nextEvent, - items = items, - addDaySeparator = addDaySeparator, - currentPosition = currentPosition, - eventIdToHighlight = eventIdToHighlight, - callback = callback + nextEvent = nextEvent, + items = items, + addDaySeparator = addDaySeparator, + currentPosition = currentPosition, + eventIdToHighlight = eventIdToHighlight, + callback = callback ) { requestModelBuild() } @@ -297,7 +296,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? { return when (currentUnreadState) { is UnreadState.HasUnread -> { - if (event.root.eventId == currentUnreadState.eventId) { + if (event.root.eventId == currentUnreadState.firstUnreadEventId) { TimelineReadMarkerItem_() .also { it.id("read_marker") @@ -307,8 +306,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec null } } - UnreadState.Unknown, - UnreadState.HasNoUnread -> null + else -> null } } @@ -359,6 +357,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val formattedDayModel: DaySeparatorItem? = null, val readMarkerModel: TimelineReadMarkerItem? = null ) { - fun shouldTriggerBuild(unreadState: UnreadState) = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null || (unreadState is UnreadState.HasUnread && unreadState.eventId == eventId) + fun shouldTriggerBuild(unreadState: UnreadState): Boolean { + return mergedHeaderModel != null + || formattedDayModel != null + || readMarkerModel != null + || (unreadState is UnreadState.HasUnread && unreadState.firstUnreadEventId == eventId) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 34e34fc7de..784a180d00 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -47,7 +47,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) - ?: false + ?: false val showInformation = addDaySeparator diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt index 11b7d68923..69b2b24899 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt @@ -27,8 +27,6 @@ class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEve override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { callback?.onReadMarkerVisible() - } else if (visibilityState == VisibilityState.INVISIBLE) { - callback?.onReadMarkerInvisible() } } } From bba52e77d1be6d857e2596120e90d6fe57a3e86d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 21 Nov 2019 16:25:55 +0100 Subject: [PATCH 176/189] Read marker: fix merged items --- .../timeline/TimelineEventController.kt | 58 ++++++++----------- .../detail/timeline/item/MergedHeaderItem.kt | 6 +- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index e3e9cf7378..503e01e5f8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -151,7 +151,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } // Update position when we are building new items - override fun intercept(models: MutableList>) { + override fun intercept(models: MutableList>) = synchronized(modelCache) { positionOfReadMarker = null adapterPositionMapping.clear() models.forEachIndexed { index, epoxyModel -> @@ -160,8 +160,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec adapterPositionMapping[it] = index } } - if (epoxyModel is TimelineReadMarkerItem) { - positionOfReadMarker = index + } + val currentUnreadState = this.unreadState + if (currentUnreadState is UnreadState.HasUnread) { + val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1) + positionOfReadMarker = position + if (position != null) { + val readMarker = TimelineReadMarkerItem_() + .also { + it.id("read_marker") + it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) + } + models.add(position, readMarker) } } } @@ -218,7 +228,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - // Timeline.LISTENER *************************************************************************** +// Timeline.LISTENER *************************************************************************** override fun onUpdated(snapshot: List) { submitSnapshot(snapshot) @@ -248,7 +258,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } else { it.eventModel } - listOf(eventModel, it?.mergedHeaderModel, it?.readMarkerModel, it?.formattedDayModel) + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) } .flatten() .filterNotNull() @@ -258,17 +268,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (modelCache.isEmpty()) { return } - val currentUnreadState = this.unreadState (0 until modelCache.size).forEach { position -> // Should be build if not cached or if cached but contains additional models // We then are sure we always have items up to date. - if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild(currentUnreadState) == true) { - modelCache[position] = buildCacheItem(position, currentSnapshot, currentUnreadState) + if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { + modelCache[position] = buildCacheItem(position, currentSnapshot) } } } - private fun buildCacheItem(currentPosition: Int, items: List, currentUnreadState: UnreadState): CacheItemData { + private fun buildCacheItem(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() @@ -289,25 +298,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) - val readMarkerItem = buildReadMarkerItem(event, currentUnreadState) - return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem) - } - - private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? { - return when (currentUnreadState) { - is UnreadState.HasUnread -> { - if (event.root.eventId == currentUnreadState.firstUnreadEventId) { - TimelineReadMarkerItem_() - .also { - it.id("read_marker") - it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) - } - } else { - null - } - } - else -> null - } + return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) } private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { @@ -354,14 +345,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val eventId: String?, val eventModel: EpoxyModel<*>? = null, val mergedHeaderModel: MergedHeaderItem? = null, - val formattedDayModel: DaySeparatorItem? = null, - val readMarkerModel: TimelineReadMarkerItem? = null + val formattedDayModel: DaySeparatorItem? = null ) { - fun shouldTriggerBuild(unreadState: UnreadState): Boolean { - return mergedHeaderModel != null - || formattedDayModel != null - || readMarkerModel != null - || (unreadState is UnreadState.HasUnread && unreadState.firstUnreadEventId == eventId) + fun shouldTriggerBuild(): Boolean { + return mergedHeaderModel != null || formattedDayModel != null + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index bbccb71ffd..3ffc8e65d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -72,7 +72,11 @@ abstract class MergedHeaderItem : BaseEventItem() { } override fun getEventIds(): List { - return attributes.mergeData.map { it.eventId } + return if (attributes.isCollapsed) { + attributes.mergeData.map { it.eventId } + } else { + emptyList() + } } data class Data( From 8e873672a9a4108f595393194998cb7ce5a8fc33 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Nov 2019 12:17:23 +0100 Subject: [PATCH 177/189] Read marker: change design --- .../res/layout/item_timeline_read_marker.xml | 53 +++++++++++++++---- vector/src/main/res/values/strings_riotX.xml | 1 + 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml index 8ee56045bf..8b4fa261d9 100644 --- a/vector/src/main/res/layout/item_timeline_read_marker.xml +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -1,18 +1,51 @@ - - + android:layout_height="wrap_content" + android:background="?riotx_background" + android:padding="8dp"> + + + + + + android:background="@color/notification_accent_color" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/itemDayTextView" + app:layout_constraintTop_toTopOf="parent" /> - \ No newline at end of file + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index dd8dd52dc6..f259a34e44 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -22,6 +22,7 @@ %1$s made the room public to whoever knows the link. %1$s made the room invite only. + Unread messages Liberate your communication Chat with people directly or in groups From 90c472fef9c74a3924c60f53f32543167011f635 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Nov 2019 12:17:54 +0100 Subject: [PATCH 178/189] Read marker: fix mark all as read --- .../database/query/ReadMarkerEntityQueries.kt | 8 ++---- .../internal/database/query/ReadQueries.kt | 27 +++++++++++++++++-- .../session/room/read/SetReadMarkersTask.kt | 24 +++-------------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt index 061634a9da..d95dc58574 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt @@ -22,13 +22,9 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where -internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery { - val query = realm.where() +internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { + return realm.where() .equalTo(ReadMarkerEntityFields.ROOM_ID, roomId) - if (eventId != null) { - query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId) - } - return query } internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index 0a925ac1ab..f1045cdb36 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.database.query import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity +import io.realm.Realm internal fun isEventRead(monarchy: Monarchy, userId: String?, @@ -39,8 +41,10 @@ internal fun isEventRead(monarchy: Monarchy, isEventRead = if (eventToCheck?.sender == userId) { true } else { - val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE + val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: return@doWithRealm + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex + ?: Int.MIN_VALUE val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE eventToCheckIndex <= readReceiptIndex @@ -49,3 +53,22 @@ internal fun isEventRead(monarchy: Monarchy, return isEventRead } + +internal fun isReadMarkerMoreRecent(monarchy: Monarchy, + roomId: String?, + eventId: String?): Boolean { + if (roomId.isNullOrBlank() || eventId.isNullOrBlank()) { + return false + } + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false + val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root + + val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false + val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex + ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE + eventToCheckIndex <= readMarkerIndex + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 7e5de176bb..83d12d0bae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -57,22 +57,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI override suspend fun execute(params: SetReadMarkersTask.Params) { val markers = HashMap() - val fullyReadEventId: String? - val readReceiptEventId: String? Timber.v("Execute set read marker with params: $params") - if (params.markAllAsRead) { + val (fullyReadEventId, readReceiptEventId) = if (params.markAllAsRead) { val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId } - fullyReadEventId = latestSyncedEventId - readReceiptEventId = latestSyncedEventId + Pair(latestSyncedEventId, latestSyncedEventId) } else { - fullyReadEventId = params.fullyReadEventId - readReceiptEventId = params.readReceiptEventId + Pair(params.fullyReadEventId, params.readReceiptEventId) } - if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) { + if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) { if (LocalEcho.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event $fullyReadEventId") } else { @@ -118,16 +114,4 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } } - - private fun isReadMarkerMoreRecent(roomId: String, newReadMarkerId: String): Boolean { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val currentReadMarkerId = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()?.eventId - ?: return true - val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = currentReadMarkerId).findFirst() - val newReadMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = newReadMarkerId).findFirst() - val currentReadMarkerIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE - val newReadMarkerIndex = newReadMarkerEvent?.root?.displayIndex ?: Int.MIN_VALUE - newReadMarkerIndex > currentReadMarkerIndex - } - } } From 0376de08f42d774abc8f90fc438f7bf0e69691d1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Nov 2019 12:30:54 +0100 Subject: [PATCH 179/189] Clean files --- .../vector/matrix/android/api/session/room/timeline/Timeline.kt | 1 - .../vector/matrix/android/internal/database/query/ReadQueries.kt | 1 - .../android/internal/session/room/read/SetReadMarkersTask.kt | 1 - .../java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt | 1 - .../riotx/features/home/room/detail/RoomDetailViewModel.kt | 1 - .../home/room/detail/timeline/TimelineEventController.kt | 1 - .../riotx/features/home/room/detail/timeline/item/NoticeItem.kt | 1 - 7 files changed, 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index a71f5b7479..b55c830c43 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -122,5 +122,4 @@ interface Timeline { */ BACKWARDS } - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index f1045cdb36..c214886ec8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -71,4 +71,3 @@ internal fun isReadMarkerMoreRecent(monarchy: Monarchy, eventToCheckIndex <= readMarkerIndex } } - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 83d12d0bae..b9dca748cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.LocalEcho -import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.* diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt index 5ba482837e..b2adde449a 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -23,7 +23,6 @@ import android.util.AttributeSet import android.view.View import android.widget.RelativeLayout import androidx.core.content.ContextCompat -import androidx.core.view.isInvisible import im.vector.riotx.R import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.* diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 2dab9264e5..642bce3319 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -829,7 +829,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro return UnreadState.HasNoUnread } - private fun observeUnreadState() { selectSubscribe(RoomDetailViewState::unreadState) { Timber.v("Unread state: $it") diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 503e01e5f8..326e19c431 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -349,7 +349,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec ) { fun shouldTriggerBuild(): Boolean { return mergedHeaderModel != null || formattedDayModel != null - } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 804990cc5c..05dedcfa22 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -36,7 +36,6 @@ abstract class NoticeItem : BaseEventItem() { attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) - override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = attributes.noticeText From 7890b929a7843b1602a20006b2b5ea9f3b2af5f8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Nov 2019 12:32:45 +0100 Subject: [PATCH 180/189] Update CHANGES --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 3ead09faac..e1302bc957 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Features ✨: Improvements 🙌: - Send mention Pills from composer - Links in message preview in the bottom sheet are now active. + - Rework the read marker to make it more usable Other changes: - Fix a small grammatical error when an empty room list is shown. From e7a47ae32ae1fa64437439ff6483c5d763c30cf8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 27 Nov 2019 18:52:22 +0100 Subject: [PATCH 181/189] Some cleanup --- .../android/api/session/room/timeline/Timeline.kt | 2 +- .../main/res/layout/item_timeline_read_marker.xml | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index b55c830c43..85dbdcaa19 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -104,7 +104,7 @@ interface Timeline { interface Listener { /** * Call when the timeline has been updated through pagination or sync. - * @param snapshot the most uptodate snapshot + * @param snapshot the most up to date snapshot */ fun onUpdated(snapshot: List) } diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml index 8b4fa261d9..fdc4fc198d 100644 --- a/vector/src/main/res/layout/item_timeline_read_marker.xml +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -7,7 +7,7 @@ android:padding="8dp"> - From 9510d71cd375b437428a46ddae4f0facf0567a64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 27 Nov 2019 18:53:29 +0100 Subject: [PATCH 182/189] Proposal for simple layout --- .../layout/item_timeline_read_marker_bma.xml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 vector/src/main/res/layout/item_timeline_read_marker_bma.xml diff --git a/vector/src/main/res/layout/item_timeline_read_marker_bma.xml b/vector/src/main/res/layout/item_timeline_read_marker_bma.xml new file mode 100644 index 0000000000..1a8be7bdf1 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_read_marker_bma.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file From f9eb80b4ece3910291aac6aedc7271c97ed0306e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Dec 2019 11:32:57 +0100 Subject: [PATCH 183/189] Simplify layout --- .../item_timeline_event_day_separator.xml | 48 ++++--------------- .../res/layout/item_timeline_read_marker.xml | 43 +++++------------ .../layout/item_timeline_read_marker_bma.xml | 28 ----------- 3 files changed, 21 insertions(+), 98 deletions(-) delete mode 100644 vector/src/main/res/layout/item_timeline_read_marker_bma.xml diff --git a/vector/src/main/res/layout/item_timeline_event_day_separator.xml b/vector/src/main/res/layout/item_timeline_event_day_separator.xml index 81e94fd68e..13b70c4243 100644 --- a/vector/src/main/res/layout/item_timeline_event_day_separator.xml +++ b/vector/src/main/res/layout/item_timeline_event_day_separator.xml @@ -1,6 +1,5 @@ - - + android:layout_marginEnd="8dp" + android:background="?riotx_header_panel_background" /> - - - - \ No newline at end of file + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml index fdc4fc198d..e76ffa3d5c 100644 --- a/vector/src/main/res/layout/item_timeline_read_marker.xml +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -1,49 +1,28 @@ - + android:background="@color/notification_accent_color" /> + android:textSize="15sp" /> - - - - \ No newline at end of file + diff --git a/vector/src/main/res/layout/item_timeline_read_marker_bma.xml b/vector/src/main/res/layout/item_timeline_read_marker_bma.xml deleted file mode 100644 index 1a8be7bdf1..0000000000 --- a/vector/src/main/res/layout/item_timeline_read_marker_bma.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - \ No newline at end of file From a6f8fe9317a4d96ec05991cf0efc9654a6c2344f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Dec 2019 12:08:18 +0100 Subject: [PATCH 184/189] Fix lint issue --- .../features/home/room/detail/timeline/item/BaseEventItem.kt | 1 - .../home/room/detail/timeline/item/MergedHeaderItem.kt | 2 +- .../src/main/res/layout/item_timeline_event_base_noinfo.xml | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 576e596f90..02b7341c72 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -19,7 +19,6 @@ import android.view.View import android.view.ViewStub import android.widget.RelativeLayout import androidx.annotation.IdRes -import androidx.core.view.marginStart import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import im.vector.riotx.R diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 3ffc8e65d6..a2a3c9ad3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -103,6 +103,6 @@ abstract class MergedHeaderItem : BaseEventItem() { } companion object { - private const val STUB_ID = R.id.messageContentMergedheaderStub + private const val STUB_ID = R.id.messageContentMergedHeaderStub } } diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index b72933b94f..c1987dccb2 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -10,7 +10,7 @@ android:id="@+id/messageSelectedBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_alignBottom="@+id/informationBottom" + android:layout_alignBottom="@+id/readReceiptsView" android:layout_alignParentTop="true" android:background="?riotx_highlighted_message_background" /> @@ -47,7 +47,7 @@ android:layout="@layout/item_timeline_event_blank_stub" /> From 3623072f083fcc8f0fa14545f0cf9d0462fc0e09 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Dec 2019 14:39:47 +0100 Subject: [PATCH 185/189] Attempt to properly cancel the crypto module when user signs out (#724) Attempt to properly cancel the crypto module when user signs out (#724) --- CHANGES.md | 1 + .../android/api/session/file/FileService.kt | 3 +- .../android/internal/crypto/CryptoModule.kt | 9 +++ .../internal/crypto/DefaultCryptoService.kt | 60 ++++++++++--------- .../crypto/IncomingRoomKeyRequestManager.kt | 5 +- .../crypto/OutgoingRoomKeyRequestManager.kt | 5 ++ .../algorithms/megolm/MXMegolmDecryption.kt | 11 ++-- .../megolm/MXMegolmDecryptionFactory.kt | 25 ++++---- .../internal/crypto/keysbackup/KeysBackup.kt | 25 ++++---- .../internal/crypto/tasks/UploadKeysTask.kt | 6 +- .../internal/session/DefaultFileService.kt | 8 ++- 11 files changed, 94 insertions(+), 64 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5e9f9ce671..a8c366204e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Bugfix 🐛: - Do not show long click help if only invitation are displayed - Fix emoji filtering not working - Fix issue of closing Realm in another thread (#725) + - Attempt to properly cancel the crypto module when user signs out (#724) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt index e1694199ed..4d9cff3e92 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.file import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import java.io.File @@ -47,5 +48,5 @@ interface FileService { fileName: String, url: String?, elementToDecrypt: ElementToDecrypt?, - callback: MatrixCallback) + callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 5a7e28b70f..a12f6e40ce 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -37,6 +37,8 @@ import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask import io.realm.RealmConfiguration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import retrofit2.Retrofit import java.io.File @@ -66,6 +68,13 @@ internal abstract class CryptoModule { .build() } + @JvmStatic + @Provides + @SessionScope + fun providesCryptoCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob()) + } + @JvmStatic @Provides @CryptoDatabase diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index cf5506a443..c50b9e2e10 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -132,7 +132,8 @@ internal class DefaultCryptoService @Inject constructor( private val loadRoomMembersTask: LoadRoomMembersTask, private val monarchy: Monarchy, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val cryptoCoroutineScope: CoroutineScope ) : CryptoService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -243,7 +244,8 @@ internal class DefaultCryptoService @Inject constructor( return } isStarting.set(true) - GlobalScope.launch(coroutineDispatchers.crypto) { + + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { internalStart(isInitialSync) } } @@ -269,10 +271,9 @@ internal class DefaultCryptoService @Inject constructor( isStarted.set(true) }, { - Timber.e("Start failed: $it") - delay(1000) isStarting.set(false) - internalStart(isInitialSync) + isStarted.set(false) + Timber.e(it, "Start failed") } ) } @@ -281,9 +282,12 @@ internal class DefaultCryptoService @Inject constructor( * Close the crypto */ fun close() = runBlocking(coroutineDispatchers.crypto) { + cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) + + outgoingRoomKeyRequestManager.stop() + olmDevice.release() cryptoStore.close() - outgoingRoomKeyRequestManager.stop() } // Aways enabled on RiotX @@ -305,19 +309,21 @@ internal class DefaultCryptoService @Inject constructor( * @param syncResponse the syncResponse */ fun onSyncCompleted(syncResponse: SyncResponse) { - GlobalScope.launch(coroutineDispatchers.crypto) { - if (syncResponse.deviceLists != null) { - deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) - } - if (syncResponse.deviceOneTimeKeysCount != null) { - val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 - oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) - } - if (isStarted()) { - // Make sure we process to-device messages before generating new one-time-keys #2782 - deviceListManager.refreshOutdatedDeviceLists() - oneTimeKeysUploader.maybeUploadOneTimeKeys() - incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + if (syncResponse.deviceLists != null) { + deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) + } + if (syncResponse.deviceOneTimeKeysCount != null) { + val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 + oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) + } + if (isStarted()) { + // Make sure we process to-device messages before generating new one-time-keys #2782 + deviceListManager.refreshOutdatedDeviceLists() + oneTimeKeysUploader.maybeUploadOneTimeKeys() + incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + } } } } @@ -511,7 +517,7 @@ internal class DefaultCryptoService @Inject constructor( eventType: String, roomId: String, callback: MatrixCallback) { - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { if (!isStarted()) { Timber.v("## encryptEventContent() : wait after e2e init") internalStart(false) @@ -571,7 +577,7 @@ internal class DefaultCryptoService @Inject constructor( * @param callback the callback to return data or null */ override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { - GlobalScope.launch { + cryptoCoroutineScope.launch { val result = runCatching { withContext(coroutineDispatchers.crypto) { internalDecryptEvent(event, timeline) @@ -621,7 +627,7 @@ internal class DefaultCryptoService @Inject constructor( * @param event the event */ fun onToDeviceEvent(event: Event) { - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { onRoomKeyEvent(event) @@ -661,7 +667,7 @@ internal class DefaultCryptoService @Inject constructor( * @param event the encryption event. */ private fun onRoomEncryptionEvent(roomId: String, event: Event) { - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { val params = LoadRoomMembersTask.Params(roomId) try { loadRoomMembersTask.execute(params) @@ -753,7 +759,7 @@ internal class DefaultCryptoService @Inject constructor( * @param callback the exported keys */ override fun exportRoomKeys(password: String, callback: MatrixCallback) { - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { runCatching { exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) }.foldToCallback(callback) @@ -791,7 +797,7 @@ internal class DefaultCryptoService @Inject constructor( password: String, progressListener: ProgressListener?, callback: MatrixCallback) { - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { runCatching { withContext(coroutineDispatchers.crypto) { Timber.v("## importRoomKeys starts") @@ -839,7 +845,7 @@ internal class DefaultCryptoService @Inject constructor( */ fun checkUnknownDevices(userIds: List, callback: MatrixCallback) { // force the refresh to ensure that the devices list is up-to-date - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { runCatching { val keys = deviceListManager.downloadKeys(userIds, true) val unknownDevices = getUnknownDevices(keys) @@ -999,7 +1005,7 @@ internal class DefaultCryptoService @Inject constructor( } override fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) { - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { runCatching { deviceListManager.downloadKeys(userIds, forceDownload) }.foldToCallback(callback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt index aeee025fdc..e8d8bf0f35 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt @@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.session.SessionScope import timber.log.Timber import javax.inject.Inject -import kotlin.collections.ArrayList @SessionScope internal class IncomingRoomKeyRequestManager @Inject constructor( @@ -51,7 +50,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( * * @param event the announcement event. */ - suspend fun onRoomKeyRequestEvent(event: Event) { + fun onRoomKeyRequestEvent(event: Event) { val roomKeyShare = event.getClearContent().toModel() when (roomKeyShare?.action) { RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event)) @@ -139,7 +138,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( if (null != receivedRoomKeyRequestCancellations) { for (request in receivedRoomKeyRequestCancellations!!) { Timber.v("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.userId - + ":" + request.deviceId + " id " + request.requestId) + + ":" + request.deviceId + " id " + request.requestId) // we should probably only notify the app of cancellations we told it // about, but we don't currently have a record of that, so we just pass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt index 433dee4fb9..5320b84b0e 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt @@ -63,6 +63,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor( */ fun stop() { isClientRunning = false + stopTimer() } /** @@ -171,6 +172,10 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor( }, SEND_KEY_REQUESTS_DELAY_MS.toLong()) } + private fun stopTimer() { + BACKGROUND_HANDLER.removeCallbacksAndMessages(null) + } + // look for and send any queued requests. Runs itself recursively until // there are no more requests, or there is an error (in which case, the // timer will be restarted before the promise resolves). diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 0230141e1b..81ac1403df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -34,7 +34,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber @@ -46,8 +46,9 @@ internal class MXMegolmDecryption(private val userId: String, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val cryptoStore: IMXCryptoStore, private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers) - : IMXDecrypting { + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) : IMXDecrypting { var newSessionListener: NewSessionListener? = null @@ -61,7 +62,7 @@ internal class MXMegolmDecryption(private val userId: String, return decryptEvent(event, timeline, true) } - private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { + private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { if (event.roomId.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } @@ -292,7 +293,7 @@ internal class MXMegolmDecryption(private val userId: String, return } val userId = request.userId ?: return - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { runCatching { deviceListManager.downloadKeys(listOf(userId), false) } .mapCatching { val deviceId = request.deviceId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index b7329221ab..7cddd27779 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -25,17 +25,21 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope import javax.inject.Inject -internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val userId: String, - private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers) { +internal class MXMegolmDecryptionFactory @Inject constructor( + @UserId private val userId: String, + private val olmDevice: MXOlmDevice, + private val deviceListManager: DeviceListManager, + private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) { fun create(): MXMegolmDecryption { return MXMegolmDecryption( @@ -47,6 +51,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val ensureOlmSessionsForDevicesAction, cryptoStore, sendToDeviceTask, - coroutineDispatchers) + coroutineDispatchers, + cryptoCoroutineScope) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt index b3ee138591..5e5291d600 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -59,7 +59,7 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.awaitCallback -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.olm.OlmException @@ -102,7 +102,8 @@ internal class KeysBackup @Inject constructor( private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, // Task executor private val taskExecutor: TaskExecutor, - private val coroutineDispatchers: MatrixCoroutineDispatchers + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope ) : KeysBackupService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -143,7 +144,7 @@ internal class KeysBackup @Inject constructor( override fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback) { - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { runCatching { withContext(coroutineDispatchers.crypto) { val olmPkDecryption = OlmPkDecryption() @@ -233,7 +234,7 @@ internal class KeysBackup @Inject constructor( } override fun deleteBackup(version: String, callback: MatrixCallback?) { - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.crypto) { // If we're currently backing up to this backup... stop. // (We start using it automatically in createKeysBackupVersion so this is symmetrical). @@ -448,7 +449,7 @@ internal class KeysBackup @Inject constructor( callback.onFailure(IllegalArgumentException("Missing element")) } else { - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { // Get current signatures, or create an empty set val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap() @@ -523,7 +524,7 @@ internal class KeysBackup @Inject constructor( callback: MatrixCallback) { Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { val isValid = withContext(coroutineDispatchers.crypto) { isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) } @@ -543,7 +544,7 @@ internal class KeysBackup @Inject constructor( callback: MatrixCallback) { Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { val recoveryKey = withContext(coroutineDispatchers.crypto) { recoveryKeyFromPassword(password, keysBackupVersion, null) } @@ -614,7 +615,7 @@ internal class KeysBackup @Inject constructor( callback: MatrixCallback) { Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { runCatching { val decryption = withContext(coroutineDispatchers.crypto) { // Check if the recovery is valid before going any further @@ -695,7 +696,7 @@ internal class KeysBackup @Inject constructor( callback: MatrixCallback) { Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { runCatching { val progressListener = if (stepProgressListener != null) { object : ProgressListener { @@ -729,8 +730,8 @@ internal class KeysBackup @Inject constructor( * parameters and always returns a KeysBackupData object through the Callback */ private suspend fun getKeys(sessionId: String?, - roomId: String?, - version: String): KeysBackupData { + roomId: String?, + version: String): KeysBackupData { return if (roomId != null && sessionId != null) { // Get key for the room and for the session val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version)) @@ -1154,7 +1155,7 @@ internal class KeysBackup @Inject constructor( keysBackupStateManager.state = KeysBackupState.BackingUp - GlobalScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.crypto) { Timber.v("backupKeys: 2 - Encrypting keys") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt index fd7a5e8e7a..db05f473b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt @@ -53,10 +53,10 @@ internal class DefaultUploadKeysTask @Inject constructor(private val cryptoApi: } return executeRequest { - if (encodedDeviceId.isNullOrBlank()) { - apiCall = cryptoApi.uploadKeys(body) + apiCall = if (encodedDeviceId.isBlank()) { + cryptoApi.uploadKeys(body) } else { - apiCall = cryptoApi.uploadKeys(encodedDeviceId, body) + cryptoApi.uploadKeys(encodedDeviceId, body) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index e530bafb18..868d63665a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -22,12 +22,14 @@ import arrow.core.Try import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.md5 +import im.vector.matrix.android.internal.util.toCancelable import im.vector.matrix.android.internal.util.writeToFile import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -55,8 +57,8 @@ internal class DefaultFileService @Inject constructor(private val context: Conte fileName: String, url: String?, elementToDecrypt: ElementToDecrypt?, - callback: MatrixCallback) { - GlobalScope.launch(coroutineDispatchers.main) { + callback: MatrixCallback): Cancelable { + return GlobalScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { val folder = getFolder(downloadMode, id) @@ -96,7 +98,7 @@ internal class DefaultFileService @Inject constructor(private val context: Conte } } .foldToCallback(callback) - } + }.toCancelable() } private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File { From a145aae0aa858ff2efd9750e4529d78bf99b5d69 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 13:19:03 +0100 Subject: [PATCH 186/189] Avoid using !! --- .../matrix/android/internal/crypto/keysbackup/KeysBackup.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt index 5e5291d600..1cc1a8a05a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -345,9 +345,7 @@ internal class KeysBackup @Inject constructor( } }) } - } - - keysBackupStateManager.addListener(keysBackupStateListener!!) + }.also { keysBackupStateManager.addListener(it) } backupKeys() } From bdb9d2fbb8322014b8ca59bb83bb8dbbcab3f4df Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Dec 2019 16:26:32 +0100 Subject: [PATCH 187/189] Improve and cleanup OneTimeKey uploader Fix boolean reset if request fails Implement https://github.com/matrix-org/matrix-js-sdk/pull/493 --- .../internal/crypto/OneTimeKeysUploader.kt | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt index e6b57d149f..ccf7173f6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt @@ -42,8 +42,6 @@ internal class OneTimeKeysUploader @Inject constructor( private var lastOneTimeKeyCheck: Long = 0 private var oneTimeKeyCount: Int? = null - private var lastPublishedOneTimeKeys: Map>? = null - /** * Stores the current one_time_key count which will be handled later (in a call of * _onSyncCompleted). The count is e.g. coming from a /sync response. @@ -59,10 +57,12 @@ internal class OneTimeKeysUploader @Inject constructor( */ suspend fun maybeUploadOneTimeKeys() { if (oneTimeKeyCheckInProgress) { + Timber.v("maybeUploadOneTimeKeys: already in progress") return } if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { // we've done a key upload recently. + Timber.v("maybeUploadOneTimeKeys: executed too recently") return } @@ -79,12 +79,8 @@ internal class OneTimeKeysUploader @Inject constructor( // discard the oldest private keys first. This will eventually clean // out stale private keys that won't receive a message. val keyLimit = floor(maxOneTimeKeys / 2.0).toInt() - if (oneTimeKeyCount != null) { - uploadOTK(oneTimeKeyCount!!, keyLimit) - } else { - // ask the server how many keys we have - val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!) - val response = uploadKeysTask.execute(uploadKeysParams) + val oneTimeKeyCountFromSync = oneTimeKeyCount + if (oneTimeKeyCountFromSync != null) { // We need to keep a pool of one time public keys on the server so that // other devices can start conversations with us. But we can only store // a finite number of private keys in the olm Account object. @@ -96,14 +92,16 @@ internal class OneTimeKeysUploader @Inject constructor( // private keys clogging up our local storage. // So we need some kind of engineering compromise to balance all of // these factors. - // TODO Why we do not set oneTimeKeyCount here? - // TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also) - val keyCount = response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE) - uploadOTK(keyCount, keyLimit) + try { + uploadOTK(oneTimeKeyCountFromSync, keyLimit) + } finally { + oneTimeKeyCheckInProgress = false + } + Timber.v("## uploadKeys() : success") + } else { + Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync") + oneTimeKeyCheckInProgress = false } - Timber.v("## uploadKeys() : success") - oneTimeKeyCount = null - oneTimeKeyCheckInProgress = false } /** @@ -119,45 +117,42 @@ internal class OneTimeKeysUploader @Inject constructor( } val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) olmDevice.generateOneTimeKeys(keysThisLoop) - val response = uploadOneTimeKeys() + val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys()) + olmDevice.markKeysAsPublished() + if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { + // Maybe upload other keys uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) } else { - Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") + Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") } } /** - * Upload my user's one time keys. + * Upload curve25519 one time keys. */ - private suspend fun uploadOneTimeKeys(): KeysUploadResponse { - val oneTimeKeys = olmDevice.getOneTimeKeys() + private suspend fun uploadOneTimeKeys(oneTimeKeys: Map>?): KeysUploadResponse { val oneTimeJson = mutableMapOf() - val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) + val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) ?: emptyMap() - if (null != curve25519Map) { - for ((key_id, value) in curve25519Map) { - val k = mutableMapOf() - k["key"] = value + curve25519Map.forEach { (key_id, value) -> + val k = mutableMapOf() + k["key"] = value - // the key is also signed - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) + // the key is also signed + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) - k["signatures"] = objectSigner.signObject(canonicalJson) + k["signatures"] = objectSigner.signObject(canonicalJson) - oneTimeJson["signed_curve25519:$key_id"] = k - } + oneTimeJson["signed_curve25519:$key_id"] = k } // For now, we set the device id explicitly, as we may not be using the // same one as used in login. val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!) - val response = uploadKeysTask.execute(uploadParams) - lastPublishedOneTimeKeys = oneTimeKeys - olmDevice.markKeysAsPublished() - return response + return uploadKeysTask.execute(uploadParams) } companion object { From f31c1b69cb9425d7c1e9bbbbc663b83cb8b45403 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Dec 2019 16:52:55 +0100 Subject: [PATCH 188/189] Remove delay when waiting for first sync to finish and add number of sent keys in the log --- .../android/internal/crypto/OneTimeKeysUploader.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt index ccf7173f6d..a0483335e5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt @@ -93,14 +93,15 @@ internal class OneTimeKeysUploader @Inject constructor( // So we need some kind of engineering compromise to balance all of // these factors. try { - uploadOTK(oneTimeKeyCountFromSync, keyLimit) + val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit) + Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent") } finally { oneTimeKeyCheckInProgress = false } - Timber.v("## uploadKeys() : success") } else { Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync") oneTimeKeyCheckInProgress = false + lastOneTimeKeyCheck = 0 } } @@ -109,11 +110,12 @@ internal class OneTimeKeysUploader @Inject constructor( * * @param keyCount the key count * @param keyLimit the limit + * @return the number of uploaded keys */ - private suspend fun uploadOTK(keyCount: Int, keyLimit: Int) { + private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int { if (keyLimit <= keyCount) { // If we don't need to generate any more keys then we are done. - return + return 0 } val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) olmDevice.generateOneTimeKeys(keysThisLoop) @@ -122,7 +124,7 @@ internal class OneTimeKeysUploader @Inject constructor( if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { // Maybe upload other keys - uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) + return keysThisLoop + uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) } else { Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") From 6b39cf3b70753dd1ba8613e908f5762ec56f0b81 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 5 Dec 2019 09:43:58 +0100 Subject: [PATCH 189/189] Prepare release 0.9.0 --- CHANGES.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 352c0a0745..472c56648a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -Changes in RiotX 0.9.0 (2019-XX-XX) +Changes in RiotX 0.9.0 (2019-12-05) =================================================== Features ✨: @@ -19,12 +19,6 @@ Bugfix 🐛: - Fix issue of closing Realm in another thread (#725) - Attempt to properly cancel the crypto module when user signs out (#724) -Translations 🗣: - - - -Build 🧱: - - - Changes in RiotX 0.8.0 (2019-11-19) ===================================================