From 01b0c7a036b1473f4807644782a9096658bbf618 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Apr 2021 12:20:47 +0200 Subject: [PATCH 001/202] Use the navigator to open the LoginActivity --- .../java/im/vector/app/features/MainActivity.kt | 15 +++++++++------ .../app/features/link/LinkHandlerActivity.kt | 9 +++++---- .../app/features/navigation/DefaultNavigator.kt | 8 ++++++++ .../vector/app/features/navigation/Navigator.kt | 3 +++ .../permalink/PermalinkHandlerActivity.kt | 8 ++++---- .../app/features/share/IncomingShareFragment.kt | 8 ++++---- 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 34e73c8702..50a86d24ed 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -34,7 +34,6 @@ import im.vector.app.core.utils.deleteAllFiles import im.vector.app.databinding.ActivityMainBinding import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.ShortcutsHandler -import im.vector.app.features.login.LoginActivity import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinLocker @@ -222,9 +221,11 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity val intent = when { args.clearCredentials && !ignoreClearCredentials - && (!args.isUserLoggedOut || args.isAccountDeactivated) -> + && (!args.isUserLoggedOut || args.isAccountDeactivated) -> { // User has explicitly asked to log out or deactivated his account - LoginActivity.newIntent(this, null) + navigator.openLogin(this, null) + null + } args.isSoftLogout -> // The homeserver has invalidated the token, with a soft logout SoftLogoutActivity.newIntent(this) @@ -240,11 +241,13 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity // The token is still invalid SoftLogoutActivity.newIntent(this) } - else -> + else -> { // First start, or no active session - LoginActivity.newIntent(this, null) + navigator.openLogin(this, null) + null + } } - startActivity(intent) + intent?.let { startActivity(it) } finish() } } diff --git a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt index 6c0e142b38..570de63437 100644 --- a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt @@ -27,7 +27,6 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityProgressBinding -import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig import im.vector.app.features.permalink.PermalinkHandler import io.reactivex.android.schedulers.AndroidSchedulers @@ -126,9 +125,11 @@ class LinkHandlerActivity : VectorBaseActivity() { * Start the login screen with identity server and home server pre-filled */ private fun startLoginActivity(uri: Uri) { - val intent = LoginActivity.newIntent(this, LoginConfig.parse(uri)) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) + navigator.openLogin( + context = this, + loginConfig = LoginConfig.parse(uri), + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK + ) finish() } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 73d8325bca..c6df7910ea 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -54,6 +54,8 @@ import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.search.SearchArgs import im.vector.app.features.home.room.filtered.FilteredRoomsActivity import im.vector.app.features.invite.InviteUsersToRoomActivity +import im.vector.app.features.login.LoginActivity +import im.vector.app.features.login.LoginConfig import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.BigImageViewerActivity @@ -96,6 +98,12 @@ class DefaultNavigator @Inject constructor( private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider ) : Navigator { + override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) { + val intent = LoginActivity.newIntent(context, loginConfig) + intent.addFlags(flags) + context.startActivity(intent) + } + override fun openRoom(context: Context, roomId: String, eventId: String?, buildTask: Boolean) { if (sessionHolder.getSafeActiveSession()?.getRoom(roomId) == null) { fatalError("Trying to open an unknown room $roomId", vectorPreferences.failFast()) diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 489cd37987..a75b27a1f0 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -23,6 +23,7 @@ import android.view.View import androidx.activity.result.ActivityResultLauncher import androidx.core.util.Pair import im.vector.app.features.crypto.recover.SetupMode +import im.vector.app.features.login.LoginConfig import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinMode import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData @@ -36,6 +37,8 @@ import org.matrix.android.sdk.api.util.MatrixItem interface Navigator { + fun openLogin(context: Context, loginConfig: LoginConfig? = null, flags: Int = 0) + fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false) fun switchToSpace(context: Context, spaceId: String, roomId: String?, openShareSheet: Boolean) diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt index 02c9c7f717..ee4e0e05b5 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt @@ -26,7 +26,6 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.FragmentProgressBinding import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.LoadingFragment -import im.vector.app.features.login.LoginActivity import javax.inject.Inject class PermalinkHandlerActivity : VectorBaseActivity() { @@ -71,9 +70,10 @@ class PermalinkHandlerActivity : VectorBaseActivity() { } private fun startLoginActivity() { - val intent = LoginActivity.newIntent(this, null) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) + navigator.openLogin( + context = this, + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK + ) finish() } } diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt index f89480046f..7b5d8a9ebd 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt @@ -41,7 +41,6 @@ import im.vector.app.databinding.FragmentIncomingShareBinding import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs -import im.vector.app.features.login.LoginActivity import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -211,9 +210,10 @@ class IncomingShareFragment @Inject constructor( } private fun startLoginActivity() { - val intent = LoginActivity.newIntent(requireActivity(), null) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) + navigator.openLogin( + context = requireActivity(), + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK + ) requireActivity().finish() } From 9972ab5d2e69a55356ededa13c60acfe9e68c2ad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Apr 2021 12:37:26 +0200 Subject: [PATCH 002/202] Cleanup --- .../java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt index 9c96cba40c..fff4f89bac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -25,7 +25,6 @@ interface LoginWizard { * @param login the login field * @param password the password field * @param deviceName the initial device name - * @param callback the matrix callback on which you'll receive the result of authentication. * @return a [Cancelable] */ suspend fun login(login: String, From 344a7e5b3d33531a24498e59235f4614b5498bd0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Apr 2021 12:52:28 +0200 Subject: [PATCH 003/202] Add facility to get profile info to the login wizard --- .../sdk/api/auth/login/LoginProfileInfo.kt | 23 +++++++++ .../android/sdk/api/auth/login/LoginWizard.kt | 5 ++ .../android/sdk/internal/auth/AuthAPI.kt | 10 ++++ .../internal/auth/login/DefaultLoginWizard.kt | 11 +++++ .../sdk/internal/auth/login/GetProfileTask.kt | 48 +++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt new file mode 100644 index 0000000000..288a6d1232 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.login + +data class LoginProfileInfo( + val matrixId: String, + val displayName: String?, + val fullAvatarUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt index fff4f89bac..cce87176d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -21,6 +21,11 @@ import org.matrix.android.sdk.api.util.Cancelable interface LoginWizard { + /** + * Get some information about a matrixId: displayName and avatar url + */ + suspend fun getProfileInfo(matrixId: String): LoginProfileInfo + /** * @param login the login field * @param password the password field diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt index f93f285c6e..5a9fa9edf6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.auth.data.Availability import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams @@ -73,6 +74,15 @@ internal interface AuthAPI { @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/available") suspend fun registerAvailable(@Query("username") username: String): Availability + /** + * Get the combined profile information for this user. + * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers. + * This API may return keys which are not limited to displayname or avatar_url. + * @param userId the user id to fetch profile info + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") + suspend fun getProfile(@Path("userId") userId: String): JsonDict + /** * Add 3Pid during registration * Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 8b81f42e03..854caf8a62 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth.login import android.util.Patterns +import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.session.Session @@ -30,6 +31,7 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver internal class DefaultLoginWizard( private val authAPI: AuthAPI, @@ -39,6 +41,15 @@ internal class DefaultLoginWizard( private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") + private val getProfileTask: GetProfileTask = DefaultGetProfileTask( + authAPI, + DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig) + ) + + override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo { + return getProfileTask.execute(GetProfileTask.Params(matrixId)) + } + override suspend fun login(login: String, password: String, deviceName: String): Session { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt new file mode 100644 index 0000000000..bb9faf49c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.login + +import org.matrix.android.sdk.api.auth.login.LoginProfileInfo +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface GetProfileTask : Task { + data class Params( + val userId: String + ) +} + +internal class DefaultGetProfileTask( + private val authAPI: AuthAPI, + private val contentUrlResolver: ContentUrlResolver +) : GetProfileTask { + + override suspend fun execute(params: GetProfileTask.Params): LoginProfileInfo { + val info = executeRequest(null) { + authAPI.getProfile(params.userId) + } + + return LoginProfileInfo( + matrixId = params.userId, + displayName = info[ProfileService.DISPLAY_NAME_KEY] as? String, + fullAvatarUrl = contentUrlResolver.resolveFullSize(info[ProfileService.AVATAR_URL_KEY] as? String) + ) + } +} From 408a0fc010aa692483036070a563d4262c21bd14 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Apr 2021 12:35:33 +0200 Subject: [PATCH 004/202] Login UX flow v2 --- CHANGES.md | 1 + vector/src/main/AndroidManifest.xml | 16 + .../im/vector/app/core/di/FragmentModule.kt | 108 ++- .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../app/features/login/LoginSplashFragment.kt | 8 + .../features/login2/AbstractLoginFragment2.kt | 163 ++++ .../login2/AbstractSSOLoginFragment2.kt | 101 +++ .../app/features/login2/LoginAction2.kt | 85 ++ .../app/features/login2/LoginActivity2.kt | 380 ++++++++ .../features/login2/LoginCaptchaFragment2.kt | 194 ++++ .../login2/LoginFragment2SigninPassword.kt | 179 ++++ .../login2/LoginFragment2SigninUsername.kt | 107 +++ .../login2/LoginFragment2SignupPassword.kt | 148 ++++ .../login2/LoginFragment2SignupUsername.kt | 141 +++ .../features/login2/LoginFragmentToAny2.kt | 227 +++++ .../LoginGenericTextInputFormFragment2.kt | 258 ++++++ .../login2/LoginResetPasswordFragment2.kt | 172 ++++ ...nResetPasswordMailConfirmationFragment2.kt | 75 ++ .../LoginResetPasswordSuccessFragment2.kt | 49 ++ .../login2/LoginServerSelectionFragment2.kt | 75 ++ .../login2/LoginServerUrlFormFragment2.kt | 143 +++ .../LoginSignUpSignInSelectionFragment2.kt | 72 ++ .../features/login2/LoginSsoOnlyFragment2.kt | 68 ++ .../app/features/login2/LoginViewEvents2.kt | 59 ++ .../app/features/login2/LoginViewModel2.kt | 828 ++++++++++++++++++ .../app/features/login2/LoginViewState2.kt | 69 ++ .../login2/LoginWaitForEmailFragment2.kt | 75 ++ .../app/features/login2/LoginWebFragment2.kt | 255 ++++++ .../vector/app/features/login2/SignMode2.kt | 27 + .../login2/terms/LoginTermsFragment2.kt | 119 +++ .../fragment_login_2_signin_password.xml | 113 +++ .../res/layout/fragment_login_2_signin_to.xml | 144 +++ .../fragment_login_2_signin_username.xml | 82 ++ .../fragment_login_2_signup_password.xml | 132 +++ .../fragment_login_2_signup_username.xml | 103 +++ .../fragment_login_reset_password_2.xml | 140 +++ ...ragment_login_reset_password_success_2.xml | 51 ++ .../fragment_login_server_selection_2.xml | 133 +++ .../fragment_login_server_url_form_2.xml | 71 ++ .../main/res/layout/fragment_login_splash.xml | 9 + .../res/layout/fragment_login_splash_2.xml | 224 +++++ .../res/layout/fragment_login_sso_only_2.xml | 46 + .../fragment_login_wait_for_email_2.xml | 54 ++ .../res/layout/item_login_password_form.xml | 2 + .../src/main/res/values/strings_login_v2.xml | 31 + 45 files changed, 5536 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginGenericTextInputFormFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordMailConfirmationFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordSuccessFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginServerUrlFormFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginSignUpSignInSelectionFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginSsoOnlyFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginWaitForEmailFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/SignMode2.kt create mode 100755 vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt create mode 100644 vector/src/main/res/layout/fragment_login_2_signin_password.xml create mode 100644 vector/src/main/res/layout/fragment_login_2_signin_to.xml create mode 100644 vector/src/main/res/layout/fragment_login_2_signin_username.xml create mode 100644 vector/src/main/res/layout/fragment_login_2_signup_password.xml create mode 100644 vector/src/main/res/layout/fragment_login_2_signup_username.xml create mode 100644 vector/src/main/res/layout/fragment_login_reset_password_2.xml create mode 100644 vector/src/main/res/layout/fragment_login_reset_password_success_2.xml create mode 100644 vector/src/main/res/layout/fragment_login_server_selection_2.xml create mode 100644 vector/src/main/res/layout/fragment_login_server_url_form_2.xml create mode 100644 vector/src/main/res/layout/fragment_login_splash_2.xml create mode 100644 vector/src/main/res/layout/fragment_login_sso_only_2.xml create mode 100644 vector/src/main/res/layout/fragment_login_wait_for_email_2.xml create mode 100644 vector/src/main/res/values/strings_login_v2.xml diff --git a/CHANGES.md b/CHANGES.md index 74b008bfc4..ee0d442c79 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Add ability to install APK from directly from Element (#2381) + - Improve login/register flow (#2585, #3172) Bugfix 🐛: - Message states cosmetic changes (#3007) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1e2bf1ab0f..b363df397e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -119,6 +119,22 @@ android:scheme="element" /> + + + + + + + + + + + : VectorBaseFragment(), OnBackPressed { + + protected val loginViewModel: LoginViewModel2 by activityViewModel() + + private var isResetPasswordStarted = false + + // Due to async, we keep a boolean to avoid displaying twice the cancellation dialog + private var displayCancelDialog = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move) + } + + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + loginViewModel.observeViewEvents { + handleLoginViewEvents(it) + } + } + + private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents2) { + when (loginViewEvents) { + is LoginViewEvents2.Failure -> showFailure(loginViewEvents.throwable) + else -> + // This is handled by the Activity + Unit + }.exhaustive + } + + override fun showFailure(throwable: Throwable) { + // Only the resumed Fragment can eventually show the error, to avoid multiple dialog display + if (!isResumed) { + return + } + + when (throwable) { + is Failure.Cancelled -> + /* Ignore this error, user has cancelled the action */ + Unit + is Failure.UnrecognizedCertificateFailure -> + showUnrecognizedCertificateFailure(throwable) + else -> + onError(throwable) + } + } + + private fun showUnrecognizedCertificateFailure(failure: Failure.UnrecognizedCertificateFailure) { + // Ask the user to accept the certificate + unrecognizedCertificateDialog.show(requireActivity(), + failure.fingerprint, + failure.url, + object : UnrecognizedCertificateDialog.Callback { + override fun onAccept() { + // User accept the certificate + loginViewModel.handle(LoginAction2.UserAcceptCertificate(failure.fingerprint)) + } + + override fun onIgnore() { + // Cannot happen in this case + } + + override fun onReject() { + // Nothing to do in this case + } + }) + } + + open fun onError(throwable: Throwable) { + super.showFailure(throwable) + } + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + return when { + displayCancelDialog && loginViewModel.isRegistrationStarted -> { + // 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.yes) { _, _ -> + displayCancelDialog = false + vectorBaseActivity.onBackPressed() + } + .setNegativeButton(R.string.no, null) + .show() + + true + } + displayCancelDialog && isResetPasswordStarted -> { + // Ask for confirmation before cancelling the reset password + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.login_reset_password_cancel_confirmation_title) + .setMessage(R.string.login_reset_password_cancel_confirmation_content) + .setPositiveButton(R.string.yes) { _, _ -> + displayCancelDialog = false + vectorBaseActivity.onBackPressed() + } + .setNegativeButton(R.string.no, null) + .show() + + true + } + else -> { + resetViewModel() + // Do not consume the Back event + false + } + } + } + + final override fun invalidate() = withState(loginViewModel) { state -> + // True when email is sent with success to the homeserver + isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not() + + updateWithState(state) + } + + open fun updateWithState(state: LoginViewState2) { + // No op by default + } + + // Reset any modification on the loginViewModel by the current fragment + abstract fun resetViewModel() +} diff --git a/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt new file mode 100644 index 0000000000..b12d9638dc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.login2 + +import android.content.ComponentName +import android.net.Uri +import androidx.browser.customtabs.CustomTabsClient +import androidx.browser.customtabs.CustomTabsServiceConnection +import androidx.browser.customtabs.CustomTabsSession +import androidx.viewbinding.ViewBinding +import com.airbnb.mvrx.withState +import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.features.login.hasSso +import im.vector.app.features.login.ssoIdentityProviders + +abstract class AbstractSSOLoginFragment2 : AbstractLoginFragment2() { + + // For sso + private var customTabsServiceConnection: CustomTabsServiceConnection? = null + private var customTabsClient: CustomTabsClient? = null + private var customTabsSession: CustomTabsSession? = null + + override fun onStart() { + super.onStart() + val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() } + if (hasSSO) { + val packageName = CustomTabsClient.getPackageName(requireContext(), null) + + // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device + if (packageName != null) { + customTabsServiceConnection = object : CustomTabsServiceConnection() { + override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { + customTabsClient = client + .also { it.warmup(0L) } + prefetchIfNeeded() + } + + override fun onServiceDisconnected(name: ComponentName?) { + } + } + .also { + CustomTabsClient.bindCustomTabsService( + requireContext(), + // Despite the API, packageName cannot be null + packageName, + it + ) + } + } + } + } + + override fun onStop() { + super.onStop() + val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() } + if (hasSSO) { + customTabsServiceConnection?.let { requireContext().unbindService(it) } + customTabsServiceConnection = null + } + } + + private fun prefetchUrl(url: String) { + if (customTabsSession == null) { + customTabsSession = customTabsClient?.newSession(null) + } + + customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) + } + + protected fun openInCustomTab(ssoUrl: String) { + openUrlInChromeCustomTab(requireContext(), customTabsSession, ssoUrl) + } + + private fun prefetchIfNeeded() { + withState(loginViewModel) { state -> + if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { + // in this case we can prefetch (not other cases for privacy concerns) + loginViewModel.getSsoUrl( + redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = null + ) + ?.let { prefetchUrl(it) } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt new file mode 100644 index 0000000000..8f3e88abbb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt @@ -0,0 +1,85 @@ +/* + * 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.app.features.login2 + +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.login.LoginConfig +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.internal.network.ssl.Fingerprint + +sealed class LoginAction2 : VectorViewModelAction { + // First action + data class UpdateSignMode(val signMode: SignMode2) : LoginAction2() + + // Signin, but user wants to choose a server + object ChooseAServerForSignin : LoginAction2() + + object EnterServerUrl : LoginAction2() + object ChooseDefaultHomeServer : LoginAction2() + data class UpdateHomeServer(val homeServerUrl: String) : LoginAction2() + data class LoginWithToken(val loginToken: String) : LoginAction2() + data class WebLoginSuccess(val credentials: Credentials) : LoginAction2() + data class InitWith(val loginConfig: LoginConfig?) : LoginAction2() + data class ResetPassword(val email: String, val newPassword: String) : LoginAction2() + object ResetPasswordMailConfirmed : LoginAction2() + + // Username to Login or Register, depending on the signMode + data class SetUserName(val username: String) : LoginAction2() + // Password to Login or Register, depending on the signMode + data class SetUserPassword(val password: String) : LoginAction2() + + // When user has selected a homeserver + data class LoginWith(val login: String, val password: String) : LoginAction2() + + // Register actions + open class RegisterAction : LoginAction2() + + data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction() + object SendAgainThreePid : RegisterAction() + + // TODO Confirm Email (from link in the email, open in the phone, intercepted by RiotX) + data class ValidateThreePid(val code: String) : RegisterAction() + + data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction() + object StopEmailValidationCheck : RegisterAction() + + data class CaptchaDone(val captchaResponse: String) : RegisterAction() + object AcceptTerms : RegisterAction() + object RegisterDummy : RegisterAction() + + // Reset actions + open class ResetAction : LoginAction2() + + object ResetHomeServerUrl : ResetAction() + object ResetSignMode : ResetAction() + object ResetLogin : ResetAction() + object ResetResetPassword : ResetAction() + + // Homeserver history + object ClearHomeServerHistory : LoginAction2() + + // For the soft logout case + data class SetupSsoForSessionRecovery(val homeServerUrl: String, + val deviceId: String, + val ssoIdentityProviders: List?) : LoginAction2() + + data class PostViewEvent(val viewEvent: LoginViewEvents2) : LoginAction2() + + data class UserAcceptCertificate(val fingerprint: Fingerprint) : LoginAction2() +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt new file mode 100644 index 0000000000..149abb69da --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt @@ -0,0 +1,380 @@ +/* + * 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.app.features.login2 + +import android.content.Context +import android.content.Intent +import android.view.View +import android.view.ViewGroup +import androidx.annotation.CallSuper +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.core.view.ViewCompat +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import com.airbnb.mvrx.viewModel +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.extensions.addFragmentToBackstack +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.ToolbarConfigurable +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityLoginBinding +import im.vector.app.features.home.HomeActivity +import im.vector.app.features.login.LoginCaptchaFragmentArgument +import im.vector.app.features.login.LoginConfig +import im.vector.app.features.login.LoginWaitForEmailFragmentArgument +import im.vector.app.features.login.isSupported +import im.vector.app.features.login.terms.LoginTermsFragmentArgument +import im.vector.app.features.login.terms.toLocalizedLoginTerms +import im.vector.app.features.login2.terms.LoginTermsFragment2 +import im.vector.app.features.pin.UnlockedActivity + +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.extensions.tryOrNull +import javax.inject.Inject + +/** + * The LoginActivity manages the fragment navigation and also display the loading View + */ +open class LoginActivity2 : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { + + private val loginViewModel: LoginViewModel2 by viewModel() + + @Inject lateinit var loginViewModelFactory: LoginViewModel2.Factory + + @CallSuper + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + private val enterAnim = R.anim.enter_fade_in + private val exitAnim = R.anim.exit_fade_out + + private val popEnterAnim = R.anim.no_anim + private val popExitAnim = R.anim.exit_fade_out + + private val topFragment: Fragment? + get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer) + + private val commonOption: (FragmentTransaction) -> Unit = { ft -> + // Find the loginLogo on the current Fragment, this should not return null + (topFragment?.view as? ViewGroup) + // Find findViewById does not work, I do not know why + // findViewById(R.id.loginLogo) + ?.children + ?.firstOrNull { it.id == R.id.loginLogo } + ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + } + + final override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun initUiAndData() { + if (isFirstCreation()) { + addFirstFragment() + } + + loginViewModel + .subscribe(this) { + updateWithState(it) + } + + loginViewModel.observeViewEvents { handleLoginViewEvents(it) } + + // Get config extra + val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG) + if (isFirstCreation()) { + // TODO Check this + loginViewModel.handle(LoginAction2.InitWith(loginConfig)) + } + } + + protected open fun addFirstFragment() { + addFragment(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment2::class.java) + } + + private fun handleLoginViewEvents(event: LoginViewEvents2) { + when (event) { + is LoginViewEvents2.RegistrationFlowResult -> { + // Check that all flows are supported by the application + if (event.flowResult.missingStages.any { !it.isSupported() }) { + // Display a popup to propose use web fallback + onRegistrationStageNotSupported() + } else { + if (event.isRegistrationStarted) { + // Go on with registration flow + handleRegistrationNavigation(event.flowResult) + } else { + /* + // First ask for login and password + // 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, + LoginFragment2::class.java, + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption + ) + + */ + } + } + } + is LoginViewEvents2.OutdatedHomeserver -> { + AlertDialog.Builder(this) + .setTitle(R.string.login_error_outdated_homeserver_title) + .setMessage(R.string.login_error_outdated_homeserver_warning_content) + .setPositiveButton(R.string.ok, null) + .show() + Unit + } + is LoginViewEvents2.OpenServerSelection -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginServerSelectionFragment2::class.java, + option = { ft -> + findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // Disable transition of text + // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // No transition here now actually + // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // TODO Disabled because it provokes a flickering + // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + }) + is LoginViewEvents2.OpenHomeServerUrlFormScreen -> { + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginServerUrlFormFragment2::class.java, + option = commonOption) + } + is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> { + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment2SigninUsername::class.java, + option = { ft -> + findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // Disable transition of text + // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // No transition here now actually + // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // TODO Disabled because it provokes a flickering + // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + }) + } + is LoginViewEvents2.OpenSsoOnlyScreen -> { + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginSsoOnlyFragment2::class.java, + option = commonOption) + } + is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event) + is LoginViewEvents2.OpenResetPasswordScreen -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordFragment2::class.java, + option = commonOption) + is LoginViewEvents2.OnResetPasswordSendThreePidDone -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordMailConfirmationFragment2::class.java, + option = commonOption) + } + is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordSuccessFragment2::class.java, + option = commonOption) + } + is LoginViewEvents2.OnResetPasswordMailConfirmationSuccessDone -> { + // Go back to the login fragment + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + } + is LoginViewEvents2.OnSendEmailSuccess -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginWaitForEmailFragment2::class.java, + LoginWaitForEmailFragmentArgument(event.email), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is LoginViewEvents2.OpenPasswordScreen -> { + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment2SigninPassword::class.java, + option = commonOption) + } + is LoginViewEvents2.OpenSignupPasswordScreen -> { + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment2SignupPassword::class.java, + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + } + is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> { + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment2SignupUsername::class.java, + option = commonOption) + } + is LoginViewEvents2.OpenSignInWithAnythingScreen -> { + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragmentToAny2::class.java, + option = commonOption) + } + is LoginViewEvents2.OnSendMsisdnSuccess -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment2::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is LoginViewEvents2.Failure -> + // This is handled by the Fragments + Unit + is LoginViewEvents2.OnLoginModeNotSupported -> + onLoginModeNotSupported(event.supportedTypes) + is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event) + }.exhaustive + } + + private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) { + // TODO Propose to set avatar and display name + val intent = HomeActivity.newIntent( + this, + accountCreation = event.newAccount + ) + startActivity(intent) + finish() + } + + private fun updateWithState(LoginViewState2: LoginViewState2) { + // Loading + views.loginLoading.isVisible = LoginViewState2.isLoading + } + + private fun onWebLoginError(onWebLoginError: LoginViewEvents2.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, onWebLoginError.description, onWebLoginError.errorCode)) + .setPositiveButton(R.string.ok, null) + .show() + } + + /** + * Handle the SSO redirection here + */ + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + intent?.data + ?.let { tryOrNull { it.getQueryParameter("loginToken") } } + ?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) } + } + + 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, + LoginWebFragment2::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun onLoginModeNotSupported(supportedTypes: List) { + AlertDialog.Builder(this) + .setTitle(R.string.app_name) + .setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) + .setPositiveButton(R.string.yes) { _, _ -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginWebFragment2::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun handleRegistrationNavigation(flowResult: FlowResult) { + // Complete all mandatory stages first + val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } + + if (mandatoryStage != null) { + doStage(mandatoryStage) + } else { + // Consider optional stages + val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } + if (optionalStage == null) { + // Should not happen... + } else { + doStage(optionalStage) + } + } + } + + 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, + LoginCaptchaFragment2::class.java, + LoginCaptchaFragmentArgument(stage.publicKey), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment2::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment2::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginTermsFragment2::class.java, + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + else -> Unit // Should not happen + } + } + + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } + + companion object { + private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" + private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" + + private const val EXTRA_CONFIG = "EXTRA_CONFIG" + + // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string + const val VECTOR_REDIRECT_URL = "element://connect" + + fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { + return Intent(context, LoginActivity2::class.java).apply { + putExtra(EXTRA_CONFIG, loginConfig) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt new file mode 100644 index 0000000000..9c3ef6b94d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt @@ -0,0 +1,194 @@ +/* + * 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.app.features.login2 + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.graphics.Bitmap +import android.net.http.SslError +import android.os.Build +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.ViewGroup +import android.webkit.SslErrorHandler +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.args +import im.vector.app.R +import im.vector.app.core.utils.AssetReader +import im.vector.app.databinding.FragmentLoginCaptchaBinding +import im.vector.app.features.login.JavascriptResponse +import im.vector.app.features.login.LoginCaptchaFragmentArgument +import org.matrix.android.sdk.internal.di.MoshiProvider +import timber.log.Timber +import java.net.URLDecoder +import java.util.Formatter +import javax.inject.Inject + +/** + * In this screen, the user is asked to confirm he is not a robot + */ +class LoginCaptchaFragment2 @Inject constructor( + private val assetReader: AssetReader +) : AbstractLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginCaptchaBinding { + return FragmentLoginCaptchaBinding.inflate(inflater, container, false) + } + + private val params: LoginCaptchaFragmentArgument by args() + + private var isWebViewLoaded = false + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebView(state: LoginViewState2) { + views.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 = state.homeServerUrl ?: error("missing url of homeserver") + views.loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null) + views.loginCaptchaWevView.requestLayout() + + views.loginCaptchaWevView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + if (!isAdded) { + return + } + + // Show loader + views.loginCaptchaProgress.isVisible = true + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + + if (!isAdded) { + return + } + + // Hide loader + views.loginCaptchaProgress.isVisible = false + } + + 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 javascriptResponse: JavascriptResponse? = null + + try { + // URL decode + json = URLDecoder.decode(json, "UTF-8") + javascriptResponse = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java).fromJson(json) + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading(): failed") + } + + val response = javascriptResponse?.response + if (javascriptResponse?.action == "verifyCallback" && response != null) { + loginViewModel.handle(LoginAction2.CaptchaDone(response)) + } + } + return true + } + } + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } + + override fun updateWithState(state: LoginViewState2) { + if (!isWebViewLoaded) { + setupWebView(state) + isWebViewLoaded = true + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt new file mode 100644 index 0000000000..9a644613e1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt @@ -0,0 +1,179 @@ +/* + * 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.app.features.login2 + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import androidx.autofill.HintConstants +import androidx.core.view.isVisible +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.extensions.showPassword +import im.vector.app.databinding.FragmentLogin2SigninPasswordBinding +import io.reactivex.rxkotlin.subscribeBy +import org.matrix.android.sdk.api.failure.isInvalidPassword +import javax.inject.Inject + +/** + * In this screen: + * In signin mode: + * - the user is asked for password to sign in to a homeserver. + * - He also can reset his password + */ +class LoginFragment2SigninPassword @Inject constructor() : AbstractSSOLoginFragment2() { + + private var passwordShown = false + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SigninPasswordBinding { + return FragmentLogin2SigninPasswordBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupSubmitButton() + setupForgottenPasswordButton() + setupPasswordReveal() + setupAutoFill() + + views.passwordField.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + } + + private fun setupForgottenPasswordButton() { + views.forgetPasswordButton.setOnClickListener { forgetPasswordClicked() } + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) + } + } + + private fun submit() { + cleanupUi() + + val password = views.passwordField.text.toString() + + // This can be called by the IME action, so deal with empty cases + var error = 0 + if (password.isEmpty()) { + views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password) + error++ + } + + if (error == 0) { + loginViewModel.handle(LoginAction2.SetUserPassword(password)) + } + } + + private fun cleanupUi() { + views.loginSubmit.hideKeyboard() + views.passwordFieldTil.error = null + } + + private fun setupUi(state: LoginViewState2) { + // Name and avatar + views.loginWelcomeBack.text = getString( + R.string.login_welcome_back, + state.loginProfileInfo?.displayName?.takeIf { it.isNotBlank() } ?: state.userIdentifier() + ) + + if (state.loginProfileInfo != null) { + views.loginUserIcon.isVisible = true + Glide.with(requireContext()) + .load(state.loginProfileInfo.fullAvatarUrl) + .apply(RequestOptions.circleCropTransform()) + .into(views.loginUserIcon) + } else { + views.loginUserIcon.isVisible = false + } + } + + private fun setupSubmitButton() { + views.loginSubmit.setOnClickListener { submit() } + views.passwordField + .textChanges() + .map { it.isNotEmpty() } + .subscribeBy { + views.passwordFieldTil.error = null + views.loginSubmit.isEnabled = it + } + .disposeOnDestroyView() + } + + private fun forgetPasswordClicked() { + loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen)) + } + + private fun setupPasswordReveal() { + passwordShown = false + + views.passwordReveal.setOnClickListener { + passwordShown = !passwordShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + views.passwordField.showPassword(passwordShown) + views.passwordReveal.render(passwordShown) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } + + override fun onError(throwable: Throwable) { + if (throwable.isInvalidPassword() && spaceInPassword()) { + views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password) + } else { + views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable) + } + } + + override fun updateWithState(state: LoginViewState2) { + setupUi(state) + + if (state.isLoading) { + // Ensure password is hidden + passwordShown = false + renderPasswordField() + } + } + + /** + * Detect if password ends or starts with spaces + */ + private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt new file mode 100644 index 0000000000..ee83a0409a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt @@ -0,0 +1,107 @@ +/* + * 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.app.features.login2 + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.autofill.HintConstants +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.databinding.FragmentLogin2SigninUsernameBinding +import io.reactivex.rxkotlin.subscribeBy +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import javax.inject.Inject + +/** + * In this screen: + * - the user is asked for its matrix ID, and have the possibility to open the screen to select a server + */ +class LoginFragment2SigninUsername @Inject constructor() : AbstractLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SigninUsernameBinding { + return FragmentLogin2SigninUsernameBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupSubmitButton() + setupAutoFill() + views.loginChooseAServer.setOnClickListener { + loginViewModel.handle(LoginAction2.ChooseAServerForSignin) + } + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) + } + } + + private fun submit() { + cleanupUi() + + val login = views.loginField.text.toString() + + // This can be called by the IME action, so deal with empty cases + var error = 0 + if (login.isEmpty()) { + views.loginFieldTil.error = getString(R.string.error_empty_field_enter_user_name) + error++ + } + + if (error == 0) { + loginViewModel.handle(LoginAction2.SetUserName(login)) + } + } + + private fun cleanupUi() { + views.loginSubmit.hideKeyboard() + views.loginFieldTil.error = null + } + + private fun setupSubmitButton() { + views.loginSubmit.setOnClickListener { submit() } + views.loginField.textChanges() + .map { it.trim().isNotEmpty() } + .subscribeBy { + views.loginFieldTil.error = null + views.loginSubmit.isEnabled = it + } + .disposeOnDestroyView() + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } + + override fun onError(throwable: Throwable) { + if (throwable is Failure.ServerError + && throwable.error.code == MatrixError.M_FORBIDDEN + && throwable.error.message.isEmpty()) { + // Login with email, but email unknown + views.loginFieldTil.error = getString(R.string.login_login_with_email_error) + } else { + views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt new file mode 100644 index 0000000000..917e97306c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt @@ -0,0 +1,148 @@ +/* + * 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.app.features.login2 + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import androidx.autofill.HintConstants +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.showPassword +import im.vector.app.databinding.FragmentLogin2SignupPasswordBinding +import io.reactivex.rxkotlin.Observables +import io.reactivex.rxkotlin.subscribeBy +import javax.inject.Inject + +/** + * In this screen: + * - the user is asked for password to sign up to a homeserver. + */ +class LoginFragment2SignupPassword @Inject constructor() : AbstractSSOLoginFragment2() { + + private var passwordsShown = false + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SignupPasswordBinding { + return FragmentLogin2SignupPasswordBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupSubmitButton() + setupAutoFill() + setupPasswordReveal() + + views.passwordFieldRepeat.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) + views.passwordFieldRepeat.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) + } + } + + private fun submit() { + cleanupUi() + + val password = views.passwordField.text.toString() + + // This can be called by the IME action, so deal with empty cases + var error = 0 + if (password.isEmpty()) { + views.passwordFieldTil.error = getString(R.string.error_empty_field_choose_password) + error++ + } + + val passwordRepeat = views.passwordFieldRepeat.text.toString() + + if (password != passwordRepeat) { + views.passwordFieldTilRepeat.error = getString(R.string.auth_password_dont_match) + error++ + } + + if (error == 0) { + loginViewModel.handle(LoginAction2.SetUserPassword(password)) + } + } + + private fun cleanupUi() { + views.loginSubmit.hideKeyboard() + views.passwordFieldTil.error = null + } + + private fun setupSubmitButton() { + views.loginSubmit.setOnClickListener { submit() } + Observables.combineLatest( + views.passwordField.textChanges(), + views.passwordFieldRepeat.textChanges() + ) + .subscribeBy { (password, passwordRepeat) -> + views.passwordFieldTil.error = null + views.passwordFieldTilRepeat.error = null + views.loginSubmit.isEnabled = password.isNotEmpty() && passwordRepeat.isNotEmpty() + } + .disposeOnDestroyView() + } + + private fun setupPasswordReveal() { + passwordsShown = false + + views.passwordReveal.setOnClickListener { + passwordsShown = !passwordsShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + views.passwordReveal.render(passwordsShown) + views.passwordField.showPassword(passwordsShown) + views.passwordFieldRepeat.showPassword(passwordsShown) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } + + override fun onError(throwable: Throwable) { + views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable) + } + + override fun updateWithState(state: LoginViewState2) { + views.loginMatrixIdentifier.text = state.userIdentifier() + + if (state.isLoading) { + // Ensure passwords are hidden + passwordsShown = false + renderPasswordField() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt new file mode 100644 index 0000000000..3667f2f1b9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt @@ -0,0 +1,141 @@ +/* + * 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.app.features.login2 + +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.autofill.HintConstants +import androidx.core.text.isDigitsOnly +import androidx.core.view.isVisible +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.databinding.FragmentLogin2SignupUsernameBinding +import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.SocialLoginButtonsView +import io.reactivex.rxkotlin.subscribeBy +import javax.inject.Inject + +/** + * In this screen: + * - the user is asked for login to sign up to a homeserver. + * - SSO option are displayed if available + */ +class LoginFragment2SignupUsername @Inject constructor() : AbstractSSOLoginFragment2() { + + // Temporary patch for https://github.com/vector-im/riotX-android/issues/1410, + // waiting for https://github.com/matrix-org/synapse/issues/7576 + private var isNumericOnlyUserIdForbidden = false + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SignupUsernameBinding { + return FragmentLogin2SignupUsernameBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupSubmitButton() + setupAutoFill() + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) + } + } + + private fun submit() { + cleanupUi() + + val login = views.loginField.text.toString().trim() + + // This can be called by the IME action, so deal with empty cases + var error = 0 + if (login.isEmpty()) { + views.loginFieldTil.error = getString(R.string.error_empty_field_choose_user_name) + error++ + } + if (isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { + views.loginFieldTil.error = "The homeserver does not accept username with only digits." + error++ + } + + if (error == 0) { + loginViewModel.handle(LoginAction2.SetUserName(login)) + } + } + + private fun cleanupUi() { + views.loginSubmit.hideKeyboard() + views.loginFieldTil.error = null + } + + private fun setupUi(state: LoginViewState2) { + views.loginSubtitle.text = getString(R.string.login_signup_to, state.homeServerUrlFromUser.toReducedUrl()) + + if (state.loginMode is LoginMode.SsoAndPassword) { + views.loginSocialLoginContainer.isVisible = true + views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders + views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { + override fun onProviderSelected(id: String?) { + loginViewModel.getSsoUrl( + redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = id + ) + ?.let { openInCustomTab(it) } + } + } + } else { + views.loginSocialLoginContainer.isVisible = false + views.loginSocialLoginButtons.ssoIdentityProviders = null + } + } + + @SuppressLint("SetTextI18n") + private fun setupSubmitButton() { + views.loginSubmit.setOnClickListener { submit() } + views.loginField.textChanges() + .map { it.trim() } + .subscribeBy { text -> + val isNotEmpty = text.isNotEmpty() + views.loginFieldTil.error = null + views.loginSubmit.isEnabled = isNotEmpty + } + .disposeOnDestroyView() + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } + + override fun onError(throwable: Throwable) { + views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable) + } + + @SuppressLint("SetTextI18n") + override fun updateWithState(state: LoginViewState2) { + isNumericOnlyUserIdForbidden = state.isNumericOnlyUserIdForbidden + + setupUi(state) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt new file mode 100644 index 0000000000..0768c9cdb9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -0,0 +1,227 @@ +/* + * 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.app.features.login2 + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import androidx.autofill.HintConstants +import androidx.core.text.isDigitsOnly +import androidx.core.view.isVisible +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.showPassword +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.databinding.FragmentLogin2SigninToBinding +import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.SocialLoginButtonsView +import io.reactivex.Observable +import io.reactivex.rxkotlin.subscribeBy +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.failure.isInvalidPassword +import javax.inject.Inject + +/** + * In this screen: + * User want to sign in and has selected a server to do so + * - the user is asked for login (or email) and password to sign in to a homeserver. + * - He also can reset his password + * - It also possible to use SSO if server support it in this screen + */ +class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2() { + + private var passwordShown = false + + // Temporary patch for https://github.com/vector-im/riotX-android/issues/1410, + // waiting for https://github.com/matrix-org/synapse/issues/7576 + private var isNumericOnlyUserIdForbidden = false + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SigninToBinding { + return FragmentLogin2SigninToBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupSubmitButton() + setupForgottenPasswordButton() + setupPasswordReveal() + setupAutoFill() + + views.passwordField.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + } + + private fun setupForgottenPasswordButton() { + views.forgetPasswordButton.setOnClickListener { forgetPasswordClicked() } + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) + views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) + } + views.loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_IN + } + + private fun submit() { + cleanupUi() + + val login = views.loginField.text.toString() + val password = views.passwordField.text.toString() + + // This can be called by the IME action, so deal with empty cases + var error = 0 + if (login.isEmpty()) { + views.loginFieldTil.error = getString(R.string.error_empty_field_enter_user_name) + error++ + } + if (isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { + views.loginFieldTil.error = "The homeserver does not accept username with only digits." + error++ + } + if (password.isEmpty()) { + views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password) + error++ + } + + if (error == 0) { + loginViewModel.handle(LoginAction2.LoginWith(login, password)) + } + } + + private fun cleanupUi() { + views.loginSubmit.hideKeyboard() + views.loginFieldTil.error = null + views.passwordFieldTil.error = null + } + + private fun setupUi(state: LoginViewState2) { + views.loginTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl()) + + if (state.loginMode is LoginMode.SsoAndPassword) { + views.loginSocialLoginContainer.isVisible = true + views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders + views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { + override fun onProviderSelected(id: String?) { + loginViewModel.getSsoUrl( + redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = id + ) + ?.let { openInCustomTab(it) } + } + } + } else { + views.loginSocialLoginContainer.isVisible = false + views.loginSocialLoginButtons.ssoIdentityProviders = null + } + } + + private fun setupSubmitButton() { + views.loginSubmit.setOnClickListener { submit() } + Observable + .combineLatest( + views.loginField.textChanges().map { it.trim().isNotEmpty() }, + views.passwordField.textChanges().map { it.isNotEmpty() }, + { isLoginNotEmpty, isPasswordNotEmpty -> + isLoginNotEmpty && isPasswordNotEmpty + } + ) + .subscribeBy { + views.loginFieldTil.error = null + views.passwordFieldTil.error = null + views.loginSubmit.isEnabled = it + } + .disposeOnDestroyView() + } + + private fun forgetPasswordClicked() { + loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OpenResetPasswordScreen)) + } + + private fun setupPasswordReveal() { + passwordShown = false + + views.passwordReveal.setOnClickListener { + passwordShown = !passwordShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + views.passwordField.showPassword(passwordShown) + views.passwordReveal.render(passwordShown) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } + + override fun onError(throwable: Throwable) { + // Show M_WEAK_PASSWORD error in the password field + if (throwable is Failure.ServerError + && throwable.error.code == MatrixError.M_WEAK_PASSWORD) { + views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable) + } else { + if (throwable is Failure.ServerError + && throwable.error.code == MatrixError.M_FORBIDDEN + && throwable.error.message.isEmpty()) { + // Login with email, but email unknown + views.loginFieldTil.error = getString(R.string.login_login_with_email_error) + } else { + // Trick to display the error without text. + views.loginFieldTil.error = " " + if (throwable.isInvalidPassword() && spaceInPassword()) { + views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password) + } else { + views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable) + } + } + } + } + + override fun updateWithState(state: LoginViewState2) { + isNumericOnlyUserIdForbidden = state.isNumericOnlyUserIdForbidden + + setupUi(state) + + if (state.isLoading) { + // Ensure password is hidden + passwordShown = false + renderPasswordField() + } + } + + /** + * Detect if password ends or starts with spaces + */ + private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginGenericTextInputFormFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginGenericTextInputFormFragment2.kt new file mode 100644 index 0000000000..d4211de3bb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginGenericTextInputFormFragment2.kt @@ -0,0 +1,258 @@ +/* + * 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.app.features.login2 + +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.autofill.HintConstants +import androidx.core.view.isVisible +import com.airbnb.mvrx.args +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.isEmail +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding +import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.is401 +import javax.inject.Inject + +enum class TextInputFormFragmentMode { + SetEmail, + SetMsisdn, + ConfirmMsisdn +} + +@Parcelize +data class LoginGenericTextInputFormFragmentArgument( + val mode: TextInputFormFragmentMode, + val mandatory: Boolean, + val extra: String = "" +) : Parcelable + +/** + * In this screen, the user is asked for a text input + */ +class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFragment2() { + + private val params: LoginGenericTextInputFormFragmentArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginGenericTextInputFormBinding { + return FragmentLoginGenericTextInputFormBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupViews() + setupUi() + setupSubmitButton() + setupTil() + setupAutoFill() + } + + private fun setupViews() { + views.loginGenericTextInputFormOtherButton.setOnClickListener { onOtherButtonClicked() } + views.loginGenericTextInputFormSubmit.setOnClickListener { submit() } + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + views.loginGenericTextInputFormTextInput.setAutofillHints( + when (params.mode) { + TextInputFormFragmentMode.SetEmail -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS + TextInputFormFragmentMode.SetMsisdn -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER + TextInputFormFragmentMode.ConfirmMsisdn -> HintConstants.AUTOFILL_HINT_SMS_OTP + } + ) + } + } + + private fun setupTil() { + views.loginGenericTextInputFormTextInput.textChanges() + .subscribe { + views.loginGenericTextInputFormTil.error = null + } + .disposeOnDestroyView() + } + + private fun setupUi() { + when (params.mode) { + TextInputFormFragmentMode.SetEmail -> { + views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) + views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) + views.loginGenericTextInputFormNotice2.setTextOrHide(null) + views.loginGenericTextInputFormTil.hint = + getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint) + views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + views.loginGenericTextInputFormOtherButton.isVisible = false + views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) + } + TextInputFormFragmentMode.SetMsisdn -> { + views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) + views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) + views.loginGenericTextInputFormNotice2.setTextOrHide(getString(R.string.login_set_msisdn_notice2)) + views.loginGenericTextInputFormTil.hint = + getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint) + views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE + views.loginGenericTextInputFormOtherButton.isVisible = false + views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + views.loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title) + views.loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra) + views.loginGenericTextInputFormNotice2.setTextOrHide(null) + views.loginGenericTextInputFormTil.hint = + getString(R.string.login_msisdn_confirm_hint) + views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER + views.loginGenericTextInputFormOtherButton.isVisible = true + views.loginGenericTextInputFormOtherButton.text = getString(R.string.login_msisdn_confirm_send_again) + views.loginGenericTextInputFormSubmit.text = getString(R.string.login_msisdn_confirm_submit) + } + } + } + + private fun onOtherButtonClicked() { + when (params.mode) { + TextInputFormFragmentMode.ConfirmMsisdn -> { + loginViewModel.handle(LoginAction2.SendAgainThreePid) + } + else -> { + // Should not happen, button is not displayed + } + } + } + + private fun submit() { + cleanupUi() + val text = views.loginGenericTextInputFormTextInput.text.toString() + + if (text.isEmpty()) { + // Perform dummy action + loginViewModel.handle(LoginAction2.RegisterDummy) + } else { + when (params.mode) { + TextInputFormFragmentMode.SetEmail -> { + loginViewModel.handle(LoginAction2.AddThreePid(RegisterThreePid.Email(text))) + } + TextInputFormFragmentMode.SetMsisdn -> { + getCountryCodeOrShowError(text)?.let { countryCode -> + loginViewModel.handle(LoginAction2.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))) + } + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + loginViewModel.handle(LoginAction2.ValidateThreePid(text)) + } + } + } + } + + private fun cleanupUi() { + views.loginGenericTextInputFormSubmit.hideKeyboard() + views.loginGenericTextInputFormSubmit.error = null + } + + private fun getCountryCodeOrShowError(text: String): String? { + // We expect an international format for the moment (see https://github.com/vector-im/riotX-android/issues/693) + if (text.startsWith("+")) { + try { + val phoneNumber = PhoneNumberUtil.getInstance().parse(text, null) + return PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode) + } catch (e: NumberParseException) { + views.loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_other) + } + } else { + views.loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_not_international) + } + + // Error + return null + } + + private fun setupSubmitButton() { + views.loginGenericTextInputFormSubmit.isEnabled = false + views.loginGenericTextInputFormTextInput.textChanges() + .subscribe { + views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(it) + } + .disposeOnDestroyView() + } + + private fun isInputValid(input: CharSequence): Boolean { + return if (input.isEmpty() && !params.mandatory) { + true + } else { + when (params.mode) { + TextInputFormFragmentMode.SetEmail -> { + input.isEmail() + } + TextInputFormFragmentMode.SetMsisdn -> { + input.isNotBlank() + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + input.isNotBlank() + } + } + } + } + + override fun onError(throwable: Throwable) { + when (params.mode) { + TextInputFormFragmentMode.SetEmail -> { + if (throwable.is401()) { + // This is normal use case, we go to the mail waiting screen + loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendEmailSuccess(loginViewModel.currentThreePid ?: ""))) + } else { + views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) + } + } + TextInputFormFragmentMode.SetMsisdn -> { + if (throwable.is401()) { + // This is normal use case, we go to the enter code screen + loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: ""))) + } else { + views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) + } + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + when { + throwable is Failure.SuccessError -> + // The entered code is not correct + views.loginGenericTextInputFormTil.error = getString(R.string.login_validation_code_is_not_correct) + throwable.is401() -> + // It can happen if user request again the 3pid + Unit + else -> + views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) + } + } + } + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt new file mode 100644 index 0000000000..3a35e0dc91 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt @@ -0,0 +1,172 @@ +/* + * 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.app.features.login2 + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.autofill.HintConstants +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.isEmail +import im.vector.app.core.extensions.showPassword +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.databinding.FragmentLoginResetPassword2Binding +import io.reactivex.Observable +import io.reactivex.rxkotlin.subscribeBy + +import javax.inject.Inject + +/** + * In this screen, the user is asked for email and new password to reset his password + */ +class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2() { + + private var passwordsShown = false + + // Show warning only once + private var showWarning = true + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPassword2Binding { + return FragmentLoginResetPassword2Binding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupSubmitButton() + setupPasswordReveal() + setupAutoFill() + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + views.resetPasswordEmail.setAutofillHints(HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS) + views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) + views.passwordFieldRepeat.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) + } + } + + private fun setupUi(state: LoginViewState2) { + views.resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlFromUser.toReducedUrl()) + } + + private fun setupSubmitButton() { + views.resetPasswordSubmit.setOnClickListener { submit() } + + Observable + .combineLatest( + views.resetPasswordEmail.textChanges().map { it.isEmail() }, + views.passwordField.textChanges().map { it.isNotEmpty() }, + { isEmail, isPasswordNotEmpty -> + isEmail && isPasswordNotEmpty + } + ) + .subscribeBy { + views.resetPasswordEmailTil.error = null + views.passwordFieldTil.error = null + views.resetPasswordSubmit.isEnabled = it + } + .disposeOnDestroyView() + } + + private fun submit() { + cleanupUi() + + var error = 0 + val password = views.passwordField.text.toString() + val passwordRepeat = views.passwordFieldRepeat.text.toString() + + if (password != passwordRepeat) { + views.passwordFieldTilRepeat.error = getString(R.string.auth_password_dont_match) + error++ + } + + if (error > 0) { + return + } + + if (showWarning) { + showWarning = false + // Display a warning as Riot-Web does first + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.login_reset_password_warning_title) + .setMessage(R.string.login_reset_password_warning_content) + .setPositiveButton(R.string.login_reset_password_warning_submit) { _, _ -> + doSubmit() + } + .setNegativeButton(R.string.cancel, null) + .show() + } else { + doSubmit() + } + } + + private fun doSubmit() { + val email = views.resetPasswordEmail.text.toString() + val password = views.passwordField.text.toString() + + loginViewModel.handle(LoginAction2.ResetPassword(email, password)) + } + + private fun cleanupUi() { + views.resetPasswordSubmit.hideKeyboard() + views.resetPasswordEmailTil.error = null + views.passwordFieldTil.error = null + views.passwordFieldTilRepeat.error = null + } + + private fun setupPasswordReveal() { + passwordsShown = false + + views.passwordReveal.setOnClickListener { + passwordsShown = !passwordsShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + views.passwordField.showPassword(passwordsShown) + views.passwordFieldRepeat.showPassword(passwordsShown) + views.passwordReveal.render(passwordsShown) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetResetPassword) + } + + override fun onError(throwable: Throwable) { + views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(throwable) + } + + override fun updateWithState(state: LoginViewState2) { + setupUi(state) + + if (state.isLoading) { + // Ensure new passwords are hidden + passwordsShown = false + renderPasswordField() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordMailConfirmationFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordMailConfirmationFragment2.kt new file mode 100644 index 0000000000..127d33eb71 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordMailConfirmationFragment2.kt @@ -0,0 +1,75 @@ +/* + * 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.app.features.login2 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import im.vector.app.R +import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmationBinding + +import org.matrix.android.sdk.api.failure.is401 +import javax.inject.Inject + +/** + * In this screen, the user is asked to check his email and to click on a button once it's done + */ +class LoginResetPasswordMailConfirmationFragment2 @Inject constructor() : AbstractLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordMailConfirmationBinding { + return FragmentLoginResetPasswordMailConfirmationBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.resetPasswordMailConfirmationSubmit.setOnClickListener { submit() } + } + + private fun setupUi(state: LoginViewState2) { + views.resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail) + } + + private fun submit() { + loginViewModel.handle(LoginAction2.ResetPasswordMailConfirmed) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetResetPassword) + } + + override fun onError(throwable: Throwable) { + // Link in email not yet clicked ? + val message = if (throwable.is401()) { + getString(R.string.auth_reset_password_error_unauthorized) + } else { + errorFormatter.toHumanReadable(throwable) + } + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } + + override fun updateWithState(state: LoginViewState2) { + setupUi(state) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordSuccessFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordSuccessFragment2.kt new file mode 100644 index 0000000000..04a1453641 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordSuccessFragment2.kt @@ -0,0 +1,49 @@ +/* + * 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.app.features.login2 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import im.vector.app.databinding.FragmentLoginResetPasswordSuccess2Binding + +import javax.inject.Inject + +/** + * In this screen, we confirm to the user that his password has been reset + */ +class LoginResetPasswordSuccessFragment2 @Inject constructor() : AbstractLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordSuccess2Binding { + return FragmentLoginResetPasswordSuccess2Binding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.resetPasswordSuccessSubmit.setOnClickListener { submit() } + } + + private fun submit() { + loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnResetPasswordMailConfirmationSuccessDone)) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt new file mode 100644 index 0000000000..b7a5d64cd4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt @@ -0,0 +1,75 @@ +/* + * 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.app.features.login2 + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import im.vector.app.databinding.FragmentLoginServerSelection2Binding +import javax.inject.Inject + +/** + * In this screen, the user will choose between matrix.org, or other type of homeserver + */ +class LoginServerSelectionFragment2 @Inject constructor() : AbstractLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerSelection2Binding { + return FragmentLoginServerSelection2Binding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initViews() + } + + private fun initViews() { + views.loginServerChoiceMatrixOrg.setOnClickListener { selectMatrixOrg() } + views.loginServerChoiceOther.setOnClickListener { selectOther() } + } + + @SuppressLint("SetTextI18n") + private fun updateUi(state: LoginViewState2) { + when (state.signMode) { + SignMode2.Unknown -> Unit + SignMode2.SignUp -> { + views.loginServerTitle.text = "Please choose a server" + } + SignMode2.SignIn -> { + views.loginServerTitle.text = "Please choose your server" + } + } + } + + private fun selectMatrixOrg() { + loginViewModel.handle(LoginAction2.ChooseDefaultHomeServer) + } + + private fun selectOther() { + loginViewModel.handle(LoginAction2.EnterServerUrl) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetHomeServerUrl) + } + + override fun updateWithState(state: LoginViewState2) { + updateUi(state) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginServerUrlFormFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginServerUrlFormFragment2.kt new file mode 100644 index 0000000000..74bc017128 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginServerUrlFormFragment2.kt @@ -0,0 +1,143 @@ +/* + * 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.app.features.login2 + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.ArrayAdapter +import androidx.core.view.isInvisible +import com.google.android.material.textfield.TextInputLayout +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.BuildConfig +import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.utils.ensureProtocol +import im.vector.app.databinding.FragmentLoginServerUrlForm2Binding +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import java.net.UnknownHostException +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +/** + * In this screen, the user is prompted to enter a homeserver url + */ +class LoginServerUrlFormFragment2 @Inject constructor() : AbstractLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerUrlForm2Binding { + return FragmentLoginServerUrlForm2Binding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupViews() + setupHomeServerField() + } + + private fun setupViews() { + views.loginServerUrlFormClearHistory.setOnClickListener { clearHistory() } + views.loginServerUrlFormSubmit.setOnClickListener { submit() } + } + + private fun setupHomeServerField() { + views.loginServerUrlFormHomeServerUrl.textChanges() + .subscribe { + views.loginServerUrlFormHomeServerUrlTil.error = null + views.loginServerUrlFormSubmit.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + + views.loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + views.loginServerUrlFormHomeServerUrl.dismissDropDown() + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + } + + private fun setupUi(state: LoginViewState2) { + val completions = state.knownCustomHomeServersUrls + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList() + views.loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter( + requireContext(), + R.layout.item_completion_homeserver, + completions + )) + views.loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU + .takeIf { completions.isNotEmpty() } + ?: TextInputLayout.END_ICON_NONE + + views.loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty() + } + + private fun clearHistory() { + loginViewModel.handle(LoginAction2.ClearHomeServerHistory) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetHomeServerUrl) + } + + @SuppressLint("SetTextI18n") + private fun submit() { + cleanupUi() + + // Static check of homeserver url, empty, malformed, etc. + val serverUrl = views.loginServerUrlFormHomeServerUrl.text.toString().trim().ensureProtocol() + + when { + serverUrl.isBlank() -> { + views.loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) + } + else -> { + views.loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/) + loginViewModel.handle(LoginAction2.UpdateHomeServer(serverUrl)) + } + } + } + + private fun cleanupUi() { + views.loginServerUrlFormSubmit.hideKeyboard() + views.loginServerUrlFormHomeServerUrlTil.error = null + } + + override fun onError(throwable: Throwable) { + views.loginServerUrlFormHomeServerUrlTil.error = if (throwable is Failure.NetworkConnection + && throwable.ioException is UnknownHostException) { + // Invalid homeserver? + getString(R.string.login_error_homeserver_not_found) + } else { + if (throwable is Failure.ServerError + && throwable.error.code == MatrixError.M_FORBIDDEN + && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { + getString(R.string.login_registration_disabled) + } else { + errorFormatter.toHumanReadable(throwable) + } + } + } + + override fun updateWithState(state: LoginViewState2) { + setupUi(state) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginSignUpSignInSelectionFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginSignUpSignInSelectionFragment2.kt new file mode 100644 index 0000000000..a5e72bfc18 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginSignUpSignInSelectionFragment2.kt @@ -0,0 +1,72 @@ +/* + * 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.app.features.login2 + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import im.vector.app.BuildConfig +import im.vector.app.databinding.FragmentLoginSplash2Binding +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +/** + * In this screen, the user is asked to sign up or to sign in to the homeserver + * This is the new splash screen + */ +class LoginSignUpSignInSelectionFragment2 @Inject constructor( + private val vectorPreferences: VectorPreferences +) : AbstractLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplash2Binding { + return FragmentLoginSplash2Binding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupViews() + } + + private fun setupViews() { + views.loginSignupSigninSignUp.setOnClickListener { signUp() } + views.loginSignupSigninSignIn.setOnClickListener { signIn() } + + if (BuildConfig.DEBUG || vectorPreferences.developerMode()) { + views.loginSplashVersion.isVisible = true + @SuppressLint("SetTextI18n") + views.loginSplashVersion.text = "Version : ${BuildConfig.VERSION_NAME}\n" + + "Branch: ${BuildConfig.GIT_BRANCH_NAME}\n" + + "Build: ${BuildConfig.BUILD_NUMBER}" + } + } + + private fun signUp() { + loginViewModel.handle(LoginAction2.UpdateSignMode(SignMode2.SignUp)) + } + + private fun signIn() { + loginViewModel.handle(LoginAction2.UpdateSignMode(SignMode2.SignIn)) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetSignMode) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginSsoOnlyFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginSsoOnlyFragment2.kt new file mode 100644 index 0000000000..edfa02f523 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginSsoOnlyFragment2.kt @@ -0,0 +1,68 @@ +/* + * 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.app.features.login2 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.databinding.FragmentLoginSsoOnly2Binding +import javax.inject.Inject + +/** + * In this screen, the user is asked to sign up or to sign in to the homeserver + */ +class LoginSsoOnlyFragment2 @Inject constructor() : AbstractSSOLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSsoOnly2Binding { + return FragmentLoginSsoOnly2Binding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupViews() + } + + private fun setupViews() { + views.loginSignupSigninSubmit.setOnClickListener { submit() } + } + + private fun setupUi(state: LoginViewState2) { + views.loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl()) + } + + private fun submit() = withState(loginViewModel) { state -> + loginViewModel.getSsoUrl( + redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = null + ) + ?.let { openInCustomTab(it) } + } + + override fun resetViewModel() { + // No op + } + + override fun updateWithState(state: LoginViewState2) { + setupUi(state) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt new file mode 100644 index 0000000000..45e093e1c7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt @@ -0,0 +1,59 @@ +/* + * 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.app.features.login2 + +import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.FlowResult + +/** + * Transient events for Login + */ +sealed class LoginViewEvents2 : VectorViewEvents { + data class Failure(val throwable: Throwable) : LoginViewEvents2() + + data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents2() + object OutdatedHomeserver : LoginViewEvents2() + + // Navigation event + object OpenPasswordScreen : LoginViewEvents2() + object OpenSignupPasswordScreen : LoginViewEvents2() + + object OpenSignInEnterIdentifierScreen : LoginViewEvents2() + + object OpenSignUpChooseUsernameScreen : LoginViewEvents2() + object OpenSignInWithAnythingScreen : LoginViewEvents2() + + object OpenSsoOnlyScreen : LoginViewEvents2() + + object OpenServerSelection : LoginViewEvents2() + object OpenHomeServerUrlFormScreen : LoginViewEvents2() + + object OpenResetPasswordScreen : LoginViewEvents2() + object OnResetPasswordSendThreePidDone : LoginViewEvents2() + object OnResetPasswordMailConfirmationSuccess : LoginViewEvents2() + object OnResetPasswordMailConfirmationSuccessDone : LoginViewEvents2() + + data class OnLoginModeNotSupported(val supportedTypes: List) : LoginViewEvents2() + + data class OnSendEmailSuccess(val email: String) : LoginViewEvents2() + data class OnSendMsisdnSuccess(val msisdn: String) : LoginViewEvents2() + + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents2() + + data class OnSessionCreated(val newAccount: Boolean): LoginViewEvents2() +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt new file mode 100644 index 0000000000..daa9631041 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt @@ -0,0 +1,828 @@ +/* + * 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.app.features.login2 + +import android.content.Context +import android.net.Uri +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.extensions.configureAndStart +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.ensureTrailingSlash +import im.vector.app.features.login.HomeServerConnectionConfigFactory +import im.vector.app.features.login.LoginConfig +import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.ReAuthHelper +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import java.util.concurrent.CancellationException + +/** + * + */ +class LoginViewModel2 @AssistedInject constructor( + @Assisted initialState: LoginViewState2, + private val applicationContext: Context, + private val authenticationService: AuthenticationService, + private val activeSessionHolder: ActiveSessionHolder, + private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider, + private val homeServerHistoryService: HomeServerHistoryService +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: LoginViewState2): LoginViewModel2 + } + + init { + getKnownCustomHomeServersUrls() + } + + private fun getKnownCustomHomeServersUrls() { + setState { + copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls()) + } + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: LoginViewState2): LoginViewModel2? { + return when (val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()) { + is LoginActivity2 -> activity.loginViewModelFactory.create(state) + // TODO is SoftLogoutActivity -> activity.loginViewModelFactory.create(state) + else -> error("Invalid Activity") + } + } + } + + // Store the last action, to redo it after user has trusted the untrusted certificate + private var lastAction: LoginAction2? = null + private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null + + private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() + + val currentThreePid: String? + get() = registrationWizard?.currentThreePid + + // True when login and password has been sent with success to the homeserver + val isRegistrationStarted: Boolean + get() = authenticationService.isRegistrationStarted + + private val registrationWizard: RegistrationWizard? + get() = authenticationService.getRegistrationWizard() + + private val loginWizard: LoginWizard? + get() = authenticationService.getLoginWizard() + + private var loginConfig: LoginConfig? = null + + private var currentJob: Job? = null + set(value) { + // Cancel any previous Job + field?.cancel() + field = value + } + + override fun handle(action: LoginAction2) { + when (action) { + is LoginAction2.EnterServerUrl -> handleEnterServerUrl() + is LoginAction2.ChooseAServerForSignin -> handleChooseAServerForSignin() + is LoginAction2.UpdateSignMode -> handleUpdateSignMode(action) + is LoginAction2.InitWith -> handleInitWith(action) + is LoginAction2.ChooseDefaultHomeServer -> handle(LoginAction2.UpdateHomeServer(matrixOrgUrl)) + is LoginAction2.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action } + is LoginAction2.SetUserName -> handleSetUserName(action).also { lastAction = action } + is LoginAction2.SetUserPassword -> handleSetUserPassword(action).also { lastAction = action } + is LoginAction2.LoginWith -> handleLoginWith(action).also { lastAction = action } + is LoginAction2.LoginWithToken -> handleLoginWithToken(action) + is LoginAction2.WebLoginSuccess -> handleWebLoginSuccess(action) + is LoginAction2.ResetPassword -> handleResetPassword(action) + is LoginAction2.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() + is LoginAction2.RegisterAction -> handleRegisterAction(action) + is LoginAction2.ResetAction -> handleResetAction(action) + is LoginAction2.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) + is LoginAction2.UserAcceptCertificate -> handleUserAcceptCertificate(action) + LoginAction2.ClearHomeServerHistory -> handleClearHomeServerHistory() + is LoginAction2.PostViewEvent -> _viewEvents.post(action.viewEvent) + }.exhaustive + } + + private fun handleChooseAServerForSignin() { + // Just post a view Event + _viewEvents.post(LoginViewEvents2.OpenServerSelection) + } + + private fun handleUserAcceptCertificate(action: LoginAction2.UserAcceptCertificate) { + // It happens when we get the login flow, or during direct authentication. + // So alter the homeserver config and retrieve again the login flow + when (val finalLastAction = lastAction) { + is LoginAction2.UpdateHomeServer -> { + currentHomeServerConnectionConfig + ?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } + ?.let { getLoginFlow(it) } + } + is LoginAction2.SetUserName -> + handleSetUserNameForSignIn( + finalLastAction, + HomeServerConnectionConfig.Builder() + // Will be replaced by the task + .withHomeServerUri("https://dummy.org") + .withAllowedFingerPrints(listOf(action.fingerprint)) + .build() + ) + is LoginAction2.SetUserPassword -> + handleSetUserPassword(finalLastAction) + is LoginAction2.LoginWith -> + handleLoginWith(finalLastAction) + } + } + + private fun rememberHomeServer(homeServerUrl: String) { + homeServerHistoryService.addHomeServerToHistory(homeServerUrl) + getKnownCustomHomeServersUrls() + } + + private fun handleClearHomeServerHistory() { + homeServerHistoryService.clearHistory() + getKnownCustomHomeServersUrls() + } + + private fun handleLoginWithToken(action: LoginAction2.LoginWithToken) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + _viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration"))) + } else { + setState { copy(isLoading = true) } + + currentJob = viewModelScope.launch { + try { + safeLoginWizard.loginWithToken(action.loginToken) + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + null + } + ?.let { onSessionCreated(it) } + + setState { copy(isLoading = false) } + } + } + } + + private fun handleSetupSsoForSessionRecovery(action: LoginAction2.SetupSsoForSessionRecovery) { + setState { + copy( + signMode = SignMode2.SignIn, + loginMode = LoginMode.Sso(action.ssoIdentityProviders), + homeServerUrlFromUser = action.homeServerUrl, + homeServerUrl = action.homeServerUrl, + isNumericOnlyUserIdForbidden = action.homeServerUrl == matrixOrgUrl, + deviceId = action.deviceId + ) + } + } + + private fun handleRegisterAction(action: LoginAction2.RegisterAction) { + when (action) { + is LoginAction2.CaptchaDone -> handleCaptchaDone(action) + is LoginAction2.AcceptTerms -> handleAcceptTerms() + is LoginAction2.RegisterDummy -> handleRegisterDummy() + is LoginAction2.AddThreePid -> handleAddThreePid(action) + is LoginAction2.SendAgainThreePid -> handleSendAgainThreePid() + is LoginAction2.ValidateThreePid -> handleValidateThreePid(action) + is LoginAction2.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action) + is LoginAction2.StopEmailValidationCheck -> handleStopEmailValidationCheck() + } + } + + private fun handleCheckIfEmailHasBeenValidated(action: LoginAction2.CheckIfEmailHasBeenValidated) { + // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state + currentJob = executeRegistrationStep(withLoading = false) { + it.checkIfEmailHasBeenValidated(action.delayMillis) + } + } + + private fun handleStopEmailValidationCheck() { + currentJob = null + } + + private fun handleValidateThreePid(action: LoginAction2.ValidateThreePid) { + currentJob = executeRegistrationStep { + it.handleValidateThreePid(action.code) + } + } + + private fun executeRegistrationStep(withLoading: Boolean = true, + block: suspend (RegistrationWizard) -> RegistrationResult): Job { + if (withLoading) { + setState { copy(isLoading = true) } + } + return viewModelScope.launch { + try { + registrationWizard?.let { block(it) } + } catch (failure: Throwable) { + if (failure !is CancellationException) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + } + null + } + ?.let { data -> + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + setState { copy(isLoading = false) } + } + } + + private fun handleAddThreePid(action: LoginAction2.AddThreePid) { + setState { copy(isLoading = true) } + currentJob = viewModelScope.launch { + try { + registrationWizard?.addThreePid(action.threePid) + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + } + setState { copy(isLoading = false) } + } + } + + private fun handleSendAgainThreePid() { + setState { copy(isLoading = true) } + currentJob = viewModelScope.launch { + try { + registrationWizard?.sendAgainThreePid() + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + } + setState { copy(isLoading = false) } + } + } + + private fun handleAcceptTerms() { + currentJob = executeRegistrationStep { + it.acceptTerms() + } + } + + private fun handleRegisterDummy() { + currentJob = executeRegistrationStep { + it.dummy() + } + } + + /** + * Check that the user name is available + */ + private fun handleSetUserNameForSignUp(action: LoginAction2.SetUserName) { + setState { copy(isLoading = true) } + + val safeRegistrationWizard = registrationWizard ?: error("Invalid") + + viewModelScope.launch { + val available = safeRegistrationWizard.registrationAvailable(action.username) + + val event = when (available) { + RegistrationAvailability.Available -> { + // Ask for a password + LoginViewEvents2.OpenSignupPasswordScreen + } + is RegistrationAvailability.NotAvailable -> { + LoginViewEvents2.Failure(available.failure) + } + } + _viewEvents.post(event) + setState { copy(isLoading = false) } + } + } + + private fun handleCaptchaDone(action: LoginAction2.CaptchaDone) { + currentJob = executeRegistrationStep { + it.performReCaptcha(action.captchaResponse) + } + } + + // TODO Update this + private fun handleResetAction(action: LoginAction2.ResetAction) { + // Cancel any request + currentJob = null + + when (action) { + LoginAction2.ResetHomeServerUrl -> { + viewModelScope.launch { + authenticationService.reset() + setState { + copy( + homeServerUrlFromUser = null, + homeServerUrl = null, + loginMode = LoginMode.Unknown + ) + } + } + } + LoginAction2.ResetSignMode -> { + setState { + copy( + signMode = SignMode2.Unknown, + loginMode = LoginMode.Unknown + ) + } + } + LoginAction2.ResetLogin -> { + viewModelScope.launch { + authenticationService.cancelPendingLoginOrRegistration() + setState { copy(isLoading = false) } + } + } + LoginAction2.ResetResetPassword -> { + setState { + copy( + resetPasswordEmail = null + ) + } + } + } + } + + private fun handleUpdateSignMode(action: LoginAction2.UpdateSignMode) { + setState { + // Always create a new state, to ensure the state is correctly reset + LoginViewState2( + signMode = action.signMode + ) + } + + when (action.signMode) { + SignMode2.SignUp -> _viewEvents.post(LoginViewEvents2.OpenServerSelection) + SignMode2.SignIn -> _viewEvents.post(LoginViewEvents2.OpenSignInEnterIdentifierScreen) + SignMode2.Unknown -> Unit + } + } + + private fun handleEnterServerUrl() { + _viewEvents.post(LoginViewEvents2.OpenHomeServerUrlFormScreen) + } + + private fun handleInitWith(action: LoginAction2.InitWith) { + loginConfig = action.loginConfig + + // If there is a pending email validation continue on this step + try { + if (registrationWizard?.isRegistrationStarted == true) { + currentThreePid?.let { + handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnSendEmailSuccess(it))) + } + } + } catch (e: Throwable) { + // NOOP. API is designed to use wizards in a login/registration flow, + // but we need to check the state anyway. + } + } + + private fun handleResetPassword(action: LoginAction2.ResetPassword) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + _viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration"))) + } else { + setState { copy(isLoading = true) } + + currentJob = viewModelScope.launch { + try { + safeLoginWizard.resetPassword(action.email, action.newPassword) + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + setState { copy(isLoading = false) } + return@launch + } + + setState { + copy( + isLoading = false, + resetPasswordEmail = action.email + ) + } + + _viewEvents.post(LoginViewEvents2.OnResetPasswordSendThreePidDone) + } + } + } + + private fun handleResetPasswordMailConfirmed() { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + _viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration"))) + } else { + setState { copy(isLoading = true) } + + currentJob = viewModelScope.launch { + try { + safeLoginWizard.resetPasswordMailConfirmed() + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + setState { copy(isLoading = false) } + return@launch + } + setState { + copy( + isLoading = false, + resetPasswordEmail = null + ) + } + + _viewEvents.post(LoginViewEvents2.OnResetPasswordMailConfirmationSuccess) + } + } + } + + private fun handleSetUserName(action: LoginAction2.SetUserName) = withState { state -> + setState { + copy( + userName = action.username + ) + } + + when (state.signMode) { + SignMode2.Unknown -> error("Developer error, invalid sign mode") + SignMode2.SignIn -> handleSetUserNameForSignIn(action, null) + SignMode2.SignUp -> handleSetUserNameForSignUp(action) + }.exhaustive + } + + private fun handleSetUserPassword(action: LoginAction2.SetUserPassword) = withState { state -> + when (state.signMode) { + SignMode2.Unknown -> error("Developer error, invalid sign mode") + SignMode2.SignIn -> handleSignInWithPassword(action) + SignMode2.SignUp -> handleRegisterWithPassword(action) + }.exhaustive + } + + private fun handleRegisterWithPassword(action: LoginAction2.SetUserPassword) = withState { state -> + val username = state.userName ?: error("Developer error, username not set") + + reAuthHelper.data = action.password + currentJob = executeRegistrationStep { + it.createAccount( + userName = username, + password = action.password, + initialDeviceDisplayName = stringProvider.getString(R.string.login_default_session_public_name) + ) + } + } + + private fun handleSignInWithPassword(action: LoginAction2.SetUserPassword) = withState { state -> + val username = state.userName ?: error("Developer error, username not set") + setState { copy(isLoading = true) } + loginWith(username, action.password) + } + + private fun handleLoginWith(action: LoginAction2.LoginWith) { + setState { copy(isLoading = true) } + loginWith(action.login, action.password) + } + + private fun loginWith(login: String, password: String) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + _viewEvents.post(LoginViewEvents2.Failure(Throwable("Bad configuration"))) + setState { copy(isLoading = false) } + } else { + currentJob = viewModelScope.launch { + try { + safeLoginWizard.login( + login = login, + password = password, + deviceName = stringProvider.getString(R.string.login_default_session_public_name) + ) + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + null + } + ?.let { + reAuthHelper.data = password + onSessionCreated(it) + } + setState { copy(isLoading = false) } + } + } + } + + /** + * Perform wellknown request + */ + private fun handleSetUserNameForSignIn(action: LoginAction2.SetUserName, homeServerConnectionConfig: HomeServerConnectionConfig?) { + setState { copy(isLoading = true) } + + currentJob = viewModelScope.launch { + val data = try { + authenticationService.getWellKnownData(action.username, homeServerConnectionConfig) + } catch (failure: Throwable) { + onDirectLoginError(failure) + return@launch + } + when (data) { + is WellknownResult.Prompt -> + onWellknownSuccess(action, data, homeServerConnectionConfig) + is WellknownResult.FailPrompt -> + // Relax on IS discovery if home server is valid + if (data.homeServerUrl != null && data.wellKnown != null) { + onWellknownSuccess(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), homeServerConnectionConfig) + } else { + onWellKnownError() + } + is WellknownResult.InvalidMatrixId -> { + setState { copy(isLoading = false) } + _viewEvents.post(LoginViewEvents2.Failure(Exception(stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)))) + } + else -> { + onWellKnownError() + } + }.exhaustive + } + } + + private fun onWellKnownError() { + _viewEvents.post(LoginViewEvents2.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) + setState { copy(isLoading = false) } + } + + private suspend fun onWellknownSuccess(action: LoginAction2.SetUserName, + wellKnownPrompt: WellknownResult.Prompt, + homeServerConnectionConfig: HomeServerConnectionConfig?) { + val alteredHomeServerConnectionConfig = homeServerConnectionConfig + ?.copy( + homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + ?: HomeServerConnectionConfig( + homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + + // Ensure login flow is retrieved, and this is not a SSO only server + val data = try { + authenticationService.getLoginFlow(alteredHomeServerConnectionConfig) + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + null + } + + if (data is LoginFlowResult.Success) { + val loginMode = when { + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) + && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported + } + + val viewEvent = when (loginMode) { + LoginMode.Password, + is LoginMode.SsoAndPassword -> { + retrieveProfileInfo(action.username) + // We can navigate to the password screen + LoginViewEvents2.OpenPasswordScreen + } + is LoginMode.Sso -> { + LoginViewEvents2.OpenSsoOnlyScreen + } + LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList()) + LoginMode.Unknown -> null + } + viewEvent?.let { _viewEvents.post(it) } + + val urlFromUser = action.username.substringAfter(":") + setState { + copy( + isLoading = false, + homeServerUrlFromUser = urlFromUser, + homeServerUrl = data.homeServerUrl, + isNumericOnlyUserIdForbidden = urlFromUser == matrixOrgUrl, + loginMode = loginMode + ) + } + + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) + || data.isOutdatedHomeserver) { + // Notify the UI + _viewEvents.post(LoginViewEvents2.OutdatedHomeserver) + } + } + } + + private suspend fun retrieveProfileInfo(username: String) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard != null) { + try { + val info = safeLoginWizard.getProfileInfo(username) + setState { + copy( + loginProfileInfo = info + ) + } + } catch (failure: Throwable) { + // Ignore error + // TODO 404 may indicates that the user does not exist, so there is a mistake in the id + } + } + } + + private fun onDirectLoginError(failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + setState { copy(isLoading = false) } + } + + private fun onFlowResponse(flowResult: FlowResult) { + // If dummy stage is mandatory, and password is already sent, do the dummy stage now + if (isRegistrationStarted + && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { + handleRegisterDummy() + } else { + // Notify the user + _viewEvents.post(LoginViewEvents2.RegistrationFlowResult(flowResult, isRegistrationStarted)) + } + } + + private suspend fun onSessionCreated(session: Session) { + activeSessionHolder.setActiveSession(session) + + authenticationService.reset() + session.configureAndStart(applicationContext) + withState { state -> + _viewEvents.post(LoginViewEvents2.OnSessionCreated(state.signMode == SignMode2.SignUp)) + } + } + + private fun handleWebLoginSuccess(action: LoginAction2.WebLoginSuccess) = withState { state -> + val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(state.homeServerUrl) + + if (homeServerConnectionConfigFinal == null) { + // Should not happen + Timber.w("homeServerConnectionConfig is null") + } else { + currentJob = viewModelScope.launch { + try { + authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials) + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + null + } + ?.let { onSessionCreated(it) } + } + } + } + + private fun handleUpdateHomeserver(action: LoginAction2.UpdateHomeServer) { + val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl) + if (homeServerConnectionConfig == null) { + // This is invalid + _viewEvents.post(LoginViewEvents2.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) + } else { + getLoginFlow(homeServerConnectionConfig) + } + } + + private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig) = withState { state -> + currentHomeServerConnectionConfig = homeServerConnectionConfig + + setState { copy(isLoading = true) } + + currentJob = viewModelScope.launch { + authenticationService.cancelPendingLoginOrRegistration() + + val data = try { + authenticationService.getLoginFlow(homeServerConnectionConfig) + } catch (failure: Throwable) { + _viewEvents.post(LoginViewEvents2.Failure(failure)) + setState { copy(isLoading = false) } + null + } + + if (data is LoginFlowResult.Success) { + // Valid Homeserver, add it to the history. + // Note: we add what the user has input, data.homeServerUrl can be different + rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString()) + + val loginMode = when { + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) + && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported + } + + val viewEvent = when (loginMode) { + LoginMode.Password, + is LoginMode.SsoAndPassword -> { + when (state.signMode) { + SignMode2.Unknown -> null + SignMode2.SignUp -> { + // Check that registration is possible on this server + try { + registrationWizard?.getRegistrationFlow() + + /* + // Simulate registration disabled + throw Failure.ServerError( + error = MatrixError( + code = MatrixError.M_FORBIDDEN, + message = "Registration is disabled" + ), + httpCode = 403 + ) + */ + + LoginViewEvents2.OpenSignUpChooseUsernameScreen + } catch (throwable: Throwable) { + // Registration disabled? + LoginViewEvents2.Failure(throwable) + } + } + SignMode2.SignIn -> LoginViewEvents2.OpenSignInWithAnythingScreen + } + } + is LoginMode.Sso -> { + LoginViewEvents2.OpenSsoOnlyScreen + } + LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList()) + LoginMode.Unknown -> null + } + viewEvent?.let { _viewEvents.post(it) } + + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) + || data.isOutdatedHomeserver) { + // Notify the UI + _viewEvents.post(LoginViewEvents2.OutdatedHomeserver) + } + + setState { + copy( + isLoading = false, + homeServerUrlFromUser = homeServerConnectionConfig.homeServerUri.toString(), + homeServerUrl = data.homeServerUrl, + isNumericOnlyUserIdForbidden = homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl, + loginMode = loginMode + ) + } + } + } + } + + fun getInitialHomeServerUrl(): String? { + return loginConfig?.homeServerUrl + } + + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { + return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) + } + + fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { + return authenticationService.getFallbackUrl(forSignIn, deviceId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt new file mode 100644 index 0000000000..3c631af18c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt @@ -0,0 +1,69 @@ +/* + * 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.app.features.login2 + +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.PersistState +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.features.login.LoginMode +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.auth.login.LoginProfileInfo + +data class LoginViewState2( + val isLoading: Boolean = false, + + // User choices + @PersistState + val signMode: SignMode2 = SignMode2.Unknown, + @PersistState + val userName: String? = null, + @PersistState + val resetPasswordEmail: String? = null, + @PersistState + val homeServerUrlFromUser: String? = null, + + // Can be modified after a Wellknown request + @PersistState + val homeServerUrl: String? = null, + + // For SSO session recovery + @PersistState + val deviceId: String? = null, + + // Network result + val loginProfileInfo: LoginProfileInfo? = null, + + // True on Matrix.org + val isNumericOnlyUserIdForbidden: Boolean = false, + + // Network result + @PersistState + val loginMode: LoginMode = LoginMode.Unknown, + + // From database + val knownCustomHomeServersUrls: List = emptyList() +) : MvRxState { + + // Pending user identifier + fun userIdentifier(): String { + return if (userName != null && MatrixPatterns.isUserId(userName)) { + userName + } else { + "@$userName:${homeServerUrlFromUser.toReducedUrl()}" + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginWaitForEmailFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginWaitForEmailFragment2.kt new file mode 100644 index 0000000000..db3c607480 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginWaitForEmailFragment2.kt @@ -0,0 +1,75 @@ +/* + * 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.app.features.login2 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import im.vector.app.R +import im.vector.app.databinding.FragmentLoginWaitForEmail2Binding +import im.vector.app.features.login.LoginWaitForEmailFragmentArgument +import org.matrix.android.sdk.api.failure.is401 +import javax.inject.Inject + +/** + * In this screen, the user is asked to check his emails + */ +class LoginWaitForEmailFragment2 @Inject constructor() : AbstractLoginFragment2() { + + private val params: LoginWaitForEmailFragmentArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWaitForEmail2Binding { + return FragmentLoginWaitForEmail2Binding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + } + + override fun onResume() { + super.onResume() + + loginViewModel.handle(LoginAction2.CheckIfEmailHasBeenValidated(0)) + } + + override fun onPause() { + super.onPause() + + loginViewModel.handle(LoginAction2.StopEmailValidationCheck) + } + + private fun setupUi() { + views.loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice_2, params.email) + } + + override fun onError(throwable: Throwable) { + if (throwable.is401()) { + // Try again, with a delay + loginViewModel.handle(LoginAction2.CheckIfEmailHasBeenValidated(10_000)) + } else { + super.onError(throwable) + } + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt new file mode 100644 index 0000000000..2acb30bd8a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt @@ -0,0 +1,255 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION") + +package im.vector.app.features.login2 + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.graphics.Bitmap +import android.net.http.SslError +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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.app.R +import im.vector.app.core.utils.AssetReader +import im.vector.app.databinding.FragmentLoginWebBinding +import im.vector.app.features.login.JavascriptResponse +import im.vector.app.features.signout.soft.SoftLogoutAction +import im.vector.app.features.signout.soft.SoftLogoutViewModel + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.di.MoshiProvider +import timber.log.Timber +import java.net.URLDecoder +import javax.inject.Inject + +/** + * This screen is displayed when the application does not support login flow or registration flow + * of the homeserver, as a fallback to login or to create an account + */ +class LoginWebFragment2 @Inject constructor( + private val assetReader: AssetReader +) : AbstractLoginFragment2() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWebBinding { + return FragmentLoginWebBinding.inflate(inflater, container, false) + } + + private var isWebViewLoaded = false + private var isForSessionRecovery = false + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupToolbar(views.loginWebToolbar) + } + + override fun updateWithState(state: LoginViewState2) { + setupTitle(state) + + isForSessionRecovery = state.deviceId?.isNotBlank() == true + + if (!isWebViewLoaded) { + setupWebView(state) + isWebViewLoaded = true + } + } + + private fun setupTitle(state: LoginViewState2) { + views.loginWebToolbar.title = when (state.signMode) { + SignMode2.SignIn -> getString(R.string.login_signin) + else -> getString(R.string.login_signup) + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebView(state: LoginViewState2) { + views.loginWebWebView.settings.javaScriptEnabled = true + + // Enable local storage to support SSO with Firefox accounts + views.loginWebWebView.settings.domStorageEnabled = true + views.loginWebWebView.settings.databaseEnabled = 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) + views.loginWebWebView.settings.userAgentString = "Mozilla/5.0 Google" + + // AppRTC requires third party cookies to work + val cookieManager = android.webkit.CookieManager.getInstance() + + // clear the cookies + if (cookieManager == null) { + launchWebView(state) + } else { + if (!cookieManager.hasCookies()) { + launchWebView(state) + } else { + try { + cookieManager.removeAllCookies { launchWebView(state) } + } catch (e: Exception) { + Timber.e(e, " cookieManager.removeAllCookie() fails") + launchWebView(state) + } + } + } + } + + private fun launchWebView(state: LoginViewState2) { + val url = loginViewModel.getFallbackUrl(state.signMode == SignMode2.SignIn, state.deviceId) ?: return + + views.loginWebWebView.loadUrl(url) + + views.loginWebWebView.webViewClient = object : WebViewClient() { + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, + error: SslError) { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.ssl_could_not_verify) + .setPositiveButton(R.string.ssl_trust) { _, _ -> handler.proceed() } + .setNegativeButton(R.string.ssl_do_not_trust) { _, _ -> handler.cancel() } + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + handler.cancel() + dialog.dismiss() + return@OnKeyListener true + } + false + }) + .setCancelable(false) + .show() + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + super.onReceivedError(view, errorCode, description, failingUrl) + + loginViewModel.handle(LoginAction2.PostViewEvent(LoginViewEvents2.OnWebLoginError(errorCode, description, failingUrl))) + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + views.loginWebToolbar.subtitle = url + } + + override fun onPageFinished(view: WebView, url: String) { + // avoid infinite onPageFinished call + if (url.startsWith("http")) { + // Generic method to make a bridge between JS and the UIWebView + assetReader.readAssetFile("sendObject.js")?.let { view.loadUrl(it) } + + if (state.signMode == SignMode2.SignIn) { + // The function the fallback page calls when the login is complete + assetReader.readAssetFile("onLogin.js")?.let { view.loadUrl(it) } + } else { + // MODE_REGISTER + // The function the fallback page calls when the registration is complete + assetReader.readAssetFile("onRegistered.js")?.let { view.loadUrl(it) } + } + } + } + + /** + * Example of (formatted) url for MODE_LOGIN: + * + *
+             * js:{
+             *     "action":"onLogin",
+             *     "credentials":{
+             *         "user_id":"@user:matrix.org",
+             *         "access_token":"[ACCESS_TOKEN]",
+             *         "home_server":"matrix.org",
+             *         "device_id":"[DEVICE_ID]",
+             *         "well_known":{
+             *             "m.homeserver":{
+             *                 "base_url":"https://matrix.org/"
+             *                 }
+             *             }
+             *         }
+             *    }
+             * 
+ * @param view + * @param url + * @return + */ + override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { + if (url == null) return super.shouldOverrideUrlLoading(view, url as String?) + + if (url.startsWith("js:")) { + var json = url.substring(3) + var javascriptResponse: JavascriptResponse? = null + + try { + // URL decode + json = URLDecoder.decode(json, "UTF-8") + val adapter = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java) + javascriptResponse = adapter.fromJson(json) + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed") + } + + // succeeds to parse parameters + if (javascriptResponse != null) { + val action = javascriptResponse.action + + if (state.signMode == SignMode2.SignIn) { + if (action == "onLogin") { + javascriptResponse.credentials?.let { notifyViewModel(it) } + } + } else { + // MODE_REGISTER + // check the required parameters + if (action == "onRegistered") { + javascriptResponse.credentials?.let { notifyViewModel(it) } + } + } + } + return true + } + + return super.shouldOverrideUrlLoading(view, url) + } + } + } + + private fun notifyViewModel(credentials: Credentials) { + if (isForSessionRecovery) { + val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() + softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials)) + } else { + loginViewModel.handle(LoginAction2.WebLoginSuccess(credentials)) + } + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + return when { + toolbarButton -> super.onBackPressed(toolbarButton) + views.loginWebWebView.canGoBack() -> views.loginWebWebView.goBack().run { true } + else -> super.onBackPressed(toolbarButton) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/SignMode2.kt b/vector/src/main/java/im/vector/app/features/login2/SignMode2.kt new file mode 100644 index 0000000000..f3d59837e7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/SignMode2.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.app.features.login2 + +enum class SignMode2 { + Unknown, + + // Account creation + SignUp, + + // Login + SignIn +} diff --git a/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt new file mode 100755 index 0000000000..ac174f4a48 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt @@ -0,0 +1,119 @@ +/* + * 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.app.features.login2.terms + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.databinding.FragmentLoginTermsBinding +import im.vector.app.features.login.terms.LocalizedFlowDataLoginTermsChecked +import im.vector.app.features.login.terms.LoginTermsFragmentArgument +import im.vector.app.features.login.terms.LoginTermsViewState +import im.vector.app.features.login.terms.PolicyController +import im.vector.app.features.login2.AbstractLoginFragment2 +import im.vector.app.features.login2.LoginAction2 +import im.vector.app.features.login2.LoginViewState2 +import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms +import javax.inject.Inject + +/** + * LoginTermsFragment displays the list of policies the user has to accept + */ +class LoginTermsFragment2 @Inject constructor( + private val policyController: PolicyController +) : AbstractLoginFragment2(), + PolicyController.PolicyControllerListener { + + private val params: LoginTermsFragmentArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginTermsBinding { + return FragmentLoginTermsBinding.inflate(inflater, container, false) + } + + private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList()) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupViews() + views.loginTermsPolicyList.configureWith(policyController) + policyController.listener = this + + val list = ArrayList() + + params.localizedFlowDataLoginTerms + .forEach { + list.add(LocalizedFlowDataLoginTermsChecked(it)) + } + + loginTermsViewState = LoginTermsViewState(list) + } + + private fun setupViews() { + views.loginTermsSubmit.setOnClickListener { submit() } + } + + override fun onDestroyView() { + views.loginTermsPolicyList.cleanup() + policyController.listener = null + super.onDestroyView() + } + + private fun renderState() { + policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked) + + // Button is enabled only if all checkboxes are checked + views.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) { + localizedFlowDataLoginTerms.localizedUrl + ?.takeIf { it.isNotBlank() } + ?.let { + openUrlInChromeCustomTab(requireContext(), null, it) + } + } + + private fun submit() { + loginViewModel.handle(LoginAction2.AcceptTerms) + } + + override fun updateWithState(state: LoginViewState2) { + policyController.homeServer = state.homeServerUrlFromUser.toReducedUrl() + renderState() + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction2.ResetLogin) + } +} diff --git a/vector/src/main/res/layout/fragment_login_2_signin_password.xml b/vector/src/main/res/layout/fragment_login_2_signin_password.xml new file mode 100644 index 0000000000..638314cf3f --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_2_signin_password.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_2_signin_to.xml b/vector/src/main/res/layout/fragment_login_2_signin_to.xml new file mode 100644 index 0000000000..7f6158530f --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_2_signin_to.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_2_signin_username.xml b/vector/src/main/res/layout/fragment_login_2_signin_username.xml new file mode 100644 index 0000000000..5521b52aab --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_2_signin_username.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_2_signup_password.xml b/vector/src/main/res/layout/fragment_login_2_signup_password.xml new file mode 100644 index 0000000000..19ccc2ff9a --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_2_signup_password.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_2_signup_username.xml b/vector/src/main/res/layout/fragment_login_2_signup_username.xml new file mode 100644 index 0000000000..8c9a7741d1 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_2_signup_username.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_reset_password_2.xml b/vector/src/main/res/layout/fragment_login_reset_password_2.xml new file mode 100644 index 0000000000..5775c8044f --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password_2.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_reset_password_success_2.xml b/vector/src/main/res/layout/fragment_login_reset_password_success_2.xml new file mode 100644 index 0000000000..054ed7795a --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password_success_2.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_selection_2.xml b/vector/src/main/res/layout/fragment_login_server_selection_2.xml new file mode 100644 index 0000000000..b00fb13e96 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_server_selection_2.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_url_form_2.xml b/vector/src/main/res/layout/fragment_login_server_url_form_2.xml new file mode 100644 index 0000000000..6774adf76f --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_server_url_form_2.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index 92655c87b6..9a39c40a68 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -204,4 +204,13 @@ tools:text="@string/settings_version" tools:visibility="visible" /> + + diff --git a/vector/src/main/res/layout/fragment_login_splash_2.xml b/vector/src/main/res/layout/fragment_login_splash_2.xml new file mode 100644 index 0000000000..0b06d1cc65 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_splash_2.xml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_sso_only_2.xml b/vector/src/main/res/layout/fragment_login_sso_only_2.xml new file mode 100644 index 0000000000..b302e04586 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_sso_only_2.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_wait_for_email_2.xml b/vector/src/main/res/layout/fragment_login_wait_for_email_2.xml new file mode 100644 index 0000000000..06fac32b8e --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_wait_for_email_2.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_login_password_form.xml b/vector/src/main/res/layout/item_login_password_form.xml index d6b5d6898e..d8a2d96809 100644 --- a/vector/src/main/res/layout/item_login_password_form.xml +++ b/vector/src/main/res/layout/item_login_password_form.xml @@ -58,6 +58,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start" + android:paddingStart="0dp" + android:paddingEnd="0dp" android:text="@string/auth_forgot_password" /> + + + + + Type it again + Welcome back %s! + Please enter your password + Please enter your Matrix identifier + Matrix identifiers start with @, for instance @alice:server.org + If you do not know your Matrix identifier, or if your account has been created using Single Sign On (for instance using a Google account), or if you want to connect using your simple name, or an email associated to your account, you have to select your server first. + Choose a server + Please choose a password + Your Matrix identifier + Press back to change + Choose a password + Enter an email associated to your Matrix account + Choose a new password + Please choose an identifier + Your identifier will be used to connect to your Matrix account + Once your account is created, your identifier cannot be modified. However you will be able to change your display name. + If you\'re not sure, select this option + Element Matrix Server and others + Try the new flow + Create a new account + I already have an account + + We just sent an email to %1$s. + Click on the link it contains to continue the account creation. + + \ No newline at end of file From c141b26212317c1c10146a074979285ccfab1a3c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Apr 2021 17:38:51 +0200 Subject: [PATCH 005/202] Login UX flow: set avatar and display name after account creation --- .../im/vector/app/core/di/FragmentModule.kt | 6 + .../app/features/home/AvatarRenderer.kt | 18 ++ .../features/login2/AbstractLoginFragment2.kt | 18 +- .../app/features/login2/LoginAction2.kt | 4 + .../app/features/login2/LoginActivity2.kt | 24 ++- .../login2/LoginFragment2SigninPassword.kt | 23 +-- .../app/features/login2/LoginViewEvents2.kt | 2 + .../app/features/login2/LoginViewModel2.kt | 6 + .../login2/created/AccountCreatedAction.kt | 25 +++ .../login2/created/AccountCreatedFragment.kt | 160 ++++++++++++++++++ .../created/AccountCreatedViewEvents.kt | 27 +++ .../login2/created/AccountCreatedViewModel.kt | 105 ++++++++++++ .../login2/created/AccountCreatedViewState.kt | 29 ++++ .../layout/fragment_login_account_created.xml | 124 ++++++++++++++ .../src/main/res/values/strings_login_v2.xml | 6 + 15 files changed, 555 insertions(+), 22 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewState.kt create mode 100644 vector/src/main/res/layout/fragment_login_account_created.xml diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 943667c5d6..aef61f3bc0 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -76,6 +76,7 @@ import im.vector.app.features.login2.LoginFragment2SigninPassword import im.vector.app.features.login2.LoginFragment2SigninUsername import im.vector.app.features.login2.LoginFragment2SignupPassword import im.vector.app.features.login2.LoginFragment2SignupUsername +import im.vector.app.features.login2.created.AccountCreatedFragment import im.vector.app.features.login2.LoginFragmentToAny2 import im.vector.app.features.login2.LoginGenericTextInputFormFragment2 import im.vector.app.features.login2.LoginResetPasswordFragment2 @@ -286,6 +287,11 @@ interface FragmentModule { @FragmentKey(LoginFragment2SigninUsername::class) fun bindLoginFragment2SigninUsername(fragment: LoginFragment2SigninUsername): Fragment + @Binds + @IntoMap + @FragmentKey(AccountCreatedFragment::class) + fun bindAccountCreatedFragment(fragment: AccountCreatedFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginFragment2SignupUsername::class) diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 23ca5eee9c..65bc5e1200 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -41,6 +41,7 @@ import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.ColorFilterTransformation +import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.util.MatrixItem @@ -113,6 +114,23 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active .into(imageView) } + @UiThread + fun render(profileInfo: LoginProfileInfo, imageView: ImageView) { + // Create a Fake MatrixItem, for the placeholder + val matrixItem = MatrixItem.UserItem( + // Need an id starting with @ + id = profileInfo.matrixId, + displayName = profileInfo.displayName + ) + + val placeholder = getPlaceholderDrawable(matrixItem) + GlideApp.with(imageView) + .load(profileInfo.fullAvatarUrl) + .apply(RequestOptions.circleCropTransform()) + .placeholder(placeholder) + .into(imageView) + } + @UiThread fun render(glideRequests: GlideRequests, matrixItem: MatrixItem, diff --git a/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt index 5175f32f05..314867ef8b 100644 --- a/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt @@ -34,7 +34,7 @@ import org.matrix.android.sdk.api.failure.Failure /** * Parent Fragment for all the login/registration screens */ -abstract class AbstractLoginFragment2 : VectorBaseFragment(), OnBackPressed { +abstract class AbstractLoginFragment2 : VectorBaseFragment(), OnBackPressed { protected val loginViewModel: LoginViewModel2 by activityViewModel() @@ -147,11 +147,19 @@ abstract class AbstractLoginFragment2 : VectorBaseFragment( } } - final override fun invalidate() = withState(loginViewModel) { state -> - // True when email is sent with success to the homeserver - isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not() + final override fun invalidate() { + withState(loginViewModel) { state -> + // True when email is sent with success to the homeserver + isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not() - updateWithState(state) + updateWithState(state) + } + + invalidateMore() + } + + protected open fun invalidateMore() { + // No op by default } open fun updateWithState(state: LoginViewState2) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt index 8f3e88abbb..63c163fd6e 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt @@ -41,6 +41,7 @@ sealed class LoginAction2 : VectorViewModelAction { // Username to Login or Register, depending on the signMode data class SetUserName(val username: String) : LoginAction2() + // Password to Login or Register, depending on the signMode data class SetUserPassword(val password: String) : LoginAction2() @@ -82,4 +83,7 @@ sealed class LoginAction2 : VectorViewModelAction { data class PostViewEvent(val viewEvent: LoginViewEvents2) : LoginAction2() data class UserAcceptCertificate(val fingerprint: Fingerprint) : LoginAction2() + + // Account customization is over + object Finish : LoginAction2() } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt index 149abb69da..dd96c14fec 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt @@ -46,6 +46,7 @@ import im.vector.app.features.login.LoginWaitForEmailFragmentArgument import im.vector.app.features.login.isSupported import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.toLocalizedLoginTerms +import im.vector.app.features.login2.created.AccountCreatedFragment import im.vector.app.features.login2.terms.LoginTermsFragment2 import im.vector.app.features.pin.UnlockedActivity @@ -245,14 +246,26 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC is LoginViewEvents2.OnLoginModeNotSupported -> onLoginModeNotSupported(event.supportedTypes) is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event) + is LoginViewEvents2.Finish -> terminate(true) }.exhaustive } private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) { - // TODO Propose to set avatar and display name + if (event.newAccount) { + // Propose to set avatar and display name + // Back on this Fragment will finish the Activity + addFragmentToBackstack(R.id.loginFragmentContainer, + AccountCreatedFragment::class.java, + option = commonOption) + } else { + terminate(false) + } + } + + private fun terminate(newAccount: Boolean) { val intent = HomeActivity.newIntent( this, - accountCreation = event.newAccount + accountCreation = newAccount ) startActivity(intent) finish() @@ -260,7 +273,12 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC private fun updateWithState(LoginViewState2: LoginViewState2) { // Loading - views.loginLoading.isVisible = LoginViewState2.isLoading + setIsLoading(LoginViewState2.isLoading) + } + + // Hack for AccountCreatedFragment + fun setIsLoading(isLoading: Boolean) { + views.loginLoading.isVisible = isLoading } private fun onWebLoginError(onWebLoginError: LoginViewEvents2.OnWebLoginError) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt index 9a644613e1..9517685a9e 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt @@ -23,16 +23,14 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants -import androidx.core.view.isVisible -import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.showPassword import im.vector.app.databinding.FragmentLogin2SigninPasswordBinding +import im.vector.app.features.home.AvatarRenderer import io.reactivex.rxkotlin.subscribeBy +import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.failure.isInvalidPassword import javax.inject.Inject @@ -42,7 +40,9 @@ import javax.inject.Inject * - the user is asked for password to sign in to a homeserver. * - He also can reset his password */ -class LoginFragment2SigninPassword @Inject constructor() : AbstractSSOLoginFragment2() { +class LoginFragment2SigninPassword @Inject constructor( + private val avatarRenderer: AvatarRenderer +) : AbstractSSOLoginFragment2() { private var passwordShown = false @@ -106,15 +106,10 @@ class LoginFragment2SigninPassword @Inject constructor() : AbstractSSOLoginFragm state.loginProfileInfo?.displayName?.takeIf { it.isNotBlank() } ?: state.userIdentifier() ) - if (state.loginProfileInfo != null) { - views.loginUserIcon.isVisible = true - Glide.with(requireContext()) - .load(state.loginProfileInfo.fullAvatarUrl) - .apply(RequestOptions.circleCropTransform()) - .into(views.loginUserIcon) - } else { - views.loginUserIcon.isVisible = false - } + avatarRenderer.render( + profileInfo = state.loginProfileInfo ?: LoginProfileInfo(state.userIdentifier(), null, null), + imageView = views.loginUserIcon + ) } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt index 45e093e1c7..91cb6cac58 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt @@ -56,4 +56,6 @@ sealed class LoginViewEvents2 : VectorViewEvents { data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents2() data class OnSessionCreated(val newAccount: Boolean): LoginViewEvents2() + + object Finish : LoginViewEvents2() } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt index daa9631041..b09fc4ac8a 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt @@ -145,9 +145,15 @@ class LoginViewModel2 @AssistedInject constructor( is LoginAction2.UserAcceptCertificate -> handleUserAcceptCertificate(action) LoginAction2.ClearHomeServerHistory -> handleClearHomeServerHistory() is LoginAction2.PostViewEvent -> _viewEvents.post(action.viewEvent) + is LoginAction2.Finish -> handleFinish() }.exhaustive } + private fun handleFinish() { + // Just post a view Event + _viewEvents.post(LoginViewEvents2.Finish) + } + private fun handleChooseAServerForSignin() { // Just post a view Event _viewEvents.post(LoginViewEvents2.OpenServerSelection) diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedAction.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedAction.kt new file mode 100644 index 0000000000..f108bfa886 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.login2.created + +import android.net.Uri +import im.vector.app.core.platform.VectorViewModelAction + +sealed class AccountCreatedAction : VectorViewModelAction { + data class SetDisplayName(val displayName: String) : AccountCreatedAction() + data class SetAvatar(val avatarUri: Uri, val filename: String) : AccountCreatedAction() +} diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt new file mode 100644 index 0000000000..4b7f43f9a1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.login2.created + +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper +import im.vector.app.core.intent.getFilenameFromUri +import im.vector.app.core.resources.ColorProvider +import im.vector.app.databinding.DialogBaseEditTextBinding +import im.vector.app.databinding.FragmentLoginAccountCreatedBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider +import im.vector.app.features.login2.AbstractLoginFragment2 +import im.vector.app.features.login2.LoginAction2 +import im.vector.app.features.login2.LoginActivity2 +import im.vector.app.features.login2.LoginViewState2 +import org.matrix.android.sdk.api.util.MatrixItem +import java.util.UUID +import javax.inject.Inject + +/** + * In this screen: + * - the account has been created and we propose the user to set an avatar and a display name + */ +class AccountCreatedFragment @Inject constructor( + val accountCreatedViewModelFactory: AccountCreatedViewModel.Factory, + private val avatarRenderer: AvatarRenderer, + private val matrixItemColorProvider: MatrixItemColorProvider, + colorProvider: ColorProvider +) : AbstractLoginFragment2(), + GalleryOrCameraDialogHelper.Listener { + + private val viewModel: AccountCreatedViewModel by fragmentViewModel() + + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginAccountCreatedBinding { + return FragmentLoginAccountCreatedBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupClickListener() + setupSubmitButton() + observeViewEvents() + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is AccountCreatedViewEvents.Failure -> displayErrorDialog(it.throwable) + } + } + } + + private fun setupClickListener() { + views.loginAccountCreatedMessage.setOnClickListener { + // Update display name + displayDialog() + } + views.loginAccountCreatedAvatar.setOnClickListener { + galleryOrCameraDialogHelper.show() + } + } + + private fun displayDialog() = withState(viewModel) { state -> + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) + val views = DialogBaseEditTextBinding.bind(layout) + views.editText.setText(state.currentUser()?.getBestName().orEmpty()) + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.settings_display_name) + .setView(layout) + .setPositiveButton(R.string.ok) { _, _ -> + val newName = views.editText.text.toString() + viewModel.handle(AccountCreatedAction.SetDisplayName(newName)) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + override fun onImageReady(uri: Uri?) { + uri ?: return + viewModel.handle(AccountCreatedAction.SetAvatar( + avatarUri = uri, + filename = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()) + ) + } + + private fun setupSubmitButton() { + views.loginAccountCreatedLater.setOnClickListener { terminate() } + views.loginAccountCreatedDone.setOnClickListener { terminate() } + } + + private fun terminate() { + loginViewModel.handle(LoginAction2.Finish) + } + + override fun invalidateMore() = withState(viewModel) { state -> + // Ugly hack... + (activity as? LoginActivity2)?.setIsLoading(state.isLoading) + + views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId) + + val user = state.currentUser() + if (user != null) { + avatarRenderer.render(user, views.loginAccountCreatedAvatar) + views.loginAccountCreatedMemberName.text = user.getBestName() + } else { + // Should not happen + views.loginAccountCreatedMemberName.text = state.userId + } + + // User color + views.loginAccountCreatedMemberName + .setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(state.userId))) + + views.loginAccountCreatedLater.isVisible = state.hasBeenModified.not() + views.loginAccountCreatedDone.isVisible = state.hasBeenModified + } + + override fun updateWithState(state: LoginViewState2) { + // No op + } + + override fun resetViewModel() { + // No op + } + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + // Just start the next Activity + terminate() + return false + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewEvents.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewEvents.kt new file mode 100644 index 0000000000..4677e1abd5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewEvents.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.app.features.login2.created + +import im.vector.app.core.platform.VectorViewEvents + +/** + * Transient events for Account Created + */ +sealed class AccountCreatedViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : AccountCreatedViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt new file mode 100644 index 0000000000..d1684a9867 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.login2.created + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.rx.unwrap + +class AccountCreatedViewModel @AssistedInject constructor( + @Assisted initialState: AccountCreatedViewState, + private val session: Session +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: AccountCreatedViewState): AccountCreatedViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: AccountCreatedViewState): AccountCreatedViewModel? { + val fragment: AccountCreatedFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.accountCreatedViewModelFactory.create(state) + } + } + + init { + setState { + copy( + userId = session.myUserId + ) + } + observeUser() + } + + private fun observeUser() { + session.rx() + .liveUser(session.myUserId) + .unwrap() + .map { it.toMatrixItem() } + .execute { + copy(currentUser = it) + } + } + + override fun handle(action: AccountCreatedAction) { + when (action) { + is AccountCreatedAction.SetAvatar -> handleSetAvatar(action) + is AccountCreatedAction.SetDisplayName -> handleSetDisplayName(action) + } + } + + private fun handleSetAvatar(action: AccountCreatedAction.SetAvatar) { + setState { copy(isLoading = true) } + viewModelScope.launch { + val result = runCatching { session.updateAvatar(session.myUserId, action.avatarUri, action.filename) } + .onFailure { _viewEvents.post(AccountCreatedViewEvents.Failure(it)) } + setState { + copy( + isLoading = false, + hasBeenModified = hasBeenModified || result.isSuccess + ) + } + } + } + + private fun handleSetDisplayName(action: AccountCreatedAction.SetDisplayName) { + setState { copy(isLoading = true) } + viewModelScope.launch { + val result = runCatching { session.setDisplayName(session.myUserId, action.displayName) } + .onFailure { _viewEvents.post(AccountCreatedViewEvents.Failure(it)) } + setState { + copy( + isLoading = false, + hasBeenModified = hasBeenModified || result.isSuccess + ) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewState.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewState.kt new file mode 100644 index 0000000000..80211b3da2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.login2.created + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.util.MatrixItem + +data class AccountCreatedViewState( + val userId: String = "", + val isLoading: Boolean = false, + val currentUser: Async = Uninitialized, + val hasBeenModified: Boolean = false +) : MvRxState diff --git a/vector/src/main/res/layout/fragment_login_account_created.xml b/vector/src/main/res/layout/fragment_login_account_created.xml new file mode 100644 index 0000000000..8885dd3a5c --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_account_created.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_login_v2.xml b/vector/src/main/res/values/strings_login_v2.xml index 61c71f2b54..a32912cc00 100644 --- a/vector/src/main/res/values/strings_login_v2.xml +++ b/vector/src/main/res/values/strings_login_v2.xml @@ -27,5 +27,11 @@ We just sent an email to %1$s. Click on the link it contains to continue the account creation. + Congratulations! + You account %s has been successfully created. + To complete your profile, you can set a profile image and/or a display name. This can also be done later from the settings. + This is how your messages will appear: + Hello Matrix world! + Click on the image and on your name to configure them. \ No newline at end of file From 51a39909dcf783fc5b787ee98381f90d3eaccbff Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Apr 2021 18:20:01 +0200 Subject: [PATCH 006/202] Login UX flow: warning if no profile can ba found --- .../app/core/extensions/MvRxExtension.kt | 32 +++++++++++++++++++ .../login2/LoginFragment2SigninPassword.kt | 12 +++++-- .../app/features/login2/LoginViewModel2.kt | 16 ++++------ .../app/features/login2/LoginViewState2.kt | 4 ++- .../fragment_login_2_signin_password.xml | 11 +++++++ vector/src/main/res/values/colors.xml | 1 + .../src/main/res/values/strings_login_v2.xml | 2 ++ 7 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/MvRxExtension.kt diff --git a/vector/src/main/java/im/vector/app/core/extensions/MvRxExtension.kt b/vector/src/main/java/im/vector/app/core/extensions/MvRxExtension.kt new file mode 100644 index 0000000000..9daf16a589 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/MvRxExtension.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.extensions + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Success + +/** + * It maybe already exist somewhere but I cannot find it + */ +suspend fun tryAsync(block: suspend () -> T): Async { + return try { + Success(block.invoke()) + } catch (failure: Throwable) { + Fail(failure) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt index 9517685a9e..39701cb01b 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt @@ -23,6 +23,8 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard @@ -31,8 +33,10 @@ import im.vector.app.databinding.FragmentLogin2SigninPasswordBinding import im.vector.app.features.home.AvatarRenderer import io.reactivex.rxkotlin.subscribeBy import org.matrix.android.sdk.api.auth.login.LoginProfileInfo +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isInvalidPassword import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection /** * In this screen: @@ -103,13 +107,17 @@ class LoginFragment2SigninPassword @Inject constructor( // Name and avatar views.loginWelcomeBack.text = getString( R.string.login_welcome_back, - state.loginProfileInfo?.displayName?.takeIf { it.isNotBlank() } ?: state.userIdentifier() + state.loginProfileInfo()?.displayName?.takeIf { it.isNotBlank() } ?: state.userIdentifier() ) avatarRenderer.render( - profileInfo = state.loginProfileInfo ?: LoginProfileInfo(state.userIdentifier(), null, null), + profileInfo = state.loginProfileInfo() ?: LoginProfileInfo(state.userIdentifier(), null, null), imageView = views.loginUserIcon ) + + views.loginWelcomeBackWarning.isVisible = ((state.loginProfileInfo as? Fail) + ?.error as? Failure.ServerError) + ?.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */ } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt index b09fc4ac8a..a6f87f74c7 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt @@ -21,6 +21,7 @@ import android.net.Uri import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted @@ -30,6 +31,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.tryAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ensureTrailingSlash @@ -665,17 +667,11 @@ class LoginViewModel2 @AssistedInject constructor( val safeLoginWizard = loginWizard if (safeLoginWizard != null) { - try { - val info = safeLoginWizard.getProfileInfo(username) - setState { - copy( - loginProfileInfo = info - ) - } - } catch (failure: Throwable) { - // Ignore error - // TODO 404 may indicates that the user does not exist, so there is a mistake in the id + setState { copy(loginProfileInfo = Loading()) } + val result = tryAsync { + safeLoginWizard.getProfileInfo(username) } + setState { copy(loginProfileInfo = result) } } } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt index 3c631af18c..261fd417ee 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt @@ -16,8 +16,10 @@ package im.vector.app.features.login2 +import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.PersistState +import com.airbnb.mvrx.Uninitialized import im.vector.app.core.extensions.toReducedUrl import im.vector.app.features.login.LoginMode import org.matrix.android.sdk.api.MatrixPatterns @@ -45,7 +47,7 @@ data class LoginViewState2( val deviceId: String? = null, // Network result - val loginProfileInfo: LoginProfileInfo? = null, + val loginProfileInfo: Async = Uninitialized, // True on Matrix.org val isNumericOnlyUserIdForbidden: Boolean = false, diff --git a/vector/src/main/res/layout/fragment_login_2_signin_password.xml b/vector/src/main/res/layout/fragment_login_2_signin_password.xml index 638314cf3f..bc23420312 100644 --- a/vector/src/main/res/layout/fragment_login_2_signin_password.xml +++ b/vector/src/main/res/layout/fragment_login_2_signin_password.xml @@ -31,6 +31,17 @@ android:textAppearance="@style/TextAppearance.Vector.Login.Text" tools:text="Welcome back user!" /> + + #70BF56 #ff4b55 + #ff812d #ff4b55 #2f9edb diff --git a/vector/src/main/res/values/strings_login_v2.xml b/vector/src/main/res/values/strings_login_v2.xml index a32912cc00..73f5ba03b4 100644 --- a/vector/src/main/res/values/strings_login_v2.xml +++ b/vector/src/main/res/values/strings_login_v2.xml @@ -25,6 +25,8 @@ Create a new account I already have an account + Warning: no profile information can be retrieved with this Matrix identifier. Please check that there is no mistake. + We just sent an email to %1$s. Click on the link it contains to continue the account creation. Congratulations! From adae66aa431fbb7cf92a1dc65aa1fd36e19dd930 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Apr 2021 18:38:02 +0200 Subject: [PATCH 007/202] Login UX flow: UI iso --- ...nResetPasswordMailConfirmationFragment2.kt | 9 ++-- .../fragment_login_2_signin_password.xml | 15 +++--- .../res/layout/fragment_login_2_signin_to.xml | 2 +- .../fragment_login_2_signup_password.xml | 2 +- .../layout/fragment_login_account_created.xml | 2 +- .../fragment_login_reset_password_2.xml | 1 + ...gin_reset_password_mail_confirmation_2.xml | 52 +++++++++++++++++++ ...ragment_login_reset_password_success_2.xml | 9 ++-- .../fragment_login_server_selection_2.xml | 5 +- .../fragment_login_server_url_form_2.xml | 7 ++- .../res/layout/fragment_login_sso_only_2.xml | 19 +++---- .../fragment_login_wait_for_email_2.xml | 11 ++-- 12 files changed, 91 insertions(+), 43 deletions(-) create mode 100644 vector/src/main/res/layout/fragment_login_reset_password_mail_confirmation_2.xml diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordMailConfirmationFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordMailConfirmationFragment2.kt index 127d33eb71..65891e670e 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordMailConfirmationFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordMailConfirmationFragment2.kt @@ -22,18 +22,17 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import im.vector.app.R -import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmationBinding - +import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmation2Binding import org.matrix.android.sdk.api.failure.is401 import javax.inject.Inject /** * In this screen, the user is asked to check his email and to click on a button once it's done */ -class LoginResetPasswordMailConfirmationFragment2 @Inject constructor() : AbstractLoginFragment2() { +class LoginResetPasswordMailConfirmationFragment2 @Inject constructor() : AbstractLoginFragment2() { - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordMailConfirmationBinding { - return FragmentLoginResetPasswordMailConfirmationBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordMailConfirmation2Binding { + return FragmentLoginResetPasswordMailConfirmation2Binding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/res/layout/fragment_login_2_signin_password.xml b/vector/src/main/res/layout/fragment_login_2_signin_password.xml index bc23420312..f75706799c 100644 --- a/vector/src/main/res/layout/fragment_login_2_signin_password.xml +++ b/vector/src/main/res/layout/fragment_login_2_signin_password.xml @@ -15,10 +15,18 @@ style="@style/LoginLogo" tools:ignore="ContentDescription" /> + + @@ -42,13 +50,6 @@ android:visibility="gone" tools:visibility="visible" /> - - + android:layout_marginTop="@dimen/layout_vertical_margin"> diff --git a/vector/src/main/res/layout/fragment_login_account_created.xml b/vector/src/main/res/layout/fragment_login_account_created.xml index 8885dd3a5c..bccc27462b 100644 --- a/vector/src/main/res/layout/fragment_login_account_created.xml +++ b/vector/src/main/res/layout/fragment_login_account_created.xml @@ -17,7 +17,7 @@ diff --git a/vector/src/main/res/layout/fragment_login_reset_password_2.xml b/vector/src/main/res/layout/fragment_login_reset_password_2.xml index 5775c8044f..61f3e52279 100644 --- a/vector/src/main/res/layout/fragment_login_reset_password_2.xml +++ b/vector/src/main/res/layout/fragment_login_reset_password_2.xml @@ -18,6 +18,7 @@ android:id="@+id/resetPasswordTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/layout_vertical_margin" android:textAppearance="@style/TextAppearance.Vector.Login.Title" tools:text="@string/login_reset_password_on" /> diff --git a/vector/src/main/res/layout/fragment_login_reset_password_mail_confirmation_2.xml b/vector/src/main/res/layout/fragment_login_reset_password_mail_confirmation_2.xml new file mode 100644 index 0000000000..afb5d88a25 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password_mail_confirmation_2.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_reset_password_success_2.xml b/vector/src/main/res/layout/fragment_login_reset_password_success_2.xml index 054ed7795a..626564325e 100644 --- a/vector/src/main/res/layout/fragment_login_reset_password_success_2.xml +++ b/vector/src/main/res/layout/fragment_login_reset_password_success_2.xml @@ -16,6 +16,7 @@ @@ -23,16 +24,16 @@ android:id="@+id/resetPasswordSuccessNotice" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="32dp" + android:layout_marginTop="@dimen/layout_vertical_margin" android:text="@string/login_reset_password_success_notice" - android:textAppearance="@style/TextAppearance.Vector.Login.Text" /> + android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" /> + android:textAppearance="@style/TextAppearance.Vector.Login.Text" /> diff --git a/vector/src/main/res/layout/fragment_login_server_selection_2.xml b/vector/src/main/res/layout/fragment_login_server_selection_2.xml index b00fb13e96..245459ef9d 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection_2.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection_2.xml @@ -12,16 +12,13 @@ diff --git a/vector/src/main/res/layout/fragment_login_server_url_form_2.xml b/vector/src/main/res/layout/fragment_login_server_url_form_2.xml index 6774adf76f..75b587f0c1 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form_2.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form_2.xml @@ -19,7 +19,7 @@ android:id="@+id/loginServerUrlFormTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="27dp" + android:layout_marginTop="@dimen/layout_vertical_margin" android:text="@string/login_server_url_form_common_notice" android:textAppearance="@style/TextAppearance.Vector.Login.Title" /> @@ -48,6 +48,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" + android:paddingStart="16dp" + android:paddingEnd="0dp" + android:paddingBottom="16dp" android:text="@string/login_clear_homeserver_history" android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" android:textColor="@color/riotx_accent" @@ -60,7 +63,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" - android:layout_marginTop="22dp" + android:layout_marginTop="4dp" android:text="@string/login_continue" /> diff --git a/vector/src/main/res/layout/fragment_login_sso_only_2.xml b/vector/src/main/res/layout/fragment_login_sso_only_2.xml index b302e04586..abcefbefce 100644 --- a/vector/src/main/res/layout/fragment_login_sso_only_2.xml +++ b/vector/src/main/res/layout/fragment_login_sso_only_2.xml @@ -1,6 +1,5 @@ - + @@ -33,12 +27,11 @@ style="@style/Style.Vector.Login.Button" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_gravity="end" android:layout_marginTop="38dp" - android:text="@string/login_signin_sso" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninTitle" /> + android:text="@string/login_signin_sso" /> - + diff --git a/vector/src/main/res/layout/fragment_login_wait_for_email_2.xml b/vector/src/main/res/layout/fragment_login_wait_for_email_2.xml index 06fac32b8e..8b7f36bb44 100644 --- a/vector/src/main/res/layout/fragment_login_wait_for_email_2.xml +++ b/vector/src/main/res/layout/fragment_login_wait_for_email_2.xml @@ -17,6 +17,7 @@ android:id="@+id/loginWaitForEmailTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/layout_vertical_margin" android:text="@string/login_wait_for_email_title" android:textAppearance="@style/TextAppearance.Vector.Login.Title" /> @@ -24,23 +25,23 @@ android:id="@+id/loginWaitForEmailNotice" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="32dp" + android:layout_marginTop="@dimen/layout_vertical_margin" android:gravity="start" - android:textAppearance="@style/TextAppearance.Vector.Login.Text" + android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" tools:text="@string/login_wait_for_email_notice_2" /> + android:textAppearance="@style/TextAppearance.Vector.Login.Text" /> Date: Wed, 14 Apr 2021 18:53:13 +0200 Subject: [PATCH 008/202] Enable login v2 by default, but keep the possibility to use v1 --- vector/build.gradle | 4 + vector/src/main/AndroidManifest.xml | 2 + .../im/vector/app/features/MainActivity.kt | 13 +- .../app/features/login/LoginSplashFragment.kt | 9 -- .../features/navigation/DefaultNavigator.kt | 7 +- .../signout/soft/SoftLogoutActivity2.kt | 119 ++++++++++++++++++ .../main/res/layout/fragment_login_splash.xml | 9 -- .../src/main/res/values/strings_login_v2.xml | 1 - 8 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt diff --git a/vector/build.gradle b/vector/build.gradle index 3ecdcea524..7b4abeb385 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -137,6 +137,10 @@ android { buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" resValue "string", "build_number", "\"${buildNumber}\"" + // The two booleans must not have the same value. We need two values for the manifest + resValue "bool", "useLoginV1", "false" + resValue "bool", "useLoginV2", "true" + buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index b363df397e..82d0fb0b0f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -106,6 +106,7 @@ @@ -122,6 +123,7 @@ diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 50a86d24ed..054b1bcff1 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -42,6 +42,7 @@ import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.signout.soft.SoftLogoutActivity +import im.vector.app.features.signout.soft.SoftLogoutActivity2 import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.ui.UiStateRepository import kotlinx.parcelize.Parcelize @@ -228,7 +229,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity } args.isSoftLogout -> // The homeserver has invalidated the token, with a soft logout - SoftLogoutActivity.newIntent(this) + getSoftLogoutActivityIntent() args.isUserLoggedOut -> // the homeserver has invalidated the token (password changed, device deleted, other security reasons) SignedOutActivity.newIntent(this) @@ -239,7 +240,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity HomeActivity.newIntent(this) } else { // The token is still invalid - SoftLogoutActivity.newIntent(this) + getSoftLogoutActivityIntent() } else -> { // First start, or no active session @@ -250,4 +251,12 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity intent?.let { startActivity(it) } finish() } + + private fun getSoftLogoutActivityIntent(): Intent { + return if (resources.getBoolean(R.bool.useLoginV2)) { + SoftLogoutActivity2.newIntent(this) + } else { + SoftLogoutActivity.newIntent(this) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt index d4fdf2e80e..bafe836a8d 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt @@ -24,9 +24,7 @@ import android.view.ViewGroup import androidx.core.view.isVisible import im.vector.app.BuildConfig import im.vector.app.databinding.FragmentLoginSplashBinding -import im.vector.app.features.login2.LoginActivity2 import im.vector.app.features.settings.VectorPreferences - import javax.inject.Inject /** @@ -56,13 +54,6 @@ class LoginSplashFragment @Inject constructor( "Branch: ${BuildConfig.GIT_BRANCH_NAME}\n" + "Build: ${BuildConfig.BUILD_NUMBER}" } - - views.loginSplashNewFlow.setOnClickListener { startNewFlow() } - } - - private fun startNewFlow() { - startActivity(LoginActivity2.newIntent(requireContext(), null)) - requireActivity().finish() } private fun getStarted() { diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index c6df7910ea..b232d1f903 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -56,6 +56,7 @@ import im.vector.app.features.home.room.filtered.FilteredRoomsActivity import im.vector.app.features.invite.InviteUsersToRoomActivity import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig +import im.vector.app.features.login2.LoginActivity2 import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.BigImageViewerActivity @@ -99,7 +100,11 @@ class DefaultNavigator @Inject constructor( ) : Navigator { override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) { - val intent = LoginActivity.newIntent(context, loginConfig) + val intent = if (context.resources.getBoolean(R.bool.useLoginV2)) { + LoginActivity2.newIntent(context, loginConfig) + } else { + LoginActivity.newIntent(context, loginConfig) + } intent.addFlags(flags) context.startActivity(intent) } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt new file mode 100644 index 0000000000..cfccc6f699 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.signout.soft + +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 +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.replaceFragment +import im.vector.app.features.MainActivity +import im.vector.app.features.MainActivityArgs +import im.vector.app.features.login2.LoginActivity2 + +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +/** + * In this screen, the user is viewing a message informing that he has been logged out + * Extends LoginActivity to get the login with SSO and forget password functionality for (nearly) free + * + * This is just a copy of SoftLogoutActivity2, which extends LoginActivity2 + */ +class SoftLogoutActivity2 : LoginActivity2() { + + private val softLogoutViewModel: SoftLogoutViewModel by viewModel() + + @Inject lateinit var softLogoutViewModelFactory: SoftLogoutViewModel.Factory + @Inject lateinit var session: Session + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun initUiAndData() { + super.initUiAndData() + + softLogoutViewModel.subscribe(this) { + updateWithState(it) + } + + softLogoutViewModel.observeViewEvents { handleSoftLogoutViewEvents(it) } + } + + private fun handleSoftLogoutViewEvents(softLogoutViewEvents: SoftLogoutViewEvents) { + when (softLogoutViewEvents) { + is SoftLogoutViewEvents.Failure -> + showError(errorFormatter.toHumanReadable(softLogoutViewEvents.throwable)) + is SoftLogoutViewEvents.ErrorNotSameUser -> { + // Pop the backstack + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + // And inform the user + showError(getString( + R.string.soft_logout_sso_not_same_user_error, + softLogoutViewEvents.currentUserId, + softLogoutViewEvents.newUserId) + ) + } + is SoftLogoutViewEvents.ClearData -> { + MainActivity.restartApp(this, MainActivityArgs(clearCredentials = true)) + } + } + } + + private fun showError(message: String) { + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } + + override fun addFirstFragment() { + replaceFragment(R.id.loginFragmentContainer, SoftLogoutFragment::class.java) + } + + private fun updateWithState(softLogoutViewState: SoftLogoutViewState) { + if (softLogoutViewState.asyncLoginAction is Success) { + MainActivity.restartApp(this, MainActivityArgs()) + } + + views.loginLoading.isVisible = softLogoutViewState.isLoading() + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, SoftLogoutActivity2::class.java) + } + } + + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { + // No op here + Timber.w("Ignoring invalid token global error") + } +} diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index 9a39c40a68..92655c87b6 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -204,13 +204,4 @@ tools:text="@string/settings_version" tools:visibility="visible" /> - - diff --git a/vector/src/main/res/values/strings_login_v2.xml b/vector/src/main/res/values/strings_login_v2.xml index 73f5ba03b4..3062929050 100644 --- a/vector/src/main/res/values/strings_login_v2.xml +++ b/vector/src/main/res/values/strings_login_v2.xml @@ -21,7 +21,6 @@ Once your account is created, your identifier cannot be modified. However you will be able to change your display name. If you\'re not sure, select this option Element Matrix Server and others - Try the new flow Create a new account I already have an account From 2d7bface27244066d4968c22a0876b092a4cd798 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Apr 2021 18:57:13 +0200 Subject: [PATCH 009/202] Better solution --- .../features/login2/AbstractLoginFragment2.kt | 16 ++++------------ .../login2/created/AccountCreatedFragment.kt | 4 +++- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt index 314867ef8b..266e69e5b1 100644 --- a/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt @@ -147,19 +147,11 @@ abstract class AbstractLoginFragment2 : VectorBaseFragment } } - final override fun invalidate() { - withState(loginViewModel) { state -> - // True when email is sent with success to the homeserver - isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not() + final override fun invalidate() = withState(loginViewModel) { state -> + // True when email is sent with success to the homeserver + isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not() - updateWithState(state) - } - - invalidateMore() - } - - protected open fun invalidateMore() { - // No op by default + updateWithState(state) } open fun updateWithState(state: LoginViewState2) { diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt index 4b7f43f9a1..a799703956 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt @@ -67,6 +67,8 @@ class AccountCreatedFragment @Inject constructor( setupClickListener() setupSubmitButton() observeViewEvents() + + viewModel.subscribe { invalidateState(it) } } private fun observeViewEvents() { @@ -121,7 +123,7 @@ class AccountCreatedFragment @Inject constructor( loginViewModel.handle(LoginAction2.Finish) } - override fun invalidateMore() = withState(viewModel) { state -> + private fun invalidateState(state: AccountCreatedViewState) { // Ugly hack... (activity as? LoginActivity2)?.setIsLoading(state.isLoading) From d812ed72d0b60364f1bb116882e440f7470cbbc8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Apr 2021 20:05:40 +0200 Subject: [PATCH 010/202] Improve layout and add a fake date --- .../login2/created/AccountCreatedFragment.kt | 5 +++ .../layout/fragment_login_account_created.xml | 40 ++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt index a799703956..861e3f9e98 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt @@ -26,6 +26,8 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.resources.ColorProvider @@ -48,6 +50,7 @@ import javax.inject.Inject class AccountCreatedFragment @Inject constructor( val accountCreatedViewModelFactory: AccountCreatedViewModel.Factory, private val avatarRenderer: AvatarRenderer, + private val dateFormatter: VectorDateFormatter, private val matrixItemColorProvider: MatrixItemColorProvider, colorProvider: ColorProvider ) : AbstractLoginFragment2(), @@ -69,6 +72,8 @@ class AccountCreatedFragment @Inject constructor( observeViewEvents() viewModel.subscribe { invalidateState(it) } + + views.loginAccountCreatedTime.text = dateFormatter.format(System.currentTimeMillis(), DateFormatKind.MESSAGE_SIMPLE) } private fun observeViewEvents() { diff --git a/vector/src/main/res/layout/fragment_login_account_created.xml b/vector/src/main/res/layout/fragment_login_account_created.xml index bccc27462b..c07a1f6a33 100644 --- a/vector/src/main/res/layout/fragment_login_account_created.xml +++ b/vector/src/main/res/layout/fragment_login_account_created.xml @@ -1,5 +1,6 @@ - + android:layout_marginTop="@dimen/layout_vertical_margin" + android:background="@drawable/bg_login_server_selector" + android:padding="4dp"> + + + android:textSize="14sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/loginAccountCreatedMemberName" + app:layout_constraintTop_toBottomOf="@+id/loginAccountCreatedMemberName" /> - + Date: Wed, 14 Apr 2021 20:37:07 +0200 Subject: [PATCH 011/202] Fix issue after rebase --- .../app/features/login2/LoginViewModel2.kt | 168 +++++++++--------- 1 file changed, 82 insertions(+), 86 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt index a6f87f74c7..c412317b93 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt @@ -618,48 +618,46 @@ class LoginViewModel2 @AssistedInject constructor( } catch (failure: Throwable) { _viewEvents.post(LoginViewEvents2.Failure(failure)) null + } ?: return + + val loginMode = when { + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) + && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } - if (data is LoginFlowResult.Success) { - val loginMode = when { - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) - && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + val viewEvent = when (loginMode) { + LoginMode.Password, + is LoginMode.SsoAndPassword -> { + retrieveProfileInfo(action.username) + // We can navigate to the password screen + LoginViewEvents2.OpenPasswordScreen } + is LoginMode.Sso -> { + LoginViewEvents2.OpenSsoOnlyScreen + } + LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList()) + LoginMode.Unknown -> null + } + viewEvent?.let { _viewEvents.post(it) } - val viewEvent = when (loginMode) { - LoginMode.Password, - is LoginMode.SsoAndPassword -> { - retrieveProfileInfo(action.username) - // We can navigate to the password screen - LoginViewEvents2.OpenPasswordScreen - } - is LoginMode.Sso -> { - LoginViewEvents2.OpenSsoOnlyScreen - } - LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList()) - LoginMode.Unknown -> null - } - viewEvent?.let { _viewEvents.post(it) } + val urlFromUser = action.username.substringAfter(":") + setState { + copy( + isLoading = false, + homeServerUrlFromUser = urlFromUser, + homeServerUrl = data.homeServerUrl, + isNumericOnlyUserIdForbidden = urlFromUser == matrixOrgUrl, + loginMode = loginMode + ) + } - val urlFromUser = action.username.substringAfter(":") - setState { - copy( - isLoading = false, - homeServerUrlFromUser = urlFromUser, - homeServerUrl = data.homeServerUrl, - isNumericOnlyUserIdForbidden = urlFromUser == matrixOrgUrl, - loginMode = loginMode - ) - } - - if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) - || data.isOutdatedHomeserver) { - // Notify the UI - _viewEvents.post(LoginViewEvents2.OutdatedHomeserver) - } + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) + || data.isOutdatedHomeserver) { + // Notify the UI + _viewEvents.post(LoginViewEvents2.OutdatedHomeserver) } } @@ -744,32 +742,31 @@ class LoginViewModel2 @AssistedInject constructor( _viewEvents.post(LoginViewEvents2.Failure(failure)) setState { copy(isLoading = false) } null + } ?: return@launch + + // Valid Homeserver, add it to the history. + // Note: we add what the user has input, data.homeServerUrl can be different + rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString()) + + val loginMode = when { + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) + && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } - if (data is LoginFlowResult.Success) { - // Valid Homeserver, add it to the history. - // Note: we add what the user has input, data.homeServerUrl can be different - rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString()) + val viewEvent = when (loginMode) { + LoginMode.Password, + is LoginMode.SsoAndPassword -> { + when (state.signMode) { + SignMode2.Unknown -> null + SignMode2.SignUp -> { + // Check that registration is possible on this server + try { + registrationWizard?.getRegistrationFlow() - val loginMode = when { - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) - && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported - } - - val viewEvent = when (loginMode) { - LoginMode.Password, - is LoginMode.SsoAndPassword -> { - when (state.signMode) { - SignMode2.Unknown -> null - SignMode2.SignUp -> { - // Check that registration is possible on this server - try { - registrationWizard?.getRegistrationFlow() - - /* + /* // Simulate registration disabled throw Failure.ServerError( error = MatrixError( @@ -780,38 +777,37 @@ class LoginViewModel2 @AssistedInject constructor( ) */ - LoginViewEvents2.OpenSignUpChooseUsernameScreen - } catch (throwable: Throwable) { - // Registration disabled? - LoginViewEvents2.Failure(throwable) - } + LoginViewEvents2.OpenSignUpChooseUsernameScreen + } catch (throwable: Throwable) { + // Registration disabled? + LoginViewEvents2.Failure(throwable) } - SignMode2.SignIn -> LoginViewEvents2.OpenSignInWithAnythingScreen } + SignMode2.SignIn -> LoginViewEvents2.OpenSignInWithAnythingScreen } - is LoginMode.Sso -> { - LoginViewEvents2.OpenSsoOnlyScreen - } - LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList()) - LoginMode.Unknown -> null } - viewEvent?.let { _viewEvents.post(it) } + is LoginMode.Sso -> { + LoginViewEvents2.OpenSsoOnlyScreen + } + LoginMode.Unsupported -> LoginViewEvents2.OnLoginModeNotSupported(data.supportedLoginTypes.toList()) + LoginMode.Unknown -> null + } + viewEvent?.let { _viewEvents.post(it) } - if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) - || data.isOutdatedHomeserver) { - // Notify the UI - _viewEvents.post(LoginViewEvents2.OutdatedHomeserver) - } + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) + || data.isOutdatedHomeserver) { + // Notify the UI + _viewEvents.post(LoginViewEvents2.OutdatedHomeserver) + } - setState { - copy( - isLoading = false, - homeServerUrlFromUser = homeServerConnectionConfig.homeServerUri.toString(), - homeServerUrl = data.homeServerUrl, - isNumericOnlyUserIdForbidden = homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl, - loginMode = loginMode - ) - } + setState { + copy( + isLoading = false, + homeServerUrlFromUser = homeServerConnectionConfig.homeServerUri.toString(), + homeServerUrl = data.homeServerUrl, + isNumericOnlyUserIdForbidden = homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl, + loginMode = loginMode + ) } } } From 71174276869698d15d85692ad83e2102d372dda5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Apr 2021 10:37:40 +0200 Subject: [PATCH 012/202] Bugfix --- .../im/vector/app/core/extensions/Activity.kt | 6 + .../app/features/login2/LoginAction2.kt | 3 +- .../app/features/login2/LoginActivity2.kt | 8 ++ .../features/login2/LoginCaptchaFragment2.kt | 2 +- .../login2/LoginFragment2SigninPassword.kt | 2 +- .../login2/LoginFragment2SigninUsername.kt | 2 +- .../login2/LoginFragment2SignupPassword.kt | 2 +- .../login2/LoginFragment2SignupUsername.kt | 2 +- .../features/login2/LoginFragmentToAny2.kt | 2 +- .../LoginGenericTextInputFormFragment2.kt | 64 ++++++---- .../app/features/login2/LoginViewEvents2.kt | 2 + .../app/features/login2/LoginViewModel2.kt | 25 +++- .../login2/LoginWaitForEmailFragment2.kt | 2 +- .../app/features/login2/LoginWebFragment2.kt | 2 +- .../login2/terms/LoginTermsFragment2.kt | 2 +- ...agment_login_generic_text_input_form_2.xml | 118 ++++++++++++++++++ .../src/main/res/values/strings_login_v2.xml | 8 ++ 17 files changed, 214 insertions(+), 38 deletions(-) create mode 100644 vector/src/main/res/layout/fragment_login_generic_text_input_form_2.xml diff --git a/vector/src/main/java/im/vector/app/core/extensions/Activity.kt b/vector/src/main/java/im/vector/app/core/extensions/Activity.kt index 5b36e4e628..55ec8b605e 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Activity.kt @@ -94,6 +94,12 @@ fun AppCompatActivity.addFragmentToBackstack( } } +fun AppCompatActivity.resetBackstack() { + repeat(supportFragmentManager.backStackEntryCount) { + supportFragmentManager.popBackStack() + } +} + fun AppCompatActivity.hideKeyboard() { currentFocus?.hideKeyboard() } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt index 63c163fd6e..a9df217e3c 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginAction2.kt @@ -69,7 +69,8 @@ sealed class LoginAction2 : VectorViewModelAction { object ResetHomeServerUrl : ResetAction() object ResetSignMode : ResetAction() - object ResetLogin : ResetAction() + object ResetSignin : ResetAction() + object ResetSignup : ResetAction() object ResetResetPassword : ResetAction() // Homeserver history diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt index dd96c14fec..deb489a34f 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt @@ -36,6 +36,7 @@ import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.resetBackstack import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityLoginBinding @@ -227,6 +228,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment2SignupUsername::class.java, + tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } is LoginViewEvents2.OpenSignInWithAnythingScreen -> { @@ -247,9 +249,15 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC onLoginModeNotSupported(event.supportedTypes) is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event) is LoginViewEvents2.Finish -> terminate(true) + is LoginViewEvents2.CancelRegistration -> handleCancelRegistration() }.exhaustive } + private fun handleCancelRegistration() { + // Cleanup the back stack + resetBackstack() + } + private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) { if (event.newAccount) { // Propose to set avatar and display name diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt index 9c3ef6b94d..682fc4089e 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt @@ -182,7 +182,7 @@ class LoginCaptchaFragment2 @Inject constructor( } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + loginViewModel.handle(LoginAction2.ResetSignup) } override fun updateWithState(state: LoginViewState2) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt index 39701cb01b..76657db891 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt @@ -154,7 +154,7 @@ class LoginFragment2SigninPassword @Inject constructor( } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + loginViewModel.handle(LoginAction2.ResetSignin) } override fun onError(throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt index ee83a0409a..1624e641a8 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt @@ -91,7 +91,7 @@ class LoginFragment2SigninUsername @Inject constructor() : AbstractLoginFragment } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + loginViewModel.handle(LoginAction2.ResetSignin) } override fun onError(throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt index 917e97306c..372d5dad79 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt @@ -129,7 +129,7 @@ class LoginFragment2SignupPassword @Inject constructor() : AbstractSSOLoginFragm } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + // loginViewModel.handle(LoginAction2.ResetSignup) } override fun onError(throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt index 3667f2f1b9..ce9add677d 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt @@ -125,7 +125,7 @@ class LoginFragment2SignupUsername @Inject constructor() : AbstractSSOLoginFragm } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + // loginViewModel.handle(LoginAction2.ResetSignup) } override fun onError(throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index 0768c9cdb9..252ae0482a 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -182,7 +182,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2() { +class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFragment2() { private val params: LoginGenericTextInputFormFragmentArgument by args() - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginGenericTextInputFormBinding { - return FragmentLoginGenericTextInputFormBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginGenericTextInputForm2Binding { + return FragmentLoginGenericTextInputForm2Binding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -77,6 +78,7 @@ class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFr private fun setupViews() { views.loginGenericTextInputFormOtherButton.setOnClickListener { onOtherButtonClicked() } views.loginGenericTextInputFormSubmit.setOnClickListener { submit() } + views.loginGenericTextInputFormLater.setOnClickListener { submit() } } private fun setupAutoFill() { @@ -102,9 +104,11 @@ class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFr private fun setupUi() { when (params.mode) { TextInputFormFragmentMode.SetEmail -> { - views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) - views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) - views.loginGenericTextInputFormNotice2.setTextOrHide(null) + views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title_2) + views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice_2) + // Text will be updated with the state + views.loginGenericTextInputFormMandatoryNotice.isVisible = params.mandatory + views.loginGenericTextInputFormNotice2.isVisible = false views.loginGenericTextInputFormTil.hint = getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint) views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS @@ -112,8 +116,10 @@ class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFr views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) } TextInputFormFragmentMode.SetMsisdn -> { - views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) - views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) + views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title_2) + views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice_2) + // Text will be updated with the state + views.loginGenericTextInputFormMandatoryNotice.isVisible = params.mandatory views.loginGenericTextInputFormNotice2.setTextOrHide(getString(R.string.login_set_msisdn_notice2)) views.loginGenericTextInputFormTil.hint = getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint) @@ -124,7 +130,8 @@ class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFr TextInputFormFragmentMode.ConfirmMsisdn -> { views.loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title) views.loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra) - views.loginGenericTextInputFormNotice2.setTextOrHide(null) + views.loginGenericTextInputFormMandatoryNotice.isVisible = false + views.loginGenericTextInputFormNotice2.isVisible = false views.loginGenericTextInputFormTil.hint = getString(R.string.login_msisdn_confirm_hint) views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER @@ -195,26 +202,31 @@ class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFr private fun setupSubmitButton() { views.loginGenericTextInputFormSubmit.isEnabled = false views.loginGenericTextInputFormTextInput.textChanges() - .subscribe { - views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(it) + .subscribe { text -> + views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(text) + text?.let { updateSubmitButtons(it) } } .disposeOnDestroyView() } + private fun updateSubmitButtons(text: CharSequence) { + if (params.mandatory) { + views.loginGenericTextInputFormSubmit.isVisible = true + views.loginGenericTextInputFormLater.isVisible = false + } else { + views.loginGenericTextInputFormSubmit.isVisible = text.isNotEmpty() + views.loginGenericTextInputFormLater.isVisible = text.isEmpty() + } + } + private fun isInputValid(input: CharSequence): Boolean { return if (input.isEmpty() && !params.mandatory) { true } else { when (params.mode) { - TextInputFormFragmentMode.SetEmail -> { - input.isEmail() - } - TextInputFormFragmentMode.SetMsisdn -> { - input.isNotBlank() - } - TextInputFormFragmentMode.ConfirmMsisdn -> { - input.isNotBlank() - } + TextInputFormFragmentMode.SetEmail -> input.isEmail() + TextInputFormFragmentMode.SetMsisdn -> input.isNotBlank() + TextInputFormFragmentMode.ConfirmMsisdn -> input.isNotBlank() } } } @@ -253,6 +265,14 @@ class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFr } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + loginViewModel.handle(LoginAction2.ResetSignup) + } + + override fun updateWithState(state: LoginViewState2) { + views.loginGenericTextInputFormMandatoryNotice.text = when (params.mode) { + TextInputFormFragmentMode.SetEmail -> getString(R.string.login_set_email_mandatory_notice_2, state.homeServerUrlFromUser.toReducedUrl()) + TextInputFormFragmentMode.SetMsisdn -> getString(R.string.login_set_msisdn_mandatory_notice_2, state.homeServerUrlFromUser.toReducedUrl()) + TextInputFormFragmentMode.ConfirmMsisdn -> null + } } } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt index 91cb6cac58..fd4cc76450 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt @@ -48,6 +48,8 @@ sealed class LoginViewEvents2 : VectorViewEvents { object OnResetPasswordMailConfirmationSuccess : LoginViewEvents2() object OnResetPasswordMailConfirmationSuccessDone : LoginViewEvents2() + object CancelRegistration: LoginViewEvents2() + data class OnLoginModeNotSupported(val supportedTypes: List) : LoginViewEvents2() data class OnSendEmailSuccess(val email: String) : LoginViewEvents2() diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt index c412317b93..2e464043e4 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt @@ -44,7 +44,6 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig -import org.matrix.android.sdk.api.auth.data.LoginFlowResult import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.FlowResult @@ -379,11 +378,26 @@ class LoginViewModel2 @AssistedInject constructor( ) } } - LoginAction2.ResetLogin -> { + LoginAction2.ResetSignin -> { viewModelScope.launch { authenticationService.cancelPendingLoginOrRegistration() - setState { copy(isLoading = false) } + setState { + copy(isLoading = false) + } } + _viewEvents.post(LoginViewEvents2.CancelRegistration) + } + LoginAction2.ResetSignup -> { + viewModelScope.launch { + authenticationService.cancelPendingLoginOrRegistration() + setState { + // Always create a new state, to ensure the state is correctly reset + LoginViewState2( + knownCustomHomeServersUrls = knownCustomHomeServersUrls + ) + } + } + _viewEvents.post(LoginViewEvents2.CancelRegistration) } LoginAction2.ResetResetPassword -> { setState { @@ -397,8 +411,7 @@ class LoginViewModel2 @AssistedInject constructor( private fun handleUpdateSignMode(action: LoginAction2.UpdateSignMode) { setState { - // Always create a new state, to ensure the state is correctly reset - LoginViewState2( + copy( signMode = action.signMode ) } @@ -667,7 +680,7 @@ class LoginViewModel2 @AssistedInject constructor( if (safeLoginWizard != null) { setState { copy(loginProfileInfo = Loading()) } val result = tryAsync { - safeLoginWizard.getProfileInfo(username) + safeLoginWizard.getProfileInfo(username) } setState { copy(loginProfileInfo = result) } } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginWaitForEmailFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginWaitForEmailFragment2.kt index db3c607480..0cac52b306 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginWaitForEmailFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginWaitForEmailFragment2.kt @@ -70,6 +70,6 @@ class LoginWaitForEmailFragment2 @Inject constructor() : AbstractLoginFragment2< } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + loginViewModel.handle(LoginAction2.ResetSignup) } } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt index 2acb30bd8a..fb9a498b97 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt @@ -242,7 +242,7 @@ class LoginWebFragment2 @Inject constructor( } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + loginViewModel.handle(LoginAction2.ResetSignin) } override fun onBackPressed(toolbarButton: Boolean): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt index ac174f4a48..fcd3268983 100755 --- a/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt @@ -114,6 +114,6 @@ class LoginTermsFragment2 @Inject constructor( } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetLogin) + loginViewModel.handle(LoginAction2.ResetSignup) } } diff --git a/vector/src/main/res/layout/fragment_login_generic_text_input_form_2.xml b/vector/src/main/res/layout/fragment_login_generic_text_input_form_2.xml new file mode 100644 index 0000000000..1ae081fd88 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_generic_text_input_form_2.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_login_v2.xml b/vector/src/main/res/values/strings_login_v2.xml index 3062929050..23cc578ebd 100644 --- a/vector/src/main/res/values/strings_login_v2.xml +++ b/vector/src/main/res/values/strings_login_v2.xml @@ -35,4 +35,12 @@ Hello Matrix world! Click on the image and on your name to configure them. + Associate an email + Associate an email to be able to later recover your account, in case you forget your password. + The server %s requires you to associate an email to create an account. + + Associate a phone number + Associate a phone number to optionally allow people you know to discover you. + The server %s requires you to associate a phone number to create an account. + \ No newline at end of file From 44861816694842917db843f368ead667c45ec872 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Apr 2021 13:56:15 +0200 Subject: [PATCH 013/202] Sort social logins buttons --- .../sdk/api/auth/data/SsoIdentityProvider.kt | 23 ++++++++++++++++++- .../login2/LoginFragment2SignupUsername.kt | 2 +- .../features/login2/LoginFragmentToAny2.kt | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt index cfaf74ce24..64b3e180aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -48,7 +48,7 @@ data class SsoIdentityProvider( */ @Json(name = "brand") val brand: String? -) : Parcelable { +) : Parcelable, Comparable { companion object { const val BRAND_GOOGLE = "org.matrix.google" @@ -58,4 +58,25 @@ data class SsoIdentityProvider( const val BRAND_TWITTER = "org.matrix.twitter" const val BRAND_GITLAB = "org.matrix.gitlab" } + + override fun compareTo(other: SsoIdentityProvider): Int { + return other.toPriority().compareTo(toPriority()) + } + + private fun toPriority(): Int { + return when (brand) { + // We are on Android, so user is more likely to have a Google account + BRAND_GOOGLE -> 5 + // Facebook is also an important SSO provider + BRAND_FACEBOOK -> 4 + // Twitter is more for professionals + BRAND_TWITTER -> 3 + // Here it's very for techie people + BRAND_GITHUB, + BRAND_GITLAB -> 2 + // And finally, if the account has been created with an iPhone... + BRAND_APPLE -> 1 + else -> 0 + } + } } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt index ce9add677d..92bd041da9 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt @@ -94,7 +94,7 @@ class LoginFragment2SignupUsername @Inject constructor() : AbstractSSOLoginFragm if (state.loginMode is LoginMode.SsoAndPassword) { views.loginSocialLoginContainer.isVisible = true - views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders + views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted() views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { override fun onProviderSelected(id: String?) { loginViewModel.getSsoUrl( diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index 252ae0482a..479902a803 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -125,7 +125,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2 Date: Thu, 15 Apr 2021 16:19:24 +0200 Subject: [PATCH 014/202] Bugfix --- .../main/java/im/vector/app/features/login2/LoginActivity2.kt | 2 ++ .../vector/app/features/login2/LoginFragment2SigninPassword.kt | 2 +- .../vector/app/features/login2/LoginResetPasswordFragment2.kt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt index deb489a34f..b7e726b9e8 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt @@ -217,6 +217,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC is LoginViewEvents2.OpenPasswordScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment2SigninPassword::class.java, + tag = FRAGMENT_LOGIN_TAG, option = commonOption) } is LoginViewEvents2.OpenSignupPasswordScreen -> { @@ -234,6 +235,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC is LoginViewEvents2.OpenSignInWithAnythingScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragmentToAny2::class.java, + tag = FRAGMENT_LOGIN_TAG, option = commonOption) } is LoginViewEvents2.OnSendMsisdnSuccess -> diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt index 76657db891..6428a15691 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt @@ -154,7 +154,7 @@ class LoginFragment2SigninPassword @Inject constructor( } override fun resetViewModel() { - loginViewModel.handle(LoginAction2.ResetSignin) + // loginViewModel.handle(LoginAction2.ResetSignin) } override fun onError(throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt index 3a35e0dc91..2ced9b5d20 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt @@ -105,12 +105,12 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 } if (showWarning) { - showWarning = false // Display a warning as Riot-Web does first AlertDialog.Builder(requireActivity()) .setTitle(R.string.login_reset_password_warning_title) .setMessage(R.string.login_reset_password_warning_content) .setPositiveButton(R.string.login_reset_password_warning_submit) { _, _ -> + showWarning = false doSubmit() } .setNegativeButton(R.string.cancel, null) From 5d65a290d25282cea9c9e79f34096554437e2cde Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Apr 2021 16:22:25 +0200 Subject: [PATCH 015/202] Bugfix --- .../java/im/vector/app/features/login2/LoginActivity2.kt | 6 +++--- .../java/im/vector/app/features/login2/LoginViewEvents2.kt | 2 +- .../java/im/vector/app/features/login2/LoginViewModel2.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt index b7e726b9e8..43dfb5cec6 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt @@ -208,19 +208,19 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC // Go back to the login fragment supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } - is LoginViewEvents2.OnSendEmailSuccess -> + is LoginViewEvents2.OnSendEmailSuccess -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginWaitForEmailFragment2::class.java, LoginWaitForEmailFragmentArgument(event.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is LoginViewEvents2.OpenPasswordScreen -> { + is LoginViewEvents2.OpenSigninPasswordScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment2SigninPassword::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) } - is LoginViewEvents2.OpenSignupPasswordScreen -> { + is LoginViewEvents2.OpenSignupPasswordScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment2SignupPassword::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt index fd4cc76450..54cf507a7e 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewEvents2.kt @@ -30,7 +30,7 @@ sealed class LoginViewEvents2 : VectorViewEvents { object OutdatedHomeserver : LoginViewEvents2() // Navigation event - object OpenPasswordScreen : LoginViewEvents2() + object OpenSigninPasswordScreen : LoginViewEvents2() object OpenSignupPasswordScreen : LoginViewEvents2() object OpenSignInEnterIdentifierScreen : LoginViewEvents2() diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt index 2e464043e4..1968abc1f1 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt @@ -646,7 +646,7 @@ class LoginViewModel2 @AssistedInject constructor( is LoginMode.SsoAndPassword -> { retrieveProfileInfo(action.username) // We can navigate to the password screen - LoginViewEvents2.OpenPasswordScreen + LoginViewEvents2.OpenSigninPasswordScreen } is LoginMode.Sso -> { LoginViewEvents2.OpenSsoOnlyScreen From 6e9c16a8894e38509e39492e60546763f87a6f46 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Apr 2021 09:52:30 +0200 Subject: [PATCH 016/202] Naming convention --- .../im/vector/app/core/di/FragmentModule.kt | 30 +++++++++---------- .../app/features/login2/LoginActivity2.kt | 12 ++++---- ...ord.kt => LoginFragmentSigninPassword2.kt} | 11 ++++--- ...ame.kt => LoginFragmentSigninUsername2.kt} | 8 ++--- ...ord.kt => LoginFragmentSignupPassword2.kt} | 10 +++---- ...ame.kt => LoginFragmentSignupUsername2.kt} | 10 +++---- .../features/login2/LoginFragmentToAny2.kt | 8 ++--- .../LoginGenericTextInputFormFragment2.kt | 21 +++---------- ...inSplashSignUpSignInSelectionFragment2.kt} | 2 +- ...l => fragment_login_signin_password_2.xml} | 0 ...xml => fragment_login_signin_to_any_2.xml} | 0 ...l => fragment_login_signin_username_2.xml} | 0 ...l => fragment_login_signup_password_2.xml} | 0 ...l => fragment_login_signup_username_2.xml} | 0 14 files changed, 50 insertions(+), 62 deletions(-) rename vector/src/main/java/im/vector/app/features/login2/{LoginFragment2SigninPassword.kt => LoginFragmentSigninPassword2.kt} (94%) rename vector/src/main/java/im/vector/app/features/login2/{LoginFragment2SigninUsername.kt => LoginFragmentSigninUsername2.kt} (91%) rename vector/src/main/java/im/vector/app/features/login2/{LoginFragment2SignupPassword.kt => LoginFragmentSignupPassword2.kt} (92%) rename vector/src/main/java/im/vector/app/features/login2/{LoginFragment2SignupUsername.kt => LoginFragmentSignupUsername2.kt} (92%) rename vector/src/main/java/im/vector/app/features/login2/{LoginSignUpSignInSelectionFragment2.kt => LoginSplashSignUpSignInSelectionFragment2.kt} (97%) rename vector/src/main/res/layout/{fragment_login_2_signin_password.xml => fragment_login_signin_password_2.xml} (100%) rename vector/src/main/res/layout/{fragment_login_2_signin_to.xml => fragment_login_signin_to_any_2.xml} (100%) rename vector/src/main/res/layout/{fragment_login_2_signin_username.xml => fragment_login_signin_username_2.xml} (100%) rename vector/src/main/res/layout/{fragment_login_2_signup_password.xml => fragment_login_signup_password_2.xml} (100%) rename vector/src/main/res/layout/{fragment_login_2_signup_username.xml => fragment_login_signup_username_2.xml} (100%) diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index aef61f3bc0..75c0974908 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -72,10 +72,10 @@ import im.vector.app.features.login.LoginWaitForEmailFragment import im.vector.app.features.login.LoginWebFragment import im.vector.app.features.login.terms.LoginTermsFragment import im.vector.app.features.login2.LoginCaptchaFragment2 -import im.vector.app.features.login2.LoginFragment2SigninPassword -import im.vector.app.features.login2.LoginFragment2SigninUsername -import im.vector.app.features.login2.LoginFragment2SignupPassword -import im.vector.app.features.login2.LoginFragment2SignupUsername +import im.vector.app.features.login2.LoginFragmentSigninPassword2 +import im.vector.app.features.login2.LoginFragmentSigninUsername2 +import im.vector.app.features.login2.LoginFragmentSignupPassword2 +import im.vector.app.features.login2.LoginFragmentSignupUsername2 import im.vector.app.features.login2.created.AccountCreatedFragment import im.vector.app.features.login2.LoginFragmentToAny2 import im.vector.app.features.login2.LoginGenericTextInputFormFragment2 @@ -84,7 +84,7 @@ import im.vector.app.features.login2.LoginResetPasswordMailConfirmationFragment2 import im.vector.app.features.login2.LoginResetPasswordSuccessFragment2 import im.vector.app.features.login2.LoginServerSelectionFragment2 import im.vector.app.features.login2.LoginServerUrlFormFragment2 -import im.vector.app.features.login2.LoginSignUpSignInSelectionFragment2 +import im.vector.app.features.login2.LoginSplashSignUpSignInSelectionFragment2 import im.vector.app.features.login2.LoginSsoOnlyFragment2 import im.vector.app.features.login2.LoginWaitForEmailFragment2 import im.vector.app.features.login2.LoginWebFragment2 @@ -284,8 +284,8 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(LoginFragment2SigninUsername::class) - fun bindLoginFragment2SigninUsername(fragment: LoginFragment2SigninUsername): Fragment + @FragmentKey(LoginFragmentSigninUsername2::class) + fun bindLoginFragmentSigninUsername2(fragment: LoginFragmentSigninUsername2): Fragment @Binds @IntoMap @@ -294,18 +294,18 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(LoginFragment2SignupUsername::class) - fun bindLoginFragment2SignupUsername(fragment: LoginFragment2SignupUsername): Fragment + @FragmentKey(LoginFragmentSignupUsername2::class) + fun bindLoginFragmentSignupUsername2(fragment: LoginFragmentSignupUsername2): Fragment @Binds @IntoMap - @FragmentKey(LoginFragment2SigninPassword::class) - fun bindLoginFragment2SigninPassword(fragment: LoginFragment2SigninPassword): Fragment + @FragmentKey(LoginFragmentSigninPassword2::class) + fun bindLoginFragmentSigninPassword2(fragment: LoginFragmentSigninPassword2): Fragment @Binds @IntoMap - @FragmentKey(LoginFragment2SignupPassword::class) - fun bindLoginFragment2SignupPassword(fragment: LoginFragment2SignupPassword): Fragment + @FragmentKey(LoginFragmentSignupPassword2::class) + fun bindLoginFragmentSignupPassword2(fragment: LoginFragmentSignupPassword2): Fragment @Binds @IntoMap @@ -354,8 +354,8 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(LoginSignUpSignInSelectionFragment2::class) - fun bindLoginSignUpSignInSelectionFragment2(fragment: LoginSignUpSignInSelectionFragment2): Fragment + @FragmentKey(LoginSplashSignUpSignInSelectionFragment2::class) + fun bindLoginSplashSignUpSignInSelectionFragment2(fragment: LoginSplashSignUpSignInSelectionFragment2): Fragment @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt index 43dfb5cec6..f5663c1f5a 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt @@ -43,7 +43,9 @@ import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.features.home.HomeActivity import im.vector.app.features.login.LoginCaptchaFragmentArgument import im.vector.app.features.login.LoginConfig +import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument import im.vector.app.features.login.LoginWaitForEmailFragmentArgument +import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.login.isSupported import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.toLocalizedLoginTerms @@ -115,7 +117,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC } protected open fun addFirstFragment() { - addFragment(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment2::class.java) + addFragment(R.id.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java) } private fun handleLoginViewEvents(event: LoginViewEvents2) { @@ -171,7 +173,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC } is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, - LoginFragment2SigninUsername::class.java, + LoginFragmentSigninUsername2::class.java, option = { ft -> findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // Disable transition of text @@ -216,19 +218,19 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC option = commonOption) is LoginViewEvents2.OpenSigninPasswordScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, - LoginFragment2SigninPassword::class.java, + LoginFragmentSigninPassword2::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) } is LoginViewEvents2.OpenSignupPasswordScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, - LoginFragment2SignupPassword::class.java, + LoginFragmentSignupPassword2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, - LoginFragment2SignupUsername::class.java, + LoginFragmentSignupUsername2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninPassword2.kt similarity index 94% rename from vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt rename to vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninPassword2.kt index 6428a15691..a13905cc5f 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninPassword.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninPassword2.kt @@ -29,7 +29,7 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.showPassword -import im.vector.app.databinding.FragmentLogin2SigninPasswordBinding +import im.vector.app.databinding.FragmentLoginSigninPassword2Binding import im.vector.app.features.home.AvatarRenderer import io.reactivex.rxkotlin.subscribeBy import org.matrix.android.sdk.api.auth.login.LoginProfileInfo @@ -40,18 +40,17 @@ import javax.net.ssl.HttpsURLConnection /** * In this screen: - * In signin mode: * - the user is asked for password to sign in to a homeserver. * - He also can reset his password */ -class LoginFragment2SigninPassword @Inject constructor( +class LoginFragmentSigninPassword2 @Inject constructor( private val avatarRenderer: AvatarRenderer -) : AbstractSSOLoginFragment2() { +) : AbstractSSOLoginFragment2() { private var passwordShown = false - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SigninPasswordBinding { - return FragmentLogin2SigninPasswordBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninPassword2Binding { + return FragmentLoginSigninPassword2Binding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt similarity index 91% rename from vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt rename to vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt index 1624e641a8..10fe0aae3a 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SigninUsername.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt @@ -25,7 +25,7 @@ import androidx.autofill.HintConstants import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.databinding.FragmentLogin2SigninUsernameBinding +import im.vector.app.databinding.FragmentLoginSigninUsername2Binding import io.reactivex.rxkotlin.subscribeBy import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError @@ -35,10 +35,10 @@ import javax.inject.Inject * In this screen: * - the user is asked for its matrix ID, and have the possibility to open the screen to select a server */ -class LoginFragment2SigninUsername @Inject constructor() : AbstractLoginFragment2() { +class LoginFragmentSigninUsername2 @Inject constructor() : AbstractLoginFragment2() { - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SigninUsernameBinding { - return FragmentLogin2SigninUsernameBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSigninUsername2Binding { + return FragmentLoginSigninUsername2Binding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt similarity index 92% rename from vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt rename to vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt index 372d5dad79..49f4a4efc0 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupPassword.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt @@ -27,21 +27,21 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.showPassword -import im.vector.app.databinding.FragmentLogin2SignupPasswordBinding +import im.vector.app.databinding.FragmentLoginSignupPassword2Binding import io.reactivex.rxkotlin.Observables import io.reactivex.rxkotlin.subscribeBy import javax.inject.Inject /** * In this screen: - * - the user is asked for password to sign up to a homeserver. + * - the user is asked to choose a password to sign up to a homeserver. */ -class LoginFragment2SignupPassword @Inject constructor() : AbstractSSOLoginFragment2() { +class LoginFragmentSignupPassword2 @Inject constructor() : AbstractSSOLoginFragment2() { private var passwordsShown = false - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SignupPasswordBinding { - return FragmentLogin2SignupPasswordBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupPassword2Binding { + return FragmentLoginSignupPassword2Binding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt similarity index 92% rename from vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt rename to vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt index 92bd041da9..9726d8b163 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragment2SignupUsername.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt @@ -29,7 +29,7 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.toReducedUrl -import im.vector.app.databinding.FragmentLogin2SignupUsernameBinding +import im.vector.app.databinding.FragmentLoginSignupUsername2Binding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SocialLoginButtonsView import io.reactivex.rxkotlin.subscribeBy @@ -37,17 +37,17 @@ import javax.inject.Inject /** * In this screen: - * - the user is asked for login to sign up to a homeserver. + * - the user is asked for an identifier to sign up to a homeserver. * - SSO option are displayed if available */ -class LoginFragment2SignupUsername @Inject constructor() : AbstractSSOLoginFragment2() { +class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragment2() { // Temporary patch for https://github.com/vector-im/riotX-android/issues/1410, // waiting for https://github.com/matrix-org/synapse/issues/7576 private var isNumericOnlyUserIdForbidden = false - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLogin2SignupUsernameBinding { - return FragmentLogin2SignupUsernameBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupUsername2Binding { + return FragmentLoginSignupUsername2Binding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index 479902a803..ab5e3f15b8 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -30,7 +30,7 @@ import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.showPassword import im.vector.app.core.extensions.toReducedUrl -import im.vector.app.databinding.FragmentLogin2SigninToBinding +import im.vector.app.databinding.FragmentLoginSigninToAny2Binding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SocialLoginButtonsView import io.reactivex.Observable @@ -47,7 +47,7 @@ import javax.inject.Inject * - He also can reset his password * - It also possible to use SSO if server support it in this screen */ -class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2() { +class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2() { private var passwordShown = false @@ -55,8 +55,8 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2= Build.VERSION_CODES.O) { views.loginGenericTextInputFormTextInput.setAutofillHints( when (params.mode) { - TextInputFormFragmentMode.SetEmail -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS - TextInputFormFragmentMode.SetMsisdn -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER + TextInputFormFragmentMode.SetEmail -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS + TextInputFormFragmentMode.SetMsisdn -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER TextInputFormFragmentMode.ConfirmMsisdn -> HintConstants.AUTOFILL_HINT_SMS_OTP } ) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginSignUpSignInSelectionFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginSplashSignUpSignInSelectionFragment2.kt similarity index 97% rename from vector/src/main/java/im/vector/app/features/login2/LoginSignUpSignInSelectionFragment2.kt rename to vector/src/main/java/im/vector/app/features/login2/LoginSplashSignUpSignInSelectionFragment2.kt index a5e72bfc18..6cfebf776a 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginSignUpSignInSelectionFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginSplashSignUpSignInSelectionFragment2.kt @@ -31,7 +31,7 @@ import javax.inject.Inject * In this screen, the user is asked to sign up or to sign in to the homeserver * This is the new splash screen */ -class LoginSignUpSignInSelectionFragment2 @Inject constructor( +class LoginSplashSignUpSignInSelectionFragment2 @Inject constructor( private val vectorPreferences: VectorPreferences ) : AbstractLoginFragment2() { diff --git a/vector/src/main/res/layout/fragment_login_2_signin_password.xml b/vector/src/main/res/layout/fragment_login_signin_password_2.xml similarity index 100% rename from vector/src/main/res/layout/fragment_login_2_signin_password.xml rename to vector/src/main/res/layout/fragment_login_signin_password_2.xml diff --git a/vector/src/main/res/layout/fragment_login_2_signin_to.xml b/vector/src/main/res/layout/fragment_login_signin_to_any_2.xml similarity index 100% rename from vector/src/main/res/layout/fragment_login_2_signin_to.xml rename to vector/src/main/res/layout/fragment_login_signin_to_any_2.xml diff --git a/vector/src/main/res/layout/fragment_login_2_signin_username.xml b/vector/src/main/res/layout/fragment_login_signin_username_2.xml similarity index 100% rename from vector/src/main/res/layout/fragment_login_2_signin_username.xml rename to vector/src/main/res/layout/fragment_login_signin_username_2.xml diff --git a/vector/src/main/res/layout/fragment_login_2_signup_password.xml b/vector/src/main/res/layout/fragment_login_signup_password_2.xml similarity index 100% rename from vector/src/main/res/layout/fragment_login_2_signup_password.xml rename to vector/src/main/res/layout/fragment_login_signup_password_2.xml diff --git a/vector/src/main/res/layout/fragment_login_2_signup_username.xml b/vector/src/main/res/layout/fragment_login_signup_username_2.xml similarity index 100% rename from vector/src/main/res/layout/fragment_login_2_signup_username.xml rename to vector/src/main/res/layout/fragment_login_signup_username_2.xml From d235d1be154eac5f7141b5333a27578d0e0bb3ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Apr 2021 12:48:10 +0200 Subject: [PATCH 017/202] More improvements --- .../login2/LoginFragmentSignupPassword2.kt | 2 +- .../login2/LoginFragmentSignupUsername2.kt | 6 +- .../features/login2/LoginFragmentToAny2.kt | 4 ++ .../login2/LoginResetPasswordFragment2.kt | 26 ++++++- .../login2/LoginServerSelectionFragment2.kt | 21 ++++-- .../login2/terms/LoginTermsFragment2.kt | 8 +-- .../fragment_login_server_selection_2.xml | 8 +-- .../res/layout/fragment_login_terms_2.xml | 68 +++++++++++++++++++ .../src/main/res/values/strings_login_v2.xml | 3 + 9 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 vector/src/main/res/layout/fragment_login_terms_2.xml diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt index 49f4a4efc0..0da00ee624 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt @@ -36,7 +36,7 @@ import javax.inject.Inject * In this screen: * - the user is asked to choose a password to sign up to a homeserver. */ -class LoginFragmentSignupPassword2 @Inject constructor() : AbstractSSOLoginFragment2() { +class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment2() { private var passwordsShown = false diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt index 9726d8b163..9a4f0780c5 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt @@ -55,6 +55,7 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm setupSubmitButton() setupAutoFill() + setupSocialLoginButtons() } private fun setupAutoFill() { @@ -63,6 +64,10 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm } } + private fun setupSocialLoginButtons() { + views.loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_UP + } + private fun submit() { cleanupUi() @@ -111,7 +116,6 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm } } - @SuppressLint("SetTextI18n") private fun setupSubmitButton() { views.loginSubmit.setOnClickListener { submit() } views.loginField.textChanges() diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index ab5e3f15b8..0f62b96d0f 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -66,6 +66,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2 if (actionId == EditorInfo.IME_ACTION_DONE) { @@ -85,6 +86,9 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2 + if (actionId == EditorInfo.IME_ACTION_DONE) { + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } } private fun setupAutoFill() { @@ -81,8 +93,6 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 } ) .subscribeBy { - views.resetPasswordEmailTil.error = null - views.passwordFieldTil.error = null views.resetPasswordSubmit.isEnabled = it } .disposeOnDestroyView() @@ -92,10 +102,20 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 cleanupUi() var error = 0 + + val email = views.resetPasswordEmail.text.toString() val password = views.passwordField.text.toString() val passwordRepeat = views.passwordFieldRepeat.text.toString() - if (password != passwordRepeat) { + if (email.isEmpty()) { + views.resetPasswordEmailTil.error = getString(R.string.auth_reset_password_missing_email) + error++ + } + + if (password.isEmpty()) { + views.passwordFieldTil.error = getString(R.string.login_please_choose_a_new_password) + error++ + } else if (password != passwordRepeat) { views.passwordFieldTilRepeat.error = getString(R.string.auth_password_dont_match) error++ } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt index b7a5d64cd4..3343abbdde 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt @@ -16,11 +16,11 @@ package im.vector.app.features.login2 -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import im.vector.app.R import im.vector.app.databinding.FragmentLoginServerSelection2Binding import javax.inject.Inject @@ -44,27 +44,34 @@ class LoginServerSelectionFragment2 @Inject constructor() : AbstractLoginFragmen views.loginServerChoiceOther.setOnClickListener { selectOther() } } - @SuppressLint("SetTextI18n") private fun updateUi(state: LoginViewState2) { when (state.signMode) { - SignMode2.Unknown -> Unit - SignMode2.SignUp -> { - views.loginServerTitle.text = "Please choose a server" + SignMode2.Unknown -> Unit + SignMode2.SignUp -> { + views.loginServerTitle.setText(R.string.login_please_choose_a_server) } - SignMode2.SignIn -> { - views.loginServerTitle.text = "Please choose your server" + SignMode2.SignIn -> { + views.loginServerTitle.setText(R.string.login_please_select_your_server) } } } private fun selectMatrixOrg() { + views.loginServerChoiceMatrixOrg.isChecked = true loginViewModel.handle(LoginAction2.ChooseDefaultHomeServer) } private fun selectOther() { + views.loginServerChoiceOther.isChecked = true loginViewModel.handle(LoginAction2.EnterServerUrl) } + override fun onResume() { + super.onResume() + views.loginServerChoiceMatrixOrg.isChecked = false + views.loginServerChoiceOther.isChecked = false + } + override fun resetViewModel() { loginViewModel.handle(LoginAction2.ResetHomeServerUrl) } diff --git a/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt index fcd3268983..0be696e1c8 100755 --- a/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/terms/LoginTermsFragment2.kt @@ -25,7 +25,7 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.utils.openUrlInChromeCustomTab -import im.vector.app.databinding.FragmentLoginTermsBinding +import im.vector.app.databinding.FragmentLoginTerms2Binding import im.vector.app.features.login.terms.LocalizedFlowDataLoginTermsChecked import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.LoginTermsViewState @@ -41,13 +41,13 @@ import javax.inject.Inject */ class LoginTermsFragment2 @Inject constructor( private val policyController: PolicyController -) : AbstractLoginFragment2(), +) : AbstractLoginFragment2(), PolicyController.PolicyControllerListener { private val params: LoginTermsFragmentArgument by args() - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginTermsBinding { - return FragmentLoginTermsBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginTerms2Binding { + return FragmentLoginTerms2Binding.inflate(inflater, container, false) } private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList()) diff --git a/vector/src/main/res/layout/fragment_login_server_selection_2.xml b/vector/src/main/res/layout/fragment_login_server_selection_2.xml index 245459ef9d..1a7d5a14c5 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection_2.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection_2.xml @@ -19,9 +19,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/layout_vertical_margin" + android:text="@string/login_please_choose_a_server" android:textAppearance="@style/TextAppearance.Vector.Login.Title" - tools:ignore="UnknownId" - tools:text="Please choose a server" /> + tools:ignore="UnknownId" /> - - + diff --git a/vector/src/main/res/layout/fragment_login_terms_2.xml b/vector/src/main/res/layout/fragment_login_terms_2.xml new file mode 100644 index 0000000000..d6ccfbba87 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_terms_2.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_login_v2.xml b/vector/src/main/res/values/strings_login_v2.xml index 23cc578ebd..aa4d2e1192 100644 --- a/vector/src/main/res/values/strings_login_v2.xml +++ b/vector/src/main/res/values/strings_login_v2.xml @@ -10,7 +10,10 @@ Matrix identifiers start with @, for instance @alice:server.org If you do not know your Matrix identifier, or if your account has been created using Single Sign On (for instance using a Google account), or if you want to connect using your simple name, or an email associated to your account, you have to select your server first. Choose a server + Please choose a server + Please select your server Please choose a password + Please choose a new password Your Matrix identifier Press back to change Choose a password From ef55ddd6837751e48982cc8b55e505432464118e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 19 Apr 2021 13:39:32 +0200 Subject: [PATCH 018/202] Do not repeat password fields for account creation or reset password --- .../login2/LoginFragmentSignupPassword2.kt | 36 +++----- .../login2/LoginResetPasswordFragment2.kt | 25 ++---- .../fragment_login_reset_password_2.xml | 81 +++++++----------- .../fragment_login_signup_password_2.xml | 83 +++++++------------ .../src/main/res/values/strings_login_v2.xml | 1 - 5 files changed, 82 insertions(+), 144 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt index 0da00ee624..c67579e98d 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt @@ -28,7 +28,6 @@ import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.showPassword import im.vector.app.databinding.FragmentLoginSignupPassword2Binding -import io.reactivex.rxkotlin.Observables import io.reactivex.rxkotlin.subscribeBy import javax.inject.Inject @@ -38,7 +37,7 @@ import javax.inject.Inject */ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment2() { - private var passwordsShown = false + private var passwordShown = false override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupPassword2Binding { return FragmentLoginSignupPassword2Binding.inflate(inflater, container, false) @@ -51,7 +50,7 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment setupAutoFill() setupPasswordReveal() - views.passwordFieldRepeat.setOnEditorActionListener { _, actionId, _ -> + views.passwordField.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { submit() return@setOnEditorActionListener true @@ -63,7 +62,6 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment private fun setupAutoFill() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) - views.passwordFieldRepeat.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) } } @@ -79,13 +77,6 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment error++ } - val passwordRepeat = views.passwordFieldRepeat.text.toString() - - if (password != passwordRepeat) { - views.passwordFieldTilRepeat.error = getString(R.string.auth_password_dont_match) - error++ - } - if (error == 0) { loginViewModel.handle(LoginAction2.SetUserPassword(password)) } @@ -98,23 +89,19 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment private fun setupSubmitButton() { views.loginSubmit.setOnClickListener { submit() } - Observables.combineLatest( - views.passwordField.textChanges(), - views.passwordFieldRepeat.textChanges() - ) - .subscribeBy { (password, passwordRepeat) -> + views.passwordField.textChanges() + .subscribeBy { password -> views.passwordFieldTil.error = null - views.passwordFieldTilRepeat.error = null - views.loginSubmit.isEnabled = password.isNotEmpty() && passwordRepeat.isNotEmpty() + views.loginSubmit.isEnabled = password.isNotEmpty() } .disposeOnDestroyView() } private fun setupPasswordReveal() { - passwordsShown = false + passwordShown = false views.passwordReveal.setOnClickListener { - passwordsShown = !passwordsShown + passwordShown = !passwordShown renderPasswordField() } @@ -123,9 +110,8 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment } private fun renderPasswordField() { - views.passwordReveal.render(passwordsShown) - views.passwordField.showPassword(passwordsShown) - views.passwordFieldRepeat.showPassword(passwordsShown) + views.passwordReveal.render(passwordShown) + views.passwordField.showPassword(passwordShown) } override fun resetViewModel() { @@ -140,8 +126,8 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment views.loginMatrixIdentifier.text = state.userIdentifier() if (state.isLoading) { - // Ensure passwords are hidden - passwordsShown = false + // Ensure password is hidden + passwordShown = false renderPasswordField() } } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt index 62cfd416bb..c37c5ee44f 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt @@ -42,7 +42,7 @@ import javax.inject.Inject */ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2() { - private var passwordsShown = false + private var passwordShown = false // Show warning only once private var showWarning = true @@ -58,9 +58,9 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 setupPasswordReveal() setupAutoFill() - autoResetTextInputLayoutErrors(listOf(views.resetPasswordEmailTil, views.passwordFieldTil, views.passwordFieldTilRepeat)) + autoResetTextInputLayoutErrors(listOf(views.resetPasswordEmailTil, views.passwordFieldTil)) - views.passwordFieldRepeat.setOnEditorActionListener { _, actionId, _ -> + views.passwordField.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { submit() return@setOnEditorActionListener true @@ -73,7 +73,6 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { views.resetPasswordEmail.setAutofillHints(HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS) views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) - views.passwordFieldRepeat.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) } } @@ -105,7 +104,6 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 val email = views.resetPasswordEmail.text.toString() val password = views.passwordField.text.toString() - val passwordRepeat = views.passwordFieldRepeat.text.toString() if (email.isEmpty()) { views.resetPasswordEmailTil.error = getString(R.string.auth_reset_password_missing_email) @@ -115,9 +113,6 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 if (password.isEmpty()) { views.passwordFieldTil.error = getString(R.string.login_please_choose_a_new_password) error++ - } else if (password != passwordRepeat) { - views.passwordFieldTilRepeat.error = getString(R.string.auth_password_dont_match) - error++ } if (error > 0) { @@ -151,14 +146,13 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 views.resetPasswordSubmit.hideKeyboard() views.resetPasswordEmailTil.error = null views.passwordFieldTil.error = null - views.passwordFieldTilRepeat.error = null } private fun setupPasswordReveal() { - passwordsShown = false + passwordShown = false views.passwordReveal.setOnClickListener { - passwordsShown = !passwordsShown + passwordShown = !passwordShown renderPasswordField() } @@ -167,9 +161,8 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 } private fun renderPasswordField() { - views.passwordField.showPassword(passwordsShown) - views.passwordFieldRepeat.showPassword(passwordsShown) - views.passwordReveal.render(passwordsShown) + views.passwordField.showPassword(passwordShown) + views.passwordReveal.render(passwordShown) } override fun resetViewModel() { @@ -184,8 +177,8 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 setupUi(state) if (state.isLoading) { - // Ensure new passwords are hidden - passwordsShown = false + // Ensure new password is hidden + passwordShown = false renderPasswordField() } } diff --git a/vector/src/main/res/layout/fragment_login_reset_password_2.xml b/vector/src/main/res/layout/fragment_login_reset_password_2.xml index 61f3e52279..a103f9a5b7 100644 --- a/vector/src/main/res/layout/fragment_login_reset_password_2.xml +++ b/vector/src/main/res/layout/fragment_login_reset_password_2.xml @@ -57,73 +57,54 @@ android:text="@string/login_reset_password_notice" android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" /> + + + android:layout_marginTop="8dp"> - + android:hint="@string/login_reset_password_password_hint" + app:errorEnabled="true" + app:errorIconDrawable="@null"> + + + + - - - - - - - - - - - - + + + android:layout_marginTop="8dp"> - + android:hint="@string/login_signup_password_hint" + app:errorEnabled="true" + app:errorIconDrawable="@null"> + + + + - - - - - - - - - - - - - - Type it again Welcome back %s! Please enter your password Please enter your Matrix identifier From 63a59dbc0c61c8154c6e3d3ab0c1e8c5c259ec1c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 19 Apr 2021 14:11:24 +0200 Subject: [PATCH 019/202] Defensive code against invalid userId --- .../login2/created/AccountCreatedViewModel.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt index d1684a9867..1acec968b6 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt @@ -25,10 +25,13 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap +import timber.log.Timber class AccountCreatedViewModel @AssistedInject constructor( @Assisted initialState: AccountCreatedViewState, @@ -62,7 +65,14 @@ class AccountCreatedViewModel @AssistedInject constructor( session.rx() .liveUser(session.myUserId) .unwrap() - .map { it.toMatrixItem() } + .map { + if (MatrixPatterns.isUserId(it.userId)) { + it.toMatrixItem() + } else { + Timber.w("liveUser() has returned an invalid user: $it") + MatrixItem.UserItem(session.myUserId, null, null) + } + } .execute { copy(currentUser = it) } From 258a378f39f604f243fdd04acb34130fcd79089c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 19 Apr 2021 15:51:56 +0200 Subject: [PATCH 020/202] Add back link to learn more on EMS --- .../java/im/vector/app/core/extensions/TextView.kt | 2 +- .../login2/LoginServerSelectionFragment2.kt | 12 ++++++++++++ .../layout/fragment_login_server_selection_2.xml | 14 +++++++++++++- vector/src/main/res/values/strings_login_v2.xml | 2 ++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index 574e25a5ee..6b3902deea 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -62,7 +62,7 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int, @StringRes coloredTextRes: Int, @AttrRes colorAttribute: Int = R.attr.colorAccent, underline: Boolean = false, - onClick: (() -> Unit)?) { + onClick: (() -> Unit)? = null) { val coloredPart = resources.getString(coloredTextRes) // Insert colored part into the full text val fullText = resources.getString(fullTextRes, coloredPart) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt index 3343abbdde..60e381b047 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginServerSelectionFragment2.kt @@ -21,7 +21,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import im.vector.app.R +import im.vector.app.core.extensions.setTextWithColoredPart +import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.databinding.FragmentLoginServerSelection2Binding +import im.vector.app.features.login.EMS_LINK import javax.inject.Inject /** @@ -42,6 +45,15 @@ class LoginServerSelectionFragment2 @Inject constructor() : AbstractLoginFragmen private fun initViews() { views.loginServerChoiceMatrixOrg.setOnClickListener { selectMatrixOrg() } views.loginServerChoiceOther.setOnClickListener { selectOther() } + + views.loginServerChoiceEmsLearnMore.setTextWithColoredPart( + fullTextRes = R.string.login_server_modular_learn_more_about_ems, + coloredTextRes = R.string.login_server_modular_learn_more, + underline = true + ) + views.loginServerChoiceEmsLearnMore.setOnClickListener { + openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK) + } } private fun updateUi(state: LoginViewState2) { diff --git a/vector/src/main/res/layout/fragment_login_server_selection_2.xml b/vector/src/main/res/layout/fragment_login_server_selection_2.xml index 1a7d5a14c5..9200a76f04 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection_2.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection_2.xml @@ -86,7 +86,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="32dp" - android:layout_marginBottom="32dp" android:background="@drawable/bg_login_server_selector" android:contentDescription="@string/login_a11y_choose_other" android:minHeight="80dp" @@ -122,6 +121,19 @@ + + diff --git a/vector/src/main/res/values/strings_login_v2.xml b/vector/src/main/res/values/strings_login_v2.xml index 91e4d8c38b..5d1e14d73e 100644 --- a/vector/src/main/res/values/strings_login_v2.xml +++ b/vector/src/main/res/values/strings_login_v2.xml @@ -44,5 +44,7 @@ Associate a phone number Associate a phone number to optionally allow people you know to discover you. The server %s requires you to associate a phone number to create an account. + + %s about Element Matrix Service. \ No newline at end of file From c09f7e0d7d722b6f42ab2ef05ae1d456094c1021 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 29 Apr 2021 16:10:52 +0200 Subject: [PATCH 021/202] Fix compilation issue after rebase --- .../im/vector/app/features/login2/AbstractLoginFragment2.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt index 266e69e5b1..39bee00ac2 100644 --- a/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt @@ -29,6 +29,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment +import kotlinx.coroutines.CancellationException import org.matrix.android.sdk.api.failure.Failure /** @@ -74,7 +75,7 @@ abstract class AbstractLoginFragment2 : VectorBaseFragment } when (throwable) { - is Failure.Cancelled -> + is CancellationException -> /* Ignore this error, user has cancelled the action */ Unit is Failure.UnrecognizedCertificateFailure -> From b3ac1a1e8ba10adb009fccddfeb70f1557f93375 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 29 Apr 2021 17:02:14 +0200 Subject: [PATCH 022/202] Using /register/available also fixes #1410 (See https://github.com/matrix-org/synapse/pull/7625) --- CHANGES.md | 2 +- .../features/login2/LoginFragmentSignupUsername2.kt | 10 ---------- .../vector/app/features/login2/LoginFragmentToAny2.kt | 10 ---------- .../im/vector/app/features/login2/LoginViewModel2.kt | 3 --- .../im/vector/app/features/login2/LoginViewState2.kt | 3 --- 5 files changed, 1 insertion(+), 27 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ee0d442c79..323eb53485 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Features ✨: Improvements 🙌: - Add ability to install APK from directly from Element (#2381) - - Improve login/register flow (#2585, #3172) + - Improve login/register flow (#1410, #2585, #3172) Bugfix 🐛: - Message states cosmetic changes (#3007) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt index 9a4f0780c5..00b06ed82d 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt @@ -42,10 +42,6 @@ import javax.inject.Inject */ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragment2() { - // Temporary patch for https://github.com/vector-im/riotX-android/issues/1410, - // waiting for https://github.com/matrix-org/synapse/issues/7576 - private var isNumericOnlyUserIdForbidden = false - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupUsername2Binding { return FragmentLoginSignupUsername2Binding.inflate(inflater, container, false) } @@ -79,10 +75,6 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm views.loginFieldTil.error = getString(R.string.error_empty_field_choose_user_name) error++ } - if (isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { - views.loginFieldTil.error = "The homeserver does not accept username with only digits." - error++ - } if (error == 0) { loginViewModel.handle(LoginAction2.SetUserName(login)) @@ -138,8 +130,6 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm @SuppressLint("SetTextI18n") override fun updateWithState(state: LoginViewState2) { - isNumericOnlyUserIdForbidden = state.isNumericOnlyUserIdForbidden - setupUi(state) } } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index 0f62b96d0f..d865e16e35 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -51,10 +51,6 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2 = Uninitialized, - // True on Matrix.org - val isNumericOnlyUserIdForbidden: Boolean = false, - // Network result @PersistState val loginMode: LoginMode = LoginMode.Unknown, From 5a538381948633d03c6151b531a77587dd61601f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Apr 2021 07:06:20 +0000 Subject: [PATCH 023/202] Bump PhotoView from 2.1.4 to 2.3.0 Bumps [PhotoView](https://github.com/Baseflow/PhotoView) from 2.1.4 to 2.3.0. - [Release notes](https://github.com/Baseflow/PhotoView/releases) - [Commits](https://github.com/Baseflow/PhotoView/compare/2.1.4...2.3.0) Signed-off-by: dependabot[bot] --- attachment-viewer/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 8db57a59af..cc41f5cf34 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -61,7 +61,7 @@ android { } dependencies { - implementation 'com.github.chrisbanes:PhotoView:2.1.4' + implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' diff --git a/vector/build.gradle b/vector/build.gradle index 3ecdcea524..d39ffc0ad1 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -406,7 +406,7 @@ dependencies { implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version" // implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' - implementation 'com.github.chrisbanes:PhotoView:2.1.4' + implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" From 257d5cd55ae35b8d3f95de90bb76a0e0c8271a05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Apr 2021 07:07:27 +0000 Subject: [PATCH 024/202] Bump sonarqube-gradle-plugin from 3.1.1 to 3.2.0 Bumps sonarqube-gradle-plugin from 3.1.1 to 3.2.0. Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 25734e3b09..d30d5c2546 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.google.gms:google-services:4.3.5' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1' + classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.2.0' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' classpath "com.likethesalad.android:string-reference:1.2.2" From 314ace9208bd0c74c6409a21e2130321fdc48f2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 May 2021 07:12:55 +0000 Subject: [PATCH 025/202] Bump konfetti from 1.2.6 to 1.3.2 Bumps [konfetti](https://github.com/DanielMartinus/Konfetti) from 1.2.6 to 1.3.2. - [Release notes](https://github.com/DanielMartinus/Konfetti/releases) - [Commits](https://github.com/DanielMartinus/Konfetti/compare/v1.2.6...v1.3.2) Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 3ecdcea524..4c3728d2a3 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -417,7 +417,7 @@ dependencies { implementation 'me.leolin:ShortcutBadger:1.1.22@aar' // Chat effects - implementation 'nl.dionsegijn:konfetti:1.2.6' + implementation 'nl.dionsegijn:konfetti:1.3.2' implementation 'com.github.jetradarmobile:android-snowfall:1.2.0' // DI implementation "com.google.dagger:dagger:$daggerVersion" From 711dfe41b7c50a58fa17a797bb2ce5a8d6f44732 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 May 2021 07:11:17 +0000 Subject: [PATCH 026/202] Bump media from 1.3.0 to 1.3.1 Bumps media from 1.3.0 to 1.3.1. Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 3ecdcea524..5a7a8861b6 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -326,7 +326,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.1.0" implementation 'androidx.core:core-ktx:1.3.2' - implementation "androidx.media:media:1.3.0" + implementation "androidx.media:media:1.3.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.9.0" From 5631a4714e3fa9b8a605d6bb420e3deab7cae0ce Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 10 May 2021 20:27:23 +0200 Subject: [PATCH 027/202] Use built in sample --- vector/src/main/res/layout/item_generic_list.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/item_generic_list.xml b/vector/src/main/res/layout/item_generic_list.xml index 0dd0313fe4..7e5a55fd24 100644 --- a/vector/src/main/res/layout/item_generic_list.xml +++ b/vector/src/main/res/layout/item_generic_list.xml @@ -58,7 +58,8 @@ app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/item_generic_title_text" - tools:text="At totam delectus et aliquid dolorem. Consectetur voluptas tempore et non blanditiis id optio. Dolorum impedit quidem minus nihil. " + tools:maxLines="3" + tools:text="@tools:sample/lorem/random" tools:visibility="visible" /> Date: Mon, 10 May 2021 22:34:13 +0200 Subject: [PATCH 028/202] Use round room and user avatars and rounded corner space avatar --- .../room_round_avatars/0_element_rainbow.png | Bin 0 -> 6304 bytes .../room_round_avatars/element_black.png | Bin 0 -> 1761 bytes .../room_round_avatars/element_ems.png | Bin 0 -> 2343 bytes .../room_round_avatars/element_multi.png | Bin 0 -> 3196 bytes .../room_round_avatars/element_sky.png | Bin 0 -> 2187 bytes .../room_round_avatars/element_verde.png | Bin 0 -> 4644 bytes .../room_round_avatars/element_web.png | Bin 0 -> 4673 bytes .../sampledata/room_round_avatars/element_x.png | Bin 0 -> 4357 bytes vector/sampledata/room_round_avatars/matrix.png | Bin 0 -> 1355 bytes .../room_round_avatars/new_vector.png | Bin 0 -> 3156 bytes vector/sampledata/room_round_avatars/ops.png | Bin 0 -> 9655 bytes .../room_round_avatars/write_club.png | Bin 0 -> 10326 bytes vector/sampledata/space_avatars/car.png | Bin 0 -> 2611 bytes vector/sampledata/space_avatars/face.png | Bin 0 -> 2747 bytes vector/sampledata/space_avatars/london.png | Bin 0 -> 3121 bytes vector/sampledata/space_avatars/paris.png | Bin 0 -> 2707 bytes vector/sampledata/space_avatars/runner.png | Bin 0 -> 2520 bytes vector/sampledata/space_avatars/snow.png | Bin 0 -> 2609 bytes .../sampledata/user_round_avatars/amandine.png | Bin 0 -> 5311 bytes vector/sampledata/user_round_avatars/ben.png | Bin 0 -> 9208 bytes vector/sampledata/user_round_avatars/benoit.png | Bin 0 -> 8277 bytes vector/sampledata/user_round_avatars/bruno.png | Bin 0 -> 7854 bytes vector/sampledata/user_round_avatars/gaelle.png | Bin 0 -> 8256 bytes vector/sampledata/user_round_avatars/manu.png | Bin 0 -> 9359 bytes .../sampledata/user_round_avatars/matthew.png | Bin 0 -> 9602 bytes vector/sampledata/user_round_avatars/nad.png | Bin 0 -> 6507 bytes vector/sampledata/user_round_avatars/nique.png | Bin 0 -> 9158 bytes vector/sampledata/user_round_avatars/toml.png | Bin 0 -> 6539 bytes vector/sampledata/user_round_avatars/victor.png | Bin 0 -> 8657 bytes vector/src/main/res/layout/activity_call.xml | 2 +- .../res/layout/alerter_incoming_call_layout.xml | 2 +- .../res/layout/alerter_verification_layout.xml | 2 +- .../layout/bottom_sheet_invited_to_space.xml | 2 +- .../bottom_sheet_room_widget_permission.xml | 2 +- .../res/layout/bottom_sheet_space_settings.xml | 2 +- .../res/layout/bottom_sheet_verification.xml | 2 +- vector/src/main/res/layout/composer_layout.xml | 3 +-- .../composer_layout_constraint_set_compact.xml | 3 +-- .../composer_layout_constraint_set_expanded.xml | 2 +- .../main/res/layout/fragment_home_drawer.xml | 2 +- .../main/res/layout/fragment_matrix_profile.xml | 2 +- .../fragment_matrix_to_room_space_card.xml | 12 ++++++------ .../res/layout/fragment_matrix_to_user_card.xml | 2 +- .../main/res/layout/fragment_room_detail.xml | 2 +- .../layout/fragment_room_preview_no_preview.xml | 4 ++-- .../layout/fragment_room_setting_generic.xml | 2 +- .../main/res/layout/fragment_room_uploads.xml | 2 +- .../main/res/layout/fragment_space_preview.xml | 2 +- .../main/res/layout/fragment_user_code_show.xml | 2 +- .../layout/item_autocomplete_matrix_item.xml | 2 +- .../item_bottom_sheet_message_preview.xml | 2 +- .../layout/item_bottom_sheet_room_preview.xml | 2 +- vector/src/main/res/layout/item_breadcrumbs.xml | 2 +- .../src/main/res/layout/item_contact_main.xml | 2 +- .../res/layout/item_create_direct_room_user.xml | 2 +- .../res/layout/item_display_read_receipt.xml | 2 +- .../main/res/layout/item_editable_avatar.xml | 2 +- vector/src/main/res/layout/item_group.xml | 2 +- vector/src/main/res/layout/item_known_user.xml | 2 +- .../res/layout/item_profile_matrix_item.xml | 2 +- .../item_profile_matrix_item_progress.xml | 2 +- vector/src/main/res/layout/item_public_room.xml | 2 +- vector/src/main/res/layout/item_room.xml | 2 +- .../main/res/layout/item_room_invitation.xml | 2 +- .../res/layout/item_room_to_add_in_space.xml | 2 +- .../src/main/res/layout/item_search_result.xml | 2 +- vector/src/main/res/layout/item_space.xml | 2 +- .../main/res/layout/item_space_roomchild.xml | 2 +- .../src/main/res/layout/item_space_subspace.xml | 2 +- .../src/main/res/layout/item_suggested_room.xml | 2 +- .../res/layout/item_timeline_event_base.xml | 2 +- .../item_timeline_event_call_tile_stub.xml | 2 +- .../src/main/res/layout/item_unknown_room.xml | 2 +- vector/src/main/res/layout/item_user.xml | 2 +- .../src/main/res/layout/vector_invite_view.xml | 2 +- .../layout/vector_message_merge_avatar_list.xml | 10 +++++----- .../res/layout/vector_settings_round_avatar.xml | 2 +- vector/src/main/res/layout/view_read_marker.xml | 10 +++++----- .../src/main/res/layout/view_read_receipts.xml | 10 +++++----- .../view_stub_room_member_profile_header.xml | 2 +- .../layout/view_stub_room_profile_header.xml | 2 +- 81 files changed, 70 insertions(+), 72 deletions(-) create mode 100644 vector/sampledata/room_round_avatars/0_element_rainbow.png create mode 100644 vector/sampledata/room_round_avatars/element_black.png create mode 100644 vector/sampledata/room_round_avatars/element_ems.png create mode 100644 vector/sampledata/room_round_avatars/element_multi.png create mode 100644 vector/sampledata/room_round_avatars/element_sky.png create mode 100644 vector/sampledata/room_round_avatars/element_verde.png create mode 100644 vector/sampledata/room_round_avatars/element_web.png create mode 100644 vector/sampledata/room_round_avatars/element_x.png create mode 100644 vector/sampledata/room_round_avatars/matrix.png create mode 100644 vector/sampledata/room_round_avatars/new_vector.png create mode 100644 vector/sampledata/room_round_avatars/ops.png create mode 100644 vector/sampledata/room_round_avatars/write_club.png create mode 100644 vector/sampledata/space_avatars/car.png create mode 100644 vector/sampledata/space_avatars/face.png create mode 100644 vector/sampledata/space_avatars/london.png create mode 100644 vector/sampledata/space_avatars/paris.png create mode 100644 vector/sampledata/space_avatars/runner.png create mode 100644 vector/sampledata/space_avatars/snow.png create mode 100644 vector/sampledata/user_round_avatars/amandine.png create mode 100644 vector/sampledata/user_round_avatars/ben.png create mode 100644 vector/sampledata/user_round_avatars/benoit.png create mode 100644 vector/sampledata/user_round_avatars/bruno.png create mode 100644 vector/sampledata/user_round_avatars/gaelle.png create mode 100644 vector/sampledata/user_round_avatars/manu.png create mode 100644 vector/sampledata/user_round_avatars/matthew.png create mode 100644 vector/sampledata/user_round_avatars/nad.png create mode 100644 vector/sampledata/user_round_avatars/nique.png create mode 100644 vector/sampledata/user_round_avatars/toml.png create mode 100644 vector/sampledata/user_round_avatars/victor.png diff --git a/vector/sampledata/room_round_avatars/0_element_rainbow.png b/vector/sampledata/room_round_avatars/0_element_rainbow.png new file mode 100644 index 0000000000000000000000000000000000000000..2efdc02312e47f9fb32a4ebc055f8597c753b3a4 GIT binary patch literal 6304 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 WV(Ug3*v-=b0000QmX6shP=7e02rRxT9&u2fJ) zMNn{|;uBx@s-P|e#Ro2YeE;(suXgXu+oPogW(rk2>kA*I3a<&1C)Frm?Ef>S>b5t4zO9Ug|-Z{=K-ZN1T{gHOvp^5JqjrP zTX3G_D{iDuFrS?FC{GYi#jOnSn|2Xupc1^xL>SpFYv&&!6egp+ht`H%BuwGt}1BMw+HkPfrhdh-Sf5ClCB4$t`i?#tn{y z!NEaJOH1Q~gaog8LMGXS;zb2Me*DOG;Pd(T>C>lP7+{`k`UvG76wcSKUE__74YrHF zfB$-61TBj0`rOH0@%i)T+}PO2?d|P6H8mC02&JW^oS2v>yB=yJ19W+503*+yJE>E>GK)GzC?%cV+9pm z$w}_*?d5OZzFFR{C<+e`51S0Iw6w$*E?kgVH_fQLB1`z;!-qLDGn4!K`#BO+RkeKn z)TvWEIy!2)zqYnkYU7Y0bWWEc^x?yYJU%|ob^s%VlXKfw*tO@|8A+c2y*z@vu80^@W+oICfO+|DKZV9j{^m69>$clyu56x;Ly+zpFe+I zN(EoNdS#OT_U+r9>#twG*2^0h7~n5ozPKr)3qden5k@EFM< zbln}dOjlP|h~+WOp;x$(<(9vG{WA4{d-v|Sk>$Kw8qv_u5Le|vlxYO&=2T81ImFeg zS54dinAtd4aEA zzmjmjL(bLJ)sYM6y8FmMm%&JD1V&_ByM6n1$a#$5Zn|Y8Vy3~EV}#2wNS_7#EHph(}_O zt;YhYECV1?Gs5i@i3k#rBqGXiC@3h1wazLG0B=az0;sW@BBn=dAMLp(Po8jfb+w)| zK<>b5pbG|a5E4@6JJ4hqz$%lIot+)=8Dxi|DIAQvygV68Xj-rVq>Tf}+ZmNXFF>9z z+%rgf+X3>8$U91^Yg(`Yl4KbGLINEDY0BSy>rJ16GPVIy$(isYzc!_SgU_MS&)} z(5$R1whIwku!tYLQof9AKp_uyhGEwSlBRZm-6XQh4^SbSkmeVV;g!I9Zxz%8BRM(Q z3+4YGY0_Rg?qM%YhE2UFUv@%^}4TqVhXHGM?Hdz?~siHCzi2l$JTZ6I`ZwWRJb3n&iX z3d_Rd#hD(xcv(C!S7^MSJC+Unt;&{h{jYaA%Wt&mRMeLIZrlBFu{<=5j1ZM~Zy z*2kPqO@8|K<@kT(a1qxoyflCnU<8Gt|NnsPW6$wFKz<3U!quqh00000NkvXXu0mjf Dn(Inn literal 0 HcmV?d00001 diff --git a/vector/sampledata/room_round_avatars/element_ems.png b/vector/sampledata/room_round_avatars/element_ems.png new file mode 100644 index 0000000000000000000000000000000000000000..63cdf3ccae85a12763d4725aa560fcf65436d3c9 GIT binary patch literal 2343 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 N002ovPDHLkV1g`?WQ_m- literal 0 HcmV?d00001 diff --git a/vector/sampledata/room_round_avatars/element_multi.png b/vector/sampledata/room_round_avatars/element_multi.png new file mode 100644 index 0000000000000000000000000000000000000000..ab0657768a942ab0dd69281f2264784c1c781f68 GIT binary patch literal 3196 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 N002ovPDHLkV1nEj`x5{F literal 0 HcmV?d00001 diff --git a/vector/sampledata/room_round_avatars/element_verde.png b/vector/sampledata/room_round_avatars/element_verde.png new file mode 100644 index 0000000000000000000000000000000000000000..778ad837250c8029c03328710771aa6d357330d0 GIT binary patch literal 4644 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 DCyn=u literal 0 HcmV?d00001 diff --git a/vector/sampledata/room_round_avatars/element_x.png b/vector/sampledata/room_round_avatars/element_x.png new file mode 100644 index 0000000000000000000000000000000000000000..bc95bf28f37286d8bde337c1d9d7e8cfb0592ba3 GIT binary patch literal 4357 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 literal 0 HcmV?d00001 diff --git a/vector/sampledata/room_round_avatars/matrix.png b/vector/sampledata/room_round_avatars/matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..a3b8f89a8abc47b7250dc4c217e28020254a51b8 GIT binary patch literal 1355 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 uji%3g|M8YD!H20000~K~#7F&3g%a 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 literal 0 HcmV?d00001 diff --git a/vector/sampledata/room_round_avatars/write_club.png b/vector/sampledata/room_round_avatars/write_club.png new file mode 100644 index 0000000000000000000000000000000000000000..12844725815823c088c928988744027d60cf31e2 GIT binary patch literal 10326 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 literal 0 HcmV?d00001 diff --git a/vector/sampledata/space_avatars/car.png b/vector/sampledata/space_avatars/car.png new file mode 100644 index 0000000000000000000000000000000000000000..7e8af6b71dd44d76d41c2baee32a841c1bfdf709 GIT binary patch literal 2611 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 TxPE{{00000NkvXXu0mjfK(`D< literal 0 HcmV?d00001 diff --git a/vector/sampledata/user_round_avatars/amandine.png b/vector/sampledata/user_round_avatars/amandine.png new file mode 100644 index 0000000000000000000000000000000000000000..2a279852b3231d0426eefdcf8ea11a8bbf70b4f4 GIT binary patch literal 5311 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 RLAw9|002ovPDHLkV1g?-`WFBI literal 0 HcmV?d00001 diff --git a/vector/sampledata/user_round_avatars/ben.png b/vector/sampledata/user_round_avatars/ben.png new file mode 100644 index 0000000000000000000000000000000000000000..14839da426fa82db2c2daea8a689415ef6795b62 GIT binary patch literal 9208 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 O0000a&K~#7FwR?Gt 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 TJ=s-M00000NkvXXu0mjf$L6#h literal 0 HcmV?d00001 diff --git a/vector/sampledata/user_round_avatars/bruno.png b/vector/sampledata/user_round_avatars/bruno.png new file mode 100644 index 0000000000000000000000000000000000000000..c0278ea73d8d45728970475debac68ce140015ca GIT binary patch literal 7854 zcmV;f9#P?mP)4` zTxWLPcemaf-3>H$fCNEO+)2>0n2KYI8kdt&JXK>&%5hCORo0{`56KJZsyyX|(mW)2 zNY!YZm*gQODKBx#uF@!p*YTLtjHWc>5e0D(Nl6sh00Clap!el=-^JhgFB)t#ni93l zWi|VD_ubC+o$s9UL&L9ld;9X`nOnEsJ^vv0@toak?vEx0W-^&fIu?t~_`Z&KB8k+< z1jeVQbEX+7>OjtOy=%UqzvtTCduPuc%i&kJY4{avfA#-fKc7rze=lT)zV-gqw`Oi# zzlvgE3!~XA?3NARm!o{_J1~ohnL`*HndWN@nqjH)9mj)Zg`n%|@p633E57HxcIwoz zEBKPPFHHfjzy5l9YHZ}f^74a=AjmP(KG(~eB4Xi?dT)C^?6!-|%}w}JKsuE~G8REB8bPhuKs-5s zL^O?-=VD}fKZdenh(rdE9vMYA5`(7e>YntVqHgFfx0=ptUsMraNC7u)eR#ohoEP~> z>2|AuN^uJwU3_C@2@6z!PGxLut|A&Xu(7_0L@JH@iz{f_HX>mQ(-Wf@9!vus&}g?% zESIT_ID^AO#EL*SObiZYG5*vkV#EOuKVWPtA=u~ z3SIfUPjiSho7EClR&r?2(v@NXcW%FrOft&YFTl1P)EiB-T5W67z*)tf}g|cW$Gq@P)9OqBEv8^^|kYuoH>k;@)E*{*8TWe(S!fenUhCf#%I_* zlLD^a{P6pPuZw;FGxZ&u`K*Hqaunw`u7{ z%R!}5LxSlqWNE0?Y81wV!Q;ifEki?$78hluGZ}w=p!n zpATRtkJv4d_Ts4%M_zhTne-<;@aFq>UJ>DgduUxZ_S8jOy6Rg${t;RXg>tckjg10U zAFjfu&?AF`h!D|50qtfRnE`@bEKKbi2ov??QkG>g{kc?#O~52Z2sY?)Zu9^bTkGq1 z>#tu&Y4ahK;h|b>pj@uv%)tX_)anco8*NR;X0?L4P36)54b#9ny+hKWN$b0Ow+W7t z{}Xiaw$ns{9+XTmB*JleL>;d0C`D#RMvxhw!oh=wkshAlYjUqA09Q_*I`-_R$!31q z0oQNdeuaY1_4`fK(PsSLy82TzN~?%PEUa(j8QUpL4rXw4YC_?|!?jgxRBM=^CzxRY zwl-nJM}pa*ME8y|?widftWb#I)JB28=}}nMG2k>?(CH1s^n^m8#OpP9J{3jVWk<*1 zaL@PJRH_xkU z;^^#tglXl0c$!u-v9IOAq<~&1j8G&TSlo56y0M8cl_G)UI1ZYvCYtpIe{b`*HB^ZO z3%Nza;&B3GgrN~pO0UwJO63|-!%3AXLYBEVp!w|AZ{8`MK6&(yKeGa^-MDpuR(i4f zyTHvPaRu*d%L{n#?Kc_sMY^)4;y4nXz@jC+SV$>CNR33V zOAa7Bpe|rYg*Z0DC%=UjKYz?~ALiGvV3qOF$BUSl9A>!as8K=VQ!{APtH=s?l0r); zi2-`NKvML@Yu9cqoH=v+=aVHeG)h zhlfWnl^sN4XqeWH!yps1Vlk$`FsyI{L^P#LLxPCe!Ph((RE+EDX!E;pDuWh})r2nF zE!625o2dc1zlWhA356|eZLDCTb^taRq!zQ7E(1QIcM+uyF7TI2l<9|O&z{Zo%Gx&z z=tlT5Pfz#z%~x9WQ7vpDzrLpOx2K4V#X_fr>5(D2eGDNADnO@F8$ClTF7<(Fh7_Xf zI)5{a9fQe(3mKBMC&wGKW;`54mbfv%@X8J(d9R`}f)J+$L!iog+x43IEW*lfs>bMf z@%uD1t8Zq|_geqv3p`;)F}&?puL@{CRLS;ArGgTyMGGyhu3?Op6ZelrqqMfANW15? z=+-tZ>F{MM7gqo$eh=mSaNPEGg-C_SBZ-HY+~cmq_FyuOG`YbDfmP}Zlgh3&Y%DL_ zgV(I^aE(gv(EZGPp8x#S`I{F%Q32xmOi`D5589;!?rQv8C92EmHKw^HDXu}w9-N#a zYl|aHMyJ#I4s*22m&e$4`M^HixlPM?)iQ&=$}pn)5=e<^{GGtZOaRFce2HU-2U)a! ziq?-4IQLCV@>)=7Q&Q>1`U>vcco(fYujzX`9`HCN@RUocczc`z==vA~=nwXrHv*A*CK84m9Iyx2M z@v~hj!>2MFatGIQ7)qw93lgjgA{ITwFm~z(d5+}=3;5vuYl?^oCy|%$?^VKfIHVm< zzt~p+VtpR?0zPfi0-zN^TePhBX^bv9#1s`5fYV57)Ip4QEyB030zWe<=`V^9vw^|v zplJdAGI_wIVmxAkr_sC=+H)ByBtpJV&oW4q84ihXSl!%FIbUN9Aq2}&y@osQ{1n%J z_IFr+aG#2>`zqj(7ibr{y`akjjPn8(pA<)(cZu6%YLMimMNXekF*r)(7d&uieTT7Z z`vw{mt|36r)WzZeF0Jnpp>*PfNrgyuFohQLGelUqR=ch4DFA71H%DyV zQv`SD0YaYSo->C}phhoD4`un;RH?OJICKEfaUOZWqco`LmpVfBp_RcQT2Q{qzX>)h zqU_>Sx3&}us}cw9=8O2?y*pUh+(MJJ{V3Vn$k-ST965m~g&!Lnz!<}1ARZ`9c)U(P z)2JZHByt@s?5oTXq7|DA0~IR1#J!5e?P@C3c94Cqrxi1IsWo*49~20=AKY$(tO;jo02uCPIr*^>r$?TZ)u@|W}H zcRWDFejlrMgzhF6o`4{Ukh8gg6nS}w4j<>aXQqabrCaOm8Ud`0GqaNz8W{#+DQqq+ zU^v49r~}$ zC^RaVV7oiYA|=7terje057t(2XmT2fPfaWff_#u*$Mi z6hx*crct2Sd3F)F82I zN(@fZQYDtH88$#erh|Pc1EZG1d|zfvlQ@yS-nn@LU!R?YPoPXPuMb6aWK(e*IB^`g zrjGymr*GgNZr>-i6gW#5Mm95mX_BD|+iOi?#MV}k0O=sf#tQmS-^ac7U|Z8YuM>%P zH9C5MuonzHl>{|J(*p=G*E5MBlTNFAE(47yt@0Iiaj#y#iTqZTHzubiJ4~?!CWPll zKl&?t_4rdr3o#8c}@2p~wUKE{e5^;`SCBCg1qYr;%o3RA+y{Aip1H z?iOaawzjTZn2j);9!s+VY$9^>7}4HEYO942W|c~lO!32em>nHsID{}cwGYE1<4PE5 zvIs@?x1Ryq*cB96#xCQ^@j;kl#-uni+GEPzeqJG`QfowQavQpSl$D>grKx_Oaz_#p zlL8J-r0^Bi?;yKtlfCI74J|f9?K;_GTT0%vDws?*HRfh#aF@7H&X18TnuxOZCtcxn zx_XtCJ$-0`ZFQR-z`}?);;`>{@bFg=9~h=m6Fb$QymxN~*`;KxM*{E82>i64RRz+I zvwSZDlucUE4Iy?sF~t6TiN%c~Dm}0=xfQK0WvIvEB$SB4%S$0eo=zU#VEJqL zy87(tj5Dz&^SHFk7FQRsu)M0g=*;Oe^oACeR+cHuFcsFs*wh#qmVvQ`1H0B>7}$(& zR*D1|X^ON6zGDLk#2Gfi1Ee+Ps^U!I*$O1a?yj}AjsAMq`;Ip=6PVpG(NE~TeRn=1 z5r2!EJGz)hGEE-O<18zv-yOQD0HPrkBd?pV2GF3zY)*Va%-hG0A5+E92OoZfwfs8P zR@YJ7oJ5M(85&~SOpi08NJVMVtD+16@(C)URB2$7$~#2R93C7(+1J%@)llIs)K`QL zsYXjDBmxd2%=MAb9ho}&=HuMH`}3|WAvBd7z~tbvNh_4tk!jMp($>^Mj3|zBO>+Ed zr9?qfc(}aGaao-g43WCB^d-0@8UHg!pF$#^#t1t|X3J;IALD~d)1188Gv(v(thYuFkNDm{RAn z`lEneyUc=y!uuh{eG`>>Np*k9`8-aXJc5zTAo2z~!!l1GPDppk6Ze+%r%5lsRKj+9 zLEs$$Aas<{F|c?*j<1<+fgi^LAB9SALCp)a&nHkrd|&ENk|C8HnD@5|72LbK!VXak z6R9i-Q=- z5ua8I5g(W*17c$ia6HyVI~Bp?S5KdbOB|OYM{J&cf;vw9B&m=y)=k*K}`YE8}I=e2q zeM9NtSLvS8ol!ZE;!E2~07g(FJTibJl@i;mVJtBSyU@T1V;r_dULQB`wQn9Ha~xoS zMR25LOQnjsrc8(HjW*Z_XRK=qZ{#Thc1Iz_`{3;g$>4bdzwHhWkxyY((AEz4jHb== z8I3E{C;EY4U#oX%(nmmz4KtaDthKvO8EA>r(Ej>p#7 z#xgb=dDv<8`;TyJHqCN2Mq$D{E@Tkhn`&W%G&h<|lljdG@=U4z&WLNbSNeEd2>83m zU9UGQ1;Nif_w2Jd<@cWJztM}5y(e`mVP_2GcPcJV^#`OCesAF+>a6sNl?rNXnTeGw zQr0*HOeTiZV6{n56R#2Mk)lVI#_&|+3wfnZ>GsM3i;b;{T36s{9lPy*wm`^k!$`|a zQvlmv@J=--3c?lHHv@tFAM&>g-R-Y_Cw9A9=d-u1(HPU;6el;gKfHs7ENwlpJ_ZyR zSu*m{ER5}&#)vb98ml?e@DzPE!qQ4-tk>(DGF25xDika9mMywYSdGp5h36y2_vktM zf*$sUf;!K2UXx>#2R!}s*F+n|#|hp2zUv`;;r#r~QKJx~Qz^vQ8e3Re!o9^stZx)) z#Uj~ULk&I*mbxKYTvDW>)DEjZj>jBQ;3AP<5dR_h!2)?ggCXGSf%QG#>l4cwc9u`> zd>?qkuKtFD8q3Ge+gIHou zaN)s2mBSBCO_3M)stMvZYD(kM!D+B_T&->q_1kP}#&Lh~0mthttUqLno0K^zQzXj@ zMqTuvfX-qMm`NM1>tk?SR|ND{g4z&<$O6 zWCmFuoIG?KfA@>GaJzD!N@!v@ox(VU3l}47ps=$;Ys;#R{MIIf&F8lk*ci<-W0>Gr z72!A)Kq@UuXtgT)3c8PuR2|pvZf@)}K|8+zp8((}w(Rzsdlc~OvuBGx{n;;mz-)P` zpTHds(7nm+TRhhC-0gWG(Vg?asp)+=m^khbOZ{}_b5bFgTB54f_u|n;OWml|6Fd5 z0%UXO1y;zu&~I@>KP*s-WpqusDPcXx2=oN0=l4%dqr{Z58rf#++xQ?FWGvR&|WgJQ2j_H?!w`;OOq zwbzK~_5zZ*m-=ObotT$uT{J5e_X8kO6T@vp77IjK{bdFRafE}6-~FBQICl6TdxjoX zHa4;Na0To1hJ3N4meZNE>ONL_``PLm&t%n#ka&!vaE`{p5>}dU{@^aEi>GU9{H|-A zcl}W)bQIE~l+H$Pdg=M+pU>_34t=(_-un4v9)13i_OwUrN>&8WbZs#Wg=%qx9a+mE zVFXKCWO=LHSXOrx3Tvt^kXDf`+<>F(Ee<=@8T%WY=Y=>7O|!X?B`%2Dr)9}J_fbVK=C-u?~NTt+RNVp%7IoQH4BN3>|YDe=HVdGDSAjQXEsgKib#tfBRp4 zYqnR`zKf*VZU4Kb<lj_pG+ZJ}`a zmuP1)C{t1X$OM{fp#_=1*cAeuen96l4=HFZ?;HJO?W=%ipM5&_=9@o%N!R`FdjX`X zWvzT#wJ6bDUqbbN{~5+k%o40Z1hnQ(oVmWIX7sZ5H%xA?%eo0^opA?`=j1X%7R%WKL^fE)pLG%Ke`Nysx|Kvshx9@-R4_`HNxBqC2ZR=QA7CyCL+MZ%*$=`nt z=8NS{0P z>c9OD|Ngt5B;ThhK(_xgH9H>)bB<<))$Cv9^s+uEa^fir{lOnVXZ}{zsrox0Pr0~Y zfOK`#%uZH-Qm{HFy0S1b!g5zti*B$`IxLf8FPQBKMz~CWj?iQ(ER{)P^UvQv{lgCs za)KU#BE0lGTbd_KWcQH3Vw*IZ4cI6<)*gvdR<|yHFaL{ge41>=lPKWN%Z2~(?bOHt z+JDY$YdZ~vHs|-T(IJFc9fSyX0&>80{m$=?eb?5o6>h~!e zk$#?PUa)`24)fLz{{r@n4|l3Z1+=n6#+PtOpxlU~WQUO8KxL2#XTVhodi6gS-ud>E z$o51EP)aCn{_s1gA@HeauCE}e5Z zKV%p<)E1!`W%joczV(CuRK5D|o>Z=a@)dpj{8Ny~;rpr8+O3`WYkSw0`jl#OA|HuDUx%$#)k?Dyrpm%#==IG3? z*Fu-GhBu?@b&kVawcuGBA42l<5hPBXpjRB=+%TaA9x}R?rL6L|1G2tA5f@rq)&O~x zI_m4IDBt=3_T5F47ni_}2wUF~lw_F6xc4ZqL*SMEe}do_a^;UMc4{PA-MFGT&cFFC z-pk=LZl6m5o$Y^GzxKjrNV~KWcW0WK27Smc>a{P$0of!A57R zR6~n1!|LKH71TtP=hy_#CWYIS^@Xf0B#Q{1H0Uu+VU*o-2r7UbgF;!y_ugN-{h9ty z>Pu6AZ0E-2W@yZdW4883**4iMnbQDWGf7Yiadv0PxJ#AH+p~MY20tiWC6&q~O%GPy zQGJejBAE$FY=)$hU7vC2pPI{g0Yk&qgI;n+4PpCSf1avG^Hj)B{bBkaoIdIC{@E3 z$lM9V6D{*k(oOGWy89Oryt9323h3Vcb>!&zo3Zu_Ee+4e`U9yNEqaE;zKpZNzT|5* zM0xiJsj~72KS-6Pvb`U$S#EXB%dP6(8>^P_#+}uhSMViozY+y>Zx`tqL!L30@a#Df z<1^&mGj!v$FRRjJ!mHAwsy1^JykPpKw5k{Qy2hZn@}18CY@cQON5*WgvY~aG-2eap M07*qoM6N<$f>)~=R{#J2 literal 0 HcmV?d00001 diff --git a/vector/sampledata/user_round_avatars/gaelle.png b/vector/sampledata/user_round_avatars/gaelle.png new file mode 100644 index 0000000000000000000000000000000000000000..a679639e005695cea317f86d8609e7445d0f56ec GIT binary patch literal 8256 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 JPDHLkV1ksfCM*B| literal 0 HcmV?d00001 diff --git a/vector/sampledata/user_round_avatars/matthew.png b/vector/sampledata/user_round_avatars/matthew.png new file mode 100644 index 0000000000000000000000000000000000000000..13d8eb3cfc088394ba7d1078aa373d54a8880cec GIT binary patch literal 9602 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 RBclKS002ovPDHLkV1nWhdM5w? literal 0 HcmV?d00001 diff --git a/vector/sampledata/user_round_avatars/nique.png b/vector/sampledata/user_round_avatars/nique.png new file mode 100644 index 0000000000000000000000000000000000000000..41480b4bb4694a237c9e62e197d66b7c7a739e72 GIT binary patch literal 9158 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 QYybcN07*qoM6N<$g7iu`v;Y7A literal 0 HcmV?d00001 diff --git a/vector/sampledata/user_round_avatars/toml.png b/vector/sampledata/user_round_avatars/toml.png new file mode 100644 index 0000000000000000000000000000000000000000..1abeb74d9437ef1234d6261dab1f3853db356c82 GIT binary patch literal 6539 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 literal 0 HcmV?d00001 diff --git a/vector/sampledata/user_round_avatars/victor.png b/vector/sampledata/user_round_avatars/victor.png new file mode 100644 index 0000000000000000000000000000000000000000..f1f91bda4fcc168e797439db3b6d7525f3d8ef5e GIT binary patch literal 8657 zcmV;?AuisDP)o2O=?l5osOi; zbeeRtlD3&<+8Ien6K9)5$~cJ?*%55X4sA&$C6NLNkpw{OczFAE_nYsWdmjkUR4iK3 z^*rMdc=x{FIlpth^PTVfq|hgPg<)9oeL59}!3j-MriqkN7*YX$Dn);n|L50o5K=u1 z@H6n=4pi;D8o1|Esnjxkf|o*{z}0izQ(8!0QdR9|*a3>7-1L>5gpzJ`AKJ}TO zpuxdWN*IPXQjMdg|2Y z<9|g?@$C-1`HerJ^|k9Xkj>G?<`$XQWmQX(8+zpQY|5n5)U_2&g0x{d3$9MEibzk)A(vY*Gdd|>?h{eKTbpIYS=+PL@@ zFpOw>yG+$uog6Qu)p7++ZPUb|ane+cs`WapAu77P9!-vo((z+=P%>juar6*Pm1byU zqD0y30No6w{C(_n#LCmDRN@)>xE;`J_MQVoo&G>zIJ8ehHfXa_Cq+w;ihSDHt&pxJsOwnNK_s}IL%D25Vkn(9DW6Hx%=>6rINI!a)|AQ*x0Pm%{?eR9bHKrUasLdC%m%ID44 z@BI|u{}1*r?sV63>glBMWx8Dl!1yx=z^D5beo*fB^N(|OeQANNo&P?0c9T3WAOi+W zO&z9-i`U3UXzRL4HVo=mHeFrWpstSv=sM{dqd9inaq&FdX8;%A6bnF@HWTFG_w7!X zOkJa$@(z9X+wMAIc8R{UVYV}?6Js%*d?X#Trt!ckgw5Mvl-iyUs zu*O?D!1I(>5utl^{zlnTqQ%_!O1ZiKwm086OK+Tg1MfR@^5p%L#4hIYIZC6%O%CQM z2N2}kcMus(+oq=DOTFjE2H*g^VBjTe2!svl_}K&wC~y)xh9f0~2r^s;=~bb3-#$+# z?thpv*}PPQP-3XRL*d@KBa#*t5~P8+(dT}o19o<5r*ThdKhpQhnsjXnJ0TQi*7yD5 zrFZB%fAuouk{t3!sgTc;h0skUOqv=h(BqFhNOMQ$sM~A;*?oXrkE-o1?Kax9jR@JQ z*1!dNs0c23c=;%2E~;+iw6a~H20mw?SKyAZ0!QZ4DXQ#lgKNlk?5j#Qs!`t!`-=8cOSRhbGsR;4mJUE9 zp`FF<_kGt7C*=nH-0Rol2F_!`VrFRm_1Ed1yN*#YlcDXM9jaC+Y?>Gz zph~?(F792ryom3qQNB>1TV1yR?t^E$A9tMIf1?+Z3^j(lz9;!6Er{}q z7dv`nitfArQ#3xDM`-x;#`y(mHJbEOpGKFLN&|ybAbgKp)cOEMcqoNThWK$3MV>3U zp33ATvUJ_R8dPFdkQ*2z=Fb;buG2~x6uD~v3p8n^aSOGYb;{&&QMB%%cGPK8a0{nU zcxGv-_Uz0|6a_aTK=V%pp|aP{_cKSuLwqHAFz384pcMrg)(F&3)A-n7@}|+gnn}8F z@jY?@atQzqjvzOgm4x-xX;;OcbY+g%DliJldA))96`;W zLv*ZH>(oOzHSt_GbZH=$r7VnR{t+oki~|Q#_P~3#h~+pv*o%Tbp!1DS_1_0VY9fKc zV{pNPId5H2Z9>^S{yl#Vlsz%{oJ_Qp?2Rw$KYQV2R7S2c;;9=~DVBd>Bo=Wi| zdri?21%HP z5=2pm&`aYx6nq))z6NI%(JD_(O;Zx}&3h>NOO;r*q}Q~Odt zIW3(VZqfikvb%UM9^+i~BOsIo;>FSy_DoWO4Poy#(8e|zE!qLc?^>=z2iI-RO#=q` z2yMn|Mp`Y22yk5iMny`iM+pW$JU5UORK;*{vWwKgT2yuh*5fO_G*aW^hv@kI55Xvs z6zJFgxbjE8ivfJEBB<(KJy7-VOZYJ_4#3xZ*7vxUq7@0_oWz$pE?nV7iY;!4giVDF zSB0|5Cy{eTI*ZWn&@P<7Do?f6pjni^;@|+K@V5+r%(lDKMk{Qg)061p=VGM z8T@!sUszZwp%$0;7S3(nS>9nSP6Ia%CENfW>qL1hZCBKVG0oK!4GYPkAHYsx@!ZB9 z86BcARELo}@0A9qw`r4VX7OBuy1QjO3MqjS>S0GYTKE|hqD%r$LPSggB+ASUkb-h) z0TVb)*mx7a=by+YM2I5&f~H0^SQYTRA;|DDeMGMtM$w}P&y>(&CzZ}EER^(SvvR_) zkzZO=*lmS7G2|I@Y)%6#Qjt_bY9Lgtco!6Lf}koTbZ~u$7wi-UF2=LcD#Y^}1|-vzA3;UQ7f7|r$n_yI(>Ze3 z5I8oUGo`E^|I`B@O12QGSTZLQk&)1~xQd11AP1>tqs)2&g36x!E}QL#r}TQge1cI~ zM+3q+&mPqESirzTd^9gg+@tgVI0E=#igJTxdDuM-{CqH*ic}eV$#m%mHMZ91JFk3` z-d|dw>zh@oFktx%SfPO9L?J_u+4C=`0X_Vr+ySJN1*& zU2j3xegc^t-0wla za1q|&UJJ?j*^zvgYNdukhE#d504w#CUI^XBrDYL3{;`Rl14rIec}HB9L{t&drJ4wwOr~p#eHPdk0yl*FR-s=>DxTT{`z3 zHPF+u!qY`r)IgMG#s+ER$Q{yP4Gv$Up|(%MSnN7jWYSDiQcX&Ax%=2Gd63HlZ}8;~ zN^HAXm0gbVT_~j(E1@n`FqCNF9T%&Z(ZnP`5~}h5 zW`JIQbQQOmQX=_1D6TR-LFv`2lvi9Dot~uZI9_OI7^SnhHECpOAWELYLa9qd1C$I* zE@7r9#E#!NiTjgTIL)Hj@iF=Sso4^Z%uG|E2rvW<&LGE!08J*MwTti%c#Uxd;57^T zWo8+uVu#kR2*OvqJ@V#KlpY{w1>Lf(62;1@8Y8C~Dw1zZNtGB?gScF&2U^W2mmK6C z=WG!VJ#l!1?82z@1YE;?i>a$lW9SxH%-{tl@O<5Y!h;f-Le=Pb77gc+8tDT0=<=9$ zr0~2BEj!}^ub20Zoy4UPTn-NCHX>#Q9>Coj3k*TbnVI`A zu!#D@^)+EclVg^aT+g z@4?$Te;5wpaFWu4FG9%`;=u+jLZloWoJRO&sRPt+)yvpb%>B|y*<}wCAJzk`UK`~r zpcnqff1@|AtZ^!jnf{WzO3(78g-$V23V7o zKo73%%|N=$&Tv!Da)nX~U4}C(usPx6cyJ!IoJGkF!u9I=>vSKCIdt-FpgF=ERCWOJ zb!}}KQdnKsV{z<|)aS(7Hq9Igs0EsvNU&oQ2up=hD5X{#Jwvw%cA1w*swvB zM(?d&k#3O%R?CWv7kz;vA)!X83!F(HEqpk!r%Hw($^ty!lX4*>PGz9?>iWW!kZ>pSYfspMj&T!55XFQ7YKlB{T?7@laYuLt{{>170U)+ zwnA!kU7@HA7BB+NF@zd>7xyMqO@NO>OgKRz0utT<2E~Da0v)~UB)#+tS581vi{k)5zC5L7b0!9j@$~5@}bVZ)&a8MGZDhXsy0r>cHM~~k}FaO2YX|@FU z+fij;;UF!_<+3QeKtULP6|{I8(wIvIA}NgmFpJuM$WPOtt5F^@ShZH6>j1eB_m?5C zTwh`3otv z(-T^9UdwX^d1(L;x+ z$p;*4gF0w_c#6{onF;i0#H9eBt}LuT^T)qYK9_?nq@5|NOK!DD9aL#}W`s|g(FS@6 z+f}KF=U|Qk^`}eUc=b)1n>|ALpe{RaLX%8_P4Xsk)#34)4eS!p1L*H_Gvq|go41c6 zFnGdwZwuW0%76I9B}MT|+?DB?D#kM#151<=r^DMOLN#X96ph{#d2ztyr|+i!`jy|H z$A9{#g_kF+8h!rWNvdIv$A=$S1c&bk>aI2TeC z`LBW~Rhu0+U!#SkHM&q&F*~RNOOliXxXw)s(SQP=MB|l)?9PWMOhlpsr*@HE>L}|M!o+HY19Vp#?7?fX+zWL<1r+PT)$qWMkS50cpJFVL$zRCP@@dKo96|c&IUDGoF0e{?)>n~ zsY8P_lo+6lml0OCw$2!mLd{>J?G|Rq<|t{YXhVFI)ypdmJ$&~RWfEPKVmyOob*wvs z*<>EVi!1mA1`%LRsf0q|H7!a_Mgcf|p89E_*qs+mze9c-i>q&7@tb6$6ougH9*k{a zR#!uZ)`W2ufUDbegqhipr~@!B9m9}A6NfRgt6~hEr-dum=*>&lh?NACSqw^QlAAsB z2HcVaiZ9Jf(?g4lGUy;w9cFM?r45JhfruWYv(@XXV3{thRXeB#CJl~F#@!w!$Y7`M zyuS&xw21d`Pyhz0I-)>pD2E9sOGXmWfy!n=S75%v8<_&JO5yj(Z1M%RT_WJizxsdX zU-^xHS9d(8;IfAAu_j?k`Eme;HWtBrJ)2S~i)~c%X@sUJL`g{x(|2FHhUv_JoCImK zTA~lWcWoUFPEL-xaV2OWf2XG=WPFweaL$h7b3pd(N}Yy=1~Etli-eIIm6|xNE260? zDs$J<$wWEJ6$>b=4&=Lt zDx@Fx6i5Xo0|_`@6Kyegrf>I+$xuG~Y&J-o;juV#XKp4EqM#^J`;81}Bu{4HPy`x1 z_qFfQ@BZm4^u)jVdD?_wyPKPIXmS!tZ9}4`(`%xPwJ8T0yNhzx=0*tN%|lHOJEFK( zsR(JS(&|cYp|hgH8YDuv0b0CXhRhCdY#|z12ap3405k&;rSVx2{LM_{1>9QbABIMc z&{ux@75cN%B%S`^BlPH9Qvmc(4q)-XMM|U04cxkB2jA+O-JfvNZ@%!7^>6>#KT4s6 zJSELg|FjqPglaSsFadNISL^hP|NW~dN3YZ4U-$yu|KP)rvpiZ^T836XPUW3lgkVp) z!8EKIhcupq2)evgfkQg9iHXrBWb+!rcLybMt=5q9L5%qPyH!krx3T*Ssw=w<8&d}bnMs+O+j^1V~8wK-~KJhb3XXMnjUv>CX^j^-gcuQRbVhbK*yjG@tlAUJ*?H*w79uTZ(rY}E1NrX zaeb2(F;2gVd=HQwM@EK`awuOQMrl412-e4Z#6oFHXA4x@-NAR{z%i=A_IQ_D)iOkv zX{1GgwJN%>xJ>`%KmRVh_U|aHXj&|&wTEQ`qHbv^^BTGe1$~@ z-gz5r=YRgrJpJ)sypCCYSNe57u<0|8{shhc)oTbdL-NZ2-Ici?{+&c6pk3(y_-wh+ zr9lAX#BhP;rpD;x+$1fnZP7X^gTsSRE{4b!=p%HY1Cb=q4q3M~EU;Og&ewz!*EOp7@e!LAA@na8Im=>!VMAoG3hPh2R>y!dvb3dt-lbH^H@Ad3Ob#MCT+&ZT>gO`)e40%(fL zvvlV__}4GoZ0`>_fG=MQp8#Ohd5o1s7xDK#eUJ3pA!slwE+&jzSzVKJKq_)|>HS5) z2yTJ-;-fADkh^*)p(|C3$}p&jXK{B{Y4${-9v>*uaIq+JzA!#K#m8Y<0NU*?vNe(3 z8;Kd{Bw5Pl2Fmx`NF7GBR4ZFzC|4{E2p`~m58NeR_kW6h44;ljcNX*$X%NgkY(p=3?iU@A|?oEZ@pj zjXvP|-~a#5&TdYBzWB|njYl@>7VRv)gWkgvFHe<9R2UpV-eyEdz4(JSDGwo(yFMOc zGiOL5rxVESCQ4)-5?KIkZ**V)Kn6w#Fza-f6{<3{=&^LxwP~eggBq(MX_*LRoiRbx zqM3xQiGI!3VdX`B*xCn&4DI&X!Yt9EZ z<10zXWK9f3xEN(PYBWXkM$U0IwvB~fL0`}dc}{4`&a?a$F!|w=V;ttp{$NJsjU~x@Ha3L_kP~Z2J5EI|kxs8Gk&4>? z>e+An;m`gk`+lSY__BMg-^6as;Hf7#An&;IxN!c$(6ERoW$<^a)1pdwlSU_|sJyiy zomUssmepVuS#9L^0Dvhse3-%kD8it}Z2&@B&4LQ2L~!BgN^xH=qDCOQE^|FNfRExj z=mHy!U8xKya1G%rrYV(xOD|L11VFjhX+MAd?8`q(AL9B@4&cjk?H3Ir#Z!$_rK5My z7D`_MXq(6GZkM+)0I9=KJ<;MkHsa%8GSeu{lpbkzEP#Dd>VF$yUEkdn0isdG3=jN(Ra+)3yjT}a6pN8Ah!HQe z(`pKON9vNo&qaIPm0f0#WfezGkMR-Oc=+MUIis3H1fN03q~i!+V#M8Jr%}sfv%mDc zSO4Uh+p+1k9Ke@nchCNlfARCbe|=-)OLn(Y&=}OP!#ltM)(TmUY5}NXo=KL+CqI3j>3gC=GYN9wAU<3vG94>Bh>R3~l}G02 zTtn3`=7V16A6~lf{jbw)UJ8AL>%_x9d3yG)d!BMUrF8M#b7)Ch$n~ytXAEMa6D66z zFi1HlS8_5;PIpHKucKk5CjiDrVi*JDa08kbIUgkFlpJv?JCq=#l(#nJ?!mz^W~4Rb z{V!iU|JrT+hf@2m+m3+!*Ve}M^LL(n=q20om7ZlC@6>BauGdVNS&p*ZsC`bvVk!?v z`4~L!An&-$?1HFOD1$oD4FDmx98}{S@YlPoXI(@%cS=FQ*6pcL6*{RO7LfOzrz;_c+Ws-1)y4 z(TwRT!T<8!@C3sDd53up$>)SAK6;ZuBLy-Pa%5njx}3}myi{xMytKP~X`Vjj z>l1On{;Nsj81YTp=fWT jshVHDbat6O!Rv1T1%vy{NMqs600000NkvXXu0mjf7|4zm literal 0 HcmV?d00001 diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index 7ea632eefb..c9ec44df4d 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -79,7 +79,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@tools:sample/avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/space_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:ignore="MissingConstraints" /> + app:layout_constraintEnd_toStartOf="parent" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/space_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> diff --git a/vector/src/main/res/layout/item_group.xml b/vector/src/main/res/layout/item_group.xml index 9cd07f3215..c575b4ee23 100644 --- a/vector/src/main/res/layout/item_group.xml +++ b/vector/src/main/res/layout/item_group.xml @@ -21,7 +21,7 @@ app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@tools:sample/avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/space_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/space_avatars" /> + tools:src="@sample/room_round_avatars" /> 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 282c3f6140..dc094fca97 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -22,7 +22,7 @@ android:layout_marginStart="8dp" android:layout_marginTop="4dp" android:contentDescription="@string/avatar" - tools:src="@tools:sample/avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/room_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/vector_settings_round_avatar.xml b/vector/src/main/res/layout/vector_settings_round_avatar.xml index 3349e092f9..596eef5d3c 100644 --- a/vector/src/main/res/layout/vector_settings_round_avatar.xml +++ b/vector/src/main/res/layout/vector_settings_round_avatar.xml @@ -11,7 +11,7 @@ android:adjustViewBounds="true" android:contentDescription="@string/avatar" android:scaleType="centerCrop" - tools:src="@tools:sample/avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> diff --git a/vector/src/main/res/layout/view_read_receipts.xml b/vector/src/main/res/layout/view_read_receipts.xml index ac4351b379..6e674406fa 100644 --- a/vector/src/main/res/layout/view_read_receipts.xml +++ b/vector/src/main/res/layout/view_read_receipts.xml @@ -26,7 +26,7 @@ android:adjustViewBounds="true" android:importantForAccessibility="no" android:scaleType="centerCrop" - tools:src="@tools:sample/avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/user_round_avatars" /> diff --git a/vector/src/main/res/layout/view_stub_room_member_profile_header.xml b/vector/src/main/res/layout/view_stub_room_member_profile_header.xml index 7df09721be..991883edbb 100644 --- a/vector/src/main/res/layout/view_stub_room_member_profile_header.xml +++ b/vector/src/main/res/layout/view_stub_room_member_profile_header.xml @@ -28,7 +28,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="spread_inside" - tools:src="@tools:sample/avatars" /> + tools:src="@sample/user_round_avatars" /> + tools:src="@sample/room_round_avatars" /> Date: Mon, 10 May 2021 22:21:22 +0200 Subject: [PATCH 029/202] Avoid using @tools:sample/full_names And make preview works for some text --- .../src/main/res/layout/item_bottom_sheet_message_preview.xml | 3 ++- vector/src/main/res/layout/item_bottom_sheet_room_preview.xml | 3 ++- vector/src/main/res/layout/item_contact_detail.xml | 2 +- vector/src/main/res/layout/item_create_direct_room_user.xml | 4 ++-- vector/src/main/res/layout/item_known_user.xml | 4 ++-- vector/src/main/res/layout/item_space_roomchild.xml | 2 +- vector/src/main/res/layout/item_space_subspace.xml | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml index 6bb803dd2f..35a6232294 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -37,7 +37,8 @@ app:layout_constraintEnd_toStartOf="@+id/bottom_sheet_message_preview_timestamp" app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar" - tools:text="@tools:sample/full_names" /> + tools:fontFamily="sans-serif" + tools:text="@sample/matrix.json/data/displayName" /> + tools:fontFamily="sans-serif" + tools:text="@sample/matrix.json/data/roomName" /> + tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/matrix.json/data/mxid" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_known_user.xml b/vector/src/main/res/layout/item_known_user.xml index a2429e159f..24eb6e94fc 100644 --- a/vector/src/main/res/layout/item_known_user.xml +++ b/vector/src/main/res/layout/item_known_user.xml @@ -54,7 +54,7 @@ app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/knownUserAvatarContainer" app:layout_constraintTop_toTopOf="parent" - tools:text="@tools:sample/full_names" /> + tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/matrix.json/data/mxid" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_roomchild.xml b/vector/src/main/res/layout/item_space_roomchild.xml index f6083b408b..799f9730a1 100644 --- a/vector/src/main/res/layout/item_space_roomchild.xml +++ b/vector/src/main/res/layout/item_space_roomchild.xml @@ -57,7 +57,7 @@ app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/childRoomAvatar" app:layout_constraintTop_toTopOf="parent" - tools:text="@tools:sample/full_names" /> + tools:text="@sample/matrix.json/data/spaceName" /> + tools:text="@sample/matrix.json/data/spaceName" /> \ No newline at end of file From cf00cc2fdac326700e485d4e5d196be1bb2f93b9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 10 May 2021 22:34:05 +0200 Subject: [PATCH 030/202] Fix issue with margin change --- .../main/res/layout/fragment_matrix_to_room_space_card.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml b/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml index 3924d98851..4635cabffe 100644 --- a/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml +++ b/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml @@ -175,17 +175,19 @@ app:layout_constraintTop_toBottomOf="@id/matrixToCardDescText" tools:src="@sample/user_round_avatars" /> + Date: Mon, 10 May 2021 22:45:08 +0200 Subject: [PATCH 031/202] Make the avatar matches the user --- vector/sampledata/matrix.json | 12 ++++++------ .../{amandine.png => 0_amandine.png} | Bin .../user_round_avatars/{benoit.png => 1_benoit.png} | Bin .../user_round_avatars/{gaelle.png => 2_gaelle.png} | Bin .../user_round_avatars/{manu.png => 3_manu.png} | Bin .../{matthew.png => 4_matthew.png} | Bin .../user_round_avatars/{nad.png => 5_nad.png} | Bin 7 files changed, 6 insertions(+), 6 deletions(-) rename vector/sampledata/user_round_avatars/{amandine.png => 0_amandine.png} (100%) rename vector/sampledata/user_round_avatars/{benoit.png => 1_benoit.png} (100%) rename vector/sampledata/user_round_avatars/{gaelle.png => 2_gaelle.png} (100%) rename vector/sampledata/user_round_avatars/{manu.png => 3_manu.png} (100%) rename vector/sampledata/user_round_avatars/{matthew.png => 4_matthew.png} (100%) rename vector/sampledata/user_round_avatars/{nad.png => 5_nad.png} (100%) diff --git a/vector/sampledata/matrix.json b/vector/sampledata/matrix.json index c69e0201ad..8339745fe1 100644 --- a/vector/sampledata/matrix.json +++ b/vector/sampledata/matrix.json @@ -1,8 +1,8 @@ { "data": [ { - "displayName": "Long display name useful to test layout with a long display name", - "mxid": "@longmatrixidbecausesometimesuserschooselongmxid:matrix.org", + "displayName": "amandine", + "mxid": "@amandine:matrix.org", "message": "William Shakespeare (bapt. 26 April 1564 – 23 April 1616) was an English poet, playwright and actor, widely regarded as the greatest writer in the English language and the world's greatest dramatist. He is often called England's national poet and the \"Bard of Avon\". His extant works, including collaborations, consist of approximately 39 plays, 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.\n\nShakespeare was born and raised in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. At age 49 (around 1613), he appears to have retired to Stratford, where he died three years later. Few records of Shakespeare's private life survive; this has stimulated considerable speculation about such matters as his physical appearance, his sexuality, his religious beliefs, and whether the works attributed to him were written by others. Such theories are often criticised for failing to adequately note that few records survive of most commoners of the period.\n\nShakespeare produced most of his known works between 1589 and 1613. His early plays were primarily comedies and histories and are regarded as some of the best work produced in these genres. Until about 1608, he wrote mainly tragedies, among them Hamlet, Othello, King Lear, and Macbeth, all considered to be among the finest works in the English language. In the last phase of his life, he wrote tragicomedies (also known as romances) and collaborated with other playwrights.\n\nMany of Shakespeare's plays were published in editions of varying quality and accuracy in his lifetime. However, in 1623, two fellow actors and friends of Shakespeare's, John Heminges and Henry Condell, published a more definitive text known as the First Folio, a posthumous collected edition of Shakespeare's dramatic works that included all but two of his plays. The volume was prefaced with a poem by Ben Jonson, in which Jonson presciently hails Shakespeare in a now-famous quote as \"not of an age, but for all time\".\n\nThroughout the 20th and 21st centuries, Shakespeare's works have been continually adapted and rediscovered by new movements in scholarship and performance. His plays remain popular and are studied, performed, and reinterpreted through various cultural and political contexts around the world.", "roomName": "Matrix HQ", "roomAlias": "#matrix:matrix.org", @@ -19,8 +19,8 @@ "roomTopic": "Room topic very loooooooong with some details" }, { - "displayName": "ganfra", - "mxid": "@ganfra:matrix.org", + "displayName": "gaelle", + "mxid": "@gawa:matrix.org", "message": "How are you?", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", @@ -37,8 +37,8 @@ "roomTopic": "Room topic very loooooooong with some details" }, { - "displayName": "Giom", - "mxid": "@giom:matrix.org", + "displayName": "Matthew", + "mxid": "@matthew:matrix.org", "message": "Let's do a picnic", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", diff --git a/vector/sampledata/user_round_avatars/amandine.png b/vector/sampledata/user_round_avatars/0_amandine.png similarity index 100% rename from vector/sampledata/user_round_avatars/amandine.png rename to vector/sampledata/user_round_avatars/0_amandine.png diff --git a/vector/sampledata/user_round_avatars/benoit.png b/vector/sampledata/user_round_avatars/1_benoit.png similarity index 100% rename from vector/sampledata/user_round_avatars/benoit.png rename to vector/sampledata/user_round_avatars/1_benoit.png diff --git a/vector/sampledata/user_round_avatars/gaelle.png b/vector/sampledata/user_round_avatars/2_gaelle.png similarity index 100% rename from vector/sampledata/user_round_avatars/gaelle.png rename to vector/sampledata/user_round_avatars/2_gaelle.png diff --git a/vector/sampledata/user_round_avatars/manu.png b/vector/sampledata/user_round_avatars/3_manu.png similarity index 100% rename from vector/sampledata/user_round_avatars/manu.png rename to vector/sampledata/user_round_avatars/3_manu.png diff --git a/vector/sampledata/user_round_avatars/matthew.png b/vector/sampledata/user_round_avatars/4_matthew.png similarity index 100% rename from vector/sampledata/user_round_avatars/matthew.png rename to vector/sampledata/user_round_avatars/4_matthew.png diff --git a/vector/sampledata/user_round_avatars/nad.png b/vector/sampledata/user_round_avatars/5_nad.png similarity index 100% rename from vector/sampledata/user_round_avatars/nad.png rename to vector/sampledata/user_round_avatars/5_nad.png From e34574f658087fdb4d302096457a602dbe42510a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 10 May 2021 22:49:11 +0200 Subject: [PATCH 032/202] Split sample step 1 --- vector/sampledata/matrix.json | 12 -------- vector/sampledata/users.json | 28 +++++++++++++++++++ vector/src/main/res/layout/activity_call.xml | 2 +- .../layout/alerter_incoming_call_layout.xml | 2 +- .../layout/bottom_sheet_invited_to_space.xml | 2 +- .../layout/bottom_sheet_space_settings.xml | 2 +- .../main/res/layout/fragment_home_drawer.xml | 4 +-- .../layout/fragment_matrix_to_user_card.xml | 4 +-- .../fragment_room_preview_no_preview.xml | 2 +- .../res/layout/fragment_space_preview.xml | 2 +- .../res/layout/fragment_user_code_show.xml | 4 +-- .../item_bottom_sheet_message_preview.xml | 2 +- .../main/res/layout/item_contact_detail.xml | 4 +-- .../layout/item_create_direct_room_user.xml | 4 +-- .../res/layout/item_display_read_receipt.xml | 2 +- .../src/main/res/layout/item_known_user.xml | 4 +-- .../res/layout/item_profile_matrix_item.xml | 4 +-- .../item_profile_matrix_item_progress.xml | 4 +-- vector/src/main/res/layout/item_room.xml | 2 +- .../main/res/layout/item_room_invitation.xml | 2 +- .../main/res/layout/item_search_result.xml | 2 +- .../res/layout/item_simple_reaction_info.xml | 2 +- .../main/res/layout/item_suggested_room.xml | 2 +- .../res/layout/item_timeline_event_base.xml | 2 +- .../item_timeline_event_call_tile_stub.xml | 2 +- vector/src/main/res/layout/item_user.xml | 4 +-- .../main/res/layout/vector_invite_view.xml | 4 +-- .../view_stub_room_member_profile_header.xml | 4 +-- 28 files changed, 65 insertions(+), 49 deletions(-) create mode 100644 vector/sampledata/users.json diff --git a/vector/sampledata/matrix.json b/vector/sampledata/matrix.json index 8339745fe1..33fe511611 100644 --- a/vector/sampledata/matrix.json +++ b/vector/sampledata/matrix.json @@ -1,8 +1,6 @@ { "data": [ { - "displayName": "amandine", - "mxid": "@amandine:matrix.org", "message": "William Shakespeare (bapt. 26 April 1564 – 23 April 1616) was an English poet, playwright and actor, widely regarded as the greatest writer in the English language and the world's greatest dramatist. He is often called England's national poet and the \"Bard of Avon\". His extant works, including collaborations, consist of approximately 39 plays, 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.\n\nShakespeare was born and raised in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. At age 49 (around 1613), he appears to have retired to Stratford, where he died three years later. Few records of Shakespeare's private life survive; this has stimulated considerable speculation about such matters as his physical appearance, his sexuality, his religious beliefs, and whether the works attributed to him were written by others. Such theories are often criticised for failing to adequately note that few records survive of most commoners of the period.\n\nShakespeare produced most of his known works between 1589 and 1613. His early plays were primarily comedies and histories and are regarded as some of the best work produced in these genres. Until about 1608, he wrote mainly tragedies, among them Hamlet, Othello, King Lear, and Macbeth, all considered to be among the finest works in the English language. In the last phase of his life, he wrote tragicomedies (also known as romances) and collaborated with other playwrights.\n\nMany of Shakespeare's plays were published in editions of varying quality and accuracy in his lifetime. However, in 1623, two fellow actors and friends of Shakespeare's, John Heminges and Henry Condell, published a more definitive text known as the First Folio, a posthumous collected edition of Shakespeare's dramatic works that included all but two of his plays. The volume was prefaced with a poem by Ben Jonson, in which Jonson presciently hails Shakespeare in a now-famous quote as \"not of an age, but for all time\".\n\nThroughout the 20th and 21st centuries, Shakespeare's works have been continually adapted and rediscovered by new movements in scholarship and performance. His plays remain popular and are studied, performed, and reinterpreted through various cultural and political contexts around the world.", "roomName": "Matrix HQ", "roomAlias": "#matrix:matrix.org", @@ -10,8 +8,6 @@ "roomTopic": "Welcome to Matrix HQ! Here is the rest of the room topic, with a https://www.example.org url and a phone number: 0102030405 which should not be clickable." }, { - "displayName": "benoit", - "mxid": "@benoit:matrix.org", "message": "Hello!", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", @@ -19,8 +15,6 @@ "roomTopic": "Room topic very loooooooong with some details" }, { - "displayName": "gaelle", - "mxid": "@gawa:matrix.org", "message": "How are you?", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", @@ -28,8 +22,6 @@ "roomTopic": "Room topic very loooooooong with some details" }, { - "displayName": "Manu", - "mxid": "@manu:matrix.org", "message": "Great weather today!", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", @@ -37,8 +29,6 @@ "roomTopic": "Room topic very loooooooong with some details" }, { - "displayName": "Matthew", - "mxid": "@matthew:matrix.org", "message": "Let's do a picnic", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", @@ -46,8 +36,6 @@ "roomTopic": "Room topic very loooooooong with some details" }, { - "displayName": "Nad", - "mxid": "@nadonomy:matrix.org", "message": "Yes, great idea", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", diff --git a/vector/sampledata/users.json b/vector/sampledata/users.json new file mode 100644 index 0000000000..0a49d27450 --- /dev/null +++ b/vector/sampledata/users.json @@ -0,0 +1,28 @@ +{ + "data": [ + { + "displayName": "amandine", + "id": "@amandine:matrix.org" + }, + { + "displayName": "benoit", + "id": "@benoit:matrix.org" + }, + { + "displayName": "gaelle", + "id": "@gawa:matrix.org" + }, + { + "displayName": "Manu", + "id": "@manu:matrix.org" + }, + { + "displayName": "Matthew", + "id": "@matthew:matrix.org" + }, + { + "displayName": "Nad", + "id": "@nadonomy:matrix.org" + } + ] +} diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index c9ec44df4d..c4bba45ebf 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -106,7 +106,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar" - tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> diff --git a/vector/src/main/res/layout/bottom_sheet_space_settings.xml b/vector/src/main/res/layout/bottom_sheet_space_settings.xml index b9b3800ab3..8cd7e9bd70 100644 --- a/vector/src/main/res/layout/bottom_sheet_space_settings.xml +++ b/vector/src/main/res/layout/bottom_sheet_space_settings.xml @@ -43,7 +43,7 @@ app:layout_constraintStart_toEndOf="@id/spaceAvatarImageView" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" - tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> + tools:text="@sample/users.json/data/displayName" /> diff --git a/vector/src/main/res/layout/fragment_space_preview.xml b/vector/src/main/res/layout/fragment_space_preview.xml index 364ae62b84..765b5f607c 100644 --- a/vector/src/main/res/layout/fragment_space_preview.xml +++ b/vector/src/main/res/layout/fragment_space_preview.xml @@ -63,7 +63,7 @@ app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@+id/spacePreviewToolbarAvatar" app:layout_constraintTop_toTopOf="parent" - tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> diff --git a/vector/src/main/res/layout/fragment_user_code_show.xml b/vector/src/main/res/layout/fragment_user_code_show.xml index 19350502cf..e77b51591c 100644 --- a/vector/src/main/res/layout/fragment_user_code_show.xml +++ b/vector/src/main/res/layout/fragment_user_code_show.xml @@ -109,7 +109,7 @@ android:textColor="?riotx_text_primary" android:textSize="15sp" app:layout_constraintTop_toTopOf="parent" - tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml index 35a6232294..2d2b33141b 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -38,7 +38,7 @@ app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar" tools:fontFamily="sans-serif" - tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_create_direct_room_user.xml b/vector/src/main/res/layout/item_create_direct_room_user.xml index 66ee9889c3..556a344359 100644 --- a/vector/src/main/res/layout/item_create_direct_room_user.xml +++ b/vector/src/main/res/layout/item_create_direct_room_user.xml @@ -55,7 +55,7 @@ app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/createDirectRoomUserAvatarContainer" app:layout_constraintTop_toTopOf="parent" - tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_display_read_receipt.xml b/vector/src/main/res/layout/item_display_read_receipt.xml index 791b4bfaff..58ddf8e5ad 100644 --- a/vector/src/main/res/layout/item_display_read_receipt.xml +++ b/vector/src/main/res/layout/item_display_read_receipt.xml @@ -22,7 +22,7 @@ + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_profile_matrix_item.xml b/vector/src/main/res/layout/item_profile_matrix_item.xml index d09086418b..db7c36658a 100644 --- a/vector/src/main/res/layout/item_profile_matrix_item.xml +++ b/vector/src/main/res/layout/item_profile_matrix_item.xml @@ -51,7 +51,7 @@ app:layout_constraintStart_toEndOf="@id/matrixItemAvatar" app:layout_constraintTop_toTopOf="parent" app:layout_goneMarginEnd="80dp" - tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/vector_invite_view.xml b/vector/src/main/res/layout/vector_invite_view.xml index 5b0691e98b..7af3262248 100644 --- a/vector/src/main/res/layout/vector_invite_view.xml +++ b/vector/src/main/res/layout/vector_invite_view.xml @@ -30,7 +30,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/inviteAvatarView" - tools:text="@sample/matrix.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> + tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/id" /> Date: Mon, 10 May 2021 22:53:51 +0200 Subject: [PATCH 033/202] Split sample step 2: messages --- vector/sampledata/matrix.json | 6 ----- vector/sampledata/messages.json | 22 +++++++++++++++++++ .../layout/bottom_sheet_space_settings.xml | 2 +- vector/src/main/res/layout/item_room.xml | 2 +- .../main/res/layout/item_room_invitation.xml | 2 +- .../main/res/layout/item_search_result.xml | 2 +- .../main/res/layout/item_suggested_room.xml | 2 +- .../item_timeline_event_text_message_stub.xml | 2 +- 8 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 vector/sampledata/messages.json diff --git a/vector/sampledata/matrix.json b/vector/sampledata/matrix.json index 33fe511611..b924e5f351 100644 --- a/vector/sampledata/matrix.json +++ b/vector/sampledata/matrix.json @@ -1,42 +1,36 @@ { "data": [ { - "message": "William Shakespeare (bapt. 26 April 1564 – 23 April 1616) was an English poet, playwright and actor, widely regarded as the greatest writer in the English language and the world's greatest dramatist. He is often called England's national poet and the \"Bard of Avon\". His extant works, including collaborations, consist of approximately 39 plays, 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.\n\nShakespeare was born and raised in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. At age 49 (around 1613), he appears to have retired to Stratford, where he died three years later. Few records of Shakespeare's private life survive; this has stimulated considerable speculation about such matters as his physical appearance, his sexuality, his religious beliefs, and whether the works attributed to him were written by others. Such theories are often criticised for failing to adequately note that few records survive of most commoners of the period.\n\nShakespeare produced most of his known works between 1589 and 1613. His early plays were primarily comedies and histories and are regarded as some of the best work produced in these genres. Until about 1608, he wrote mainly tragedies, among them Hamlet, Othello, King Lear, and Macbeth, all considered to be among the finest works in the English language. In the last phase of his life, he wrote tragicomedies (also known as romances) and collaborated with other playwrights.\n\nMany of Shakespeare's plays were published in editions of varying quality and accuracy in his lifetime. However, in 1623, two fellow actors and friends of Shakespeare's, John Heminges and Henry Condell, published a more definitive text known as the First Folio, a posthumous collected edition of Shakespeare's dramatic works that included all but two of his plays. The volume was prefaced with a poem by Ben Jonson, in which Jonson presciently hails Shakespeare in a now-famous quote as \"not of an age, but for all time\".\n\nThroughout the 20th and 21st centuries, Shakespeare's works have been continually adapted and rediscovered by new movements in scholarship and performance. His plays remain popular and are studied, performed, and reinterpreted through various cultural and political contexts around the world.", "roomName": "Matrix HQ", "roomAlias": "#matrix:matrix.org", "spaceName": "Runner's world", "roomTopic": "Welcome to Matrix HQ! Here is the rest of the room topic, with a https://www.example.org url and a phone number: 0102030405 which should not be clickable." }, { - "message": "Hello!", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", "spaceName": "Matrix Org", "roomTopic": "Room topic very loooooooong with some details" }, { - "message": "How are you?", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", "spaceName": "Rennes", "roomTopic": "Room topic very loooooooong with some details" }, { - "message": "Great weather today!", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", "spaceName": "Est London", "roomTopic": "Room topic very loooooooong with some details" }, { - "message": "Let's do a picnic", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", "spaceName": "Element HQ", "roomTopic": "Room topic very loooooooong with some details" }, { - "message": "Yes, great idea", "roomName": "Room name very loooooooong with some details", "roomAlias": "#matrix:matrix.org", "spaceName": "My Company", diff --git a/vector/sampledata/messages.json b/vector/sampledata/messages.json new file mode 100644 index 0000000000..8145277f6b --- /dev/null +++ b/vector/sampledata/messages.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "message": "William Shakespeare (bapt. 26 April 1564 – 23 April 1616) was an English poet, playwright and actor, widely regarded as the greatest writer in the English language and the world's greatest dramatist. He is often called England's national poet and the \"Bard of Avon\". His extant works, including collaborations, consist of approximately 39 plays, 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.\n\nShakespeare was born and raised in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. At age 49 (around 1613), he appears to have retired to Stratford, where he died three years later. Few records of Shakespeare's private life survive; this has stimulated considerable speculation about such matters as his physical appearance, his sexuality, his religious beliefs, and whether the works attributed to him were written by others. Such theories are often criticised for failing to adequately note that few records survive of most commoners of the period.\n\nShakespeare produced most of his known works between 1589 and 1613. His early plays were primarily comedies and histories and are regarded as some of the best work produced in these genres. Until about 1608, he wrote mainly tragedies, among them Hamlet, Othello, King Lear, and Macbeth, all considered to be among the finest works in the English language. In the last phase of his life, he wrote tragicomedies (also known as romances) and collaborated with other playwrights.\n\nMany of Shakespeare's plays were published in editions of varying quality and accuracy in his lifetime. However, in 1623, two fellow actors and friends of Shakespeare's, John Heminges and Henry Condell, published a more definitive text known as the First Folio, a posthumous collected edition of Shakespeare's dramatic works that included all but two of his plays. The volume was prefaced with a poem by Ben Jonson, in which Jonson presciently hails Shakespeare in a now-famous quote as \"not of an age, but for all time\".\n\nThroughout the 20th and 21st centuries, Shakespeare's works have been continually adapted and rediscovered by new movements in scholarship and performance. His plays remain popular and are studied, performed, and reinterpreted through various cultural and political contexts around the world." + }, + { + "message": "Hello!" + }, + { + "message": "How are you?" + }, + { + "message": "Great weather today!" + }, + { + "message": "Let's do a picnic" + }, + { + "message": "Yes, great idea" + } + ] +} diff --git a/vector/src/main/res/layout/bottom_sheet_space_settings.xml b/vector/src/main/res/layout/bottom_sheet_space_settings.xml index 8cd7e9bd70..a7ec33a9b0 100644 --- a/vector/src/main/res/layout/bottom_sheet_space_settings.xml +++ b/vector/src/main/res/layout/bottom_sheet_space_settings.xml @@ -61,7 +61,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/spaceAvatarImageView" app:layout_constraintTop_toBottomOf="@+id/spaceNameView" - tools:text="@sample/matrix.json/data/message" + tools:text="@sample/messages.json/data/message" tools:visibility="visible" /> diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index e3722fda6a..4cfd2cf1bc 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -179,7 +179,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/roomNameView" app:layout_constraintTop_toBottomOf="@+id/roomNameView" - tools:text="@sample/matrix.json/data/message" /> + tools:text="@sample/messages.json/data/message" /> + tools:text="@sample/messages.json/data/message" /> + tools:text="@sample/messages.json/data/message" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_suggested_room.xml b/vector/src/main/res/layout/item_suggested_room.xml index f952f5be06..f1966f9c8e 100644 --- a/vector/src/main/res/layout/item_suggested_room.xml +++ b/vector/src/main/res/layout/item_suggested_room.xml @@ -70,7 +70,7 @@ app:layout_constraintEnd_toStartOf="@id/joinSuggestedRoomButton" app:layout_constraintStart_toStartOf="@+id/roomNameView" app:layout_constraintTop_toBottomOf="@+id/roomNameView" - tools:text="@sample/matrix.json/data/message" /> + tools:text="@sample/messages.json/data/message" /> + tools:text="@sample/messages.json/data/message" /> Date: Mon, 10 May 2021 23:07:08 +0200 Subject: [PATCH 034/202] Split sample step 3: rooms and spaces --- vector/sampledata/matrix.json | 40 ------------------- vector/sampledata/rooms.json | 14 +++++++ vector/sampledata/spaces.json | 28 +++++++++++++ .../layout/bottom_sheet_space_settings.xml | 4 +- .../res/layout/fragment_matrix_profile.xml | 2 +- .../fragment_matrix_to_room_space_card.xml | 7 ++-- .../main/res/layout/fragment_room_detail.xml | 4 +- .../fragment_room_preview_no_preview.xml | 4 +- .../layout/fragment_room_setting_generic.xml | 2 +- .../main/res/layout/fragment_room_uploads.xml | 2 +- .../res/layout/fragment_space_add_rooms.xml | 2 +- .../layout/item_bottom_sheet_room_preview.xml | 2 +- .../res/layout/item_expandable_textview.xml | 2 +- .../src/main/res/layout/item_public_room.xml | 6 +-- .../main/res/layout/item_room_invitation.xml | 4 +- .../res/layout/item_room_to_add_in_space.xml | 2 +- .../item_room_to_add_in_space_placeholder.xml | 22 +--------- .../main/res/layout/item_space_roomchild.xml | 4 +- .../main/res/layout/item_space_subspace.xml | 2 +- .../res/layout/item_space_top_summary.xml | 2 +- .../main/res/layout/item_suggested_room.xml | 2 +- ...meline_event_merged_room_creation_stub.xml | 2 +- .../src/main/res/layout/item_unknown_room.xml | 2 +- .../layout/view_stub_room_profile_header.xml | 4 +- 24 files changed, 74 insertions(+), 91 deletions(-) delete mode 100644 vector/sampledata/matrix.json create mode 100644 vector/sampledata/rooms.json create mode 100644 vector/sampledata/spaces.json diff --git a/vector/sampledata/matrix.json b/vector/sampledata/matrix.json deleted file mode 100644 index b924e5f351..0000000000 --- a/vector/sampledata/matrix.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "data": [ - { - "roomName": "Matrix HQ", - "roomAlias": "#matrix:matrix.org", - "spaceName": "Runner's world", - "roomTopic": "Welcome to Matrix HQ! Here is the rest of the room topic, with a https://www.example.org url and a phone number: 0102030405 which should not be clickable." - }, - { - "roomName": "Room name very loooooooong with some details", - "roomAlias": "#matrix:matrix.org", - "spaceName": "Matrix Org", - "roomTopic": "Room topic very loooooooong with some details" - }, - { - "roomName": "Room name very loooooooong with some details", - "roomAlias": "#matrix:matrix.org", - "spaceName": "Rennes", - "roomTopic": "Room topic very loooooooong with some details" - }, - { - "roomName": "Room name very loooooooong with some details", - "roomAlias": "#matrix:matrix.org", - "spaceName": "Est London", - "roomTopic": "Room topic very loooooooong with some details" - }, - { - "roomName": "Room name very loooooooong with some details", - "roomAlias": "#matrix:matrix.org", - "spaceName": "Element HQ", - "roomTopic": "Room topic very loooooooong with some details" - }, - { - "roomName": "Room name very loooooooong with some details", - "roomAlias": "#matrix:matrix.org", - "spaceName": "My Company", - "roomTopic": "Room topic very loooooooong with some details" - } - ] -} diff --git a/vector/sampledata/rooms.json b/vector/sampledata/rooms.json new file mode 100644 index 0000000000..9304aa308d --- /dev/null +++ b/vector/sampledata/rooms.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "name": "Matrix HQ", + "alias": "#matrix:matrix.org", + "topic": "Welcome to Matrix HQ! Here is the rest of the room topic, with a https://www.example.org url and a phone number: 0102030405 which should not be clickable." + }, + { + "name": "Room name very loooooooong with some details", + "alias": "#matrix:matrix.org", + "topic": "Room topic very loooooooong with some details" + } + ] +} diff --git a/vector/sampledata/spaces.json b/vector/sampledata/spaces.json new file mode 100644 index 0000000000..f58c2ca5da --- /dev/null +++ b/vector/sampledata/spaces.json @@ -0,0 +1,28 @@ +{ + "data": [ + { + "name": "Runner's world", + "topic": "Space about running around the world!" + }, + { + "name": "Matrix Org", + "topic": "Space about matrix.org!" + }, + { + "name": "Rennes", + "topic": "Venez visiter Rennes!" + }, + { + "name": "Est London", + "topic": "All about Est London!" + }, + { + "name": "Element HQ", + "topic": "All about Element!" + }, + { + "name": "My Company", + "topic": "All about My company!" + } + ] +} diff --git a/vector/src/main/res/layout/bottom_sheet_space_settings.xml b/vector/src/main/res/layout/bottom_sheet_space_settings.xml index a7ec33a9b0..5679347766 100644 --- a/vector/src/main/res/layout/bottom_sheet_space_settings.xml +++ b/vector/src/main/res/layout/bottom_sheet_space_settings.xml @@ -43,7 +43,7 @@ app:layout_constraintStart_toEndOf="@id/spaceAvatarImageView" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" - tools:text="@sample/users.json/data/displayName" /> + tools:text="@sample/spaces.json/data/name" /> diff --git a/vector/src/main/res/layout/fragment_matrix_profile.xml b/vector/src/main/res/layout/fragment_matrix_profile.xml index 697541497a..398aaf2c4a 100644 --- a/vector/src/main/res/layout/fragment_matrix_profile.xml +++ b/vector/src/main/res/layout/fragment_matrix_profile.xml @@ -87,7 +87,7 @@ app:layout_constraintStart_toEndOf="@+id/matrixProfileToolbarAvatarImageView" app:layout_constraintTop_toTopOf="parent" tools:alpha="1" - tools:text="@sample/matrix.json/data/roomName" /> + tools:text="@sample/rooms.json/data/name" /> diff --git a/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml b/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml index 4635cabffe..efe21ddfb1 100644 --- a/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml +++ b/vector/src/main/res/layout/fragment_matrix_to_room_space_card.xml @@ -52,7 +52,7 @@ android:textSize="15sp" android:textStyle="bold" app:layout_constraintTop_toBottomOf="@+id/matrixToCardAvatar" - tools:text="@sample/matrix.json/data/roomName" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/alias" + tools:visibility="visible" /> + tools:text="@sample/rooms.json/data/topic" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/topic" /> diff --git a/vector/src/main/res/layout/fragment_room_preview_no_preview.xml b/vector/src/main/res/layout/fragment_room_preview_no_preview.xml index d840d52f21..dc23a8f33f 100644 --- a/vector/src/main/res/layout/fragment_room_preview_no_preview.xml +++ b/vector/src/main/res/layout/fragment_room_preview_no_preview.xml @@ -93,7 +93,7 @@ android:textAppearance="@style/TextAppearance.Vector.Title" android:textSize="15sp" android:textStyle="bold" - tools:text="@sample/matrix.json/data/roomName" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/topic" /> + tools:text="@sample/rooms.json/data/name" /> diff --git a/vector/src/main/res/layout/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml index 421059bd75..39ba60f1a7 100644 --- a/vector/src/main/res/layout/fragment_room_uploads.xml +++ b/vector/src/main/res/layout/fragment_room_uploads.xml @@ -62,7 +62,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/roomUploadsToolbarAvatarImageView" app:layout_constraintTop_toTopOf="parent" - tools:text="@sample/matrix.json/data/roomName" /> + tools:text="@sample/rooms.json/data/name" /> diff --git a/vector/src/main/res/layout/fragment_space_add_rooms.xml b/vector/src/main/res/layout/fragment_space_add_rooms.xml index 6619d06cca..a084fa943d 100644 --- a/vector/src/main/res/layout/fragment_space_add_rooms.xml +++ b/vector/src/main/res/layout/fragment_space_add_rooms.xml @@ -63,7 +63,7 @@ android:maxLines="1" android:textColor="?riotx_text_secondary" android:textSize="16sp" - tools:text="@sample/matrix.json/data/spaceName" /> + tools:text="@sample/spaces.json/data/name" /> diff --git a/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml index 384a097f18..9e98774eaf 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml @@ -42,7 +42,7 @@ app:layout_constraintStart_toEndOf="@id/bottomSheetRoomPreviewAvatar" app:layout_constraintTop_toTopOf="@id/bottomSheetRoomPreviewAvatar" tools:fontFamily="sans-serif" - tools:text="@sample/matrix.json/data/roomName" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/topic" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/alias" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/topic" /> + tools:text="@sample/rooms.json/data/name" /> - - - - - - - - - - - - - + app:layout_constraintVertical_chainStyle="packed" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_roomchild.xml b/vector/src/main/res/layout/item_space_roomchild.xml index 799f9730a1..536a749344 100644 --- a/vector/src/main/res/layout/item_space_roomchild.xml +++ b/vector/src/main/res/layout/item_space_roomchild.xml @@ -57,7 +57,7 @@ app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/childRoomAvatar" app:layout_constraintTop_toTopOf="parent" - tools:text="@sample/matrix.json/data/spaceName" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/topic" /> + tools:text="@sample/spaces.json/data/name" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_space_top_summary.xml b/vector/src/main/res/layout/item_space_top_summary.xml index 27b21ece9b..fd451e7b93 100644 --- a/vector/src/main/res/layout/item_space_top_summary.xml +++ b/vector/src/main/res/layout/item_space_top_summary.xml @@ -44,6 +44,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/spaceSummaryMemberCountIcon" - tools:text="@sample/matrix.json/data/roomTopic" /> + tools:text="@sample/spaces.json/data/topic" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_suggested_room.xml b/vector/src/main/res/layout/item_suggested_room.xml index f1966f9c8e..dd1c5ba01e 100644 --- a/vector/src/main/res/layout/item_suggested_room.xml +++ b/vector/src/main/res/layout/item_suggested_room.xml @@ -70,7 +70,7 @@ app:layout_constraintEnd_toStartOf="@id/joinSuggestedRoomButton" app:layout_constraintStart_toStartOf="@+id/roomNameView" app:layout_constraintTop_toBottomOf="@+id/roomNameView" - tools:text="@sample/messages.json/data/message" /> + tools:text="@sample/rooms.json/data/topic" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/alias" /> From 9bc02d09c788c484ec0ac5b9199d46df28f005f6 Mon Sep 17 00:00:00 2001 From: gradle-update-robot Date: Tue, 11 May 2021 00:06:36 +0000 Subject: [PATCH 035/202] Update Gradle Wrapper from 7.0 to 7.0.1. Signed-off-by: gradle-update-robot --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9d174797f7..099f10061c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=81003f83b0056d20eedf48cddd4f52a9813163d4ba185bcf8abd34b8eeea4cbd -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip +distributionSha256Sum=ca42877db3519b667cd531c414be517b294b0467059d401e7133f0e55b9bf265 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From f31c44963bb522ea65079a4fe78af6dd36dcec71 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 May 2021 11:12:27 +0200 Subject: [PATCH 036/202] Cleanup the existing code --- .../sdk/internal/database/RealmKeysUtils.kt | 4 +- .../securestorage/SecretStoringUtils.kt | 70 +++---------------- 2 files changed, 12 insertions(+), 62 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt index 0e3a7a2c49..d5ff7a0f84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt @@ -71,7 +71,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context, val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING) val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias) sharedPreferences.edit { - putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING)) + putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING)) } return key } @@ -84,7 +84,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context, val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null) val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING) val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias) - return Base64.decode(b64!!, Base64.NO_PADDING) + return Base64.decode(b64, Base64.NO_PADDING) } fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt index c3b2d7f161..c6c8cedf9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt @@ -58,23 +58,19 @@ import javax.security.auth.x500.X500Principal * is not available. * * Android [K-M[ - * For android >=KITKAT and =L and Older androids - * For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt. - * The salt and iv are stored with encrypted data. - * * Sample usage: * * val secret = "The answer is 42" - * val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context) + * val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias") * //This can be stored anywhere e.g. encoded in b64 and stored in preference for example * * //to get back the secret, just call - * val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context) + * val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted, "myAlias") * * * You can also just use this utility to store a secret key, and use any encryption algorithm that you want. @@ -91,7 +87,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte private const val FORMAT_API_M: Byte = 0 private const val FORMAT_1: Byte = 1 - private const val FORMAT_2: Byte = 2 } private val keyStore: KeyStore by lazy { @@ -113,15 +108,14 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte /** * Encrypt the given secret using the android Keystore. * On android >= M, will directly use the keystore to generate a symmetric key - * On android >= KitKat and = Lollipop and = Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) else -> encryptString(secret, keyAlias) @@ -132,7 +126,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte * Decrypt a secret that was encrypted by #securelyStoreString() */ @Throws(Exception::class) - fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? { + fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String { return when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) else -> decryptString(encrypted, keyAlias) @@ -180,7 +174,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte - Store the encrypted AES Generate a key pair for encryption */ - fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry { + private fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry { val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry) if (privateKeyEntry != null) return privateKeyEntry @@ -205,7 +199,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte } @RequiresApi(Build.VERSION_CODES.M) - fun encryptStringM(text: String, keyAlias: String): ByteArray? { + private fun encryptStringM(text: String, keyAlias: String): ByteArray { val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) val cipher = Cipher.getInstance(AES_MODE) @@ -229,7 +223,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte return String(cipher.doFinal(encryptedText), Charsets.UTF_8) } - private fun encryptString(text: String, keyAlias: String): ByteArray? { + private fun encryptString(text: String, keyAlias: String): ByteArray { // we generate a random symmetric key val key = ByteArray(16) secureRandom.nextBytes(key) @@ -246,7 +240,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte return format1Make(encryptedKey, iv, encryptedBytes) } - private fun decryptString(data: ByteArray, keyAlias: String): String? { + private fun decryptString(data: ByteArray, keyAlias: String): String { val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) // we need to decrypt the key @@ -307,22 +301,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte output.write(bos1.toByteArray()) } -// @RequiresApi(Build.VERSION_CODES.M) -// @Throws(IOException::class) -// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) { -// FileOutputStream(file).use { -// saveSecureObjectM(keyAlias, it, writeObject) -// } -// } -// -// @RequiresApi(Build.VERSION_CODES.M) -// @Throws(IOException::class) -// fun loadSecureObjectM(keyAlias: String, file: File): T? { -// FileInputStream(file).use { -// return loadSecureObjectM(keyAlias, it) -// } -// } - @RequiresApi(Build.VERSION_CODES.M) @Throws(IOException::class) private fun loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { @@ -443,32 +421,4 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte return bos.toByteArray() } - - private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray { - val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size) - bos.write(FORMAT_2.toInt()) - bos.write(salt.size) - bos.write(salt) - bos.write(iv.size) - bos.write(iv) - bos.write(encryptedBytes) - - return bos.toByteArray() - } - - private fun format2Extract(bis: InputStream): Triple { - val format = bis.read() - assert(format.toByte() == FORMAT_2) - - val saltSize = bis.read() - val salt = ByteArray(saltSize) - bis.read(salt) - - val ivSize = bis.read() - val iv = ByteArray(ivSize) - bis.read(iv) - - val encrypted = bis.readBytes() - return Triple(salt, iv, encrypted) - } } From cef4cf09ec8f85e5f36eab2fd112709dfcf87642 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 May 2021 11:55:54 +0200 Subject: [PATCH 037/202] Create a BuildVersionSdkIntProvider to be able to inject it and do some test To merge with BuildVersionSdkIntProvider To merge with fix add module To merge with fix buildVersionSdkIntProvider --- .../android/sdk/common/TestMatrixComponent.kt | 2 ++ .../sdk/internal/di/MatrixComponent.kt | 2 ++ .../sdk/internal/session/SessionComponent.kt | 2 ++ .../securestorage/SecretStoringUtils.kt | 27 ++++++++++++------- .../util/system/BuildVersionSdkIntProvider.kt | 24 +++++++++++++++++ .../DefaultBuildVersionSdkIntProvider.kt | 25 +++++++++++++++++ .../sdk/internal/util/system/SystemModule.kt | 27 +++++++++++++++++++ 7 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt index 33de345630..1d05e655af 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.di.MatrixModule import org.matrix.android.sdk.internal.di.MatrixScope import org.matrix.android.sdk.internal.di.NetworkModule import org.matrix.android.sdk.internal.raw.RawModule +import org.matrix.android.sdk.internal.util.system.SystemModule @Component(modules = [ TestModule::class, @@ -33,6 +34,7 @@ import org.matrix.android.sdk.internal.raw.RawModule NetworkModule::class, AuthModule::class, RawModule::class, + SystemModule::class, TestNetworkModule::class ]) @MatrixScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index 9d6fa29bb2..5bc519e960 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.TestInterceptor import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.system.SystemModule import org.matrix.olm.OlmManager import java.io.File @@ -44,6 +45,7 @@ import java.io.File NetworkModule::class, AuthModule::class, RawModule::class, + SystemModule::class, NoOpTestModule::class ]) @MatrixScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 541c877b1d..9a936b73c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -64,6 +64,7 @@ import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModul import org.matrix.android.sdk.internal.session.widgets.WidgetModule import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.system.SystemModule @Component(dependencies = [MatrixComponent::class], modules = [ @@ -80,6 +81,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers CacheModule::class, MediaModule::class, CryptoModule::class, + SystemModule::class, PushersModule::class, OpenIdModule::class, WidgetModule::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt index c6c8cedf9d..5be608ce13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt @@ -18,12 +18,14 @@ package org.matrix.android.sdk.internal.session.securestorage +import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import androidx.annotation.RequiresApi +import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider import timber.log.Timber import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -78,7 +80,10 @@ import javax.security.auth.x500.X500Principal * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you * add a pin or change the schema); So you might and with a useless pile of bytes. */ -internal class SecretStoringUtils @Inject constructor(private val context: Context) { +internal class SecretStoringUtils @Inject constructor( + private val context: Context, + private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider +) { companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" @@ -114,36 +119,40 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte * * The secret is encrypted using the following method: AES/GCM/NoPadding */ + @SuppressLint("NewApi") @Throws(Exception::class) fun securelyStoreString(secret: String, keyAlias: String): ByteArray { return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) - else -> encryptString(secret, keyAlias) + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) + else -> encryptString(secret, keyAlias) } } /** * Decrypt a secret that was encrypted by #securelyStoreString() */ + @SuppressLint("NewApi") @Throws(Exception::class) fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String { return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) - else -> decryptString(encrypted, keyAlias) + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) + else -> decryptString(encrypted, keyAlias) } } + @SuppressLint("NewApi") fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) - else -> saveSecureObject(keyAlias, output, any) + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) + else -> saveSecureObject(keyAlias, output, any) } } + @SuppressLint("NewApi") fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream) - else -> loadSecureObject(keyAlias, inputStream) + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream) + else -> loadSecureObject(keyAlias, inputStream) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt new file mode 100644 index 0000000000..1736f10108 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util.system + +internal interface BuildVersionSdkIntProvider { + /** + * Return the current version of the Android SDK + */ + fun get(): Int +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt new file mode 100644 index 0000000000..a206918dec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util.system + +import android.os.Build +import javax.inject.Inject + +internal class DefaultBuildVersionSdkIntProvider @Inject constructor() + : BuildVersionSdkIntProvider { + override fun get() = Build.VERSION.SDK_INT +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt new file mode 100644 index 0000000000..fdbb5c9d97 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util.system + +import dagger.Binds +import dagger.Module + +@Module +internal abstract class SystemModule { + + @Binds + abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider +} From 91be2b6f3f444a589ec22c415fb07b28ae5ea7b1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 May 2021 13:55:29 +0200 Subject: [PATCH 038/202] Add test and handle system upgrade --- CHANGES.md | 1 + .../securestorage/SecretStoringUtilsTest.kt | 184 ++++++++++++++++++ .../TestBuildVersionSdkIntProvider.kt | 25 +++ .../securestorage/SecretStoringUtils.kt | 38 ++-- 4 files changed, 228 insertions(+), 20 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt diff --git a/CHANGES.md b/CHANGES.md index 89121bdf54..100c9972ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,7 @@ Bugfix 🐛: - Properly clean the back stack if the user cancel registration when waiting for email validation - Fix read marker visibility/position when filtering some events - Fix user invitation in case of restricted profile api (#3306) + - Make sure the SDK can retrieve the secret storage if the system is upgraded (#3304) Translations 🗣: - diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt new file mode 100644 index 0000000000..7ee6caed0d --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.securestorage + +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.amshove.kluent.shouldBeEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import java.io.ByteArrayOutputStream +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SecretStoringUtilsTest : InstrumentedTest { + + private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() + private val secretStoringUtils = SecretStoringUtils(context(), buildVersionSdkIntProvider) + + companion object { + const val TEST_STR = "This is something I want to store safely!" + } + + @Test + fun testStringNominalCaseApi21() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + // Encrypt + val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + // Decrypt + val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testStringNominalCaseApi23() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + // Encrypt + val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + // Decrypt + val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testStringNominalCaseApi30() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.R + // Encrypt + val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + // Decrypt + val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testStringMigration21_23() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + // Encrypt + val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + + // Simulate a system upgrade + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + + // Decrypt + val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testObjectNominalCaseApi21() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + + // Encrypt + val encrypted = ByteArrayOutputStream().also { outputStream -> + outputStream.use { + secretStoringUtils.securelyStoreObject(TEST_STR, alias, it) + } + } + .toByteArray() + .toBase64NoPadding() + // Decrypt + val decrypted = encrypted.fromBase64().inputStream().use { + secretStoringUtils.loadSecureSecret(it, alias) + } + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testObjectNominalCaseApi23() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + + // Encrypt + val encrypted = ByteArrayOutputStream().also { outputStream -> + outputStream.use { + secretStoringUtils.securelyStoreObject(TEST_STR, alias, it) + } + } + .toByteArray() + .toBase64NoPadding() + // Decrypt + val decrypted = encrypted.fromBase64().inputStream().use { + secretStoringUtils.loadSecureSecret(it, alias) + } + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testObjectNominalCaseApi30() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.R + + // Encrypt + val encrypted = ByteArrayOutputStream().also { outputStream -> + outputStream.use { + secretStoringUtils.securelyStoreObject(TEST_STR, alias, it) + } + } + .toByteArray() + .toBase64NoPadding() + // Decrypt + val decrypted = encrypted.fromBase64().inputStream().use { + secretStoringUtils.loadSecureSecret(it, alias) + } + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testObjectMigration21_23() { + val alias = generateAlias() + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + + // Encrypt + val encrypted = ByteArrayOutputStream().also { outputStream -> + outputStream.use { + secretStoringUtils.securelyStoreObject(TEST_STR, alias, it) + } + } + .toByteArray() + .toBase64NoPadding() + + // Simulate a system upgrade + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + + // Decrypt + val decrypted = encrypted.fromBase64().inputStream().use { + secretStoringUtils.loadSecureSecret(it, alias) + } + decrypted shouldBeEqualTo TEST_STR + secretStoringUtils.safeDeleteKey(alias) + } + + private fun generateAlias() = UUID.randomUUID().toString() +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt new file mode 100644 index 0000000000..e44a62e24e --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.securestorage + +import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider + +class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider { + var value: Int = 0 + + override fun get() = value +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt index 5be608ce13..fad1840e51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt @@ -34,6 +34,7 @@ import java.io.InputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.OutputStream +import java.lang.IllegalArgumentException import java.math.BigInteger import java.security.KeyPairGenerator import java.security.KeyStore @@ -134,9 +135,13 @@ internal class SecretStoringUtils @Inject constructor( @SuppressLint("NewApi") @Throws(Exception::class) fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String { - return when { - buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) - else -> decryptString(encrypted, keyAlias) + encrypted.inputStream().use { inputStream -> + // First get the format + return when (val format = inputStream.read().toByte()) { + FORMAT_API_M -> decryptStringM(inputStream, keyAlias) + FORMAT_1 -> decryptString(inputStream, keyAlias) + else -> throw IllegalArgumentException("Unknown format $format") + } } } @@ -150,9 +155,11 @@ internal class SecretStoringUtils @Inject constructor( @SuppressLint("NewApi") fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { - return when { - buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream) - else -> loadSecureObject(keyAlias, inputStream) + // First get the format + return when (val format = inputStream.read().toByte()) { + FORMAT_API_M -> loadSecureObjectM(keyAlias, inputStream) + FORMAT_1 -> loadSecureObject(keyAlias, inputStream) + else -> throw IllegalArgumentException("Unknown format $format") } } @@ -196,7 +203,7 @@ internal class SecretStoringUtils @Inject constructor( .setAlias(alias) .setSubject(X500Principal("CN=$alias")) .setSerialNumber(BigInteger.TEN) - // .setEncryptionRequired() requires that the phone as a pin/schema + // .setEncryptionRequired() requires that the phone has a pin/schema .setStartDate(start.time) .setEndDate(end.time) .build() @@ -220,8 +227,8 @@ internal class SecretStoringUtils @Inject constructor( } @RequiresApi(Build.VERSION_CODES.M) - private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String { - val (iv, encryptedText) = formatMExtract(encryptedChunk.inputStream()) + private fun decryptStringM(inputStream: InputStream, keyAlias: String): String { + val (iv, encryptedText) = formatMExtract(inputStream) val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) @@ -249,8 +256,8 @@ internal class SecretStoringUtils @Inject constructor( return format1Make(encryptedKey, iv, encryptedBytes) } - private fun decryptString(data: ByteArray, keyAlias: String): String { - val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) + private fun decryptString(inputStream: InputStream, keyAlias: String): String { + val (encryptedKey, iv, encrypted) = format1Extract(inputStream) // we need to decrypt the key val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) @@ -315,9 +322,6 @@ internal class SecretStoringUtils @Inject constructor( private fun loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) - val format = inputStream.read() - assert(format.toByte() == FORMAT_API_M) - val ivSize = inputStream.read() val iv = ByteArray(ivSize) inputStream.read(iv, 0, ivSize) @@ -380,9 +384,6 @@ internal class SecretStoringUtils @Inject constructor( } private fun formatMExtract(bis: InputStream): Pair { - val format = bis.read().toByte() - assert(format == FORMAT_API_M) - val ivSize = bis.read() val iv = ByteArray(ivSize) bis.read(iv, 0, ivSize) @@ -401,9 +402,6 @@ internal class SecretStoringUtils @Inject constructor( } private fun format1Extract(bis: InputStream): Triple { - val format = bis.read() - assert(format.toByte() == FORMAT_1) - val keySizeBig = bis.read() val keySizeLow = bis.read() val encryptedKeySize = keySizeBig.shl(8) + keySizeLow From 824a8a5c66a2d4afc6bd57ede404f2b902cb32f6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 May 2021 13:50:27 +0200 Subject: [PATCH 039/202] Fix copyright --- .../session/securestorage/TestBuildVersionSdkIntProvider.kt | 2 +- .../sdk/internal/util/system/BuildVersionSdkIntProvider.kt | 2 +- .../internal/util/system/DefaultBuildVersionSdkIntProvider.kt | 2 +- .../org/matrix/android/sdk/internal/util/system/SystemModule.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt index e44a62e24e..b08c88fb24 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt index 1736f10108..b660796ad8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt index a206918dec..d9f0064f38 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt index fdbb5c9d97..8a7b50175a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Matrix.org Foundation C.I.C. + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 3ddf1812245ef5a301ad7ac3656b2c1e8250bf0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 05:58:36 +0000 Subject: [PATCH 040/202] Bump firebase-messaging from 21.1.0 to 22.0.0 Bumps firebase-messaging from 21.1.0 to 22.0.0. Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 3ecdcea524..05f9334b0f 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -424,7 +424,7 @@ dependencies { kapt "com.google.dagger:dagger-compiler:$daggerVersion" // gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:21.1.0') { + gplayImplementation('com.google.firebase:firebase-messaging:22.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' From 8e28b31170c24f1967da82373862582ae1ccb633 Mon Sep 17 00:00:00 2001 From: Jeanne Lavoie Date: Tue, 11 May 2021 22:48:38 +0000 Subject: [PATCH 041/202] Translated using Weblate (French) Currently translated at 99.5% (2442 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- vector/src/main/res/values-fr/strings.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index acd069818d..1811c56de5 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -2742,8 +2742,8 @@ Créons un salon pour chacun d’entre eux. Vous pourrez en ajouter plus tard, y compris certains déjà existant. Sur quels projets travaillez-vous \? Nous allons créer les salons pour ces sujets. Vous pourrez en ajouter d’autres plus tard. - De quoi allez vous parler dans %s \? - Donnez lui un nom pour poursuivre. + De quoi allez-vous parler dans %s \? + Donnez-lui un nom pour poursuivre. Ajoutez quelques informations pour renforcer son identité. Vous pourrez changer ceci plus tard. Ajoutez quelques informations pour l’aider à se démarquer. Vous pourrez changer ceci plus tard. Créer un espace @@ -2758,4 +2758,9 @@ Assurez-vous que les accès à %s sont accordés aux bonnes personnes. Vous pourrez changer ceci plus tard. Avec qui travaillez-vous \? Pour rejoindre un espace existant, il vous faut une invitation. + Le fichier est trop volumineux pour être téléversé. + Compression de la vidéo %d %% + Compression de l’image… + Utiliser par défaut et ne plus demander + Toujours demander \ No newline at end of file From a42a9b5e32cb7f10b2ea9932783d843678dd2f8b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 May 2021 15:15:24 +0200 Subject: [PATCH 042/202] Version++ --- CHANGES.md | 27 +++++++++++++++++++++++++++ vector/build.gradle | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c1d3b8b733..8c10320816 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,30 @@ +Changes in Element 1.1.8 (2021-XX-XX) +=================================================== + +Features ✨: + - + +Improvements 🙌: + - + +Bugfix 🐛: + - + +Translations 🗣: + - + +SDK API changes ⚠️: + - + +Build 🧱: + - + +Test: + - + +Other changes: + - + Changes in Element 1.1.7 (2021-05-12) =================================================== diff --git a/vector/build.gradle b/vector/build.gradle index 3ecdcea524..95a6713127 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -14,7 +14,7 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 1 -ext.versionPatch = 7 +ext.versionPatch = 8 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From 278b0d7f750c910b43437120f73d16a9ff3865c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 May 2021 16:20:55 +0200 Subject: [PATCH 043/202] Sign APK with build tools 30.0.3 --- CHANGES.md | 2 +- tools/release/sign_apk.sh | 2 +- tools/release/sign_apk_unsafe.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8c10320816..39987201cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ SDK API changes ⚠️: - Build 🧱: - - + - Sign APK with build tools 30.0.3 Test: - diff --git a/tools/release/sign_apk.sh b/tools/release/sign_apk.sh index 77af5823c4..7697f58ceb 100755 --- a/tools/release/sign_apk.sh +++ b/tools/release/sign_apk.sh @@ -17,7 +17,7 @@ PARAM_KEYSTORE_PATH=$1 PARAM_APK=$2 # Other params -BUILD_TOOLS_VERSION="29.0.3" +BUILD_TOOLS_VERSION="30.0.3" MIN_SDK_VERSION=21 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." diff --git a/tools/release/sign_apk_unsafe.sh b/tools/release/sign_apk_unsafe.sh index b145ad45da..af5b0f0e32 100755 --- a/tools/release/sign_apk_unsafe.sh +++ b/tools/release/sign_apk_unsafe.sh @@ -23,7 +23,7 @@ PARAM_KS_PASS=$3 PARAM_KEY_PASS=$4 # Other params -BUILD_TOOLS_VERSION="29.0.3" +BUILD_TOOLS_VERSION="30.0.3" MIN_SDK_VERSION=21 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." From 31df08477e74483f1aa301a311e58130e3fbb4c1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 May 2021 16:24:20 +0200 Subject: [PATCH 044/202] Cleanup Travis config. --- .travis.yml | 46 ++-------------------------------------------- 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/.travis.yml b/.travis.yml index 85bddac7f3..6e67639284 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -# FTR: Configuration on https://travis-ci.org/vector-im/riotX-android/settings +# FTR: Configuration on https://travis-ci.org/github/vector-im/element-android/settings # # - Build only if .travis.yml is present -> On # - Limit concurrent jobs -> Off @@ -8,53 +8,11 @@ # - Auto cancel branch builds -> On # - Auto cancel pull request builds -> On -language: android -jdk: oraclejdk8 sudo: false notifications: email: false -android: - components: - # Uncomment the lines below if you want to - # use the latest revision of Android SDK Tools - - tools - - platform-tools - - # The BuildTools version used by your project - - build-tools-29.0.3 - - # The SDK version used to compile your project - - android-29 - -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ - - $HOME/.android/build-cache - -# Build with the development SDK -before_script: - # Not necessary for the moment - # - /bin/sh ./set_debug_env.sh - -# Just build the project for now +# Just run a simple script here script: - # Build app (assembleGplayRelease assembleFdroidRelease) - # Build Android test (assembleAndroidTest) (disabled for now) - # Code quality (lintGplayRelease lintFdroidRelease) - # Split into two steps because if a task contain Fdroid, PlayService will be disabled - # Done by Buildkite now: - ./gradlew clean assembleGplayRelease lintGplayRelease --stacktrace - # Done by Buildkite now: - ./gradlew clean assembleFdroidRelease lintFdroidRelease --stacktrace - # Run unitary test (Disable for now, see https://travis-ci.org/vector-im/riot-android/builds/502504370) - # - ./gradlew testGplayReleaseUnitTest --stacktrace - # Other code quality check - # Done by Buildkite now: - ./tools/check/check_code_quality.sh - ./tools/travis/check_pr.sh - # Check that indonesians file are identical. Due to Android issue, the resource folder must be value-in/, and Weblate export data into value-id/. - # Done by Buildkite now: - diff ./vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-in/strings.xml From dfc8860ade5b944bc3464b787a52246aced29e1e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 May 2021 16:43:15 +0200 Subject: [PATCH 045/202] Fix avatar sample after PR merge --- vector/src/main/res/layout/item_editable_square_avatar.xml | 2 +- vector/src/main/res/layout/item_room_to_manage_in_space.xml | 2 +- vector/src/main/res/layout/item_sub_space.xml | 2 +- .../src/main/res/layout/item_timeline_event_default_stub.xml | 2 +- .../layout/item_timeline_event_merged_room_creation_stub.xml | 4 ++-- .../src/main/res/layout/item_timeline_event_notice_stub.xml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vector/src/main/res/layout/item_editable_square_avatar.xml b/vector/src/main/res/layout/item_editable_square_avatar.xml index d931e807ea..bfd806c520 100644 --- a/vector/src/main/res/layout/item_editable_square_avatar.xml +++ b/vector/src/main/res/layout/item_editable_square_avatar.xml @@ -28,7 +28,7 @@ android:importantForAccessibility="no" android:scaleType="fitCenter" tools:alpha="0.3" - tools:src="@tools:sample/avatars" /> + tools:src="@sample/space_avatars" /> diff --git a/vector/src/main/res/layout/item_room_to_manage_in_space.xml b/vector/src/main/res/layout/item_room_to_manage_in_space.xml index d6b5eb2acd..8513649ad1 100644 --- a/vector/src/main/res/layout/item_room_to_manage_in_space.xml +++ b/vector/src/main/res/layout/item_room_to_manage_in_space.xml @@ -38,7 +38,7 @@ app:layout_constraintStart_toEndOf="@id/itemAddRoomRoomAvatar" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" - tools:text="@sample/matrix.json/data/roomName" /> + tools:text="@sample/rooms.json/data/name" /> + tools:src="@sample/space_avatars" /> + tools:srcCompat="@sample/user_round_avatars" /> + tools:srcCompat="@sample/room_round_avatars" /> + tools:srcCompat="@sample/user_round_avatars" /> + tools:srcCompat="@sample/user_round_avatars" /> Date: Wed, 12 May 2021 14:50:20 +0000 Subject: [PATCH 046/202] Bump daggerVersion from 2.35 to 2.35.1 Bumps `daggerVersion` from 2.35 to 2.35.1. Updates `dagger` from 2.35 to 2.35.1 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.35...dagger-2.35.1) Updates `dagger-compiler` from 2.35 to 2.35.1 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.35...dagger-2.35.1) Signed-off-by: dependabot[bot] --- matrix-sdk-android/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 108240f94d..451d56970a 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -112,7 +112,7 @@ dependencies { def lifecycle_version = '2.2.0' def arch_version = '2.1.0' def markwon_version = '3.1.0' - def daggerVersion = '2.35' + def daggerVersion = '2.35.1' def work_version = '2.5.0' def retrofit_version = '2.9.0' diff --git a/vector/build.gradle b/vector/build.gradle index 720ae4d3d6..5aaa60beb1 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -297,7 +297,7 @@ dependencies { def big_image_viewer_version = '1.8.0' def glide_version = '4.12.0' def moshi_version = '1.12.0' - def daggerVersion = '2.35' + def daggerVersion = '2.35.1' def autofill_version = "1.1.0" def work_version = '2.5.0' def arch_version = '2.1.0' From cbd9ec33fe78782207bc95a18c44a4579aa5edc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 14:50:50 +0000 Subject: [PATCH 047/202] Bump android-snowfall from 1.2.0 to 1.2.1 Bumps [android-snowfall](https://github.com/JetradarMobile/android-snowfall) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/JetradarMobile/android-snowfall/releases) - [Changelog](https://github.com/JetradarMobile/android-snowfall/blob/master/CHANGELOG.md) - [Commits](https://github.com/JetradarMobile/android-snowfall/compare/1.2.0...1.2.1) Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 45c9e5fa5a..76c06b3a01 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -418,7 +418,7 @@ dependencies { // Chat effects implementation 'nl.dionsegijn:konfetti:1.3.2' - implementation 'com.github.jetradarmobile:android-snowfall:1.2.0' + implementation 'com.github.jetradarmobile:android-snowfall:1.2.1' // DI implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" From 3c0ac0a5768b9b216aee624e9e22d67b7d00cd68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 14:51:49 +0000 Subject: [PATCH 048/202] Bump kotlin_version from 1.4.32 to 1.5.0 Bumps `kotlin_version` from 1.4.32 to 1.5.0. Updates `kotlin-gradle-plugin` from 1.4.32 to 1.5.0 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/commits) Updates `kotlin-stdlib-jdk7` from 1.4.32 to 1.5.0 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/commits) Updates `kotlin-stdlib` from 1.4.32 to 1.5.0 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/commits) Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 78d9c39036..f0ba2c461f 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.4.32' + ext.kotlin_version = '1.5.0' ext.kotlin_coroutines_version = "1.4.2" repositories { google() From cd9b8c55f4d1009028da213f7cb8b04814a2c5b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 May 2021 06:27:41 +0000 Subject: [PATCH 049/202] Bump realm-gradle-plugin from 10.4.0 to 10.5.0 Bumps [realm-gradle-plugin](https://github.com/realm/realm-java) from 10.4.0 to 10.5.0. - [Release notes](https://github.com/realm/realm-java/releases) - [Changelog](https://github.com/realm/realm-java/blob/v10.5.0/CHANGELOG.md) - [Commits](https://github.com/realm/realm-java/compare/v10.4.0...v10.5.0) Signed-off-by: dependabot[bot] --- matrix-sdk-android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 451d56970a..85904758ef 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -9,7 +9,7 @@ buildscript { mavenCentral() } dependencies { - classpath "io.realm:realm-gradle-plugin:10.4.0" + classpath "io.realm:realm-gradle-plugin:10.5.0" } } From 90f6f9f7c1b0884baedf2c809af284cc6f66b29a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 May 2021 06:30:31 +0000 Subject: [PATCH 050/202] Bump libphonenumber from 8.12.22 to 8.12.23 Bumps [libphonenumber](https://github.com/google/libphonenumber) from 8.12.22 to 8.12.23. - [Release notes](https://github.com/google/libphonenumber/releases) - [Changelog](https://github.com/google/libphonenumber/blob/master/making-metadata-changes.md) - [Commits](https://github.com/google/libphonenumber/compare/v8.12.22...v8.12.23) Signed-off-by: dependabot[bot] --- matrix-sdk-android/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 451d56970a..1b0559366c 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -169,7 +169,7 @@ dependencies { implementation 'com.otaliastudios:transcoder:0.10.3' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.22' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.23' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.5.1' diff --git a/vector/build.gradle b/vector/build.gradle index d2b5c8d705..a9a8ba0924 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -343,7 +343,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.22' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.23' // rx implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' From 82bb0f19dd5f93459c2e43304f2da5a92dbf296d Mon Sep 17 00:00:00 2001 From: zeritti Date: Tue, 11 May 2021 19:59:24 +0000 Subject: [PATCH 051/202] Translated using Weblate (Czech) Currently translated at 100.0% (2454 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- vector/src/main/res/values-cs/strings.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index ae136a0274..4b2b5233a9 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -2804,4 +2804,24 @@ Dovolit hostům vstoupit Pozvání Doporučené místnosti + Správa místností a prostorů + Označit za nikoli doporučené + Označit za doporučené + Doporučeno + Učinit tento prostor veřejným + Správa místností + Hledáte někoho, kdo není v %s\? + %s Vás zve + Tato místnosti je veřejná + Poslat media v původní velikosti + + Poslat video v původní velikosti + Poslat videa v původní velikosti + Poslat videa v původní velikosti + + Soubor je pro nahrání příliš velký. + Komprimuji video %d%% + Komprimuji obrázek… + Použít výchozí a dále se neptat + Vždy se dotázat \ No newline at end of file From 39ee7bbff792e722c73b1089387df28689926c97 Mon Sep 17 00:00:00 2001 From: libexus Date: Wed, 12 May 2021 19:41:06 +0000 Subject: [PATCH 052/202] Translated using Weblate (German) Currently translated at 99.7% (2449 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- vector/src/main/res/values-de/strings.xml | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 9eb7301176..6c79ea7cd5 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -2750,7 +2750,7 @@ Vorgeschlagene Räume Spaces Jeder kann im Raum anklopfen, Mitglieder können dann zustimmen oder ablehnen - + Momentan bist nur du hier. Mit anderen Leuten wird %s noch viel besser. Diese werden in der Lage sein %s zu durchsuchen Diese werden kein Teil von %s sein Tritt meinem Space %1$s %2$s bei @@ -2806,4 +2806,32 @@ Space erstellen Jeder, der sich in einem Space mit diesem Raum befindet, kann diesen Raum finden und ihm beitreten. Nur die Administratoren des Raums können diesen zu einem Space hinzufügen. Spaces + + %d Person, die du kennst, ist bereits beigetreten + %d Personen, die du kennst, sind bereits beigetreten + + Suchst du jemanden außerhalb %s\? + Wir erstellen dir für jedes einen Raum. Du kannst jederzeit weitere Räume hinzufügen. + Räume und Spaces verwalten + Als \"vorgeschlagen\" markieren + Als \"nicht vorgeschlagen\" markieren + Vorgeschlagen + Teile deinen öffentlichen Space mit der Welt + Räume verwalten + %s lädt dich ein + Dieser Alias ist gerade nicht verfügbar. +\nVersuche es später noch einmal oder kontaktiere einen Admin. + Wir erstellen dir für jedes einen Raum. Du kannst jederzeit neue oder existierende Räume hinzufügen. + An welchen Projekten arbeitest du gerade\? + Dieser Raum ist öffentlich + Mediendatei in Originalgröße senden + + Video in Originalauflösung senden + Videos in Originalauflösung senden + + Die Datei ist zu groß. + Video wird komprimiert (%d%) + Bild wird komprimiert… + Als Standard festsetzen und nicht mehr fragen + Jedes Mal fragen \ No newline at end of file From 915d82f8352a1621c49f65a804df0e33a965fa69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 11 May 2021 16:35:47 +0000 Subject: [PATCH 053/202] Translated using Weblate (Estonian) Currently translated at 100.0% (2454 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/et/ --- vector/src/main/res/values-et/strings.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index 8047422a1f..cd58abcbf7 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -2752,4 +2752,23 @@ Kogukonnakeskused Kutsed Soovitatud jututoad + Halda jututubasid ja kogukonnakeskuseid + Eemalda soovitus + Märgi soovituseks + Soovitatud + Tee see jututuba avalikuks + Halda jututubasid + Kas sa otsid kedagi, kes ei leidu %s kogukonnas\? + %s kutsus sind + See jututuba on avalik + Saada meedia algses suuruses + + Saada videofail algses suuruses + Saada videofailid algses suuruses + + See fail on üleslaadimiseks liiga suur. + Teen video väiksemaks %d%% + Teen pildi väiksemaks… + Kasuta vaikeseadistusena ja ära küsi uuesti + Alati küsi \ No newline at end of file From 7289f28babea414e003ee920299000c31046a614 Mon Sep 17 00:00:00 2001 From: Jeanne Lavoie Date: Tue, 11 May 2021 22:48:22 +0000 Subject: [PATCH 054/202] Translated using Weblate (French (Canada)) Currently translated at 98.1% (2409 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr_CA/ --- vector/src/main/res/values-fr-rCA/strings.xml | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/vector/src/main/res/values-fr-rCA/strings.xml b/vector/src/main/res/values-fr-rCA/strings.xml index cf3e08d791..e9e6e66d7c 100644 --- a/vector/src/main/res/values-fr-rCA/strings.xml +++ b/vector/src/main/res/values-fr-rCA/strings.xml @@ -2673,4 +2673,53 @@ Vous avez envoyé une image. %1$s a envoyé une image. %1$s : %2$s + Donnez-lui un nom pour poursuivre. + Ajoutez quelques informations pour renforcer son identité. Vous pourrez changer ceci plus tard. + Ajoutez quelques informations pour l’aider à se démarquer. Vous pourrez changer ceci plus tard. + Créer un espace + Sur invitation, idéal pour vous-même ou les équipes + Privé + Ouvert à tous, idéal pour les communautés + Public + Un espace privé pour vous et votre équipe + Moi et mon équipe + Un espace privé pour organiser vos salons + Seulement moi + Assurez-vous que les accès à %s sont accordés aux bonnes personnes. Vous pourrez changer ceci plus tard. + Avec qui travaillez-vous\? + Pour rejoindre un espace existant, il vous faut une invitation. + Vous pouvez changer ceci plus tard + Quel type d’espace voulez-vous créer\? + Les espaces sont un nouveau moyen de regrouper les salons et les personnes + Votre espace privé + Votre espace public + Ajouter un espace + Partir du salon correspondant à l’identifiant donné (ou le salon actuel si aucun n’est fourni) + Rejoindre l’espace avec l’identifiant donné + Créer un espace + Ce salon est public + Décoché + Envoyer le média avec la taille originale + + Envoyer la vidéo avec sa taille originale + Envoyer les vidéos avec leur taille originale + + Le fichier est trop volumineux pour être téléversé. + Rechercher par nom + Compression de la vidéo %d %% + Compression de l’image… + Tout membre d’un espace contenant ce salon peut le trouver et le rejoindre. Seuls les administrateurs de ce salon peuvent l’ajouter à un espace. + Espaces + Tout le monde peut trouver ce salon et le rejoindre + Public + Seules les personnes invitées peuvent le trouver et le rejoindre + Privé + Paramètre d’accès inconnu (%s) + Tout le monde peut frapper à la porte du salon, les membres peuvent accepter ou rejeter la demande + Permettre l’accès aux visiteurs + Utiliser par défaut et ne plus demander + Toujours demander + Espaces + Invitations + Salons recommandés \ No newline at end of file From 5a4b6b7351990fe7d199155ce711f85916fc2b16 Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Wed, 12 May 2021 12:51:44 +0000 Subject: [PATCH 055/202] Translated using Weblate (French) Currently translated at 100.0% (2454 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- vector/src/main/res/values-fr/strings.xml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 1811c56de5..9fb56dd860 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -1956,8 +1956,8 @@ Supprimer… Voulez-vous envoyer cette pièce jointe à %1$s \? - Envoyer l’image en taille originale - Envoyer les images en taille originale + Envoyer l’image en taille d’origine + Envoyer les images en taille d’origine Confirmer la suppression Voulez-vous vraiment supprimer cet évènement \? Notez que si vous supprimez un changement de nom ou de sujet du salon, cela pourrait annuler le changement. @@ -2758,9 +2758,24 @@ Assurez-vous que les accès à %s sont accordés aux bonnes personnes. Vous pourrez changer ceci plus tard. Avec qui travaillez-vous \? Pour rejoindre un espace existant, il vous faut une invitation. - Le fichier est trop volumineux pour être téléversé. + Le fichier est trop volumineux pour être envoyé. Compression de la vidéo %d %% Compression de l’image… Utiliser par défaut et ne plus demander Toujours demander + Attention, nécessite la prise en charge par le serveur ainsi qu’une version de salon expérimentale + Gérer les salons et les espaces + Marquer comme non recommandé + Marquer comme recommandé + Recommandé + Rendre cet espace public + Gérer les salons + Vous cherchez quelqu’un qui n’est pas dans %s \? + %s vous invite + Ce salon est public + Envoyer le média en taille d’origine + + Envoyer la vidéo en taille originale + Envoyer les vidéos en taille originale + \ No newline at end of file From e61c38818e21f35f86510d0a558c2f0a7da39c55 Mon Sep 17 00:00:00 2001 From: random Date: Thu, 13 May 2021 10:02:45 +0000 Subject: [PATCH 056/202] Translated using Weblate (Italian) Currently translated at 100.0% (2454 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/it/ --- vector/src/main/res/values-it/strings.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 542ab5783b..520fac1900 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -2814,4 +2814,23 @@ Spazi Inviti Stanze suggerite + Gestisci stanze e spazi + Segna come non consigliato + Segna come consigliato + Consigliato + Rendi questo spazio pubblico + Gestisci stanze + Cerchi qualcuno non in %s\? + %s ti ha invitato + Questa stanza è pubblica + Invia i file multimediali nella dimensione originale + + Invia il video nella dimensione originale + Invia i video nella dimensione originale + + Il file è troppo grande per essere inviato. + Compressione video %d%% + Compressione immagine… + Usa come predefinito e non chiedere più + Chiedi sempre \ No newline at end of file From 03750348736b848c4b4b198e142d41fa86b5e4a6 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 12 May 2021 02:01:05 +0000 Subject: [PATCH 057/202] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2454 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/ --- vector/src/main/res/values-zh-rTW/strings.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index beff6c35fd..f4b6e7bacf 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -2700,4 +2700,22 @@ 空間 邀請 建議的聊天室 + 管理聊天室與空間 + 標記為不建議 + 標記為建議 + 建議 + 讓此空間公開 + 管理聊天室 + 正在尋找不在 %s 中的人? + %s 邀請您 + 此聊天室是公開的 + 傳送原始大小的媒體 + + 傳送原始大小的影片 + + 檔案太大了,無法上傳。 + 正在壓縮影片 %d%% + 正在壓縮圖片…… + 預設使用,不再詢問 + 總是詢問 \ No newline at end of file From e09230b3cf0f605426e7368503e36f913292dcde Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Wed, 12 May 2021 12:54:54 +0000 Subject: [PATCH 058/202] Translated using Weblate (French) Currently translated at 94.4% (17 of 18 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fr/ --- fastlane/metadata/android/fr/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/fr/short_description.txt b/fastlane/metadata/android/fr/short_description.txt index 6d86b77a6b..f9d18863ce 100644 --- a/fastlane/metadata/android/fr/short_description.txt +++ b/fastlane/metadata/android/fr/short_description.txt @@ -1 +1 @@ -Messagerie de groupes - messages chiffrés, groupés et appels vidéos +Messagerie de groupe - messages chiffrés, groupes et appels vidéos From dca4a31fc63e1dc62e7e83b8630b3d2506abb2da Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 11:26:24 +0200 Subject: [PATCH 059/202] Fix a problem with database migration on nightly builds (#3335) --- CHANGES.md | 2 +- .../internal/database/RealmSessionStoreMigration.kt | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8c10320816..beda769d87 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - Bugfix 🐛: - - + - Fix a problem with database migration on nightly builds (#3335) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 211059a345..d810c8b1a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -44,7 +44,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 12L + const val SESSION_STORE_SCHEMA_VERSION = 13L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -62,6 +62,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 9) migrateTo10(realm) if (oldVersion <= 10) migrateTo11(realm) if (oldVersion <= 11) migrateTo12(realm) + if (oldVersion <= 12) migrateTo13(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -274,4 +275,14 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java) ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true) } + + private fun migrateTo13(realm: DynamicRealm) { + Timber.d("Step 12 -> 13") + + // Fix issue with the nightly build. Eventually play again the migration which has been included in migrateTo12() + realm.schema.get("SpaceChildSummaryEntity") + ?.takeIf { !it.hasField(SpaceChildSummaryEntityFields.SUGGESTED) } + ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java) + ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true) + } } From 946208a84d5a1a9b5584ddd2363c1b8443ccadb7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 11:39:02 +0200 Subject: [PATCH 060/202] Add documentation on LoginWizard and RegistrationWizard (#3303) ee --- CHANGES.md | 2 +- .../sdk/api/auth/AuthenticationService.kt | 4 ++ .../android/sdk/api/auth/login/LoginWizard.kt | 28 +++++--- .../auth/registration/RegistrationWizard.kt | 68 +++++++++++++++++-- 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8c10320816..0bb48c4f8c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,7 @@ Test: - Other changes: - - + - Add documentation on LoginWizard and RegistrationWizard (#3303) Changes in Element 1.1.7 (2021-05-12) =================================================== diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index a7f5163774..5e35917243 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -51,11 +51,15 @@ interface AuthenticationService { /** * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. + * + * See [LoginWizard] for more details */ fun getLoginWizard(): LoginWizard /** * Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first. + * + * See [RegistrationWizard] for more details. */ fun getRegistrationWizard(): RegistrationWizard diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt index 9c96cba40c..da6eb0c3ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -17,34 +17,46 @@ package org.matrix.android.sdk.api.auth.login import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.util.Cancelable +/** + * Set of methods to be able to login to an existing account on a homeserver. + * + * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signin.md + */ interface LoginWizard { - /** - * @param login the login field - * @param password the password field + * Login to the homeserver. + * + * @param login the login field. Can be a user name, or a msisdn (email or phone number) associated to the account + * @param password the password of the account * @param deviceName the initial device name - * @param callback the matrix callback on which you'll receive the result of authentication. - * @return a [Cancelable] + * @return a [Session] if the login is successful */ suspend fun login(login: String, password: String, deviceName: String): Session /** - * Exchange a login token to an access token + * Exchange a login token to an access token. + * + * @param loginToken login token, obtain when login has happen in a WebView, using SSO + * @return a [Session] if the login is successful */ suspend fun loginWithToken(loginToken: String): Session /** - * Reset user password + * Ask the homeserver to reset the user password. The password will not be reset until + * [resetPasswordMailConfirmed] is successfully called. + * + * @param email an email previously associated to the account the user wants the password to be reset. + * @param newPassword the desired new password */ suspend fun resetPassword(email: String, newPassword: String) /** * Confirm the new password, once the user has checked their email + * When this method succeed, tha account password will be effectively modified. */ suspend fun resetPasswordMailConfirmed() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt index f059bf26c4..2fcb501f26 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt @@ -16,32 +16,92 @@ package org.matrix.android.sdk.api.auth.registration +/** + * Set of methods to be able to create an account on a homeserver. + * + * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signup.md + */ interface RegistrationWizard { - + /** + * Call this method to get the possible registration flow of the current homeserver. + * It can be useful to ensure that your application implementation supports all the stages + * required to create an account. If it is not the case, you will have to use the web fallback + * to let the user create an account with your application. + * See [org.matrix.android.sdk.api.auth.AuthenticationService.getFallbackUrl] + */ suspend fun getRegistrationFlow(): RegistrationResult + /** + * Can be call to check is the desired userName is available for registration on the current homeserver. + * It may also fails if the desired userName is not correctly formatted or does not follow any restriction on + * the homeserver. Ex: userName with only digits may be rejected. + * @param userName the desired username. Ex: "alice" + */ + suspend fun registrationAvailable(userName: String): RegistrationAvailability + + /** + * This is the first method to call in order to create an account and start the registration process. + * + * @param userName the desired username. Ex: "alice" + * @param password the desired password + * @param initialDeviceDisplayName the device display name + */ suspend fun createAccount(userName: String?, password: String?, initialDeviceDisplayName: String?): RegistrationResult + /** + * Perform the "m.login.recaptcha" stage. + * + * @param response the response from ReCaptcha + */ suspend fun performReCaptcha(response: String): RegistrationResult + /** + * Perform the "m.login.terms" stage. + */ suspend fun acceptTerms(): RegistrationResult + /** + * Perform the "m.login.dummy" stage. + */ suspend fun dummy(): RegistrationResult + /** + * Perform the "m.login.email.identity" or "m.login.msisdn" stage. + * + * @param threePid the threePid to add to the account. If this is an email, the homeserver will send an email + * to validate it. For a msisdn a SMS will be sent. + */ suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult + /** + * Ask the homeserver to send again the current threePid (email or msisdn). + */ suspend fun sendAgainThreePid(): RegistrationResult + /** + * Send the code received by SMS to validate a msisdn. + * If the code is correct, the registration request will be executed to validate the msisdn. + */ suspend fun handleValidateThreePid(code: String): RegistrationResult + /** + * Useful to poll the homeserver when waiting for the email to be validated by the user. + * Once the email is validated, this method will return successfully. + * @param delayMillis the SDK can wait before sending the request + */ suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult - suspend fun registrationAvailable(userName: String): RegistrationAvailability - + /** + * This is the current ThreePid, waiting for validation. The SDK will store it in database, so it can be + * restored even if the app has been killed during the registration + */ val currentThreePid: String? - // True when login and password has been sent with success to the homeserver + /** + * True when login and password have been sent with success to the homeserver, i.e. [createAccount] has been + * called successfully. + */ val isRegistrationStarted: Boolean } From d6e3bb59f493deb63065e375c856e1fd8da32596 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 May 2021 12:41:19 +0200 Subject: [PATCH 061/202] Minor cleanup on the doc --- .../main/java/org/matrix/android/sdk/api/session/room/Room.kt | 2 +- .../java/im/vector/app/core/utils/ExternalApplicationsUtil.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index f3eeb902a8..8c434fc440 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -83,7 +83,7 @@ interface Room : * @param beforeLimit how many events before the result are returned. * @param afterLimit how many events after the result are returned. * @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned. - * @param callback Callback to get the search result + * @return The search result */ suspend fun search(searchTerm: String, nextBatch: String?, diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index 859df7d714..90cbb3a7a5 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -509,7 +509,7 @@ fun selectTxtFileToWrite( * @param sourceFile the file source path * @param dstDirPath the dst path * @param outputFilename optional the output filename - * @param callback the asynchronous callback + * @return the created file */ @Suppress("DEPRECATION") fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?): File? { From c27b7aec265eeeacb0d86361ff93e782a30fa1c9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 11:44:44 +0200 Subject: [PATCH 062/202] Add more doc --- .../android/sdk/api/auth/registration/RegistrationWizard.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt index 2fcb501f26..621253faa5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt @@ -19,7 +19,13 @@ package org.matrix.android.sdk.api.auth.registration /** * Set of methods to be able to create an account on a homeserver. * + * Common scenario to register an account successfully: + * - Call [getRegistrationFlow] to check that you application supports all the mandatory registration stages + * - Call [createAccount] to start the account creation + * - Fulfill all mandatory stages using the methods [performReCaptcha] [acceptTerms] [dummy], etc. + * * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signup.md + * and https://matrix.org/docs/spec/client_server/latest#account-registration-and-management */ interface RegistrationWizard { /** From 0354151fa7a4d83ef26bbaf5ef91e4959dcb53f5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 12:33:52 +0200 Subject: [PATCH 063/202] kotlin_coroutines_version = "1.5.0-RC" since kotlin_version = '1.5.0' --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f0ba2c461f..ec5d35a72c 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { // Ref: https://kotlinlang.org/releases.html ext.kotlin_version = '1.5.0' - ext.kotlin_coroutines_version = "1.4.2" + ext.kotlin_coroutines_version = "1.5.0-RC" repositories { google() jcenter() From 58a2fd8c77b0662124ed20edf883d1a15c9fe451 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 12:45:33 +0200 Subject: [PATCH 064/202] Fix warning 1.5: String.capitalize is now deprecated --- .../sdk/api/session/widgets/model/WidgetContent.kt | 3 ++- .../matrix/android/sdk/internal/util/StringUtils.kt | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt index 2c4c03b7d4..7a4231c277 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.util.safeCapitalize /** * Ref: https://github.com/matrix-org/matrix-doc/issues/1236 @@ -39,6 +40,6 @@ data class WidgetContent( @SuppressLint("DefaultLocale") fun getHumanName(): String { - return (name ?: type ?: "").capitalize() + return (name ?: type ?: "").safeCapitalize() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index 2fabca4be8..05fb732001 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.util import timber.log.Timber +import java.util.Locale /** * Convert a string to an UTF8 String @@ -78,3 +79,14 @@ internal val spaceChars = "[\u00A0\u2000-\u200B\u2800\u3000]".toRegex() * Strip all the UTF-8 chars which are actually spaces */ internal fun String.replaceSpaceChars() = replace(spaceChars, "") + +// String.capitalize is now deprecated +internal fun String.safeCapitalize(): String { + return replaceFirstChar { char -> + if (char.isLowerCase()) { + char.titlecase(Locale.getDefault()) + } else { + char.toString() + } + } +} From 7a28be941c06ee84f0675a27a40019d09f9a72e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 12:47:09 +0200 Subject: [PATCH 065/202] Fix warning 1.5: 'Char.toInt(): Int' is deprecated --- .../main/java/org/matrix/android/sdk/api/util/MatrixItem.kt | 6 +++--- vector/src/main/java/im/vector/app/core/utils/Emoji.kt | 6 +++--- .../room/detail/timeline/helper/MatrixItemColorProvider.kt | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index 7b2fae86ef..ce50d90cb0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -117,15 +117,15 @@ sealed class MatrixItem( var first = dn[startIndex] // LEFT-TO-RIGHT MARK - if (dn.length >= 2 && 0x200e == first.toInt()) { + if (dn.length >= 2 && 0x200e == first.code) { startIndex++ first = dn[startIndex] } // check if it’s the start of a surrogate pair - if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) { + if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) { val second = dn[startIndex + 1] - if (second.toInt() in 0xDC00..0xDFFF) { + if (second.code in 0xDC00..0xDFFF) { length++ } } diff --git a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt index 66907ded10..d73af1e917 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt @@ -42,13 +42,13 @@ fun CharSequence.splitEmoji(): List { while (index < length) { val firstChar = get(index) - if (firstChar.toInt() == 0x200e) { + if (firstChar.code == 0x200e) { // Left to right mark. What should I do with it? - } else if (firstChar.toInt() in 0xD800..0xDBFF && index + 1 < length) { + } else if (firstChar.code in 0xD800..0xDBFF && index + 1 < length) { // We have the start of a surrogate pair val secondChar = get(index + 1) - if (secondChar.toInt() in 0xDC00..0xDFFF) { + if (secondChar.code in 0xDC00..0xDFFF) { // We have an emoji result.add("$firstChar$secondChar") index++ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt index 6a590206cb..0a9bf96a22 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt @@ -50,7 +50,7 @@ class MatrixItemColorProvider @Inject constructor( fun getColorFromUserId(userId: String?): Int { var hash = 0 - userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.toInt() } + userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.code } return when (abs(hash) % 8) { 1 -> R.color.riotx_username_2 @@ -66,7 +66,7 @@ class MatrixItemColorProvider @Inject constructor( @ColorRes private fun getColorFromRoomId(roomId: String?): Int { - return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) { + return when ((roomId?.toList()?.sumBy { it.code } ?: 0) % 3) { 1 -> R.color.riotx_avatar_fill_2 2 -> R.color.riotx_avatar_fill_3 else -> R.color.riotx_avatar_fill_1 From c70445a9a1a503f643ec7ab17c296569ce0f0cb0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 12:47:53 +0200 Subject: [PATCH 066/202] Fix warning 1.5: 'toUpperCase(Locale): String' and 'toLowerCase(Locale): String' are deprecated --- .../main/java/org/matrix/android/sdk/api/util/MatrixItem.kt | 2 +- .../crypto/verification/SASDefaultVerificationTransaction.kt | 4 ++-- .../sdk/internal/session/identity/IdentityBulkLookupTask.kt | 2 +- .../main/java/org/matrix/android/sdk/internal/util/Hash.kt | 2 +- .../src/main/java/im/vector/app/core/intent/VectorMimeType.kt | 2 +- vector/src/main/java/im/vector/app/core/utils/FileUtils.kt | 2 +- .../app/features/home/room/detail/RoomDetailViewModel.kt | 2 +- .../main/java/im/vector/app/features/settings/VectorLocale.kt | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index ce50d90cb0..3bf3f66e40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -132,7 +132,7 @@ sealed class MatrixItem( dn.substring(startIndex, startIndex + length) } - .toUpperCase(Locale.ROOT) + .uppercase(Locale.ROOT) } companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index c7885ce449..4bf01a2809 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -345,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction( } protected fun hashUsingAgreedHashMethod(toHash: String): String? { - if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) { + if ("sha256" == accepted?.hash?.lowercase(Locale.ROOT)) { val olmUtil = OlmUtility() val hashBytes = olmUtil.sha256(toHash) olmUtil.releaseUtility() @@ -355,7 +355,7 @@ internal abstract class SASDefaultVerificationTransaction( } private fun macUsingAgreedMethod(message: String, info: String): String? { - return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) { + return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) { SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info) SAS_MAC_SHA256 -> getSAS().calculateMac(message, info) else -> null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index 4f6e906766..114695062c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -117,7 +117,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( return withOlmUtility { olmUtility -> threePids.map { threePid -> base64ToBase64Url( - olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + olmUtility.sha256(threePid.value.lowercase(Locale.ROOT) + " " + threePid.toMedium() + " " + pepper) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt index e19b1bcca7..47f20913ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -27,7 +27,7 @@ fun String.md5() = try { digest.update(toByteArray()) digest.digest() .joinToString("") { String.format("%02X", it) } - .toLowerCase(Locale.ROOT) + .lowercase(Locale.ROOT) } catch (exc: Exception) { // Should not happen, but just in case hashCode().toString() diff --git a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt index 1299f4086b..e68b5e1b07 100644 --- a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt +++ b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt @@ -45,7 +45,7 @@ fun getMimeTypeFromUri(context: Context, uri: Uri): String? { if (null != mimeType) { // the mimetype is sometimes in uppercase. - mimeType = mimeType.toLowerCase(Locale.ROOT) + mimeType = mimeType.lowercase(Locale.ROOT) } } catch (e: Exception) { Timber.e(e, "Failed to open resource input stream") diff --git a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt index b5ce922487..fc9aa692b1 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt @@ -112,7 +112,7 @@ fun getFileExtension(fileUri: String): String? { val ext = filename.substring(dotPos + 1) if (ext.isNotBlank()) { - return ext.toLowerCase(Locale.ROOT) + return ext.lowercase(Locale.ROOT) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index cd7f6a5730..3a9969b43c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -449,7 +449,7 @@ class RoomDetailViewModel @AssistedInject constructor( widgetSessionId = widgetSessionId.substring(0, 7) } val roomId: String = room.roomId - val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorLocale.applicationLocale) + val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale) val preferredJitsiDomain = tryOrNull { rawService.getElementWellknown(session.myUserId) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt index cff4ca0cb9..f558ba28c6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt @@ -181,7 +181,7 @@ object VectorLocale { } } // sort by human display names - .sortedBy { localeToLocalisedString(it).toLowerCase(it) } + .sortedBy { localeToLocalisedString(it).lowercase(it) } supportedLocales.clear() supportedLocales.addAll(list) From babbcedd870df1cee50a7832dd76ab80562a59b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 12:49:38 +0200 Subject: [PATCH 067/202] Fix warning 1.5: 'Char.toByte(): Byte' is deprecated --- .../crypto/verification/qrcode/QrCodeTest.kt | 12 ++++++------ .../crypto/verification/qrcode/Extensions.kt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt index ee604fc9ab..76bf6dc040 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -226,12 +226,12 @@ class QrCodeTest : InstrumentedTest { private fun checkHeader(byteArray: ByteArray) { // MATRIX - byteArray[0] shouldBeEqualTo 'M'.toByte() - byteArray[1] shouldBeEqualTo 'A'.toByte() - byteArray[2] shouldBeEqualTo 'T'.toByte() - byteArray[3] shouldBeEqualTo 'R'.toByte() - byteArray[4] shouldBeEqualTo 'I'.toByte() - byteArray[5] shouldBeEqualTo 'X'.toByte() + byteArray[0] shouldBeEqualTo 'M'.code.toByte() + byteArray[1] shouldBeEqualTo 'A'.code.toByte() + byteArray[2] shouldBeEqualTo 'T'.code.toByte() + byteArray[3] shouldBeEqualTo 'R'.code.toByte() + byteArray[4] shouldBeEqualTo 'I'.code.toByte() + byteArray[5] shouldBeEqualTo 'X'.code.toByte() // Version byteArray[6] shouldBeEqualTo 2 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt index 6bc3483e65..76e88442b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt @@ -48,7 +48,7 @@ fun QrCodeData.toEncodedString(): String { // TransactionId transactionId.forEach { - result += it.toByte() + result += it.code.toByte() } // Keys From 4a94426d38ccbe8690787c8f47da9169503b15ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 12:52:21 +0200 Subject: [PATCH 068/202] Fix warning 1.5: 'sumBy((T) -> Int): Int' is deprecated --- .../matrix/android/sdk/internal/session/DefaultFileService.kt | 2 +- vector/src/main/java/im/vector/app/core/utils/FileUtils.kt | 2 +- .../home/room/detail/timeline/helper/MatrixItemColorProvider.kt | 2 +- .../im/vector/app/features/reactions/EmojiRecyclerAdapter.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 891858d857..a284d976d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -291,7 +291,7 @@ internal class DefaultFileService @Inject constructor( Timber.v("Get size of ${it.absolutePath}") true } - .sumBy { it.length().toInt() } + .sumOf { it.length().toInt() } } override fun clearCache() { diff --git a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt index fc9aa692b1..7ce6dd1c67 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt @@ -131,5 +131,5 @@ fun getSizeOfFiles(root: File): Int { Timber.v("Get size of ${it.absolutePath}") true } - .sumBy { it.length().toInt() } + .sumOf { it.length().toInt() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt index 0a9bf96a22..ed343d52ef 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt @@ -66,7 +66,7 @@ class MatrixItemColorProvider @Inject constructor( @ColorRes private fun getColorFromRoomId(roomId: String?): Int { - return when ((roomId?.toList()?.sumBy { it.code } ?: 0) % 3) { + return when ((roomId?.toList()?.sumOf { it.code } ?: 0) % 3) { 1 -> R.color.riotx_avatar_fill_2 2 -> R.color.riotx_avatar_fill_3 else -> R.color.riotx_avatar_fill_1 diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index 92bc21be25..1533ce7fdb 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -221,7 +221,7 @@ class EmojiRecyclerAdapter @Inject constructor( } override fun getItemCount() = dataSource.rawData.categories - .sumBy { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size } + .sumOf { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size } abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { abstract fun bind(s: String?) From df68cd4b565237860ee41ab3a82e81ad44390fb2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 13:15:19 +0200 Subject: [PATCH 069/202] Fix warning 1.5: String.capitalize is now deprecated --- .../im/vector/app/core/utils/StringUtils.kt | 30 +++++++++++++++++++ .../settings/locale/LocalePickerController.kt | 5 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/utils/StringUtils.kt diff --git a/vector/src/main/java/im/vector/app/core/utils/StringUtils.kt b/vector/src/main/java/im/vector/app/core/utils/StringUtils.kt new file mode 100644 index 0000000000..6d681007ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/StringUtils.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import java.util.Locale + +// String.capitalize is now deprecated +fun String.safeCapitalize(locale: Locale): String { + return replaceFirstChar { char -> + if (char.isLowerCase()) { + char.titlecase(locale) + } else { + char.toString() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt index 9654eb2190..effb593add 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt @@ -24,6 +24,7 @@ import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.epoxy.profiles.profileSectionItem import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.safeCapitalize import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorPreferences import java.util.Locale @@ -46,7 +47,7 @@ class LocalePickerController @Inject constructor( } localeItem { id(data.currentLocale.toString()) - title(VectorLocale.localeToLocalisedString(data.currentLocale).capitalize(data.currentLocale)) + title(VectorLocale.localeToLocalisedString(data.currentLocale).safeCapitalize(data.currentLocale)) if (vectorPreferences.developerMode()) { subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) } @@ -75,7 +76,7 @@ class LocalePickerController @Inject constructor( .forEach { localeItem { id(it.toString()) - title(VectorLocale.localeToLocalisedString(it).capitalize(it)) + title(VectorLocale.localeToLocalisedString(it).safeCapitalize(it)) if (vectorPreferences.developerMode()) { subtitle(VectorLocale.localeToLocalisedStringInfo(it)) } From 08aefa270e9fa888109a51de5b1afda0a95d05a7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 13:15:35 +0200 Subject: [PATCH 070/202] internal --- .../org/matrix/android/sdk/internal/util/StringUtils.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index 05fb732001..aa0b92aa45 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -25,7 +25,7 @@ import java.util.Locale * @param s the string to convert * @return the utf-8 string */ -fun convertToUTF8(s: String): String { +internal fun convertToUTF8(s: String): String { return try { val bytes = s.toByteArray(Charsets.UTF_8) String(bytes) @@ -41,7 +41,7 @@ fun convertToUTF8(s: String): String { * @param s the string to convert * @return the utf-16 string */ -fun convertFromUTF8(s: String): String { +internal fun convertFromUTF8(s: String): String { return try { val bytes = s.toByteArray() String(bytes, Charsets.UTF_8) @@ -57,7 +57,7 @@ fun convertFromUTF8(s: String): String { * @param subString the string to search for * @return whether a match was found */ -fun String.caseInsensitiveFind(subString: String): Boolean { +internal fun String.caseInsensitiveFind(subString: String): Boolean { // add sanity checks if (subString.isEmpty() || isEmpty()) { return false From aeda8bcc811f6939e5d930e1266c33259f3c0cd4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 14:29:01 +0200 Subject: [PATCH 071/202] Remove usage of GlobalScope --- .../main/java/im/vector/app/AppStateHandler.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index a2a242a3d9..c5eac7e3e0 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -22,10 +22,10 @@ import androidx.lifecycle.OnLifecycleEvent import arrow.core.Option import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.utils.BehaviorDataSource +import im.vector.app.features.session.coroutineScope import im.vector.app.features.ui.UiStateRepository import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session @@ -63,30 +63,30 @@ class AppStateHandler @Inject constructor( fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? = selectedSpaceDataSource.currentValue?.orNull() fun setCurrentSpace(spaceId: String?, session: Session? = null) { - val uSession = session ?: activeSessionHolder.getSafeActiveSession() + val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace && spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return - val spaceSum = spaceId?.let { uSession?.getRoomSummary(spaceId) } + val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) } selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum))) if (spaceId != null) { - GlobalScope.launch(Dispatchers.IO) { + uSession.coroutineScope.launch(Dispatchers.IO) { tryOrNull { - uSession?.getRoom(spaceId)?.loadRoomMembersIfNeeded() + uSession.getRoom(spaceId)?.loadRoomMembersIfNeeded() } } } } fun setCurrentGroup(groupId: String?, session: Session? = null) { - val uSession = session ?: activeSessionHolder.getSafeActiveSession() + val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.ByLegacyGroup && groupId == selectedSpaceDataSource.currentValue?.orNull()?.group()?.groupId) return - val activeGroup = groupId?.let { uSession?.getGroupSummary(groupId) } + val activeGroup = groupId?.let { uSession.getGroupSummary(groupId) } selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.ByLegacyGroup(activeGroup))) if (groupId != null) { - GlobalScope.launch { + uSession.coroutineScope.launch { tryOrNull { - uSession?.getGroup(groupId)?.fetchGroupData() + uSession.getGroup(groupId)?.fetchGroupData() } } } From baa4b95e18e8cbe28570cf64ac7491d8e6a16eaf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 14:35:08 +0200 Subject: [PATCH 072/202] Remove usage of GlobalScope --- .../app/features/call/webrtc/WebRtcCall.kt | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index fcd6bf0a77..a3a1a29c4b 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -33,11 +33,12 @@ import im.vector.app.features.call.utils.awaitCreateOffer import im.vector.app.features.call.utils.awaitSetLocalDescription import im.vector.app.features.call.utils.awaitSetRemoteDescription import im.vector.app.features.call.utils.mapToCallCandidate +import im.vector.app.features.session.coroutineScope import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.ReplaySubject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -103,6 +104,9 @@ class WebRtcCall(val mxCall: MxCall, private val listeners = CopyOnWriteArrayList() + private val sessionScope: CoroutineScope? + get() = sessionProvider.get()?.coroutineScope + fun addListener(listener: Listener) { listeners.add(listener) } @@ -191,7 +195,7 @@ class WebRtcCall(val mxCall: MxCall, fun onIceCandidate(iceCandidate: IceCandidate) = iceCandidateSource.onNext(iceCandidate) fun onRenegotiationNeeded(restartIce: Boolean) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { if (mxCall.state != CallState.CreateOffer && mxCall.opponentVersion == 0) { Timber.v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event") return@launch @@ -262,7 +266,7 @@ class WebRtcCall(val mxCall: MxCall, localSurfaceRenderers.addIfNeeded(localViewRenderer) remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { when (mode) { VectorCallActivity.INCOMING_ACCEPT -> { internalAcceptIncomingCall() @@ -283,7 +287,7 @@ class WebRtcCall(val mxCall: MxCall, } fun acceptIncomingCall() { - GlobalScope.launch { + sessionScope?.launch { Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}") if (mxCall.state == CallState.LocalRinging) { internalAcceptIncomingCall() @@ -564,7 +568,7 @@ class WebRtcCall(val mxCall: MxCall, } fun updateRemoteOnHold(onHold: Boolean) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { if (remoteOnHold == onHold) return@launch val direction: RtpTransceiver.RtpTransceiverDirection if (onHold) { @@ -688,7 +692,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onAddStream(stream: MediaStream) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { // reportError("Weird-looking stream: " + stream); if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) { Timber.e("## VOIP StreamObserver weird looking stream: $stream") @@ -712,7 +716,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onRemoveStream() { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { remoteSurfaceRenderers .mapNotNull { it.get() } .forEach { remoteVideoTrack?.removeSink(it) } @@ -734,7 +738,7 @@ class WebRtcCall(val mxCall: MxCall, } val wasRinging = mxCall.state is CallState.LocalRinging mxCall.state = CallState.Terminated - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { release() } onCallEnded(callId) @@ -750,7 +754,7 @@ class WebRtcCall(val mxCall: MxCall, // Call listener fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { iceCandidatesContent.candidates.forEach { if (it.sdpMid.isNullOrEmpty() || it.candidate.isNullOrEmpty()) { return@forEach @@ -763,7 +767,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { Timber.v("## VOIP onCallAnswerReceived ${callAnswerContent.callId}") val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp) try { @@ -779,7 +783,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { val description = callNegotiateContent.description val type = description?.type val sdpText = description?.sdp From 257b2ef593b363baa62db82c79d6d248b5ebb41a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 14:36:36 +0200 Subject: [PATCH 073/202] Remove usage of GlobalScope --- .../java/im/vector/app/features/crypto/keys/KeysExporter.kt | 4 ++-- .../java/im/vector/app/features/crypto/keys/KeysImporter.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt index 282f7b1a71..c7e4c26385 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt @@ -18,8 +18,8 @@ package im.vector.app.features.crypto.keys import android.content.Context import android.net.Uri +import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback @@ -33,7 +33,7 @@ class KeysExporter(private val session: Session) { * Export keys and return the file path with the callback */ fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback) { - GlobalScope.launch(Dispatchers.Main) { + session.coroutineScope.launch(Dispatchers.Main) { runCatching { withContext(Dispatchers.IO) { val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysImporter.kt b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysImporter.kt index 8932bb9489..3d93b26edd 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysImporter.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysImporter.kt @@ -20,8 +20,8 @@ import android.content.Context import android.net.Uri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.resources.openResource +import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback @@ -41,7 +41,7 @@ class KeysImporter(private val session: Session) { mimetype: String?, password: String, callback: MatrixCallback) { - GlobalScope.launch(Dispatchers.Main) { + session.coroutineScope.launch(Dispatchers.Main) { runCatching { withContext(Dispatchers.IO) { val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri)) From bf14251c3df752d7f8837354251f9f60b5f6edc3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 14:43:28 +0200 Subject: [PATCH 074/202] Remove usage of GlobalScope --- .../crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt | 4 ++-- .../crypto/recover/BootstrapSaveRecoveryKeyFragment.kt | 4 ++-- .../crypto/verification/VerificationBottomSheetViewModel.kt | 2 +- .../java/im/vector/app/features/home/HomeActivityViewModel.kt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 9a3b5fa874..8833702e35 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -25,6 +25,7 @@ import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import arrow.core.Try import com.google.android.material.bottomsheet.BottomSheetDialog import im.vector.app.R @@ -37,7 +38,6 @@ import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentKeysBackupSetupStep3Binding import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.IOException @@ -163,7 +163,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment if (activityResult.resultCode == Activity.RESULT_OK) { val uri = activityResult.data?.data ?: return@registerStartForActivityResult - GlobalScope.launch(Dispatchers.IO) { + lifecycleScope.launch(Dispatchers.IO) { try { sharedViewModel.handle(BootstrapActions.SaveKeyToUri(requireContext().contentResolver!!.openOutputStream(uri)!!)) } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index a67cb96d37..8aa3d5423e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -427,7 +427,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } private fun tentativeRestoreBackup(res: Map?) { - GlobalScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { try { val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { Timber.v("## Keybackup secret not restored from SSSS") diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 447a567cf4..bfedbd6f52 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -27,9 +27,9 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.ReAuthHelper +import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth @@ -184,7 +184,7 @@ class HomeActivityViewModel @AssistedInject constructor( private fun maybeBootstrapCrossSigningAfterInitialSync() { // We do not use the viewModel context because we do not want to tie this action to activity view model - GlobalScope.launch(Dispatchers.IO) { + activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch(Dispatchers.IO) { val session = activeSessionHolder.getSafeActiveSession() ?: return@launch tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") { From 555eada37ac083a5fc14c3c7edc70e2e3a67107e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 14:55:02 +0200 Subject: [PATCH 075/202] Remove usage of GlobalScope --- .../features/media/BaseAttachmentProvider.kt | 2 +- .../media/DataAttachmentRoomProvider.kt | 32 +++++++---------- .../media/RoomEventsAttachmentProvider.kt | 34 +++++++------------ .../media/VectorAttachmentViewerActivity.kt | 17 +++++++--- 4 files changed, 40 insertions(+), 45 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt index 53996171a5..b35ebfb503 100644 --- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt @@ -178,5 +178,5 @@ abstract class BaseAttachmentProvider( // TODO("Not yet implemented") } - abstract fun getFileForSharing(position: Int, callback: ((File?) -> Unit)) + abstract suspend fun getFileForSharing(position: Int): File? } diff --git a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt index 630433506f..41fe82e797 100644 --- a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt @@ -19,10 +19,7 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.lib.attachmentviewer.AttachmentInfo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -78,20 +75,17 @@ class DataAttachmentRoomProvider( return room?.getTimeLineEvent(item.eventId) } - override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { - val item = getItem(position) - GlobalScope.launch { - val result = runCatching { - fileService.downloadFile( - fileName = item.filename, - mimeType = item.mimeType, - url = item.url, - elementToDecrypt = item.elementToDecrypt - ) - } - withContext(Dispatchers.Main) { - callback(result.getOrNull()) - } - } + override suspend fun getFileForSharing(position: Int): File? { + return getItem(position) + .let { item -> + tryOrNull { + fileService.downloadFile( + fileName = item.filename, + mimeType = item.mimeType, + url = item.url, + elementToDecrypt = item.elementToDecrypt + ) + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index fc6d5a1f22..43de84f19e 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -19,10 +19,7 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.lib.attachmentviewer.AttachmentInfo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -121,24 +118,19 @@ class RoomEventsAttachmentProvider( return getItem(position) } - override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { - getItem(position).let { timelineEvent -> - - val messageContent = timelineEvent.root.getClearContent().toModel() - as? MessageWithAttachmentContent - ?: return@let - GlobalScope.launch { - val result = runCatching { - fileService.downloadFile( - fileName = messageContent.body, - mimeType = messageContent.mimeType, - url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) + override suspend fun getFileForSharing(position: Int): File? { + return getItem(position) + .let { timelineEvent -> + timelineEvent.root.getClearContent().toModel() as? MessageWithAttachmentContent } - withContext(Dispatchers.Main) { - callback(result.getOrNull()) + ?.let { messageContent -> + tryOrNull { + fileService.downloadFile( + fileName = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) + } } - } - } } } diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt index c632a008ce..5a62454566 100644 --- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt @@ -28,7 +28,7 @@ import androidx.core.transition.addListener import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.transition.Transition import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -42,6 +42,9 @@ import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.themes.ThemeUtils import im.vector.lib.attachmentviewer.AttachmentCommands import im.vector.lib.attachmentviewer.AttachmentViewerActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import timber.log.Timber import javax.inject.Inject @@ -264,9 +267,15 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen } override fun onShareTapped() { - currentSourceProvider?.getFileForSharing(currentPosition) { data -> - if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) + lifecycleScope.launch(Dispatchers.IO) { + val file = currentSourceProvider?.getFileForSharing(currentPosition) ?: return@launch + + withContext(Dispatchers.Main) { + shareMedia( + this@VectorAttachmentViewerActivity, + file, + getMimeTypeFromUri(this@VectorAttachmentViewerActivity, file.toUri()) + ) } } } From 4112d2812706ce10e0248a5dd555d7c4952c4f29 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 14:56:31 +0200 Subject: [PATCH 076/202] Remove usage of GlobalScope --- .../vector/app/features/media/VideoContentRenderer.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt index 80d2d8ba45..635de2ba16 100644 --- a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt @@ -25,8 +25,9 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.files.LocalFilesHelper +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -39,6 +40,9 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc private val activeSessionHolder: ActiveSessionHolder, private val errorFormatter: ErrorFormatter) { + private val sessionScope: CoroutineScope + get() = activeSessionHolder.getActiveSession().coroutineScope + @Parcelize data class Data( override val eventId: String, @@ -76,7 +80,7 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc thumbnailView.isVisible = true loadingView.isVisible = true - GlobalScope.launch { + sessionScope.launch { val result = runCatching { activeSessionHolder.getActiveSession().fileService() .downloadFile( @@ -119,7 +123,7 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc thumbnailView.isVisible = true loadingView.isVisible = true - GlobalScope.launch { + sessionScope.launch { val result = runCatching { activeSessionHolder.getActiveSession().fileService() .downloadFile( From 0711ecc7f4ac718e43cbab55d86b6790af417113 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 15:09:29 +0200 Subject: [PATCH 077/202] Remove usage of GlobalScope - I guess for those ones this is OK... --- vector/src/main/java/im/vector/app/features/pin/PinLocker.kt | 1 + .../java/im/vector/app/features/rageshake/VectorFileLogger.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt index adc618d82e..9c55b88805 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt @@ -60,6 +60,7 @@ class PinLocker @Inject constructor( return liveState } + @Suppress("EXPERIMENTAL_API_USAGE") private fun computeState() { GlobalScope.launch { val state = if (shouldBeLocked && pinCodeStore.hasEncodedPin()) { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt index 304720dfb0..2f8e013f46 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt @@ -88,6 +88,7 @@ class VectorFileLogger @Inject constructor( } } + @Suppress("EXPERIMENTAL_API_USAGE") override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { fileHandler ?: return GlobalScope.launch(Dispatchers.IO) { From 25b4c32fd0fa0e7515965edd7023fdcf431141b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 15:19:04 +0200 Subject: [PATCH 078/202] Remove usage of GlobalScope --- .../vector/app/features/reactions/EmojiChooserFragment.kt | 3 +++ .../vector/app/features/reactions/EmojiRecyclerAdapter.kt | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt index 095e250602..51dc62af8b 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt @@ -19,6 +19,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.EmojiChooserFragmentBinding @@ -51,6 +52,8 @@ class EmojiChooserFragment @Inject constructor( } } + override fun getCoroutineScope() = lifecycleScope + override fun firstVisibleSectionChange(section: Int) { viewModel.setCurrentSection(section) } diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index 1533ce7fdb..45d26e81eb 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -31,8 +31,8 @@ import androidx.transition.AutoTransition import androidx.transition.TransitionManager import im.vector.app.R import im.vector.app.features.reactions.data.EmojiDataSource +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.math.abs @@ -278,6 +278,7 @@ class EmojiRecyclerAdapter @Inject constructor( } interface InteractionListener { + fun getCoroutineScope(): CoroutineScope fun firstVisibleSectionChange(section: Int) } @@ -323,11 +324,11 @@ class EmojiRecyclerAdapter @Inject constructor( // Log.i("SCROLL SPEED","scroll speed $dy") isFastScroll = abs(dy) > 50 val visible = (recyclerView.layoutManager as GridLayoutManager).findFirstCompletelyVisibleItemPosition() - GlobalScope.launch { + interactionListener?.getCoroutineScope()?.launch { val section = getSectionForAbsoluteIndex(visible) if (section != currentFirstVisibleSection) { currentFirstVisibleSection = section - GlobalScope.launch(Dispatchers.Main) { + interactionListener?.getCoroutineScope()?.launch(Dispatchers.Main) { interactionListener?.firstVisibleSectionChange(currentFirstVisibleSection) } } From 1e708b113b28966d4636fd3acd139c827469c3b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 15:20:22 +0200 Subject: [PATCH 079/202] Remove usage of GlobalScope --- .../app/features/settings/VectorSettingsGeneralFragment.kt | 3 +-- .../app/features/spaces/SpaceSettingsMenuBottomSheet.kt | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index 334464e304..adab8f8630 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -52,7 +52,6 @@ import im.vector.app.features.MainActivityArgs import im.vector.app.features.workers.signout.SignOutUiWorker import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -224,7 +223,7 @@ class VectorSettingsGeneralFragment @Inject constructor( it.summary = TextUtils.formatFileSize(requireContext(), size.toLong()) it.onPreferenceClickListener = Preference.OnPreferenceClickListener { - GlobalScope.launch(Dispatchers.Main) { + lifecycleScope.launch(Dispatchers.Main) { // On UI Thread displayLoadingView() diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt index 1586b16ff6..01a0ba4b56 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt @@ -36,11 +36,11 @@ import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.ReportType import im.vector.app.features.roomprofile.RoomProfileActivity +import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceManageActivity import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.events.model.EventType @@ -140,7 +140,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment - GlobalScope.launch { + session.coroutineScope.launch { try { session.getRoom(spaceArgs.spaceId)?.leave(null) } catch (failure: Throwable) { From 043781447914976266bcb7e98ff72913777a4c36 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 15:24:04 +0200 Subject: [PATCH 080/202] Remove usage of GlobalScope --- .../im/vector/app/features/widgets/WidgetPostAPIHandler.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 9fa04aabbb..f9acfb3ce6 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -22,7 +22,7 @@ import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.resources.StringProvider -import kotlinx.coroutines.GlobalScope +import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue @@ -465,7 +465,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo } private fun launchWidgetAPIAction(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict, block: suspend () -> Unit): Job { - return GlobalScope.launch { + // We should probably use a scope tight to the lifecycle here... + return session.coroutineScope.launch { kotlin.runCatching { block() }.fold( From 8211cc266fd53ee95fd7de1f33af9ae8e341adaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 15:26:16 +0200 Subject: [PATCH 081/202] Remove usage of GlobalScope --- .../settings/troubleshoot/TestPushFromPushGateway.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt index 15c7e88bac..d429b293b2 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -19,13 +19,14 @@ import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.pushers.PushersManager import im.vector.app.core.resources.StringProvider +import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.troubleshoot.TroubleshootTest import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -38,7 +39,8 @@ import javax.inject.Inject class TestPushFromPushGateway @Inject constructor(private val context: AppCompatActivity, private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter, - private val pushersManager: PushersManager) + private val pushersManager: PushersManager, + private val activeSessionHolder: ActiveSessionHolder) : TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { private var action: Job? = null @@ -50,7 +52,7 @@ class TestPushFromPushGateway @Inject constructor(private val context: AppCompat status = TestStatus.FAILED return } - action = GlobalScope.launch { + action = activeSessionHolder.getActiveSession().coroutineScope.launch { val result = runCatching { pushersManager.testPush(fcmToken) } withContext(Dispatchers.Main) { From 1f7482922d3dd01a38701dcf4d0554258defaf01 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 15:44:22 +0200 Subject: [PATCH 082/202] Remove usage of GlobalScope --- .../media/AttachmentProviderFactory.kt | 34 ++++++++++++------- .../features/media/BaseAttachmentProvider.kt | 5 +-- .../media/DataAttachmentRoomProvider.kt | 11 +++++- .../media/RoomEventsAttachmentProvider.kt | 11 +++++- .../media/VectorAttachmentViewerActivity.kt | 4 +-- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt b/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt index b549e01551..f067cd7599 100644 --- a/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt +++ b/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt @@ -18,6 +18,7 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -30,24 +31,31 @@ class AttachmentProviderFactory @Inject constructor( private val session: Session ) { - fun createProvider(attachments: List): RoomEventsAttachmentProvider { + fun createProvider(attachments: List, + coroutineScope: CoroutineScope + ): RoomEventsAttachmentProvider { return RoomEventsAttachmentProvider( - attachments, - imageContentRenderer, - vectorDateFormatter, - session.fileService(), - stringProvider + attachments = attachments, + imageContentRenderer = imageContentRenderer, + dateFormatter = vectorDateFormatter, + fileService = session.fileService(), + coroutineScope = coroutineScope, + stringProvider = stringProvider ) } - fun createProvider(attachments: List, room: Room?): DataAttachmentRoomProvider { + fun createProvider(attachments: List, + room: Room?, + coroutineScope: CoroutineScope + ): DataAttachmentRoomProvider { return DataAttachmentRoomProvider( - attachments, - room, - imageContentRenderer, - vectorDateFormatter, - session.fileService(), - stringProvider + attachments = attachments, + room = room, + imageContentRenderer = imageContentRenderer, + dateFormatter = vectorDateFormatter, + fileService = session.fileService(), + coroutineScope = coroutineScope, + stringProvider = stringProvider ) } } diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt index b35ebfb503..ca469bfbcb 100644 --- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt @@ -31,8 +31,8 @@ import im.vector.lib.attachmentviewer.AttachmentInfo import im.vector.lib.attachmentviewer.AttachmentSourceProvider import im.vector.lib.attachmentviewer.ImageLoaderTarget import im.vector.lib.attachmentviewer.VideoLoaderTarget +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.events.model.isVideoMessage @@ -44,6 +44,7 @@ abstract class BaseAttachmentProvider( private val attachments: List, private val imageContentRenderer: ImageContentRenderer, protected val fileService: FileService, + private val coroutineScope: CoroutineScope, private val dateFormatter: VectorDateFormatter, private val stringProvider: StringProvider ) : AttachmentSourceProvider { @@ -155,7 +156,7 @@ abstract class BaseAttachmentProvider( target.onVideoURLReady(info.uid, data.url) } else { target.onVideoFileLoading(info.uid) - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.IO) { val result = runCatching { fileService.downloadFile( fileName = data.filename, diff --git a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt index 41fe82e797..31162f309f 100644 --- a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt @@ -19,6 +19,7 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.lib.attachmentviewer.AttachmentInfo +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.Room @@ -32,8 +33,16 @@ class DataAttachmentRoomProvider( imageContentRenderer: ImageContentRenderer, dateFormatter: VectorDateFormatter, fileService: FileService, + coroutineScope: CoroutineScope, stringProvider: StringProvider -) : BaseAttachmentProvider(attachments, imageContentRenderer, fileService, dateFormatter, stringProvider) { +) : BaseAttachmentProvider( + attachments = attachments, + imageContentRenderer = imageContentRenderer, + fileService = fileService, + coroutineScope = coroutineScope, + dateFormatter = dateFormatter, + stringProvider = stringProvider +) { override fun getAttachmentInfoAt(position: Int): AttachmentInfo { return getItem(position).let { diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index 43de84f19e..1e0a3a2ad9 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -19,6 +19,7 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.lib.attachmentviewer.AttachmentInfo +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService @@ -38,8 +39,16 @@ class RoomEventsAttachmentProvider( imageContentRenderer: ImageContentRenderer, dateFormatter: VectorDateFormatter, fileService: FileService, + coroutineScope: CoroutineScope, stringProvider: StringProvider -) : BaseAttachmentProvider(attachments, imageContentRenderer, fileService, dateFormatter, stringProvider) { +) : BaseAttachmentProvider( + attachments = attachments, + imageContentRenderer = imageContentRenderer, + fileService = fileService, + coroutineScope = coroutineScope, + dateFormatter = dateFormatter, + stringProvider = stringProvider +) { override fun getAttachmentInfoAt(position: Int): AttachmentInfo { return getItem(position).let { diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt index 5a62454566..bc3acf3eec 100644 --- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt @@ -122,11 +122,11 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen val inMemoryData = intent.getParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA) val sourceProvider = if (inMemoryData != null) { initialIndex = inMemoryData.indexOfFirst { it.eventId == args.eventId }.coerceAtLeast(0) - dataSourceFactory.createProvider(inMemoryData, room) + dataSourceFactory.createProvider(inMemoryData, room, lifecycleScope) } else { val events = room?.getAttachmentMessages().orEmpty() initialIndex = events.indexOfFirst { it.eventId == args.eventId }.coerceAtLeast(0) - dataSourceFactory.createProvider(events) + dataSourceFactory.createProvider(events, lifecycleScope) } sourceProvider.interactionListener = this setSourceProvider(sourceProvider) From 535d266cc2483af96e4a566509ff6d694d055bd4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 May 2021 15:56:51 +0200 Subject: [PATCH 083/202] Remove usage of GlobalScope --- .../core/utils/ExternalApplicationsUtil.kt | 141 +++++++++--------- .../home/room/detail/RoomDetailFragment.kt | 23 ++- .../uploads/RoomUploadsFragment.kt | 25 +++- 3 files changed, 101 insertions(+), 88 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index 859df7d714..005ea362a6 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -43,8 +43,7 @@ import im.vector.app.R import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.themes.ThemeUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okio.buffer import okio.sink import okio.source @@ -57,6 +56,7 @@ import timber.log.Timber import java.io.File import java.io.FileInputStream import java.io.FileOutputStream +import java.lang.IllegalStateException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -344,90 +344,93 @@ private fun appendTimeToFilename(name: String): String { return """${filename}_$dateExtension.$fileExtension""" } -fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val filename = appendTimeToFilename(title) +suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { + withContext(Dispatchers.IO) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val filename = appendTimeToFilename(title) - val values = ContentValues().apply { - put(MediaStore.Images.Media.TITLE, filename) - put(MediaStore.Images.Media.DISPLAY_NAME, filename) - put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType) - put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) - put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) - } - val externalContentUri = when { - mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI - } + val values = ContentValues().apply { + put(MediaStore.Images.Media.TITLE, filename) + put(MediaStore.Images.Media.DISPLAY_NAME, filename) + put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType) + put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) + put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + } + val externalContentUri = when { + mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI + } - val uri = context.contentResolver.insert(externalContentUri, values) - if (uri == null) { - Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show() - } else { - val source = file.inputStream().source().buffer() - context.contentResolver.openOutputStream(uri)?.sink()?.buffer()?.let { sink -> - source.use { input -> - sink.use { output -> - output.writeAll(input) + val uri = context.contentResolver.insert(externalContentUri, values) + if (uri == null) { + Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show() + throw IllegalStateException(context.getString(R.string.error_saving_media_file)) + } else { + val source = file.inputStream().source().buffer() + context.contentResolver.openOutputStream(uri)?.sink()?.buffer()?.let { sink -> + source.use { input -> + sink.use { output -> + output.writeAll(input) + } } } + notificationUtils.buildDownloadFileNotification( + uri, + filename, + mediaMimeType ?: MimeTypes.OctetStream + ).let { notification -> + notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification) + } } - notificationUtils.buildDownloadFileNotification( - uri, - filename, - mediaMimeType ?: MimeTypes.OctetStream - ).let { notification -> - notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification) - } + } else { + saveMediaLegacy(context, mediaMimeType, title, file) } - } else { - saveMediaLegacy(context, mediaMimeType, title, file) } } @Suppress("DEPRECATION") -private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: String, file: File) { +private fun saveMediaLegacy(context: Context, + mediaMimeType: String?, + title: String, + file: File) { val state = Environment.getExternalStorageState() if (Environment.MEDIA_MOUNTED != state) { context.toast(context.getString(R.string.error_saving_media_file)) - return + throw IllegalStateException(context.getString(R.string.error_saving_media_file)) } - GlobalScope.launch(Dispatchers.IO) { - val dest = when { - mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES - mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES - mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC - else -> Environment.DIRECTORY_DOWNLOADS + val dest = when { + mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES + mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES + mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC + else -> Environment.DIRECTORY_DOWNLOADS + } + val downloadDir = Environment.getExternalStoragePublicDirectory(dest) + try { + val outputFilename = if (title.substringAfterLast('.', "").isEmpty()) { + val extension = mediaMimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } + "$title.$extension" + } else { + title } - val downloadDir = Environment.getExternalStoragePublicDirectory(dest) - try { - val outputFilename = if (title.substringAfterLast('.', "").isEmpty()) { - val extension = mediaMimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } - "$title.$extension" - } else { - title - } - val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename) - if (savedFile != null) { - val downloadManager = context.getSystemService() - downloadManager?.addCompletedDownload( - savedFile.name, - title, - true, - mediaMimeType ?: MimeTypes.OctetStream, - savedFile.absolutePath, - savedFile.length(), - true) - addToGallery(savedFile, mediaMimeType, context) - } - } catch (error: Throwable) { - GlobalScope.launch(Dispatchers.Main) { - context.toast(context.getString(R.string.error_saving_media_file)) - } + val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename) + if (savedFile != null) { + val downloadManager = context.getSystemService() + downloadManager?.addCompletedDownload( + savedFile.name, + title, + true, + mediaMimeType ?: MimeTypes.OctetStream, + savedFile.absolutePath, + savedFile.length(), + true) + addToGallery(savedFile, mediaMimeType, context) } + } catch (error: Throwable) { + context.toast(context.getString(R.string.error_saving_media_file)) + throw error } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index cabd69ecf9..534bf55b33 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1745,20 +1745,19 @@ class RoomDetailFragment @Inject constructor( session.coroutineScope.launch { val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } if (!isAdded) return@launch - result.fold( - { - saveMedia( - context = requireContext(), - file = it, - title = action.messageContent.body, - mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), - notificationUtils = notificationUtils - ) - }, - { + result.mapCatching { + saveMedia( + context = requireContext(), + file = it, + title = action.messageContent.body, + mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), + notificationUtils = notificationUtils + ) + } + .onFailure { + if (!isAdded) return@onFailure showErrorInSnackbar(it) } - ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index 3867485e6f..dde71d75ad 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -36,6 +37,7 @@ import im.vector.app.databinding.FragmentRoomUploadsBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.roomprofile.RoomProfileArgs +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -76,13 +78,22 @@ class RoomUploadsFragment @Inject constructor( shareMedia(requireContext(), it.file, getMimeTypeFromUri(requireContext(), it.file.toUri())) } is RoomUploadsViewEvents.FileReadyForSaving -> { - saveMedia( - context = requireContext(), - file = it.file, - title = it.title, - mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()), - notificationUtils = notificationUtils - ) + lifecycleScope.launch { + runCatching { + saveMedia( + context = requireContext(), + file = it.file, + title = it.title, + mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()), + notificationUtils = notificationUtils + ) + }.onFailure { failure -> + if (!isAdded) return@onFailure + showErrorInSnackbar(failure) + } + + } + Unit } is RoomUploadsViewEvents.Failure -> showFailure(it.throwable) }.exhaustive From 14629f2041ee8811aa8418a5585abbb52d95e382 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 14 May 2021 17:13:57 +0200 Subject: [PATCH 084/202] Fix space invite issues --- CHANGES.md | 3 +- .../im/vector/app/core/di/ScreenComponent.kt | 4 +- .../vector/app/features/home/HomeActivity.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 6 +- .../features/spaces/ShareSpaceBottomSheet.kt | 110 --------------- .../spaces/SpaceSettingsMenuBottomSheet.kt | 3 +- .../spaces/people/SpacePeopleActivity.kt | 2 +- .../features/spaces/share/ShareSpaceAction.kt | 24 ++++ .../spaces/share/ShareSpaceBottomSheet.kt | 127 ++++++++++++++++++ .../spaces/share/ShareSpaceViewEvents.kt | 24 ++++ .../spaces/share/ShareSpaceViewModel.kt | 98 ++++++++++++++ .../spaces/share/ShareSpaceViewState.kt | 35 +++++ vector/src/main/res/values/strings.xml | 1 + 13 files changed, 320 insertions(+), 119 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewState.kt diff --git a/CHANGES.md b/CHANGES.md index 39987201cd..b9a972cd6a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,8 @@ Improvements 🙌: - Bugfix 🐛: - - + - Space Invite by link not always displayed for public space (#3345) + - Wrong copy in share space bottom sheet (#3346) Translations 🗣: - diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index c16c602530..dbfbcbdd1e 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -78,12 +78,12 @@ import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.app.features.share.IncomingShareActivity import im.vector.app.features.signout.soft.SoftLogoutActivity import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet -import im.vector.app.features.spaces.ShareSpaceBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.spaces.SpaceExploreActivity -import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet +import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet import im.vector.app.features.spaces.manage.SpaceManageActivity +import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.usercode.UserCodeActivity diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 766955f354..7714a69196 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -59,11 +59,11 @@ import im.vector.app.features.rageshake.ReportType import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity -import im.vector.app.features.spaces.ShareSpaceBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet +import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewState diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index cabd69ecf9..18a2f32073 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -93,9 +93,9 @@ import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.CurrentCallsView +import im.vector.app.core.ui.views.FailedMessagesWarningView import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.KnownCallsViewHolder -import im.vector.app.core.ui.views.FailedMessagesWarningView import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.DimensionConverter @@ -164,7 +164,7 @@ import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData -import im.vector.app.features.spaces.ShareSpaceBottomSheet +import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs @@ -677,7 +677,7 @@ class RoomDetailFragment @Inject constructor( private fun handleSpaceShare() { roomDetailArgs.openShareSpaceForId?.let { spaceId -> - ShareSpaceBottomSheet.show(childFragmentManager, spaceId) + ShareSpaceBottomSheet.show(childFragmentManager, spaceId, true) view?.post { handleChatEffect(ChatEffect.CONFETTI) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt deleted file mode 100644 index 2f69ba89b9..0000000000 --- a/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.spaces - -import android.os.Bundle -import android.os.Parcelable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.FragmentManager -import im.vector.app.R -import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.app.core.utils.startSharePlainTextIntent -import im.vector.app.databinding.BottomSheetSpaceInviteBinding -import im.vector.app.features.invite.InviteUsersToRoomActivity -import kotlinx.parcelize.Parcelize -import javax.inject.Inject - -class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment() { - - @Parcelize - data class Args( - val spaceId: String - ) : Parcelable - - override val showExpanded = true - - @Inject - lateinit var activeSessionHolder: ActiveSessionHolder - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSpaceInviteBinding { - return BottomSheetSpaceInviteBinding.inflate(inflater, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - // Not going for full view model for now, as it may change - - val args: Args = arguments?.getParcelable(EXTRA_ARGS) - ?: return Unit.also { dismiss() } - val summary = activeSessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(args.spaceId)?.spaceSummary() - - val spaceName = summary?.name - views.descriptionText.text = getString(R.string.invite_people_to_your_space_desc, spaceName) - - // XXX enable back when supported - views.inviteByMailButton.isVisible = false - views.inviteByMailButton.debouncedClicks { - } - - views.inviteByMxidButton.debouncedClicks { - val intent = InviteUsersToRoomActivity.getIntent(requireContext(), args.spaceId) - startActivity(intent) - } - - views.inviteByLinkButton.debouncedClicks { - activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createRoomPermalink(args.spaceId)?.let { permalink -> - startSharePlainTextIntent( - fragment = this, - activityResultLauncher = null, - chooserTitle = getString(R.string.share_by_text), - text = getString(R.string.share_space_link_message, spaceName, permalink), - extraTitle = getString(R.string.share_space_link_message, spaceName, permalink) - ) - } - } - -// views.skipButton.debouncedClicks { -// dismiss() -// } - } - - companion object { - - const val EXTRA_ARGS = "EXTRA_ARGS" - - fun show(fragmentManager: FragmentManager, spaceId: String): ShareSpaceBottomSheet { - return ShareSpaceBottomSheet().apply { - isCancelable = true - arguments = Bundle().apply { - this.putParcelable(EXTRA_ARGS, ShareSpaceBottomSheet.Args(spaceId = spaceId)) - } - }.also { - it.show(fragmentManager, ShareSpaceBottomSheet::class.java.name) - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt index 1586b16ff6..c4df939e97 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt @@ -43,6 +43,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.util.toMatrixItem @@ -105,7 +106,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() { diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceAction.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceAction.kt new file mode 100644 index 0000000000..d97e17fabb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.share + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class ShareSpaceAction : VectorViewModelAction { + object InviteByMxId : ShareSpaceAction() + object InviteByLink : ShareSpaceAction() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt new file mode 100644 index 0000000000..08c75698eb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.share + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.utils.startSharePlainTextIntent +import im.vector.app.databinding.BottomSheetSpaceInviteBinding +import im.vector.app.features.invite.InviteUsersToRoomActivity +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment(), ShareSpaceViewModel.Factory { + + @Parcelize + data class Args( + val spaceId: String, + val postCreation: Boolean = false + ) : Parcelable + + override val showExpanded = true + + private val viewModel: ShareSpaceViewModel by fragmentViewModel(ShareSpaceViewModel::class) + + @Inject lateinit var viewModelFactory: ShareSpaceViewModel.Factory + + override fun create(initialState: ShareSpaceViewState): ShareSpaceViewModel = viewModelFactory.create(initialState) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSpaceInviteBinding { + return BottomSheetSpaceInviteBinding.inflate(inflater, container, false) + } + + override fun invalidate() = withState(viewModel) { state -> + super.invalidate() + val summary = state.spaceSummary.invoke() + + val spaceName = summary?.name + + if (state.postCreation) { + views.headerText.text = getString(R.string.invite_people_to_your_space) + views.descriptionText.setTextOrHide(getString(R.string.invite_people_to_your_space_desc, spaceName)) + } else { + views.headerText.text = getString(R.string.invite_to_space, spaceName) + views.descriptionText.setTextOrHide(null) + } + + views.inviteByMailButton.isVisible = false // not yet implemented + views.inviteByLinkButton.isVisible = state.canShareLink + views.inviteByMxidButton.isVisible = state.canInviteByMxId + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // XXX enable back when supported + views.inviteByMailButton.isVisible = false + views.inviteByMailButton.debouncedClicks { + } + + views.inviteByMxidButton.debouncedClicks { + viewModel.handle(ShareSpaceAction.InviteByMxId) + } + + views.inviteByLinkButton.debouncedClicks { + viewModel.handle(ShareSpaceAction.InviteByLink) + } + + viewModel.observeViewEvents { event -> + when (event) { + is ShareSpaceViewEvents.NavigateToInviteUser -> { + val intent = InviteUsersToRoomActivity.getIntent(requireContext(), event.spaceId) + startActivity(intent) + } + is ShareSpaceViewEvents.ShowInviteByLing -> { + startSharePlainTextIntent( + fragment = this, + activityResultLauncher = null, + chooserTitle = getString(R.string.share_by_text), + text = getString(R.string.share_space_link_message, event.spaceName, event.permalink), + extraTitle = getString(R.string.share_space_link_message, event.spaceName, event.permalink) + ) + } + } + } + } + + companion object { + + fun show(fragmentManager: FragmentManager, spaceId: String, postCreation: Boolean = false): ShareSpaceBottomSheet { + return ShareSpaceBottomSheet().apply { + isCancelable = true + setArguments(Args(spaceId = spaceId, postCreation = postCreation)) + }.also { + it.show(fragmentManager, ShareSpaceBottomSheet::class.java.name) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewEvents.kt new file mode 100644 index 0000000000..a7fc4ae27c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.share + +import im.vector.app.core.platform.VectorViewEvents + +sealed class ShareSpaceViewEvents : VectorViewEvents { + data class NavigateToInviteUser(val spaceId: String) : ShareSpaceViewEvents() + data class ShowInviteByLing(val permalink: String, val spaceName: String) : ShareSpaceViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewModel.kt new file mode 100644 index 0000000000..b7920caf40 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewModel.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.share + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper + +class ShareSpaceViewModel @AssistedInject constructor( + @Assisted private val initialState: ShareSpaceViewState, + private val session: Session) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: ShareSpaceViewState): ShareSpaceViewModel + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: ShareSpaceViewState): ShareSpaceViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + init { + val roomSummary = session.getRoomSummary(initialState.spaceId) + setState { + copy( + spaceSummary = roomSummary?.let { Success(it) } ?: Uninitialized, + canShareLink = roomSummary?.isPublic.orFalse() + ) + } + observePowerLevel() + } + + private fun observePowerLevel() { + val room = session.getRoom(initialState.spaceId) ?: return + PowerLevelsObservableFactory(room) + .createObservable() + .subscribe { powerLevelContent -> + val powerLevelsHelper = PowerLevelsHelper(powerLevelContent) + setState { + copy( + canInviteByMxId = powerLevelsHelper.isUserAbleToInvite(session.myUserId) + ) + } + } + .disposeOnClear() + } + + override fun handle(action: ShareSpaceAction) { + when (action) { + ShareSpaceAction.InviteByLink -> { + val roomSummary = session.getRoomSummary(initialState.spaceId) + val alias = roomSummary?.canonicalAlias + val permalink = if (alias != null) { + session.permalinkService().createPermalink(alias) + } else { + session.permalinkService().createRoomPermalink(initialState.spaceId) + } + if (permalink != null) { + _viewEvents.post(ShareSpaceViewEvents.ShowInviteByLing(permalink, roomSummary?.name ?: "")) + } + } + ShareSpaceAction.InviteByMxId -> { + _viewEvents.post(ShareSpaceViewEvents.NavigateToInviteUser(initialState.spaceId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewState.kt new file mode 100644 index 0000000000..97606e9506 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewState.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.spaces.share + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +data class ShareSpaceViewState( + val spaceId: String, + val spaceSummary: Async = Uninitialized, + val canInviteByMxId: Boolean = false, + val canShareLink: Boolean = false, + val postCreation: Boolean = false +) : MvRxState { + constructor(args: ShareSpaceBottomSheet.Args) : this( + spaceId = args.spaceId, + postCreation = args.postCreation + ) +} diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index aa85a52ec3..94273a62c7 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3323,6 +3323,7 @@ Description Invite people to your space Invite people + Invite to %s It’s just you at the moment. %s will be even better with others. Invite by email Invite by username From 39b89ff103a3ad6ce7c7808a69a0c43b2974b926 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 14 May 2021 18:54:11 +0200 Subject: [PATCH 085/202] Code review --- .../vector/app/features/spaces/share/ShareSpaceBottomSheet.kt | 3 +-- .../vector/app/features/spaces/share/ShareSpaceViewEvents.kt | 2 +- .../im/vector/app/features/spaces/share/ShareSpaceViewModel.kt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt index 08c75698eb..4289af7b3b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt @@ -100,7 +100,7 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment { + is ShareSpaceViewEvents.ShowInviteByLink -> { startSharePlainTextIntent( fragment = this, activityResultLauncher = null, @@ -117,7 +117,6 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment { From ec56e689aea626c20cf2c21a79ded5bf0b20d39b Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 14 May 2021 19:35:14 +0000 Subject: [PATCH 086/202] Translated using Weblate (German) Currently translated at 99.7% (2449 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- vector/src/main/res/values-de/strings.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 6c79ea7cd5..14bdf89bfd 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -9,23 +9,23 @@ %1$s hat den Raum verlassen %1$s hat die Einladung abgelehnt %1$s hat %2$s gekickt - %1$s hat die Sperre von %2$s aufgehoben - %1$s hat %2$s verbannt + %1$s hat den Bann von %2$s aufgehoben + %1$s hat %2$s gebannt %1$s hat die Einladung für %2$s zurückgezogen %1$s hat das Profilbild geändert %1$s hat den Anzeigenamen geändert in %2$s %1$s hat den Anzeigenamen von %2$s auf %3$s geändert - %1$s hat den Anzeigenamen gelöscht (%2$s) + %1$s hat den Anzeigenamen gelöscht (war %2$s) %1$s hat das Raumthema geändert auf: %2$s %1$s hat den Raumnamen geändert in: %2$s - %s hat einen Videoanruf durchgeführt. + %s hat einen Videoanruf gestartet. %s hat einen Sprachanruf getätigt. %s hat den Anruf angenommen. %s hat den Anruf beendet. %1$s hat den zukünftigen Chatverlauf sichtbar gemacht für %2$s - Alle Mitglieder (ab dem Zeitpunkt, an dem sie eingeladen wurden). - Alle Mitglieder (ab dem Zeitpunkt, an dem sie den Raum betreten haben). - alle Raum-Mitglieder. + Mitglieder (ab Einladung) + Mitglieder (ab Beitreten) + Mitglieder Jeder. Unbekannt (%s). %1$s hat die Ende-zu-Ende-Verschlüsselung aktiviert (%2$s) @@ -133,7 +133,7 @@ Du hast dein Profilbild geändert Du hast deinen Anzeigenamen zu %1$s geändert Du hast deinen Anzeigenamen von %1$s zu %2$s geändert - Du hast deinen Anzeigenamen entfernt (er war %1$s) + Du hast deinen Anzeigenamen gelöscht (war %1$s) Du hast das Thema geändert auf: %1$s %1$s hat das Bild des Raumes geändert Du hast das Bild des Raumes geändert @@ -2791,7 +2791,7 @@ Meine Teamkameraden und ich Ein privater Space um deine Räume zu organisieren Um einem bereits existierenden Space beizutreten, benötigst du eine Einladung. - Spaces sind eine neue Art Personen und Räume zu gruppieren + Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt. Dein Privater space Dein Öffentliches Space Betrete einen Space mit der angegebenen ID From 0bfa2578a7eb7851376e5ad51b55230fc6797101 Mon Sep 17 00:00:00 2001 From: Trendyne Date: Sat, 15 May 2021 12:44:37 +0000 Subject: [PATCH 087/202] Translated using Weblate (Icelandic) Currently translated at 27.4% (674 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/is/ --- vector/src/main/res/values-is/strings.xml | 220 +++++----------------- 1 file changed, 46 insertions(+), 174 deletions(-) diff --git a/vector/src/main/res/values-is/strings.xml b/vector/src/main/res/values-is/strings.xml index 99c024c320..6512cc3b7d 100644 --- a/vector/src/main/res/values-is/strings.xml +++ b/vector/src/main/res/values-is/strings.xml @@ -1,9 +1,8 @@ - + %1$s: %2$s %1$s sendi mynd. %1$s sendi límmerki. - %s sendi boð um þátttöku %1$s bauð %2$s %1$s bauð þér @@ -21,20 +20,14 @@ óþekktur (%s). VoIP-símafundur hafinn VoIP-símafundi lokið - (einnig var skipt um auðkennismynd) ** Mistókst að afkóða: %s ** - Gat ekki sent skilaboð - Gat ekki sent inn mynd - Villa í netkerfi Villa í Matrix - Tölvupóstfang Símanúmer - %1$s tók til baka boð frá %2$s %1$s setti birtingarnafn sitt sem %2$s %1$s breytti birtingarnafni sínu úr %2$s í %3$s @@ -46,7 +39,6 @@ %s svaraði símtalinu. %s lauk símtalinu. %1$s kveikti á enda-í-enda dulritun (%2$s) - %1$s bað um VoIP-símafund %1$s fjarlægði heiti spjallrásar %1$s fjarlægði umfjöllunarefni spjallrásar @@ -54,31 +46,24 @@ %1$s uppfærði notandasniðið sitt %2$s %1$s sendi boð til %2$s um þátttöku í spjallrásinni %1$s samþykkti boð um að taka þátt í %2$s - Tæki sendandans hefur ekki sent okkur dulritunarlyklana fyrir þessi skilaboð. - Gat ekki ritstýrt Ekki er í augnablikinu hægt að taka aftur þátt í spjallrás sem er tóm. - Boð á spjallrás %1$s og %2$s - %1$s og 1 annar %1$s og %2$d aðrir - Tóm spjallrás Boð frá %s Ljóst þema Dökkt þema Svart þema - - Samstilli + Samstilli… Hlusta eftir atburðum Háværar tilkynningar Hljóðlegar tilkynningar - Skilaboð Spjallrás Stillingar @@ -86,9 +71,7 @@ Ferilskráning Villuskýrsla Nánar um samfélag - Hleð inn… - Í lagi Hætta við Vista @@ -109,7 +92,6 @@ eða Bjóða Ónettengt - Fara út Aðgerðir Skrá út @@ -122,32 +104,26 @@ Loka Afritað á klippispjald Gera óvirkt - Staðfesting Aðvörun - Heim Eftirlæti Fólk Spjallrásir Boðsgestir Lítill forgangur - Samtöl Nafnaskrá á þessari tölvu Engin samtöl Engar niðurstöður - Spjallrásir Engar spjallrásir %d notandi %d notendur - Bjóða Engir hópar - Senda atvikaskrá Senda hrunskrár Senda skjámynd @@ -155,23 +131,19 @@ Lýstu vandamálinu þínu hér Senda inn í Lesið - Taka þátt í spjallrás Notandanafn Skrá þig Innskráning Útskráning Leita - Hefja nýtt spjall Hefja raddsamtal Hefja myndsamtal - Senda skrár Taka ljósmynd eða myndskeið Taka ljósmynd Taka myndskeið - Innskráning Nýskrá Senda inn @@ -197,49 +169,40 @@ Lykilorðin stemma ekki Gleymt lykilorð? Notandanafn er í notkun - Gallað JSON %d breyting á aðild %d breytingar á aðild - - "Senda sem " + Senda sem Upprunalegt Stórt Miðlungs Lítið - Hætta við niðurhalið? Hætta við innsendinguna? %d sek %1$dm %2$ds - Í gær Í dag - Nafn spjallrásar Umfjöllunarefni spjallrásar - Samtal Hringi… Innhringing Innhringing myndsamtals Innhringing raddsamtals - Samtal í gangi - + Samtal í gangi… Upplýsingar Vistað - Vista í niðurhalsmöppu + Vista í niðurhalsmöppu\? NEI Halda áfram - Fjarlægja Taka þátt Forskoðun Hafna - Listi yfir meðlimi Opna haus Samstilli… @@ -254,20 +217,16 @@ %d meðlimir 1 meðlimur - Fara af spjallrás Ertu viss um að þú viljir fara út spjallrásinni? Búa til - Nettengt Ónettengt Iðjulaust - KERFISSTJÓRNUNARTÓL SAMTAL BEINT SPJALL TÆKI - Bjóða Fara af spjallrás Fjarlægja úr þessari spjallrás @@ -283,7 +242,6 @@ %d ný skilaboð %d ný skilaboð - Treysta Ekki treysta Útskráning @@ -294,8 +252,6 @@ Skrár Stillingar Hætta við niðurhal - - Leita Sía meðlimi spjallrásar Engar niðurstöður @@ -303,7 +259,6 @@ SKILABOÐ FÓLK SKRÁR - TAKA ÞÁTT MAPPA EFTIRLÆTI @@ -331,16 +286,14 @@ Athugasemdir frá þriðja aðila Höfundarréttur Meðferð persónuupplýsinga - Notandamynd Birtingarnafn Tölvupóstfang Bæta við tölvupóstfangi Símanúmer Bæta við símanúmeri - Kerfisupplýsingar forrits + Kerfisupplýsingar forrits. Upplýsingar um forrit - Útgáfa Útgáfa olm Skilmálar og kvaðir @@ -365,12 +318,10 @@ Auðkenning Lykilorð: Senda inn - Skráð inn sem Notandaviðmót Tungumál notandaviðmóts Veldu tungumál - Sannvottun í bið Breyta lykilorði eldra lykilorð @@ -378,50 +329,42 @@ staðfestu lykilorð Mistókst að uppfæra lykilorð Lykilorðið þitt hefur verið uppfært - Sýna öll skilaboð frá %s? - + Sýna öll skilaboð frá %s\? +\n +\nAthugaðu að þessi aðgerð mun endurræsa forritið og það getur tekið nokkurn tíma. Veldu land - Land Veldu land Símanúmer Sannprófun símanúmers Kóði - - 3 dagar 1 vika 1 mánuður Að eilífu - Mynd spjallrásar Nafn spjallrásar Umfjöllunarefni Merki spjallrásar Merkt sem: - Eftirlæti Lítill forgangur Ekkert - Aðgengi og sýnileiki Tilkynningar Hver getur lesið ferilskráningu? Hver sem er Bannaðir notendur - Ítarlegt Vistföng Enda-í-enda dulritun Mappa Þema - Upplýsingar um atburð Notandaauðkenni Reiknirit Auðkenni setu Afkóðunarvilla - Upplýsingar um tæki sendanda Heiti tækis Heiti @@ -429,30 +372,24 @@ Dulritunarlykill tækis Sannvottun Ed25519 fingrafar - Flytja út Settu inn lykilsetningu (passphrase) Staðfestu lykilsetningu Flytja inn óþekkt tæki ekkert - Sannreyna Afturkalla sannvottun Bannlisti Taka af bannlista - Allar spjallrásir á %s vefþjóninum Allar innbyggðar %s-spjallrásir - %d spjallrás %d spjallrásir %1$s í %2$s - Leita í ferilskráningu - Stærð leturs Örsmátt Lítið @@ -461,12 +398,10 @@ Stærra Stærst Flennistórt - %d virkur viðmótshluti %d virkir viðmótshlutar - Þú ert ekki á þessari spjallrás. Þú hefur ekki réttindi til þess að gera þetta á þessari spjallrás. @@ -474,53 +409,42 @@ Aðvörun! Skipanavilla Óþekkt skipun: %s - Slökkt Hávært - Dulrituð skilaboð - Búa til Dæmi dæmi - Heim Fólk Spjallrásir Engir notendur - Spjallrásir Sía meðlimi hóps Sía spjallrásahópa - Ástæða: %1$s Gleyma spjallrás - Auðkennismynd kvittunar Auðkennismynd tilkynningar Auðkennismynd - Skoða afkóðaða upprunaskrá Virkt samtal Mistókst að hefja samtal, reyndu aftur síðar Mistókst að hefja samtal Snöggt svar Samfélög - Leita að spjallrásum Leita að eftirlætum Leita að fólki Leita að spjallrásum Leita að samfélagi - Einungis tengiliðir í Matrix Engar opinberar spjallrásir tiltækar Samfélög Framvinda (%s%%) - Senda endurstillingarpóst Fara aftur í innskráningargluggann Þetta lítur ekki út eins og gilt tölvupóstfang @@ -535,15 +459,12 @@ Of margar beiðnir hafa verið sendar Þetta notandanafn er þegar í notkun Listi yfir hópa - Samtal tengt Samtal tengist… Samtali lokið samtali svarað annars staðar - Boð um samtal Nota innbyggða myndavél - Búa til samfélag Heiti samfélags @@ -551,25 +472,20 @@ Minnst á Aðeins minnst á Titra þegar minnst er á - Ertu viss um að þú viljir fjarlægja %1$s %2$s? - Ógilt símanúmer fyrir valið land Við höfum sent SMS-skilaboð með virkjunarkóða. Settu þennan kóða inn hér fyrir neðan. Settu inn virkjunarkóða Villa við að sannreyna símanúmerið þitt Hlutverksmerki - Aðgangur að spjallrás Lesanleiki ferilskrár spjallrásar Hver hefur aðgang að þessari spjallrás? - Enda-í-enda dulritun er virk - " Þessi spjallrás sýnir ekki hlutverksmerki fyrir nein samfélög" + Þessi spjallrás sýnir ekki hlutverksmerki fyrir nein samfélög Dulritun er virk í þessari spjallrás. Dulritun er óvirk í þessari spjallrás. Enda-í-enda dulritunarupplýsingar - Curve25519 auðkennislykill Tilkynnti Ed25519 fingrafarslykil Flytja út E2E dulritunarlykla spjallrásar @@ -581,13 +497,11 @@ Ekki sannreynt Sannreynt Á bannlista - Sannreyna tæki Bæta við Matrix-forritum Hefja sannvottun Deila án sannvottunar Hunsa beiðni - Þér hefur verið sparkað úr %1$s af %2$s Þú hefur verið settur í bann á %1$s af %2$s @@ -600,7 +514,6 @@ Ertu viss að þú viljir byrja nýtt spjall við %s? Ertu viss að þú viljir byrja raddsamtal? Ertu viss að þú viljir byrja myndsamtal? - Nota sérsniðna valkosti vefþjóns (ítarlegt) Heimaþjónn: Auðkennisþjónn: @@ -613,9 +526,7 @@ Gat ekki frumstillt myndavélina Taktu mynd eða myndskeið Get ekki tekið upp myndskeið - Fara í fyrstu ólesin skilaboð. - Nýtt spjall Banna Afbanna @@ -627,9 +538,7 @@ Birta lista yfir tæki SKRÁ YFIR NOTENDUR (%s) Einungis notendur Matrix - Tölvupóstfang eða Matrix-auðkenni - Senda skilaboð (ódulrituð)… Tenging við vefþjón hefur rofnað. Endursenda allt @@ -637,20 +546,18 @@ Eyða ósendum skilaboðum Þú hefur ekki heimild til að senda skilaboð á þessa spjallrás Gat ekki sannreynt auðkenni fjartengds þjóns. - BOÐIÐ SKRÁÐUR - Ástæður fyrir tilkynningu um vankanta á þessu efni - Viltu fela öll skilaboð frá þessum notanda? + Viltu fela öll skilaboð frá þessum notanda\? +\n +\nAthugaðu að þessi aðgerð mun endurræsa forritið og það getur tekið nokkurn tíma. Hætta við innsendingu Leita í yfirlitsskrá… - Öll skilaboð (hávært) Minnka forgang Hætta í samtali Bæta við flýtileið á aðalskjá - Friðhelgi tilkynninga Venjulegt Minnkuð friðhelgi @@ -658,10 +565,8 @@ Virkja tilkynningar fyrir þennan notandaaðgang Virkja tilkynningar á þessu tæki Kveikja á skjá í 3 sekúndur - Þegar mér er boðið á spjallrás Skilaboð send af vélmennum - Virkja í ræsingu Samstilling í bakgrunni Virkja samstillingu í bakgrunnsvinnslu @@ -672,46 +577,36 @@ Friðhelgi tilkynninga Gefa heimild Veldu annan valkost - Gagnavistunarhamur - Nánar um tæki Þessi aðgerð krefst viðbótar-auðkenningar. -Til að halda áfram skaltu setja inn lykilorðið þitt. +\nTil að halda áfram skaltu setja inn lykilorðið þitt. Heimaþjónn Auðkennisþjónn - - Þetta tölvupóstfang er nú þegar í notkun - Gat ekki sent tölvupóst: Þetta tölvupóstfang fannst ekki - Þetta símanúmer er nú þegar í notkun - + Þetta tölvupóstfang er nú þegar í notkun. + Gat ekki sent tölvupóst: Þetta tölvupóstfang fannst ekki. + Þetta símanúmer er nú þegar í notkun. Setja þessa spjallrás á skrá yfir spjallrásir Einungis meðlimir (síðan þessi kostur var valinn) Einungis meðlimir (síðan þeim var boðið) Einungis meðlimir (síðan þeir skráðu sig) - Innra auðkenni þessarar spjallrásar Nýtt vistfang (t.d. #foo:matrix.org) - Nýtt samfélag (t.d. #foo:matrix.org) Ógilt auðkenni samfélags \'%s\' er ekki gilt auðkenni samfélags - - Afrita auðkenni spjallrásar Afrita vistfang spjallrásar - Ég staðfesti hvort dulritunarlyklarnir samsvari - Spjallrás inniheldur óþekkt tæki Veldu skrá yfir spjallrásir URL-slóð heimaþjóns - %d ólesin tilkynnt skilaboð + %d ólesið tilkynnt skilaboð %d ólesin tilkynnt skilaboð - %d ólesin tilkynnt skilaboð + %d ólesið tilkynnt skilaboð %d ólesin tilkynnt skilaboð Þú þarft aðgangsheimildir til að sýsla með viðmótshluta á þessari spjallrás @@ -724,17 +619,14 @@ Til að halda áfram skaltu setja inn lykilorðið þitt. Boðið Taka þátt aftur Listi yfir leskvittanir - Þér hefur verið boðið af %s að taka þátt í þessari spjallrás Ertu viss um að þú viljir fjarlægja %s úr þessu spjalli? Ertu viss um að þú viljir bjóða %s á þetta spjall? - Ertu viss um að þú viljir banna þennan notanda á þessu spjalli? - + Bönun notanda mun henda þeim út úr þessu herbergi og halda þeim frá því að koma aftur. Bjóða miðað við auðkenni TENGILIÐIR Á TÆKI (%d) Bjóða notendum miðað við auðkenni Settu inn auðkenni eða samheiti spjallrásar - %1$s spjallrás fannst fyrir %2$s %1$s spjallrásir fundust fyrir %2$s @@ -745,7 +637,6 @@ Til að halda áfram skaltu setja inn lykilorðið þitt. • Efni skilaboða í tilkynningum er staðsett beint og öruggt á Matrix heimavefþjóninum • Tilkynningar innihalda efni skilaboða og lýsigögn • Tilkynningar munu ekki birta efni skilaboða - Skilaboð innihalda birtingarnafn mitt Skilaboð innihalda notandanafn mitt Skilaboð í maður-á-mann spjalli @@ -758,7 +649,6 @@ Til að halda áfram skaltu setja inn lykilorðið þitt. Þar sem ýmsar heimildir vantar, eru sumir eiginleikar ekki tiltækir… Þú heimilaðir ${app_name} ekki aðgang að tengiliðum á tækinu Hristu ákveðið til að senda villutilkynningu - Það tókst að senda villuskýrsluna Mistókst að senda villuskýrsluna (%s) Þessi heimavefþjónn vill ganga úr skugga um að þú sért ekki vélmenni @@ -768,82 +658,70 @@ Til að halda áfram skaltu setja inn lykilorðið þitt. Gat ekki sannprófað tölvupóstfang: gakktu úr skugga um að þú hafir smellt á tengilinn í tölvupóstinum Tenging gagnamiðils mistókst Því miður, aðgerðin var ekki framkvæmd þar sem nauðsynlegar heimildir vantaði - Þú getur ekki afturkallað þessa aðgerð, þar sem þú ert að gefa notandanum jafn mikil völd og þú hefur sjálf/ur. -Ertu alveg viss? - +\nErtu alveg viss\? Settu inn eitt eða fleir tölvupóstföng eða Matrix auðkenni Skilaboð ekki send. %1$s eða %2$s núna? Skilaboð ekki send vegna þess að vart var við óþekkt tæki. %1$s eða %2$s núna? Hlé milli tveggja samstillingarbeiðna Halda gögnum - ${app_name} getur keyrt í bakgrunni og stýrt tilkynningum á öruggan hátt (getur haft áhrif á rafhlöðunotkun). - Skoðaðu tölvupóstinn þinn og smelltu á tengilinn sem hann inniheldur. Þegar því er lokið skaltu smella á að halda áfram. - Tókst ekki að sannreyna tölvupóstfang. Skoðaðu tölvupóstinn þinn og smelltu á tengilinn sem hann inniheldur. Þegar því er lokið skaltu smella á að halda áfram + Tókst ekki að sannreyna tölvupóstfang. Skoðaðu tölvupóstinn þinn og smelltu á tengilinn sem hann inniheldur. Þegar því er lokið skaltu smella á að halda áfram. Aðeins fólk sem hefur verið boðið \'%s\' er ekki gilt snið fyrir samnefni - Virkja dulritun -(aðvörun: er ekki hægt að gera aftur óvirkt!) - E2E-dulritunarlyklar spjallrásar hafa verið vistaðir í \'%s\' - + Virkja dulritun +\n(aðvörun: er ekki hægt að gera aftur óvirkt!) + E2E-dulritunarlyklar spjallrásar hafa verið vistaðir í \'%s\' +\n +\nViðvörun: Þessi skrá gæti verið eytt ef forritið er fjarlægt. Aðeins dulrita til sannvottaðra tækja - Aldrei senda dulrituð skilaboð af þessu tæki til ósannvottaðra tækja - + Aldrei senda dulrituð skilaboð af þessu tæki til ósannvottaðra tækja. Völd verða að vera jákvæð heiltala. Þú bættir við nýju tæki \'%s\', sem er að krefjast dulritunarlykla. ósannvottaða tækið þitt \'%s\' er að krefjast dulritunarlykla. Símafundir eru í þróun og gætu verið óáreiðanlegir. - Kerfisstjóri samfélagsins hefur ekki gefið upp ítarlega lýsingu fyrir þetta samfélag. - Skrifaðu heimanetþjón til að telja upp opinberar spjallrásir á Vantar spjallrásarauðkenni í beiðni. Vantar notandaauðkenni í beiðni. Senda límmerki - Lykilorðið þitt hefur verið endurstillt. - -Þú hefur verið skráður út af öllum tækjum og munt ekki lengur fá ýti-tilkynningar. Til að endurvirkja tilkynningar, þarf að skrá sig aftur inn á hverju tæki fyrir sig. - +\n +\nÞú hefur verið skráður út af öllum tækjum og munt ekki lengur fá ýti-tilkynningar. Til að endurvirkja tilkynningar, þarf að skrá sig aftur inn á hverju tæki fyrir sig. Tiltekið aðgangsteikn þekktist ekki Ekki var svarað á fjartengda endanum. ${app_name} þarf heimild til að nota mynda- og myndskeiðasafn svo hægt sé að senda og vista viðhengi. - -Leyfðu aðgang í næsta sprettglugga til þess að geta sent skrár úr símanum. +\n +\nLeyfðu aðgang í næsta sprettglugga til þess að geta sent skrár úr símanum. ${app_name} þarf heimild til að nota myndavélina svo hægt sé að taka myndir og hringja myndsímtöl. - - -Leyfðu aðgang í næsta sprettglugga til þess að geta hringt. + " +\n +\nLeyfðu aðgang í næsta sprettglugga til þess að geta hringt." ${app_name} þarf heimild til að nota hljóðnemann svo hægt sé að hringja hljóðsímtöl. - - -Leyfðu aðgang í næsta sprettglugga til þess að geta hringt. + " +\n +\nLeyfðu aðgang í næsta sprettglugga til þess að geta hringt." ${app_name} þarf heimild til að nota myndavélina og hljóðnemann svo hægt sé að hringja myndsímtöl. - -Leyfðu aðgang í næstu sprettgluggum til þess að geta hringt. +\n +\nLeyfðu aðgang í næstu sprettgluggum til þess að geta hringt. ${app_name} þarf heimild til að nota tengiliði í nafnaskránni svo hægt sé að finna aðra Matrix-notendur eftir tölvupóstföngum og símanúmerum þeirra. Leyfðu aðgang í næsta sprettglugga til þess að finna þá notendur í nafnaskránni sem hægt er að hafa samband við úr ${app_name}. ${app_name} þarf heimild til að nota tengiliði í nafnaskránni svo hægt sé að finna aðra Matrix-notendur eftir tölvupóstföngum og símanúmerum þeirra. - -Leyfa ${app_name} nota tengiliðina ? - +\n +\nLeyfa ${app_name} að nota tengiliðina\? Gera notandaaðgang óvirkann Gera notandaaðganginn minn óvirkann - Senda greiningargögn - Já, ég vil hjálpa til - + Já, ég vil hjálpa til! Yfirfara núna - Gera notandaaðgang óvirkann Til að halda áfram, settu inn lykilorðið þitt: Gera notandaaðgang óvirkann - - "Símafundur í gangi.\nTaka þátt með %1$s eða %2$s." + Símafundur í gangi. +\nTaka þátt með %1$s eða %2$s hljóð- myndsímtali Þú þarft aðgangsheimildir til að bjóða til símafundar á þessari spjallrás @@ -854,31 +732,25 @@ Leyfa ${app_name} nota tengiliðina ? Forritið hrundi síðast. Myndirðu vilja senda inn villuskýrslu? Senda límmerki Tölvupósttengill sem ekki er enn búið að smella á - Rangt formað auðkenni. Ætti að vera tölvupóstfang eða Matrix-auðkenni á borð við\'@sérheiti:lén\' ${app_name} safnar nafnlausum greiningargögnum til að gera okkur kleift að bæta forritið. Endilega virkjaðu greiningargögn til að hjálpa okkur að bæta ${app_name}. Til að tengja við spjallrás verður hún að vera með vistfang. Þú ert að reyna að tengjast %s. Myndirðu vilja gerast meðlimur til að geta tekið þátt í samræðunni? Þetta er forskoðun á spjallrásinni. Samskipti spjallrásarinnar hafa verið gerð óvirk. - Heimaskjár Festa spjallrásir með óskoðuðum tilkynningum Festa spjallrásir með ólesnum skilaboðum Sjálfgefið virkja forskoðun innfelldra vefslóða Hver sá sem þekkir slóðina á spjallrásina, fyrir utan gesti Hver sá sem þekkir slóðina á spjallrásina, að gestum meðtöldum - Þetta eru eiginleikar á tilraunastigi sem gætu bilað á óvæntan hátt. Notist með varúð. Þú þarft að skrá þig út til að geta virkjað dulritunina. Aðeins dulrita til sannvottaðra tækja Aldrei senda dulrituð skilaboð af þessu tæki til ósannvottaðra tækja. - Aðvaranir vegna aðalvistfangs - Setja sem aðalvistfang Ekki setja sem aðalvistfang Nauðsynlegt gildi vantar. Gildið er ekki gilt. - - + \ No newline at end of file From 28122aba2cad107820164b9a436d77e7b49354ce Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 14 May 2021 07:40:12 +0000 Subject: [PATCH 088/202] Translated using Weblate (Albanian) Currently translated at 99.5% (2443 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/ --- vector/src/main/res/values-sq/strings.xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index 543061a8eb..2a597dd978 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -2740,4 +2740,25 @@ Hapësira Ftesa Dhoma të Këshilluara + Administroni dhoma dhe hapësira + Hiqi shenjë si e sugjeruar + Vëri shenjë si e sugjeruar + E sugjeruar + Bëje publike këtë hapësirë + Administroni dhoma + Po kërkoni për dikë jo në %s\? + %1$s ju fton + Kjo dhomë është publike + E pakontrolluar + U kontrollua + Dërgoje median në madhësinë origjinale + + Dërgoje videon në madhësinë origjinale + Dërgoji videot në madhësinë origjinale + + Kartela është shumë e madhe për t’u ngarkuar. + Po ngjeshet video %d%% + Po ngjeshet figurë… + Përdore si parazgjedhje dhe mos pyet sërish + Pyet përherë \ No newline at end of file From 0c92949e78c3ac8435c09937750c7e9562ce39ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 14:51:34 +0000 Subject: [PATCH 089/202] Bump gradle from 4.1.3 to 4.2.0 Bumps gradle from 4.1.3 to 4.2.0. Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ec5d35a72c..e7fab91e74 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.0' classpath 'com.google.gms:google-services:4.3.5' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.2.0' From 37cc0dc8b6229fe0d03c433de5ecbc79aaafb540 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 May 2021 10:25:31 +0200 Subject: [PATCH 090/202] Ignore lint issues false positive --- vector/src/main/res/values/font_certs.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/res/values/font_certs.xml b/vector/src/main/res/values/font_certs.xml index 141bfc01d9..5ce2ca424d 100644 --- a/vector/src/main/res/values/font_certs.xml +++ b/vector/src/main/res/values/font_certs.xml @@ -1,17 +1,17 @@ - + @array/com_google_android_gms_fonts_certs_dev @array/com_google_android_gms_fonts_certs_prod - + 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 - \ No newline at end of file + From 552424befb614bbd0eaff0515b883500f4486e5e Mon Sep 17 00:00:00 2001 From: gradle-update-robot Date: Sat, 15 May 2021 00:11:38 +0000 Subject: [PATCH 091/202] Update Gradle Wrapper from 7.0.1 to 7.0.2. Signed-off-by: gradle-update-robot --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 099f10061c..e1e2fd2c75 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=ca42877db3519b667cd531c414be517b294b0467059d401e7133f0e55b9bf265 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.1-all.zip +distributionSha256Sum=13bf8d3cf8eeeb5770d19741a59bde9bd966dd78d17f1bbad787a05ef19d1c2d +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 00b4ab8db9cecf60f37425f772d766bef048a137 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 May 2021 11:39:42 +0200 Subject: [PATCH 092/202] ktlint --- .../crypto/verification/VerificationBottomSheetViewModel.kt | 1 - .../app/features/roomprofile/uploads/RoomUploadsFragment.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 8aa3d5423e..8e21412715 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -33,7 +33,6 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index dde71d75ad..2141b6bf27 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -91,7 +91,6 @@ class RoomUploadsFragment @Inject constructor( if (!isAdded) return@onFailure showErrorInSnackbar(failure) } - } Unit } From 2e2667fd69335b64aa67c168042bc1c6ff77fabd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 May 2021 11:49:13 +0200 Subject: [PATCH 093/202] Changelog --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8c10320816..d00bd7cd75 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,8 @@ SDK API changes ⚠️: - Build 🧱: - - + - Compile with Kotlin 1.5. + - Upgrade some dependencies: gradle wrapper, third party lib, etc. Test: - From 722bccb0bbaab49580d88e09fc56421b81c11aa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 May 2021 16:26:08 +0000 Subject: [PATCH 094/202] Bump google-services from 4.3.5 to 4.3.8 Bumps google-services from 4.3.5 to 4.3.8. Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e7fab91e74..c7d49f19a1 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:4.2.0' - classpath 'com.google.gms:google-services:4.3.5' + classpath 'com.google.gms:google-services:4.3.8' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.2.0' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' From 448eda86242b24eafe5826e967ddf1927439a158 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 May 2021 19:11:57 +0200 Subject: [PATCH 095/202] Replace unbreakable space by a regular space --- CHANGES.md | 4 +- .../internal/crypto/DefaultCryptoService.kt | 52 ++++++++-------- .../sdk/internal/crypto/DeviceListManager.kt | 60 +++++++++---------- .../sdk/internal/crypto/EventDecryptor.kt | 18 +++--- .../algorithms/megolm/MXMegolmDecryption.kt | 4 +- .../algorithms/megolm/MXMegolmEncryption.kt | 38 ++++++------ .../sdk/internal/crypto/tools/HkdfSha256.kt | 2 +- .../internal/session/space/JoinSpaceTask.kt | 2 +- .../session/sync/CryptoSyncHandler.kt | 6 +- 9 files changed, 93 insertions(+), 93 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8ab3b56984..e0baf71e5c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -98,7 +98,7 @@ Changes in Element 1.1.4 (2021-04-09) Improvements 🙌: - Split network request `/keys/query` into smaller requests (250 users max) (#2925) - - Crypto improvement | Bulk send NO_OLM withheld code + - Crypto improvement | Bulk send NO_OLM withheld code - Display the room shield in all room setting screens - Improve message with Emoji only detection (#3017) - Picture preview when replying. Also add the image preview in the message detail bottomsheet (#2916) @@ -657,7 +657,7 @@ Improvements 🙌: - Sending events is now retried only 3 times, so we avoid blocking the sending queue too long. - Display warning when fail to send events in room list - Improve UI of edit role action in member profile - - Moderation | New screen to display list of banned users in room settings, with unban action + - Moderation | New screen to display list of banned users in room settings, with unban action Bugfix 🐛: - Fix theme issue on Room directory screen (#1613) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 2163b2a5e0..7f5cfe8df1 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -545,14 +545,14 @@ internal class DefaultCryptoService @Inject constructor( val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { - Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") return false } val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) if (!encryptingClass) { - Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") + Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") return false } @@ -649,17 +649,17 @@ internal class DefaultCryptoService @Inject constructor( val safeAlgorithm = alg if (safeAlgorithm != null) { val t0 = System.currentTimeMillis() - Timber.v("## CRYPTO | encryptEventContent() starts") + Timber.v("## CRYPTO | encryptEventContent() starts") runCatching { val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) - Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") MXEncryptEventContentResult(content, EventType.ENCRYPTED) }.foldToCallback(callback) } else { val algorithm = getEncryptionAlgorithm(roomId) val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) - Timber.e("## CRYPTO | encryptEventContent() : $reason") + Timber.e("## CRYPTO | encryptEventContent() : $reason") callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) } } @@ -769,7 +769,7 @@ internal class DefaultCryptoService @Inject constructor( } val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) if (alg == null) { - Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") + Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") return } alg.onRoomKeyEvent(event, keysBackupService) @@ -777,7 +777,7 @@ internal class DefaultCryptoService @Inject constructor( private fun onKeyWithHeldReceived(event: Event) { val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { - Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") + Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") } Timber.i("## CRYPTO | onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>") val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) @@ -790,16 +790,16 @@ internal class DefaultCryptoService @Inject constructor( } private fun onSecretSendReceived(event: Event) { - Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") + Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") if (!event.isEncrypted()) { // secret send messages must be encrypted - Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event") + Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event") return } // Was that sent by us? if (event.senderId != userId) { - Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") + Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") return } @@ -809,13 +809,13 @@ internal class DefaultCryptoService @Inject constructor( .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } if (existingRequest == null) { - Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") return } if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { // TODO Ask to application layer? - Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK") + Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK") } } @@ -972,13 +972,13 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.main) { runCatching { withContext(coroutineDispatchers.crypto) { - Timber.v("## CRYPTO | importRoomKeys starts") + Timber.v("## CRYPTO | importRoomKeys starts") val t0 = System.currentTimeMillis() val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) val t1 = System.currentTimeMillis() - Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") + Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") val importedSessions = MoshiProvider.providesMoshi() .adapter>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) @@ -986,7 +986,7 @@ internal class DefaultCryptoService @Inject constructor( val t2 = System.currentTimeMillis() - Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms") + Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms") if (importedSessions == null) { throw Exception("Error") @@ -1125,7 +1125,7 @@ internal class DefaultCryptoService @Inject constructor( */ override fun reRequestRoomKeyForEvent(event: Event) { val wireContent = event.content.toModel() ?: return Unit.also { - Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content") + Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content") } val requestBody = RoomKeyRequestBody( @@ -1140,18 +1140,18 @@ internal class DefaultCryptoService @Inject constructor( override fun requestRoomKeyForEvent(event: Event) { val wireContent = event.content.toModel() ?: return Unit.also { - Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") + Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") } cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { // if (!isStarted()) { -// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init") +// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init") // internalStart(false) // } roomDecryptorProvider .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) ?.requestKeysForEvent(event, false) ?: run { - Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") + Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") } } } @@ -1180,11 +1180,11 @@ internal class DefaultCryptoService @Inject constructor( // val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 // val now = System.currentTimeMillis() // if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { -// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") +// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") // return // } // -// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") +// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") // lastNewSessionForcedDates.setObject(senderId, deviceKey, now) // // cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { @@ -1201,7 +1201,7 @@ internal class DefaultCryptoService @Inject constructor( // val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) // val sendToDeviceMap = MXUsersDevicesMap() // sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) -// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") +// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") // val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) // sendToDeviceTask.execute(sendToDeviceParams) // } @@ -1290,12 +1290,12 @@ internal class DefaultCryptoService @Inject constructor( override fun prepareToEncrypt(roomId: String, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date") + Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date") // Ensure to load all room members try { loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) } catch (failure: Throwable) { - Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members") + Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members") callback.onFailure(failure) return@launch } @@ -1308,7 +1308,7 @@ internal class DefaultCryptoService @Inject constructor( if (alg == null) { val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) - Timber.e("## CRYPTO | prepareToEncrypt() : $reason") + Timber.e("## CRYPTO | prepareToEncrypt() : $reason") callback.onFailure(IllegalArgumentException("Missing algorithm")) return@launch } @@ -1318,7 +1318,7 @@ internal class DefaultCryptoService @Inject constructor( }.fold( { callback.onSuccess(Unit) }, { - Timber.e("## CRYPTO | prepareToEncrypt() failed.") + Timber.e("## CRYPTO | prepareToEncrypt() failed.") callback.onFailure(it) } ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index e5f1c011f8..63f15aaf6e 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -111,7 +111,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM res = !notReadyToRetryHS.contains(userId.substringAfter(':')) } } catch (e: Exception) { - Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") + Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") } } @@ -150,7 +150,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM for (userId in userIds) { if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { - Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") + Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD isUpdated = true } @@ -178,7 +178,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM for (userId in changed) { if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId") + Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId") deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD isUpdated = true } @@ -186,7 +186,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM for (userId in left) { if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId") + Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId") deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED isUpdated = true } @@ -276,7 +276,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param forceDownload Always download the keys even if cached. */ suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { - Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds") + Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds") // Map from userId -> deviceId -> MXDeviceInfo val stored = MXUsersDevicesMap() @@ -305,13 +305,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } } return if (downloadUsers.isEmpty()) { - Timber.v("## CRYPTO | downloadKeys() : no new user device") + Timber.v("## CRYPTO | downloadKeys() : no new user device") stored } else { - Timber.v("## CRYPTO | downloadKeys() : starts") + Timber.v("## CRYPTO | downloadKeys() : starts") val t0 = System.currentTimeMillis() val result = doKeyDownloadForUsers(downloadUsers) - Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") result.also { it.addEntriesFromMap(stored) } @@ -324,7 +324,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param downloadUsers the user ids list */ private suspend fun doKeyDownloadForUsers(downloadUsers: List): MXUsersDevicesMap { - Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}") + Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}") // get the user ids which did not already trigger a keys download val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } if (filteredUsers.isEmpty()) { @@ -335,16 +335,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM val response = try { downloadKeysForUsersTask.execute(params) } catch (throwable: Throwable) { - Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") + Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") onKeysDownloadFailed(filteredUsers) throw throwable } - Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") + Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") for (userId in filteredUsers) { // al devices = val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } - Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models") + Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models") if (!models.isNullOrEmpty()) { val workingCopy = models.toMutableMap() for ((deviceId, deviceInfo) in models) { @@ -377,13 +377,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") } val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") } val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") } cryptoStore.storeUserCrossSigningKeys( userId, @@ -411,28 +411,28 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM */ private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean { if (null == deviceKeys) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") return false } if (null == deviceKeys.keys) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") return false } if (null == deviceKeys.signatures) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") return false } // Check that the user_id and device_id in the received deviceKeys are correct if (deviceKeys.userId != userId) { - Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") return false } if (deviceKeys.deviceId != deviceId) { - Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") + Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") return false } @@ -440,21 +440,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM val signKey = deviceKeys.keys[signKeyId] if (null == signKey) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") return false } val signatureMap = deviceKeys.signatures[userId] if (null == signatureMap) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") return false } val signature = signatureMap[signKeyId] if (null == signature) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") return false } @@ -469,7 +469,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } if (!isVerified) { - Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + deviceKeys.deviceId + " with error " + errorMessage) return false } @@ -480,12 +480,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM // best off sticking with the original keys. // // Should we warn the user about it somehow? - Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + deviceKeys.deviceId + " has changed : " + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) - Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") - Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") + Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") + Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") return false } @@ -499,7 +499,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * This method must be called on getEncryptingThreadHandler() thread. */ suspend fun refreshOutdatedDeviceLists() { - Timber.v("## CRYPTO | refreshOutdatedDeviceLists()") + Timber.v("## CRYPTO | refreshOutdatedDeviceLists()") val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId -> @@ -518,10 +518,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM doKeyDownloadForUsers(users) }.fold( { - Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done") + Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done") }, { - Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") + Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") } ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 32324896fa..8d86380e39 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -92,20 +92,20 @@ internal class EventDecryptor @Inject constructor( private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val eventContent = event.content if (eventContent == null) { - Timber.e("## CRYPTO | decryptEvent : empty event content") + Timber.e("## CRYPTO | decryptEvent : empty event content") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) } else { val algorithm = eventContent["algorithm"]?.toString() val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) if (alg == null) { val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) - Timber.e("## CRYPTO | decryptEvent() : $reason") + Timber.e("## CRYPTO | decryptEvent() : $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) } else { try { return alg.decryptEvent(event, timeline) } catch (mxCryptoError: MXCryptoError) { - Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") + Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") if (algorithm == MXCRYPTO_ALGORITHM_OLM) { if (mxCryptoError is MXCryptoError.Base && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { @@ -119,7 +119,7 @@ internal class EventDecryptor @Inject constructor( markOlmSessionForUnwedging(event.senderId ?: "", it) } ?: run { - Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging") + Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging") } } } @@ -137,18 +137,18 @@ internal class EventDecryptor @Inject constructor( val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 val now = System.currentTimeMillis() if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { - Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") + Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") return } - Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") + Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") lastNewSessionForcedDates.setObject(senderId, deviceKey, now) // offload this from crypto thread (?) cryptoCoroutineScope.launch(coroutineDispatchers.computation) { val ensured = ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) - Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}") + Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}") // Now send a blank message on that session so the other side knows about it. // (The keyshare request is sent in the clear so that won't do) @@ -161,13 +161,13 @@ internal class EventDecryptor @Inject constructor( val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap() sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) - Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}") + Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}") withContext(coroutineDispatchers.io) { val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) try { sendToDeviceTask.execute(sendToDeviceParams) } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}") + Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 787d16defc..a29ac457fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -74,7 +74,7 @@ internal class MXMegolmDecryption(private val userId: String, @Throws(MXCryptoError::class) private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { - Timber.v("## CRYPTO | decryptEvent ${event.eventId} , requestKeysOnFail:$requestKeysOnFail") + Timber.v("## CRYPTO | decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") if (event.roomId.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } @@ -360,7 +360,7 @@ internal class MXMegolmDecryption(private val userId: String, }, { // TODO - Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body") + Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body") } ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 697711d051..a756444475 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -80,9 +80,9 @@ internal class MXMegolmEncryption( eventType: String, userIds: List): Content { val ts = System.currentTimeMillis() - Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") + Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") val devices = getDevicesInRoom(userIds) - Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") + Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") val outboundSession = ensureOutboundSession(devices.allowedDevices) return encryptContent(outboundSession, eventType, eventContent) @@ -91,7 +91,7 @@ internal class MXMegolmEncryption( // annoyingly we have to serialize again the saved outbound session to store message index :/ // if not we would see duplicate message index errors olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) - Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis") + Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis") } } @@ -118,13 +118,13 @@ internal class MXMegolmEncryption( override suspend fun preshareKey(userIds: List) { val ts = System.currentTimeMillis() - Timber.v("## CRYPTO | preshareKey : getDevicesInRoom") + Timber.v("## CRYPTO | preshareKey : getDevicesInRoom") val devices = getDevicesInRoom(userIds) val outboundSession = ensureOutboundSession(devices.allowedDevices) notifyWithheldForSession(devices.withHeldDevices, outboundSession) - Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis") + Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis") } /** @@ -133,7 +133,7 @@ internal class MXMegolmEncryption( * @return the session description */ private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { - Timber.v("## CRYPTO | prepareNewSessionInRoom() ") + Timber.v("## CRYPTO | prepareNewSessionInRoom() ") val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId) val keysClaimedMap = HashMap() @@ -153,7 +153,7 @@ internal class MXMegolmEncryption( * @param devicesInRoom the devices list */ private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo { - Timber.v("## CRYPTO | ensureOutboundSession start") + Timber.v("## CRYPTO | ensureOutboundSession start") var session = outboundSession if (session == null // Need to make a brand new session? @@ -190,7 +190,7 @@ internal class MXMegolmEncryption( devicesByUsers: Map>) { // nothing to send, the task is done if (devicesByUsers.isEmpty()) { - Timber.v("## CRYPTO | shareKey() : nothing more to do") + Timber.v("## CRYPTO | shareKey() : nothing more to do") return } // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) @@ -203,7 +203,7 @@ internal class MXMegolmEncryption( break } } - Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") + Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") shareUserDevicesKey(session, subMap) val remainingDevices = devicesByUsers - subMap.keys shareKey(session, remainingDevices) @@ -232,11 +232,11 @@ internal class MXMegolmEncryption( payload["content"] = submap var t0 = System.currentTimeMillis() - Timber.v("## CRYPTO | shareUserDevicesKey() : starts") + Timber.v("## CRYPTO | shareUserDevicesKey() : starts") val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) Timber.v( - """## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" + """## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" .trimMargin() ) val contentMap = MXUsersDevicesMap() @@ -257,7 +257,7 @@ internal class MXMegolmEncryption( noOlmToNotify.add(UserDevice(userId, deviceID)) continue } - Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") + Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) haveTargets = true } @@ -289,17 +289,17 @@ internal class MXMegolmEncryption( if (haveTargets) { t0 = System.currentTimeMillis() - Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target") + Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) try { sendToDeviceTask.execute(sendToDeviceParams) - Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... - Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") + Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") } } else { - Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey") + Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey") } if (noOlmToNotify.isNotEmpty()) { @@ -317,7 +317,7 @@ internal class MXMegolmEncryption( sessionId: String, senderKey: String?, code: WithHeldCode) { - Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code") + Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code") val withHeldContent = RoomKeyWithHeldContent( roomId = roomId, senderKey = senderKey, @@ -336,7 +336,7 @@ internal class MXMegolmEncryption( try { sendToDeviceTask.execute(params) } catch (failure: Throwable) { - Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") + Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") } } @@ -473,7 +473,7 @@ internal class MXMegolmEncryption( val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap() sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId") + Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) return try { sendToDeviceTask.execute(sendToDeviceParams) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt index 6d52e682bc..6839ccd326 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt @@ -57,7 +57,7 @@ object HkdfSha256 { /* The output OKM is calculated as follows: - Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument; + Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument; N = ceil(L/HashLen) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt index 5e1b829249..e9d5ba5193 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -83,7 +83,7 @@ internal class DefaultJoinSpaceTask @Inject constructor( Timber.v("## Space: > Sync done ...") // after that i should have the children (? do I need to paginate to get state) val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) - Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.spaceChildren?.size}") + Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.spaceChildren?.size}") summary?.spaceChildren?.forEach { // val childRoomSummary = it.roomSummary ?: return@forEach Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt index ae60faf905..411a9c5c06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt @@ -64,7 +64,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: * @return true if the event has been decrypted */ private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { - Timber.v("## CRYPTO | decryptToDeviceEvent") + Timber.v("## CRYPTO | decryptToDeviceEvent") if (event.getClearType() == EventType.ENCRYPTED) { var result: MXEventDecryptionResult? = null try { @@ -76,7 +76,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: val deviceId = cryptoService.getCryptoDeviceInfo(event.senderId!!).firstOrNull { it.identityKey() == senderKey }?.deviceId ?: senderKey - Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") + Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") } if (null != result) { @@ -89,7 +89,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: return true } else { // should not happen - Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}") + Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}") } } From ffba75a49eb576bd8b506b432b5d6cc62f741ee6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 May 2021 18:15:06 +0000 Subject: [PATCH 096/202] Bump gradle from 4.1.3 to 4.2.1 Bumps gradle from 4.1.3 to 4.2.1. Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c7d49f19a1..470e9a06b4 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.2.0' + classpath 'com.android.tools.build:gradle:4.2.1' classpath 'com.google.gms:google-services:4.3.8' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.2.0' From e5cc6ceba7114b9189fe5da5975b8e9efef70c4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 May 2021 18:18:15 +0000 Subject: [PATCH 097/202] Bump epoxy_version from 4.5.0 to 4.6.1 Bumps `epoxy_version` from 4.5.0 to 4.6.1. Updates `epoxy` from 4.5.0 to 4.6.1 - [Release notes](https://github.com/airbnb/epoxy/releases) - [Changelog](https://github.com/airbnb/epoxy/blob/master/CHANGELOG.md) - [Commits](https://github.com/airbnb/epoxy/compare/4.5.0...4.6.1) Updates `epoxy-glide-preloading` from 4.5.0 to 4.6.1 - [Release notes](https://github.com/airbnb/epoxy/releases) - [Changelog](https://github.com/airbnb/epoxy/blob/master/CHANGELOG.md) - [Commits](https://github.com/airbnb/epoxy/compare/4.5.0...4.6.1) Updates `epoxy-processor` from 4.5.0 to 4.6.1 - [Release notes](https://github.com/airbnb/epoxy/releases) - [Changelog](https://github.com/airbnb/epoxy/blob/master/CHANGELOG.md) - [Commits](https://github.com/airbnb/epoxy/compare/4.5.0...4.6.1) Updates `epoxy-paging` from 4.5.0 to 4.6.1 - [Release notes](https://github.com/airbnb/epoxy/releases) - [Changelog](https://github.com/airbnb/epoxy/blob/master/CHANGELOG.md) - [Commits](https://github.com/airbnb/epoxy/compare/4.5.0...4.6.1) Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index a9a8ba0924..1fcb720cc2 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -290,7 +290,7 @@ android { dependencies { - def epoxy_version = '4.5.0' + def epoxy_version = '4.6.1' def fragment_version = '1.3.3' def arrow_version = "0.8.2" def markwon_version = '4.1.2' From 9d7f092016b681b53a0ccc8c1cf825fd81474d6c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 May 2021 21:23:50 +0200 Subject: [PATCH 098/202] Ignore lint false positive --- vector/lint.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/lint.xml b/vector/lint.xml index caed34f2d6..e7e5fe78fe 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -75,4 +75,8 @@ + + + + From 8f4735827e1ce95e340f21e6b17ad69c7cc878cb Mon Sep 17 00:00:00 2001 From: Julian Heinzel Date: Tue, 18 May 2021 22:18:11 +0000 Subject: [PATCH 099/202] Translated using Weblate (German) Currently translated at 99.8% (2451 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- vector/src/main/res/values-de/strings.xml | 200 +++++++++++----------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 14bdf89bfd..75de908bea 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -23,9 +23,9 @@ %s hat den Anruf angenommen. %s hat den Anruf beendet. %1$s hat den zukünftigen Chatverlauf sichtbar gemacht für %2$s - Mitglieder (ab Einladung) - Mitglieder (ab Beitreten) - Mitglieder + alle Mitglieder, ab Einladung. + alle Mitglieder, ab Beitritt. + alle Mitglieder. Jeder. Unbekannt (%s). %1$s hat die Ende-zu-Ende-Verschlüsselung aktiviert (%2$s) @@ -75,7 +75,7 @@ Erste Synchronisation: \nImportiere Benutzerkonto… Erste Synchronisation: -\nImportiere Cryptoschlüssel +\nImportiere Kryptoschlüssel Erste Synchronisation: \nImportiere Räume Erste Synchronisation: @@ -144,7 +144,7 @@ Du hast den Anruf beendet. Du hast den zukünftigen Nachrichtenverlauf für %1$s sichtbar gemacht Du hast Ende-zu-Ende-Verschlüsselung aktiviert (%1$s) - Du hast den Raum aufgwertet. + Du hast den Raum aufgewertet. Du hast eine VoIP-Konferenz angefordert Du hast den Raumnamen entfernt Du hast das Raumthema entfernt @@ -181,16 +181,16 @@ Du hast die Einladung von %1$s angenommen. Grund: %2$s Du hast die Einladung von %1$s abgelehnt. Grund: %2$s - Du hast die Raumaddresse %1$s hinzugefügt. - Du hast die Raumaddressen %1$s hinzugefügt. + Du hast die Raumadresse %1$s hinzugefügt. + Du hast die Raumadressen %1$s hinzugefügt. Du hast die Raum-Adresse %1$s vom Raum entfernt. Du hast die Raum-Adressen %1$s vom Raum entfernt. - Du hast den Raumaddressen %1$s hinzugefügt und %2$s entfernt. - Du hast die Hauptaddresse für diesen Raum auf %1$s gesetzt. - Du hast die Hauptaddresse des Raums entfernt. + Du hast die Raumadressen %1$s hinzugefügt und %2$s entfernt. + Du hast die Hauptadresse für diesen Raum auf %1$s gesetzt. + Du hast die Hauptadresse des Raums entfernt. Du hast Gästen erlaubt dem Raum beizutreten. Du hast Gästen untersagt dem Raum beizutreten. Du hast Ende-zu-Ende-Verschlüsselung aktiviert. @@ -252,7 +252,7 @@ Keine Änderung. • Server, die mit IPs übereinstimmen, sind jetzt gesperrt. • Server, die mit IPs übereinstimmen, sind nicht erlaubt. - • Server, die mit %s übereinstimmen, wurden von der Elaubten-Liste entfernt. + • Server, die mit %s übereinstimmen, wurden von der Erlaubten-Liste entfernt. • Server, die mit %s übereinstimmen, sind jetzt erlaubt. • Server, die mit %s übereinstimmen, werden von der Sperrliste entfernt. • Server, die mit %s übereinstimmen, sind jetzt gesperrt. @@ -396,7 +396,7 @@ E-Mail-Adresse E-Mail-Adresse (optional) Telefonnummer - Telefonummer (optional) + Telefonnummer (optional) Passwort wiederholen Neues Passwort bestätigen Benutzername und/oder Passwort falsch @@ -494,7 +494,7 @@ ${app_name} benötigt die Berechtigung, auf Kamera und Mikrofon zu zugreifen, um Video-Anrufe durchzuführen. \n \nBitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen. - ${app_name} kann dein Adressbuch durchsuchen, um andere Matrix-Nutzer anhand ihrer Email-Adresse und Telefonnummer zu finden. Wenn du der Nutzung deines Adressbuchs zu diesem Zweck zustimmst, erlaube den Zugriff im nächsten Popup-Fenster. + ${app_name} kann dein Adressbuch durchsuchen, um andere Matrix-Nutzer anhand ihrer E-Mail-Adresse und Telefonnummer zu finden. Wenn du der Nutzung deines Adressbuchs zu diesem Zweck zustimmst, erlaube den Zugriff im nächsten Pop-up-Fenster. ${app_name} kann dein Adressbuch durchsuchen, um andere Matrix-Nutzer anhand ihrer E-Mail-Adresse und Telefonnummer zu finden. \n \nStimmst du der Nutzung deines Adressbuchs zu diesem Zweck zu\? @@ -557,7 +557,7 @@ Nur Matrix-Benutzer Benutzer per ID einladen Bitte gib eine oder mehrere E-Mail-Adressen oder eine Matrix-ID ein - E-Mail or Matrix-ID + E-Mail oder Matrix-ID Suchen %s schreibt… @@ -660,7 +660,7 @@ Timeout für Synchronisierungsanfragen Verzögerung zwischen jeder Synchronisierung Version - OLM Version + OLM-Version Nutzungsbedingungen Nutzungshinweise von Drittanbietern Urheberrechtserklärung @@ -719,7 +719,7 @@ Ungültige Mobilfunknummer für das gewählte Land Telefonische Verifizierung Wir haben dir einen Aktivierungscode per SMS gesendet. Bitte trage diesen Code hier ein. - Hier Aktivierungsccode eintragen + Hier Aktivierungscode eintragen Fehler beim Verifizieren der Telefonnummer Code @@ -868,7 +868,7 @@ Zeige Zeitstempel im 12-Stunden-Format Um Widgets in diesem Raum zu verwalten, ist eine Berechtigung erforderlich Widget konnte nicht erstellt werden - Konferenzgespräche mit jitsi durchführen + Konferenzgespräche mit Jitsi durchführen Soll das Widget wirklich aus diesem Raum gelöscht werden? Widget konnte nicht erstellt werden. @@ -1013,7 +1013,7 @@ Normal Verringerter Datenschutz Dies App braucht die Berechtigung im Hintergrund zu laufen - • Benachrichtigungen werden über Firebase Cloud Messaging versendet + • Benachrichtigungen werden über Firebase-Cloud-Messaging versendet • Benachrichtigungen enthalten nur Metadaten • Der Nachrichteninhalt der Benachrichtigung wird sicher vom Matrix-Home-Server abgerufen • Benachrichtigungen enthalten Metadaten und Nachrichteninhalte @@ -1037,8 +1037,8 @@ Ein Parameter ist nicht valide. Um %1$s weiter zu verwenden, musst die Geschäftsbedingungen begutachten und ihnen zustimmen. Jetzt prüfen - Account deaktiveren - Dies wir dein Konto permanent unbenutzbar machen. Du wirst dich nicht anmelden können und keiner wird denselben Nutzernamen erneut registrieren können. Du verlässt automatisch alle Räume, in denen du bist, und deine Kontoangaben werden vom Identitätsserver gelöscht. Diese Aktion ist unumkehrbar. + Account deaktivieren + Dies wird dein Konto permanent unbenutzbar machen. Du wirst dich nicht anmelden können und keiner wird denselben Nutzernamen erneut registrieren können. Du verlässt automatisch alle Räume, in denen du bist, und deine Kontoangaben werden vom Identitätsserver gelöscht. Diese Aktion ist unumkehrbar. \n \nDie Deaktivierung deines Konto wird standardmäßig keine deiner gesendeten Nachrichten löschen. Wenn du möchtest, dass auch deine Nachrichten gelöscht werden, wähle zusätzlich die Option unten. \n @@ -1156,7 +1156,7 @@ Formatiere Nachrichten mittels Markdown-Syntax, bevor sie gesendet werden. Dies erlaubt erweiterte Formatierungen, etwa Sternchen (*) um kursiven Text anzuzeigen. Zeige Lesebestätigungen Klicke auf die Lesebestätigungen für eine detailliertere Liste. - Einladungen, Kicks und Bans bleiben unberührt. + Einladungen, Kicks und Banns bleiben unberührt. Passwort Starte die System-Kamera anstelle der angepassten Kamera. Diese Option erfordert eine externe Anwendung um Sprachnachrichten aufzuzeichnen. @@ -1191,7 +1191,7 @@ Benachrichtigungen sind für diese Sitzung nicht aktiviert. \nBitte überprüfe die Einstellungen für ${app_name}. Aktiviere - ${app_name} benutzt Google Play Dienste um Push-Nachrichten zu übermitteln, doch scheinen sie nicht korrekt konfiguriert zu sein: + ${app_name} benutzt Google-Play-Dienste um Push-Nachrichten zu übermitteln, doch scheinen sie nicht korrekt konfiguriert zu sein: \n%1$s Repariere Play-Dienste Firebase-Token @@ -1199,7 +1199,7 @@ \n%1$s Abfragen des FCM-Tokens fehlgeschlagen: \n%1$s - FCM-Token erfolgreich beim Home-Sserver registriert. + FCM-Token erfolgreich beim Home-Server registriert. FCM-Token konnte nicht beim Home-Server registriert werden: \n%1$s Benachrichtigungsdienst @@ -1209,7 +1209,7 @@ Starte Dienst Automatischer Neustart des Benachrichtigungsdienstes Dienst wurde automatisch gestoppt und erneut gestartet. - Dienst konnte nicht neugestartet werden + Dienst konnte nicht neu gestartet werden Starte beim Hochfahren Dienst wird starten, wenn das Gerät neu gestartet wird. Dieser Dienst wird nicht starten, wenn das Gerät neu gestartet wird. Du wirst keine Benachrichtigungen bekommen bis ${app_name} einmal geöffnet wurde. @@ -1225,9 +1225,9 @@ ${app_name} wird nicht von Batterieoptimierungen beeinflusst. Fehler bei Benachrichtigungen finden Diagnose von Fehlern - Basisdiagnose ist OK. Wenn du immer noch keine Benachrichtigungen bekommst, sende bitte einen Fehlerbericht, um uns beim nachforschen zu helfen. + Basisdiagnose ist OK. Wenn du immer noch keine Benachrichtigungen bekommst, sende bitte einen Fehlerbericht, um uns beim Nachforschen zu helfen. Prüfung der Play-Dienste - Google Play-Dienste-APK ist verfügbar und aktuell. + Google-Play-Dienste-APK ist verfügbar und aktuell. Token-Registrierung Wenn ein Benutzer ein abgestecktes Gerät mit ausgeschaltetem Bildschirm eine Weile nicht bewegt, wechselt es in den Bereitschaftsmodus. Dies hindert Apps daran, auf das Netzwerk zuzugreifen und verzögert die Ausführung von Aufgaben, Synchronisierungen und Standard-Alarmen. Ignoriere Optimierungen @@ -1285,10 +1285,10 @@ [%1$s] \nDieser Fehler ist außerhalb von ${app_name} passiert. Google sagt, dass dieses Gerät zu viele Apps registriert hat um FCM zu nutzen. Der Fehler taucht nur auf, wenn sehr viele Apps installiert sind. Er sollte also den Durchschnittsnutzer nicht betreffen. [%1$s] -\nDieser Fehler liegt nicht unter der Kontrolle von ${app_name}. Er kann aus verschiedenen Gründen auftreten. Vielleicht wird es funktionieren, wenn du es später noch einmal probierst. Außerdem kannst Du prüfen, ob die Datennutzung der Google Play-Dienste unbeschränkt ist und die Geräteuhr richtig eingestellt ist. Der Fehler kann aber auch unter Custom-ROMs auftreten. +\nDieser Fehler liegt nicht unter der Kontrolle von ${app_name}. Er kann aus verschiedenen Gründen auftreten. Vielleicht wird es funktionieren, wenn du es später noch einmal probierst. Außerdem kannst Du prüfen, ob die Datennutzung der Google-Play-Dienste unbeschränkt ist und die Geräteuhr richtig eingestellt ist. Der Fehler kann aber auch unter Custom-ROMs auftreten. [%1$s] \nDieser Fehler ist außerhalb von ${app_name} passiert. Es gibt kein Google-Konto auf dem Gerät. Bitte füge ein Google-Konto hinzu. - Verwaltung der Krypto-Schlüssel + Verwaltung der Kryptoschlüssel Schlüssel-Sicherung verwalten Nachrichten in verschlüsselten Räumen sind mit Ende-zu-Ende-Verschlüsselung gesichert. Nur du und der Empfänger haben die Schlüssel um diese Nachrichten zu lesen. \n @@ -1451,7 +1451,7 @@ Warte auf Bestätigung des Partners… Verifiziert! Du hast diese Sitzung erfolgreich verifiziert. - Sichere Nachrichten mit diesem Benutzer sind Ende-zu-Ende verschlüsselt und können nicht von Dritten mitgelesen werden. + Sichere Nachrichten mit diesem Benutzer sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten mitgelesen werden. Verstanden Schlüssel-Verifizierung Anfrage abgebrochen @@ -1508,7 +1508,7 @@ Schlüsselaustausch anfragen Es sieht so aus, als hättest du bereits ein Setup-Schlüssel-Backup von einer anderen Sitzung. Möchtest du es durch das, was du gerade erstellt hast, ersetzen\? Für maximale Sicherheit empfehlen wir, dies persönlich zu tun, oder ein anderes vertrautes Kommunikationsmedium zu nutzen. - Überprüfe diese Sitzung, um sie als vertrauenswürdig zu markieren. Sitzungen von anderen Nutzern zu vertrauen gibt dir zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende verschlüsselten Nachrichten. + Überprüfe diese Sitzung, um sie als vertrauenswürdig zu markieren. Sitzungen von anderen Nutzern zu vertrauen gibt dir zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende-verschlüsselten Nachrichten. Durch Verifizieren dieser Sitzung wird sie bei dir und deinem Gegenüber als vertrauenswürdig markiert. Verifiziere diese Sitzung, indem du bestätigst, dass das folgende Emoji auf dem Bildschirm deines Gegenübers angezeigt wird Verifiziere diese Sitzung, indem du bestätigst, dass die folgenden Zahlen auf dem Bildschirm deines Gegenübers angezeigt werden @@ -1524,7 +1524,7 @@ Fehlerhaftes Ereignis, kann nicht angezeigt werden Beim Abrufen der Vertrauensinformationen ist ein Fehler aufgetreten Beim Abrufen der Schlüsselsicherungsdaten ist ein Fehler aufgetreten - Matrix SDK Version + Matrix-SDK-Version Sonstige Hinweise Dritter Du siehst diesen Raum bereits! Schnelle Reaktionen @@ -1584,7 +1584,7 @@ Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen Versuche es mit %s Nicht erneut fragen - Richte eine E-Mail für die Kontowiederherstellung ein. Optional, kannst du später einrichten, dass Personen dich über diese Adresse finden. + Richte eine E-Mail für die Kontowiederherstellung ein. Optional kannst du später einrichten, dass Personen dich über diese Adresse finden. Richte eine Telefonnummer ein. Später kannst du einrichten, dass Personen dich über diese finden. Lege eine E-Mail-Adresse für die Kontowiederherstellung fest. Später kann optional eine E-Mail-Adresse oder eine Telefonnummer dazu verwendet werden, um von anderen Personen gefunden zu werden. Lege eine E-Mail-Adresse für die Kontowiederherstellung fest. Später kann optional eine E-Mail-Adresse oder eine Telefonnummer dazu verwendet werden, um von anderen Personen gefunden zu werden. @@ -1629,7 +1629,7 @@ Du kannst dies nicht auf einem mobilen ${app_name} tun Authentifizierung benötigt Hintergrund-Synchronisierungsmodus - ${app_name} wird sich im Hintergrund auf eine Art synchronisieren die Ressourcen des Geräts (Akku) schont. + ${app_name} wird sich im Hintergrund auf eine Art synchronisieren, die Ressourcen des Geräts (Akku) schont. \nAbhängig vom Ressourcen-Status deines Geräts kann dein System die Synchronisierung verschieben. ${app_name} wird sich im Hintergrund periodisch zu einem bestimmten Zeitpunkt synchronisieren (konfigurierbar). \nDies wird Funk- und Akkunutzung beeinflussen. Es wird eine permanente Benachrichtigung geben, die sagt, dass ${app_name} auf Ereignisse lauscht. @@ -1684,7 +1684,7 @@ Schließe das Raumerstellungsmenü… Starte einen neuen Privatchat Erstelle einen neuen Raum - Schließe Key Backup Einblendung + Schließe Key-Backup-Einblendung Passwort anzeigen Passwort verstecken Zum Ende springen @@ -1724,7 +1724,7 @@ \nWenn du keine weiteren Inhalte dieses Nutzers sehen möchtest, kannst ihn ignorieren, um jene Nachrichten auszublenden. ${app_name} benötigt Berechtigungen, um deine E2E Schlüssel zu speichern. \n -\nBitte erlaube den Zugriff im nächsten Pop-Up sodass du deine Schlüssel manuell exportieren kannst. +\nBitte erlaube den Zugriff im nächsten Pop-up sodass du deine Schlüssel manuell exportieren kannst. Aktuell besteht keine Netzwerkverbindung Nutzer ignorieren Alle Nachrichten (laut) @@ -1744,14 +1744,14 @@ Beginne Wähle einen Server Genau wie bei E-Mails haben Accounts ein Zuhause, auch wenn du mit jedem kommunizieren kannst - Folge Millionen anderen kostenlos auf dem größten öffentlichen Server - Premium Hosting für Organisationen + Folge Millionen Anderen kostenlos auf dem größten öffentlichen Server + Premium-Hosting für Organisationen Mehr erfahren Andere Benutzerdefinierte & erweiterte Einstellungen Fortfahren Eine Trennung von deinem Identitätsserver würde bedeuten, dass du weder von anderen Nutzern gefunden werden, noch diese per E-Mail oder Telefonnummer einladen kannst. - Du teilst deine Email Adressen oder Telefonnummern momentan auf dem Identitätsserver %1$s. Du wirst dich erneut mit %2$s verbinden müssen, um mit dem Teilen aufzuhören. + Du teilst deine E-Mail-Adressen oder Telefonnummern momentan auf dem Identitätsserver %1$s. Du wirst dich erneut mit %2$s verbinden müssen, um mit dem Teilen aufzuhören. Stimme den Nutzungsbedingungen des Identitätsservers (%s) zu, um zu erlauben per E-Mail oder Telefonnummer gefunden zu werden. Zu teilende Daten nicht verarbeitbar Erweitere & individualisiere dein Benutzererlebnis @@ -1762,7 +1762,7 @@ Registrieren Anmelden Mit einmaligem Anmelden fortfahren - Element Matrix Services Adresse + Element Matrix Services-Adresse Adresse Ein Fehler beim Laden der Seite %1$s (%2$d) ist aufgetreten Es tut uns leid. Dieser Server akzeptiert keine neuen Benutzerkonten. @@ -1778,7 +1778,7 @@ Fortfahren Diese E-Mail-Adresse ist mit keinem Benutzerkonto verknüpft Prüfe deinen Posteingang - Eine Bestätigungsemail wurde an %1$s versendet. + Eine Bestätigungsmail wurde an %1$s versendet. Klicke auf den Link um dein neues Passwort zu bestätigen. Sobald du dem enthaltenen Link gefolgt bist, klicke unten. Ich habe meine E-Mail-Adresse bestätigt Erfolgreich! @@ -1818,9 +1818,9 @@ Die Anwendung kann sich nicht bei diesem Home-Server anmelden. Der Home-Server unterstützt die folgenden Anmeldetypen: %1$s. \n \nMöchtest du dich mit einem Webclient anmelden\? - Eine Bestätigungs-E-Mail wird an dich gesendet, um dein neues Passwort zu bestätigen. + Eine Bestätigungsmail wird an dich gesendet, um dein neues Passwort zu bestätigen. Weiter - Du wurdest von allen Sitzungen abgemeldet und erhälst keine Push-Benachrichtigungen mehr. Um Benachrichtigungen wieder zu aktivieren, melde dich auf jedem Gerät erneut an. + Du wurdest von allen Sitzungen abgemeldet und erhältst keine Push-Benachrichtigungen mehr. Um Benachrichtigungen wieder zu aktivieren, melde dich auf jedem Gerät erneut an. Warnung Lege eine E-Mail-Adresse fest, um dein Konto wiederherzustellen. Später kannst du optional zulassen, dass Personen dich anhand dieser E-Mail-Adresse entdecken. Weiter @@ -1855,7 +1855,7 @@ Du bist abgemeldet Anmelden Dein Home-Server-Administrator (%1$s) hat dich von deinem Konto %2$s (%3$s) abgemeldet. - Melden dich an, um ausschließlich auf diesem Gerät gespeicherte Verschlüsselungsschlüssel wiederherzustellen. Du benötigst sie, um deine verschlüsselten Nachrichten auf jedem Gerät zu lesen. + Melde dich an, um ausschließlich auf diesem Gerät gespeicherte Verschlüsselungsschlüssel wiederherzustellen. Du benötigst sie, um deine verschlüsselten Nachrichten auf jedem Gerät zu lesen. Anmelden Passwort Persönliche Daten löschen @@ -1875,8 +1875,8 @@ Initiale Synchronisierung… Alle meine Sitzungen anzeigen Erweiterte Einstellungen - Entwicklungsmodus - Der Entwicklungsmodus aktiviert versteckte Funktionen und kann die Anwendung weniger stabil machen. Nur für Entwickler! + Entwicklermodus + Der Entwicklermodus aktiviert versteckte Funktionen und kann die Anwendung weniger stabil machen. Nur für Entwickler! Wutschütteln Erkennungsschwelle Schüttel dein Telefon, um die Erkennungsschwelle zu testen @@ -1932,8 +1932,8 @@ Für zusätzliche Sicherheit überprüfe %s, indem ihr auf beiden Geräten einen einzigartigen Code überprüft. \n \nFür maximale Sicherheit macht dies persönlich. - Nachrichten in diesem Raum sind nicht Ende-zu-Ende verschlüsselt. - Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. + Nachrichten in diesem Raum sind nicht Ende-zu-Ende-verschlüsselt. + Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. \n \nDeine Nachrichten sind gesichert und nur du und dein Gegenüber haben die eindeutigen Schlüssel, um sie zu entsperren. Sicherheit @@ -1958,7 +1958,7 @@ Springen & als gelesen markieren ${app_name} kann keine Ereignisse vom Typ \'%1$s\' ${app_name} beherrscht keine Nachrichten vom Typ \'%1$s\' - ${app_name} ist beim verarbeiten des Ereignisinhalts mit der ID \'%1$s\' auf ein Problem gestoßen + ${app_name} ist beim Verarbeiten des Ereignisinhalts mit der ID \'%1$s\' auf ein Problem gestoßen Nicht ignorieren Diese Sitzung kann diese Verifizierung nicht mit deinen anderen Sitzungen teilen. \nDie Überprüfung wird lokal gespeichert und in einer zukünftigen Version der App freigegeben. @@ -1973,11 +1973,11 @@ Verschlüsselung aktivieren\? Nach der Aktivierung kann die Verschlüsselung für einen Raum nicht deaktiviert werden. In einem verschlüsselten Raum gesendete Nachrichten können vom Server nicht gesehen werden, nur von den Teilnehmenden des Raums. Durch die Verschlüsselung funktionieren viele Bots und Bridges möglicherweise nicht ordnungsgemäß. Verschlüsselung aktivieren - Um sicher zu gehen, verifiziere %s, indem ein einmaligen Code überprüft wird. + Um sicher zu gehen, verifiziere %s, indem ein einmaliger Code überprüft wird. Um sicher zu sein, tut dies persönlich oder verwendet einen anderen Kommunikationsweg. Vergleiche die einzigartigen Emoji und stell sicher, dass sie in derselben Reihenfolge angezeigt werden. Vergleiche den Code mit dem Code auf dem Bildschirm deines Gegenübers. - Nachrichten mit diesem Gegenüber sind Ende-zu-Ende verschlüsselt und können nicht von Dritten gelesen werden. + Nachrichten mit diesem Gegenüber sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden. Deine neue Sitzung ist jetzt verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer sehen sie als vertrauenswürdig an. Cross-Signing Cross-Signing ist aktiviert @@ -2016,11 +2016,11 @@ Initialisiere Cross-Signing Schlüssel zurücksetzen QR-Code - Fast geschaft! Zeigt %s dasselbe Schild an\? + Fast geschafft! Zeigt %s dasselbe Schild an\? Ja Nein Verbindung zum Server wurde unterbrochen - Entwicklungswerkzeuge + Entwicklerwerkzeuge Kontodaten %d Stimme @@ -2060,9 +2060,9 @@ Neu laden Neue Anmeldung. Warst du das\? Tippe für eine Überprüfung & Verifikation - Benutze diese Sitzung um deine neue zu verfizieren, damit sie auf verschlüsselte Nachrichten zugreifen kann. + Benutze diese Sitzung um deine neue zu verifizieren, damit sie auf verschlüsselte Nachrichten zugreifen kann. Das war ich nicht - Dein Account ist möglicherweise komprimittiert + Dein Account ist möglicherweise kompromittiert Wenn du abbrichst, wirst du auf diesem Gerät keine verschlüsselten Nachrichten lesen können, und andere Benutzer werden ihm nicht vertrauen Wenn du abbrichst, wirst du auf deinem neuen Gerät keine verschlüsselten Nachrichten lesen können, und andere Benutzer werden ihm nicht vertrauen Du wirst %1$s (%2$s) nicht verifizieren, wenn du jetzt abbrichst. Beginne in deren Nutzerprofil erneut. @@ -2078,7 +2078,7 @@ Verifizierung abgebrochen Generiere einen Nachrichtenschlüssel Bestätige %s - Gibe dein %s ein um fortzufahren. + Gib dein %s ein um fortzufahren. Gib deine %s für eine Bestätigung erneut ein. Benutze dein Accountpasswort nicht mehrfach. Dies könnte einige Sekunden dauern, gedulde dich bitte. @@ -2090,7 +2090,7 @@ Benutze diesen %1$s als Sicherheit für den Fall, dass du deine %2$s vergisst. Veröffentliche erstellte Identitätsschlüssel Generiere sicheren Schlüssel von der Passphrase - Definiere SSSS Standardschlüssel + Definiere SSSS-Standardschlüssel Synchronisiere Hauptschlüssel Synchronisiere Benutzerschlüssel Synchronisiere selbstsignierenden Schlüssel @@ -2115,7 +2115,7 @@ Dies kann nicht von einem mobilen Gerät erfolgen Wenn Räume verbessert werden Verschlüsselung aktiviert - Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. Erfahre mehr & verifiziere Benutzer in deren Profil. + Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Erfahre mehr & verifiziere Benutzer in deren Profil. Die Verschlüsselung in diesem Raum wird nicht unterstützt Warte auf %s… %s setzen @@ -2141,32 +2141,32 @@ Überprüfe Wiederherstellungsschlüssel Überprüfe Sicherungsstatus (%s) Erzeuge Kurvenschlüssel - Generiere SSSS Schlüssel aus dem Passwort - Generiere SSSS Schlüssel aus dem Passwort (%s) - Generiere SSSS Schlüssel aus dem Wiederherstellungsschlüssel - Speichere Schlüsselbackup Schlüssel in SSSS + Generiere SSSS-Schlüssel aus dem Passwort + Generiere SSSS-Schlüssel aus dem Passwort (%s) + Generiere SSSS-Schlüssel aus dem Wiederherstellungsschlüssel + Speichere Schlüsselbackup-Schlüssel in SSSS %1$s (%2$s) Gib dein Passwort für das Schlüsselbackup ein, um fortzufahren. - nutze deinen Schlüsselbackup Wiederherstellungsschlüssel - Wenn du dein Schlüsselbackup Passwort nicht weißt, kannst du %s. - Schlüsselbackup Wiederherstellungsschlüssel + nutze deinen Schlüsselbackup-Wiederherstellungsschlüssel + Wenn du dein Schlüsselbackup-Passwort nicht weißt, kannst du %s. + Schlüsselbackup-Wiederherstellungsschlüssel Verhindere Screenshots innerhalb der Anwendung Das Aktivieren dieser Einstellung setzt das FLAG_SECURE in allen Aktivitäten. Starte die Anwendung neu, damit die Änderung wirksam wird. Datei wurde der Galerie hinzugefügt Datei konnte nicht zur Galerie hinzugefügt werden Neues Benutzerpasswort festlegen… - Nutze die neueste Version von ${app_name} auf deinen anderen Geräten, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} für Android oder einen anderen cross-signing fähigen Matrix client + Nutze die neueste Version von ${app_name} auf deinen anderen Geräten, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} für Android oder einen anderen cross-signing-fähigen Matrix-Client ${app_name} Web \n${app_name} Desktop ${app_name} iOS \n${app_name} Android - oder einen anderen cross-signing fähigen Matrix Client + oder einen anderen cross-signing-fähigen Matrix Client Nutze die neueste Version von ${app_name} auf deinen anderen Geräten: Erzwingt das Verwerfen der aktuell ausgehende Gruppensitzung in einem verschlüsseltem Raum Wird nur in verschlüsselten Räumen unterstützt Benutze deine %1$s oder deinen %2$s um fortzufahren. Wiederherstellungsschlüssel verwenden - Wähle deinen Wiederherstellungsschüssel, gib ihn ein oder füge ihn aus der Zwischenablage ein + Wähle deinen Wiederherstellungsschlüssel, gib ihn ein oder füge ihn aus der Zwischenablage ein Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden. Bitte stelle sicher, dass du den korrekten Wiederherstellungsschlüssel eingegeben hast. Konnte nicht auf gesicherten Speicher zugreifen Unverschlüsselt @@ -2292,7 +2292,7 @@ Sticker Administrative Aktionen Standard in %1$s - Dein Serveradministrator hat in privaten Räumen und Direktnachrichten Ende-zu-Ende Verschlüsselung standardmäßig deaktiviert. + Dein Serveradministrator hat in privaten Räumen und Direktnachrichten Ende-zu-Ende-Verschlüsselung standardmäßig deaktiviert. Flugzeugmodus ist aktiv Gib eine Sicherheitsphrase ein, die nur du kennst. Diese wird benutzt um deine Daten auf dem Server geheim zu halten. Wenn du jetzt abbrichst und den Zugriff zu deinen Sitzungen verlierst, kannst du verschlüsselte Nachrichten & Daten verlieren. @@ -2300,21 +2300,21 @@ \nDu kannst auch ein Backup einrichten & deine Schlüssel in den Einstellungen verwalten. Du hast den Raum erstellt und konfiguriert. Dieser Account ist deaktiviert worden. - Aktiviere Cross Signing + Aktiviere Cross-Signing Konnte Mediendatei nicht speichern Aktuelle Sprache Andere verfügbare Sprachen Lade verfügbare Sprachen… Öffne AGBs von %s - Trenne Verbingung zu Identitätsserver %s\? + Trenne Verbindung zu Identitätsserver %s\? Dieser Identitätsserver ist veraltet. ${app_name} unterstützt nur API V2. - Diese Operation is nicht möglich. Der Homeserver ist veraltet. + Diese Operation ist nicht möglich. Der Home-Server ist veraltet. Bitte konfiguriere zuerst einen Identitätsserver. Bitte akzeptiere zuerst die AGB des Identitätsservers in den Einstellungen. - Deiner Privatssphäre wegen unterstützt ${app_name} nur das Senden gehashter Emailaddressen und Telefonnummern. + Deiner Privatsphäre wegen unterstützt ${app_name} nur das Senden gehashter E-Mail-Adressen und Telefonnummern. Die Assoziierung ist fehlgeschlagen. Für diese Kennung gibt es aktuell keine Assoziierung. - Dein Homeserver (%1$s) schlägt %2$s als Identitätsserver vor + Dein Home-Server (%1$s) schlägt %2$s als Identitätsserver vor Benutze %1$s Alternativ kannst du die URL eines beliebigen anderen Identitätsservers angeben Gib die URL von einem Identitätsserver ein @@ -2347,7 +2347,7 @@ Du kannst auf diese Nachricht nicht zugreifen Warte auf diese Nachricht. Das könnte eine Weile dauern Kann nicht entschlüsselt werden - Wegen der Ende-zu-Ende Verschlüsselung könnte es sein, dass du auf jemandes Nachricht warten musst, weil die Verschlüsselungsschlüssel nicht ordnungsgemäß gesendet worden sind. + Wegen der Ende-zu-Ende-Verschlüsselung könnte es sein, dass du auf jemandes Nachricht warten musst, weil die Verschlüsselungsschlüssel nicht ordnungsgemäß gesendet worden sind. Du kannst auf diese Nachricht nicht zugreifen, weil der Sender dich blockiert hat Du kannst auf diese Nachricht nicht zugreifen, weil der Sender deiner Sitzung nicht vertraut Du kannst auf diese Nachricht nicht zugreifen, weil der Sender absichtlich die Schlüssel nicht gesendet hat @@ -2388,7 +2388,7 @@ Es ist bereits eine Konferenz aktiv! Starte eine Videokonferenz Starte eine Audiokonferenz - Konferenzen nutzen die Jitsi Sicherheits- und Berechtigungsrichtlinien. Alle im Raum Anwessenden können während der Konferenz beitreten. + Konferenzen nutzen die Jitsi-Sicherheits- und Berechtigungsrichtlinien. Alle im Raum Anwesenden können während der Konferenz beitreten. Du kannst dich nicht selbst anrufen Du kannst dich nicht selbst anrufen, warte bis Teilnehmer die Einladung annehmen Hinzufügen des Widgets fehlgeschlagen @@ -2407,14 +2407,14 @@ Von %1$s, %2$s und %3$d anderen gelesen - Falscher code, %d verbleibender Versuch - Falscher code, %d verbleibende Versuche + Falscher Code, %d verbleibender Versuch + Falscher Code, %d verbleibende Versuche Warnung! Letzter Versuch bevor du ausgeloggt wirst! Zu viele Fehler. Du wurdest ausgeloggt Diese Telefonnummer ist bereits registriert. Deinem Konto wurde keine Telefonnummer hinzugefügt - E-mail-Adressen + E-Mail-Adressen Deinem Konto wurde keine E-Mail hinzugefügt Telefonnummern %s entfernen\? @@ -2469,14 +2469,14 @@ Alle Wiederherstellungsoptionen vergessen oder verloren\? Alles zurücksetzen Du bist beigetreten. %s ist beigetreten. - Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. + Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Verlassen Einstellungen - Nachrichten hier sind Ende-zu-Ende verschlüsselt. + Nachrichten hier sind Ende-zu-Ende-verschlüsselt. \n \nDeine Nachrichten sind mit digitalen Schlüsseln gesichert. Nur du und der Empfänger haben die einzigen Schlüssel, um jene zu entsperren. - Nachrichten hier sind nicht Ende-zu-Ende verschlüsselt. - Dieser Homeserver läuft mit einer alten Version. Bitte deinen Homeserver-Administrator um eine Aktualisierung. Du kannst fortfahren, aber einige Funktionen funktionieren möglicherweise nicht richtig. + Nachrichten hier sind nicht Ende-zu-Ende-verschlüsselt. + Dieser Home-Server läuft mit einer alten Version. Bitte deinen Home-Server-Administrator um eine Aktualisierung. Du kannst fortfahren, aber einige Funktionen funktionieren möglicherweise nicht richtig. Du hast dies auf Einladungen beschränkt. %1$s hat dies auf Einladungen beschränkt. Zeige vollständigen Verlauf in verschlüsselten Räumen an @@ -2514,8 +2514,8 @@ E-Mails und Telefonnummern senden Autorisieren Meine Zustimmung widerrufen - Du hast zugestimmt E-Mails und Telefonnummern an diesen Identitätsserver zu senden, um von anderen Nutzenden entdeckt zu werden. - Du hast nicht zugestimmt E-Mails und Telefonnummern an diesen Identitätsserver zu senden, um von anderen Nutzenden entdeckt zu werden. + Du hast zugestimmt E-Mails und Telefonnummern an diesen Identitätsserver zu senden, um von anderen Nutzern entdeckt zu werden. + Du hast nicht zugestimmt E-Mails und Telefonnummern an diesen Identitätsserver zu senden, um von anderen Nutzern entdeckt zu werden. E-Mails und Telefonnummern senden Vorschläge Kontakte @@ -2531,7 +2531,7 @@ Raumadressen und Sichtbarkeit im Raumverzeichnis ansehen und bearbeiten. Raumadresse Raumzugang - Änderungen daran, wer die Chronik lesen kann, gellten nur für kommende Nachrichten in diesem Raum. Die Sichtbarkeit der bestehenden Chronik bleibt unverändert. + Änderungen daran, wer die Chronik lesen kann, gelten nur für kommende Nachrichten in diesem Raum. Die Sichtbarkeit der bestehenden Chronik bleibt unverändert. Zurückziehen Hinzufügen Mit Nachricht teilen @@ -2540,7 +2540,7 @@ Die Sichtbarkeit des Raums konnte nicht abgerufen werden (%1$s). Matrix-Link QR-Code nicht gescannt! - Inkorrekter QR code (invalide URL)! + Inkorrekter QR-Code (invalide URL)! Du kannst dir selbst keine Direktnachricht schicken! Ändere deine aktuelle PIN PIN ändern @@ -2550,7 +2550,7 @@ Teile diesen Code mit Leuten, damit sie ihn scannen und mit dir chatten können. Meinen Code teilen Mein Code - Scanne einen QR code + Scanne einen QR-Code Das ist kein korrekter QR-Code von Matrix 🔐️ Komm mit zu ${app_name} Hey, schreibe mit mir auf ${app_name}: %s @@ -2568,10 +2568,10 @@ Bitte gib eine Raumadresse an Diese Adresse ist bereits vergeben Raumadresse - Aktivieren, wenn der Raum nur von Mitgliedern deines Homeservers zur internen Kommunikation verwendet wird. Das kann später nicht mehr geändert werden. + Aktivieren, wenn der Raum nur von Mitgliedern deines Home-Servers zur internen Kommunikation verwendet wird. Das kann später nicht mehr geändert werden. Begrenze Zugang zu diesem Raum (für immer!) auf Mitglieder von %s %1$d von %2$d - QR-Code scannen, um eine neue Direktnachtricht schicken + QR-Code scannen, um eine neue Direktnachricht schicken Um bestehende Kontakte zu finden, ist es notwendig, Ihre Kontaktdaten (Telefonnummern und/oder E-Mails) mit dem ausgewählten Identitätsserver (%1$s) zu teilen. \n \nFür mehr Datenschutz werden die gesendeten Daten vor dem Versenden gehasht. @@ -2639,7 +2639,7 @@ Authentifizierung fehlgeschlagen Deine Anmeldeinformationen müssen für ${app_name} eingegeben werden, um diese Aktion auszuführen. Erneute Authentifizierung erforderlich - Cross Signing konnte nicht eingerichtet werden + Cross-Signing konnte nicht eingerichtet werden Nicht autorisierte, fehlende gültige Authentifizierungsdaten Nutzer Beim Übertragen des Anrufs ist ein Fehler aufgetreten @@ -2677,23 +2677,23 @@ Ereignisinhalt Statusschlüssel Typ - Benutzendendefiniertes Status-Event senden + Benutzerdefiniertes Status-Event senden Inhalt bearbeiten Status-Events Status-Event senden - Benutzendendefiniertes Ereignis senden + Benutzerdefiniertes Ereignis senden Raum-Status erkunden - Entwicklungswerkzeuge + Entwicklerwerkzeuge Lesebestätigungen anzeigen Nicht benachrichtigen Mit Ton benachrichtigen Ohne Ton benachrichtigen - Nachricht wegen eines Errors nicht gesendet + Nachricht aufgrund eines Fehlers nicht gesendet Geprüft Emoji-Auswahl schließen Emoji-Auswahl öffnen Vertraute Vertrauensstufe - Warnungsvertrauensstufe + Warnungs-Vertrauensstufe Standard-Vertrauensstufe Ausgewählt Video @@ -2710,10 +2710,10 @@ %d Einträge Die Obergrenze ist nicht bekannt. - Dein Homeserver akzeptiert Anhänge (wie Dateien, Medien, etc.) mit einer Größe bis zu %s. + Dein Home-Server akzeptiert Anhänge (wie Dateien, Medien, etc.) mit einer Größe bis zu %s. Datei-Upload-Obergrenze des Servers - Server-Version - Severname + Serverversion + Servername Raum-Einstellungen Derzeitige Konferenz verlassen und zu einer anderen wechseln\? Raum-Version @@ -2727,7 +2727,7 @@ Wechseln Zeige alle Räume im Raumverzeichnis, inklusive der Räume mit anstößigen Inhalten. Zeige Räume mit anstößigen Inhalten - Bist du dir sicher, dass du alle nicht gesendete Nachrichten in diesem Raum löschen willst\? + Bist du dir sicher, dass du alle nicht gesendeten Nachrichten in diesem Raum löschen willst\? Nicht gesendete Nachrichten löschen Fehlgeschlagen Willst du zu sendende Nachrichten zurückziehen\? @@ -2745,7 +2745,7 @@ Nur Eingeladene können es finden und beitreten Privat Unbekannte Zugriffseinstellung (%s) - Gäste erlauben beizutreten + Gästen erlauben beizutreten Einladungen Vorgeschlagene Räume Spaces From 4e554754292d28c027506c35a9142a8015263496 Mon Sep 17 00:00:00 2001 From: Michael Sasser Date: Tue, 18 May 2021 01:02:54 +0000 Subject: [PATCH 100/202] Translated using Weblate (German) Currently translated at 99.8% (2451 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- vector/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 75de908bea..071577b5ac 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -2768,7 +2768,7 @@ Stelle sicher, dass die richtigen Personen Zugang zu %s besitzen. Du kannst das später noch ändern. Mit wem arbeitest du zusammen\? Verlasse den Raum mit der angegebenen ID (oder den aktuellen Raum, indem du keine ID angibst) - Name des Spaces + Name suchen Du wurdest eingeladen Bist du dir sicher, dass du den Space verlassen willst\? Space verlassen From 5bd3aed51c0d8ef3e582a13d959b9acb54b34bd6 Mon Sep 17 00:00:00 2001 From: Black616Angel Date: Mon, 17 May 2021 11:56:47 +0000 Subject: [PATCH 101/202] Translated using Weblate (German) Currently translated at 99.8% (2451 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- vector/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 071577b5ac..f37a501ae0 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -2767,7 +2767,7 @@ Zugang nur mit Einladung. Am besten für Teams oder dich selbst Stelle sicher, dass die richtigen Personen Zugang zu %s besitzen. Du kannst das später noch ändern. Mit wem arbeitest du zusammen\? - Verlasse den Raum mit der angegebenen ID (oder den aktuellen Raum, indem du keine ID angibst) + Verlasse den Raum mit der angegebenen ID (oder den aktuellen Raum, wenn keine ID angegeben wird) Name suchen Du wurdest eingeladen Bist du dir sicher, dass du den Space verlassen willst\? From 5629d3093255a3284f3eccc48b4be79c00d2e785 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 May 2021 06:20:33 +0000 Subject: [PATCH 102/202] Bump appcompat from 1.2.0 to 1.3.0 Bumps appcompat from 1.2.0 to 1.3.0. Signed-off-by: dependabot[bot] --- attachment-viewer/build.gradle | 2 +- matrix-sdk-android-rx/build.gradle | 2 +- matrix-sdk-android/build.gradle | 2 +- multipicker/build.gradle | 2 +- vector/build.gradle | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index aa35f06767..6935e1d46b 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -54,7 +54,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.3.0' implementation "androidx.recyclerview:recyclerview:1.2.0" implementation 'com.google.android.material:material:1.3.0' diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 37b0ff8d00..0d4aa7fc84 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -35,7 +35,7 @@ android { dependencies { implementation project(":matrix-sdk-android") - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version" diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index a65dc6298e..99c43ce144 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -120,7 +120,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.appcompat:appcompat:1.2.0" + implementation "androidx.appcompat:appcompat:1.3.0" implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" diff --git a/multipicker/build.gradle b/multipicker/build.gradle index 5eff2ec3ec..25d8adfdc6 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -42,7 +42,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.3.0' implementation "androidx.fragment:fragment-ktx:1.3.3" implementation 'androidx.exifinterface:exifinterface:1.3.2' diff --git a/vector/build.gradle b/vector/build.gradle index a9a8ba0924..58634b122d 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -321,7 +321,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.recyclerview:recyclerview:1.2.0" - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.3.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.1.0" From a0d04c40e9c0912bcc047b4543380877d76f4668 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 10:35:28 +0200 Subject: [PATCH 103/202] Change related to Epoxy 4.6.0 upgrade - step 1: handle DSL restriction --- .../BottomSheetGenericController.kt | 3 +- .../preview/AttachmentPreviewControllers.kt | 3 +- .../command/AutocompleteCommandController.kt | 5 +- .../emoji/AutocompleteEmojiController.kt | 5 +- .../group/AutocompleteGroupController.kt | 5 +- .../member/AutocompleteMemberController.kt | 5 +- .../room/AutocompleteRoomController.kt | 5 +- .../contactsbook/ContactsBookController.kt | 20 +++-- ...eysBackupSettingsRecyclerViewController.kt | 68 ++++++++-------- .../cancel/VerificationCancelController.kt | 26 +++--- .../cancel/VerificationNotMeController.kt | 19 ++--- .../VerificationChooseMethodController.kt | 39 ++++----- .../VerificationConclusionController.kt | 26 +++--- .../emoji/VerificationEmojiCodeController.kt | 33 ++++---- .../VerificationQRWaitingController.kt | 5 +- .../VerificationQrScannedByOtherController.kt | 21 ++--- .../request/VerificationRequestController.kt | 45 ++++++----- .../devtools/RoomDevToolRootController.kt | 13 +-- .../devtools/RoomDevToolSendFormController.kt | 13 +-- .../devtools/RoomStateListController.kt | 15 ++-- .../discovery/DiscoverySettingsController.kt | 79 ++++++++++-------- .../room/breadcrumbs/BreadcrumbsController.kt | 6 +- .../detail/search/SearchResultController.kt | 9 ++- .../timeline/TimelineEventController.kt | 8 +- .../action/MessageActionsEpoxyController.kt | 31 +++---- .../ViewEditHistoryEpoxyController.kt | 12 ++- .../reactions/ViewReactionsEpoxyController.kt | 7 +- .../detail/widget/RoomWidgetsController.kt | 11 +-- .../room/list/RoomListFooterController.kt | 5 +- .../RoomListQuickActionsEpoxyController.kt | 14 ++-- .../features/login/terms/PolicyController.kt | 7 +- .../reactions/EmojiSearchResultController.kt | 9 ++- .../roomdirectory/PublicRoomsController.kt | 21 ++--- .../createroom/CreateRoomController.kt | 43 +++++----- .../picker/RoomDirectoryPickerController.kt | 14 ++-- .../RoomMemberProfileController.kt | 5 +- .../devices/DeviceListEpoxyController.kt | 42 +++++----- .../devices/DeviceTrustInfoEpoxyController.kt | 25 +++--- .../roomprofile/RoomProfileController.kt | 9 +-- .../roomprofile/alias/RoomAliasController.kt | 55 +++++++------ .../detail/RoomAliasBottomSheetController.kt | 3 +- .../banned/RoomBannedMemberListController.kt | 7 +- .../members/RoomMemberListController.kt | 16 ++-- .../permissions/RoomPermissionsController.kt | 10 ++- .../settings/RoomSettingsController.kt | 19 ++--- .../uploads/files/UploadsFileController.kt | 16 ++-- .../uploads/media/UploadsMediaController.kt | 14 ++-- .../CrossSigningSettingsController.kt | 37 ++++----- ...ceVerificationInfoBottomSheetController.kt | 81 ++++++++++--------- .../settings/devices/DevicesController.kt | 28 ++++--- .../devtools/AccountDataEpoxyController.kt | 9 ++- .../GossipingTrailPagedEpoxyController.kt | 9 ++- .../IncomingKeyRequestPagedController.kt | 3 +- .../HomeserverSettingsController.kt | 8 +- .../ignored/IgnoredUsersController.kt | 7 +- .../settings/locale/LocalePickerController.kt | 17 ++-- .../settings/push/PushGateWayController.kt | 5 +- .../settings/push/PushRulesController.kt | 3 +- .../threepids/ThreePidsSettingsController.kt | 74 +++++++++-------- .../features/share/IncomingShareController.kt | 3 +- .../signout/soft/SoftLogoutController.kt | 43 +++++----- .../features/spaces/SpaceSummaryController.kt | 45 ++++++----- .../create/SpaceDefaultRoomEpoxyController.kt | 23 +++--- .../create/SpaceDetailEpoxyController.kt | 19 ++--- .../explore/SpaceDirectoryController.kt | 27 ++++--- .../spaces/manage/AddRoomListController.kt | 10 ++- .../manage/SpaceManageRoomsController.kt | 9 ++- .../spaces/manage/SpaceSettingsController.kt | 19 ++--- .../people/SpacePeopleListController.kt | 29 +++---- .../spaces/preview/SpacePreviewController.kt | 10 ++- .../app/features/terms/TermsController.kt | 12 +-- .../userdirectory/UserListController.kt | 33 ++++---- 72 files changed, 776 insertions(+), 658 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt index c5e0c51047..5edaa3400b 100644 --- a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt @@ -34,12 +34,13 @@ abstract class BottomSheetGenericController bottomSheetTitleItem { id("title") title(title) - subTitle(getSubTitle()) + subTitle(host.getSubTitle()) } // dividerItem { diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewControllers.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewControllers.kt index e20188c80f..4a1ea0cae3 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewControllers.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewControllers.kt @@ -41,13 +41,14 @@ class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyCon var callback: Callback? = null override fun buildModels(data: AttachmentsPreviewViewState) { + val host = this data.attachments.forEachIndexed { index, contentAttachmentData -> attachmentMiniaturePreviewItem { id(contentAttachmentData.queryUri.toString()) attachment(contentAttachmentData) checked(data.currentAttachmentIndex == index) clickListener { _ -> - callback?.onAttachmentClicked(index, contentAttachmentData) + host.callback?.onAttachmentClicked(index, contentAttachmentData) } } } diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandController.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandController.kt index a5eecf24ba..c8811a6081 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandController.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandController.kt @@ -30,14 +30,15 @@ class AutocompleteCommandController @Inject constructor(private val stringProvid if (data.isNullOrEmpty()) { return } + val host = this data.forEach { command -> autocompleteCommandItem { id(command.command) name(command.command) parameters(command.parameters) - description(stringProvider.getString(command.description)) + description(host.stringProvider.getString(command.description)) clickListener { _ -> - listener?.onItemClick(command) + host.listener?.onItemClick(command) } } } diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiController.kt index 2f99a64221..ea5d997495 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiController.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -43,17 +43,18 @@ class AutocompleteEmojiController @Inject constructor( if (data.isNullOrEmpty()) { return } + val host = this data .take(MAX) .forEach { emojiItem -> autocompleteEmojiItem { id(emojiItem.name) emojiItem(emojiItem) - emojiTypeFace(emojiTypeface) + emojiTypeFace(host.emojiTypeface) onClickListener( object : ReactionClickListener { override fun onReactionSelected(reaction: String) { - listener?.onItemClick(reaction) + host.listener?.onItemClick(reaction) } } ) diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/group/AutocompleteGroupController.kt b/vector/src/main/java/im/vector/app/features/autocomplete/group/AutocompleteGroupController.kt index ee0473cd3d..03f9a1c3e7 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/group/AutocompleteGroupController.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/group/AutocompleteGroupController.kt @@ -34,13 +34,14 @@ class AutocompleteGroupController @Inject constructor() : TypedEpoxyController autocompleteMatrixItem { id(groupSummary.groupId) matrixItem(groupSummary.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) clickListener { _ -> - listener?.onItemClick(groupSummary) + host.listener?.onItemClick(groupSummary) } } } diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt index 9a90506141..66c6705d14 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt @@ -34,13 +34,14 @@ class AutocompleteMemberController @Inject constructor() : TypedEpoxyController< if (data.isNullOrEmpty()) { return } + val host = this data.forEach { user -> autocompleteMatrixItem { id(user.userId) matrixItem(user.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) clickListener { _ -> - listener?.onItemClick(user) + host.listener?.onItemClick(user) } } } diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/room/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/app/features/autocomplete/room/AutocompleteRoomController.kt index 324fb01e57..309c194272 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/room/AutocompleteRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/room/AutocompleteRoomController.kt @@ -32,14 +32,15 @@ class AutocompleteRoomController @Inject constructor(private val avatarRenderer: if (data.isNullOrEmpty()) { return } + val host = this data.forEach { roomSummary -> autocompleteMatrixItem { id(roomSummary.roomId) matrixItem(roomSummary.toMatrixItem()) subName(roomSummary.canonicalAlias) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) clickListener { _ -> - listener?.onItemClick(roomSummary) + host.listener?.onItemClick(roomSummary) } } } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt index 59c23f4ac7..5da66661fd 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt @@ -61,16 +61,18 @@ class ContactsBookController @Inject constructor( } private fun renderLoading() { + val host = this loadingItem { id("loading") - loadingText(stringProvider.getString(R.string.loading_contact_book)) + loadingText(host.stringProvider.getString(R.string.loading_contact_book)) } } private fun renderFailure(failure: Throwable) { + val host = this errorWithRetryItem { id("error") - text(errorFormatter.toHumanReadable(failure)) + text(host.errorFormatter.toHumanReadable(failure)) } } @@ -85,11 +87,12 @@ class ContactsBookController @Inject constructor( } private fun renderContacts(mappedContacts: List, onlyBoundContacts: Boolean) { + val host = this for (mappedContact in mappedContacts) { contactItem { id(mappedContact.id) mappedContact(mappedContact) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) } mappedContact.emails .forEachIndexed { index, it -> @@ -101,9 +104,9 @@ class ContactsBookController @Inject constructor( matrixId(it.matrixId) clickListener { if (it.matrixId != null) { - callback?.onMatrixIdClick(it.matrixId) + host.callback?.onMatrixIdClick(it.matrixId) } else { - callback?.onThreePidClick(ThreePid.Email(it.email)) + host.callback?.onThreePidClick(ThreePid.Email(it.email)) } } } @@ -118,9 +121,9 @@ class ContactsBookController @Inject constructor( matrixId(it.matrixId) clickListener { if (it.matrixId != null) { - callback?.onMatrixIdClick(it.matrixId) + host.callback?.onMatrixIdClick(it.matrixId) } else { - callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber)) + host.callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber)) } } } @@ -129,6 +132,7 @@ class ContactsBookController @Inject constructor( } private fun renderEmptyState(hasSearch: Boolean) { + val host = this val noResultRes = if (hasSearch) { R.string.no_result_placeholder } else { @@ -136,7 +140,7 @@ class ContactsBookController @Inject constructor( } noResultItem { id("noResult") - text(stringProvider.getString(noResultRes)) + text(host.stringProvider.getString(noResultRes)) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index ca5f88968a..e214437232 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -46,6 +46,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s return } + val host = this var isBackupAlreadySetup = false val keyBackupState = data.keysBackupState @@ -55,8 +56,8 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s KeysBackupState.Unknown -> { errorWithRetryItem { id("summary") - text(stringProvider.getString(R.string.keys_backup_unable_to_get_keys_backup_data)) - listener { listener?.loadKeysBackupState() } + text(host.stringProvider.getString(R.string.keys_backup_unable_to_get_keys_backup_data)) + listener { host.listener?.loadKeysBackupState() } } // Nothing else to display @@ -65,17 +66,17 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s KeysBackupState.CheckingBackUpOnHomeserver -> { loadingItem { id("summary") - loadingText(stringProvider.getString(R.string.keys_backup_settings_checking_backup_state)) + loadingText(host.stringProvider.getString(R.string.keys_backup_settings_checking_backup_state)) } } KeysBackupState.Disabled -> { genericItem { id("summary") - title(stringProvider.getString(R.string.keys_backup_settings_status_not_setup)) + title(host.stringProvider.getString(R.string.keys_backup_settings_status_not_setup)) style(ItemStyle.BIG_TEXT) if (data.keysBackupVersionTrust()?.usable == false) { - description(stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) + description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) } } @@ -86,10 +87,10 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s KeysBackupState.Enabling -> { genericItem { id("summary") - title(stringProvider.getString(R.string.keys_backup_settings_status_ko)) + title(host.stringProvider.getString(R.string.keys_backup_settings_status_ko)) style(ItemStyle.BIG_TEXT) if (data.keysBackupVersionTrust()?.usable == false) { - description(stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) + description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) } else { description(keyBackupState.toString()) } @@ -101,12 +102,12 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s KeysBackupState.ReadyToBackUp -> { genericItem { id("summary") - title(stringProvider.getString(R.string.keys_backup_settings_status_ok)) + title(host.stringProvider.getString(R.string.keys_backup_settings_status_ok)) style(ItemStyle.BIG_TEXT) if (data.keysBackupVersionTrust()?.usable == false) { - description(stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) + description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) } else { - description(stringProvider.getString(R.string.keys_backup_info_keys_all_backup_up)) + description(host.stringProvider.getString(R.string.keys_backup_info_keys_all_backup_up)) } endIconResourceId(R.drawable.unit_test_ok) } @@ -117,19 +118,19 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s KeysBackupState.BackingUp -> { genericItem { id("summary") - title(stringProvider.getString(R.string.keys_backup_settings_status_ok)) + title(host.stringProvider.getString(R.string.keys_backup_settings_status_ok)) style(ItemStyle.BIG_TEXT) hasIndeterminateProcess(true) - val totalKeys = session.cryptoService().inboundGroupSessionsCount(false) - val backedUpKeys = session.cryptoService().inboundGroupSessionsCount(true) + val totalKeys = host.session.cryptoService().inboundGroupSessionsCount(false) + val backedUpKeys = host.session.cryptoService().inboundGroupSessionsCount(true) val remainingKeysToBackup = totalKeys - backedUpKeys if (data.keysBackupVersionTrust()?.usable == false) { - description(stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) + description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) } else { - description(stringProvider.getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup)) + description(host.stringProvider.getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup)) } } @@ -141,13 +142,13 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s // Add infos genericItem { id("version") - title(stringProvider.getString(R.string.keys_backup_info_title_version)) + title(host.stringProvider.getString(R.string.keys_backup_info_title_version)) description(keyVersionResult?.version ?: "") } genericItem { id("algorithm") - title(stringProvider.getString(R.string.keys_backup_info_title_algorithm)) + title(host.stringProvider.getString(R.string.keys_backup_info_title_algorithm)) description(keyVersionResult?.algorithm ?: "") } @@ -161,19 +162,20 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s id("footer") if (isBackupAlreadySetup) { - textButton1(stringProvider.getString(R.string.keys_backup_settings_restore_backup_button)) - clickOnButton1(View.OnClickListener { listener?.didSelectRestoreMessageRecovery() }) + textButton1(host.stringProvider.getString(R.string.keys_backup_settings_restore_backup_button)) + clickOnButton1(View.OnClickListener { host.listener?.didSelectRestoreMessageRecovery() }) - textButton2(stringProvider.getString(R.string.keys_backup_settings_delete_backup_button)) - clickOnButton2(View.OnClickListener { listener?.didSelectDeleteSetupMessageRecovery() }) + textButton2(host.stringProvider.getString(R.string.keys_backup_settings_delete_backup_button)) + clickOnButton2(View.OnClickListener { host.listener?.didSelectDeleteSetupMessageRecovery() }) } else { - textButton1(stringProvider.getString(R.string.keys_backup_setup)) - clickOnButton1(View.OnClickListener { listener?.didSelectSetupMessageRecovery() }) + textButton1(host.stringProvider.getString(R.string.keys_backup_setup)) + clickOnButton1(View.OnClickListener { host.listener?.didSelectSetupMessageRecovery() }) } } } private fun buildKeysBackupTrust(keysVersionTrust: Async) { + val host = this when (keysVersionTrust) { is Uninitialized -> Unit is Loading -> { @@ -185,7 +187,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s keysVersionTrust().signatures.forEach { genericItem { id(UUID.randomUUID().toString()) - title(stringProvider.getString(R.string.keys_backup_info_title_signature)) + title(host.stringProvider.getString(R.string.keys_backup_info_title_signature)) val isDeviceKnown = it.device != null val isDeviceVerified = it.device?.isVerified ?: false @@ -193,19 +195,19 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s val deviceId: String = it.deviceId ?: "" if (!isDeviceKnown) { - description(stringProvider.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)) + description(host.stringProvider.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)) endIconResourceId(R.drawable.e2e_warning) } else { if (isSignatureValid) { - if (session.sessionParams.deviceId == it.deviceId) { - description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_this_device)) + if (host.session.sessionParams.deviceId == it.deviceId) { + description(host.stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_this_device)) endIconResourceId(R.drawable.e2e_verified) } else { if (isDeviceVerified) { - description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)) + description(host.stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)) endIconResourceId(R.drawable.e2e_verified) } else { - description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)) + description(host.stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)) endIconResourceId(R.drawable.e2e_warning) } } @@ -213,9 +215,9 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s // Invalid signature endIconResourceId(R.drawable.e2e_warning) if (isDeviceVerified) { - description(stringProvider.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)) + description(host.stringProvider.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)) } else { - description(stringProvider.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)) + description(host.stringProvider.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)) } } } @@ -225,8 +227,8 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s is Fail -> { errorWithRetryItem { id("trust") - text(stringProvider.getString(R.string.keys_backup_unable_to_get_trust_info)) - listener { listener?.loadTrustData() } + text(host.stringProvider.getString(R.string.keys_backup_unable_to_get_trust_info)) + listener { host.listener?.loadTrustData() } } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt index fc0d31af1a..b36b2e2baa 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt @@ -44,17 +44,17 @@ class VerificationCancelController @Inject constructor( override fun buildModels() { val state = viewState ?: return - + val host = this if (state.isMe) { if (state.currentDeviceCanCrossSign) { bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted)) + notice(host.stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted)) } } else { bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted)) + notice(host.stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted)) } } } else { @@ -63,9 +63,9 @@ class VerificationCancelController @Inject constructor( bottomSheetVerificationNoticeItem { id("notice") notice( - stringProvider.getString(R.string.verify_cancel_other, otherDisplayName, otherUserID) + host.stringProvider.getString(R.string.verify_cancel_other, otherDisplayName, otherUserID) .toSpannable() - .colorizeMatchingText(otherUserID, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) + .colorizeMatchingText(otherUserID, host.colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) ) } } @@ -76,11 +76,11 @@ class VerificationCancelController @Inject constructor( bottomSheetVerificationActionItem { id("cancel") - title(stringProvider.getString(R.string.skip)) - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + title(host.stringProvider.getString(R.string.skip)) + titleColor(host.colorProvider.getColor(R.color.riotx_destructive_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { listener?.onTapCancel() } + iconColor(host.colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { host.listener?.onTapCancel() } } dividerItem { @@ -89,11 +89,11 @@ class VerificationCancelController @Inject constructor( bottomSheetVerificationActionItem { id("continue") - title(stringProvider.getString(R.string._continue)) - titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) + title(host.stringProvider.getString(R.string._continue)) + titleColor(host.colorProvider.getColor(R.color.riotx_positive_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) - listener { listener?.onTapContinue() } + iconColor(host.colorProvider.getColor(R.color.riotx_positive_accent)) + listener { host.listener?.onTapContinue() } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt index a1c482f8d3..97ff79c933 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt @@ -43,9 +43,10 @@ class VerificationNotMeController @Inject constructor( } override fun buildModels() { + val host = this bottomSheetVerificationNoticeItem { id("notice") - notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verify_not_me_self_verification))) + notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verify_not_me_self_verification))) } dividerItem { @@ -54,11 +55,11 @@ class VerificationNotMeController @Inject constructor( bottomSheetVerificationActionItem { id("skip") - title(stringProvider.getString(R.string.skip)) - titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + title(host.stringProvider.getString(R.string.skip)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.onTapSkip() } + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { host.listener?.onTapSkip() } } dividerItem { @@ -67,11 +68,11 @@ class VerificationNotMeController @Inject constructor( bottomSheetVerificationActionItem { id("settings") - title(stringProvider.getString(R.string.settings)) - titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) + title(host.stringProvider.getString(R.string.settings)) + titleColor(host.colorProvider.getColor(R.color.riotx_positive_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) - listener { listener?.onTapSettings() } + iconColor(host.colorProvider.getColor(R.color.riotx_positive_accent)) + listener { host.listener?.onTapSettings() } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt index ed3bdc5825..be727a3243 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -42,11 +42,12 @@ class VerificationChooseMethodController @Inject constructor( override fun buildModels() { val state = viewState ?: return + val host = this if (state.otherCanScanQrCode || state.otherCanShowQrCode) { bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString(R.string.verification_scan_notice)) + notice(host.stringProvider.getString(R.string.verification_scan_notice)) } if (state.otherCanScanQrCode && !state.qrCodeText.isNullOrBlank()) { @@ -63,11 +64,11 @@ class VerificationChooseMethodController @Inject constructor( if (state.otherCanShowQrCode) { bottomSheetVerificationActionItem { id("openCamera") - title(stringProvider.getString(R.string.verification_scan_their_code)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.verification_scan_their_code)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_camera) - iconColor(colorProvider.getColor(R.color.riotx_accent)) - listener { listener?.openCamera() } + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) + listener { host.listener?.openCamera() } } dividerItem { @@ -77,21 +78,21 @@ class VerificationChooseMethodController @Inject constructor( bottomSheetVerificationActionItem { id("openEmoji") - title(stringProvider.getString(R.string.verification_scan_emoji_title)) - titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - subTitle(stringProvider.getString(R.string.verification_scan_emoji_subtitle)) + title(host.stringProvider.getString(R.string.verification_scan_emoji_title)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + subTitle(host.stringProvider.getString(R.string.verification_scan_emoji_subtitle)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.doVerifyBySas() } + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { host.listener?.doVerifyBySas() } } } else if (state.sasModeAvailable) { bottomSheetVerificationActionItem { id("openEmoji") - title(stringProvider.getString(R.string.verification_no_scan_emoji_title)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.verification_no_scan_emoji_title)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.doVerifyBySas() } + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { host.listener?.doVerifyBySas() } } } @@ -102,12 +103,12 @@ class VerificationChooseMethodController @Inject constructor( bottomSheetVerificationActionItem { id("wasnote") - title(stringProvider.getString(R.string.verify_new_session_was_not_me)) - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - subTitle(stringProvider.getString(R.string.verify_new_session_compromized)) + title(host.stringProvider.getString(R.string.verify_new_session_was_not_me)) + titleColor(host.colorProvider.getColor(R.color.riotx_destructive_accent)) + subTitle(host.stringProvider.getString(R.string.verify_new_session_compromized)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.onClickOnWasNotMe() } + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { host.listener?.onClickOnWasNotMe() } } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt index fec27d3e01..49b3c07e78 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt @@ -45,12 +45,13 @@ class VerificationConclusionController @Inject constructor( override fun buildModels() { val state = viewState ?: return + val host = this when (state.conclusionState) { ConclusionState.SUCCESS -> { bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString( + notice(host.stringProvider.getString( if (state.isSelfVerification) R.string.verification_conclusion_ok_self_notice else R.string.verification_conclusion_ok_notice)) } @@ -65,7 +66,7 @@ class VerificationConclusionController @Inject constructor( ConclusionState.WARNING -> { bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString(R.string.verification_conclusion_not_secure)) + notice(host.stringProvider.getString(R.string.verification_conclusion_not_secure)) } bottomSheetVerificationBigImageItem { @@ -75,7 +76,7 @@ class VerificationConclusionController @Inject constructor( bottomSheetVerificationNoticeItem { id("warning_notice") - notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verification_conclusion_compromised))) + notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verification_conclusion_compromised))) } bottomDone() @@ -83,7 +84,7 @@ class VerificationConclusionController @Inject constructor( ConclusionState.CANCELLED -> { bottomSheetVerificationNoticeItem { id("notice_cancelled") - notice(stringProvider.getString(R.string.verify_cancelled_notice)) + notice(host.stringProvider.getString(R.string.verify_cancelled_notice)) } dividerItem { @@ -92,28 +93,29 @@ class VerificationConclusionController @Inject constructor( bottomSheetVerificationActionItem { id("got_it") - title(stringProvider.getString(R.string.sas_got_it)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.sas_got_it)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_accent)) - listener { listener?.onButtonTapped() } + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) + listener { host.listener?.onButtonTapped() } } } } } private fun bottomDone() { + val host = this dividerItem { id("sep0") } bottomSheetVerificationActionItem { id("done") - title(stringProvider.getString(R.string.done)) - titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + title(host.stringProvider.getString(R.string.done)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.onButtonTapped() } + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { host.listener?.onButtonTapped() } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt index 851219c54c..fd939fe11f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt @@ -58,11 +58,12 @@ class VerificationEmojiCodeController @Inject constructor( } private fun buildEmojiItem(state: VerificationEmojiCodeViewState) { + val host = this when (val emojiDescription = state.emojiDescription) { is Success -> { bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString(R.string.verification_emoji_notice)) + notice(host.stringProvider.getString(R.string.verification_emoji_notice)) } bottomSheetVerificationEmojisItem { @@ -81,24 +82,25 @@ class VerificationEmojiCodeController @Inject constructor( is Fail -> { errorWithRetryItem { id("error") - text(errorFormatter.toHumanReadable(emojiDescription.error)) + text(host.errorFormatter.toHumanReadable(emojiDescription.error)) } } else -> { bottomSheetVerificationWaitingItem { id("waiting") - title(stringProvider.getString(R.string.please_wait)) + title(host.stringProvider.getString(R.string.please_wait)) } } } } private fun buildDecimal(state: VerificationEmojiCodeViewState) { + val host = this when (val decimalDescription = state.decimalDescription) { is Success -> { bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString(R.string.verification_code_notice)) + notice(host.stringProvider.getString(R.string.verification_code_notice)) } bottomSheetVerificationDecimalCodeItem { @@ -111,19 +113,20 @@ class VerificationEmojiCodeController @Inject constructor( is Fail -> { errorWithRetryItem { id("error") - text(errorFormatter.toHumanReadable(decimalDescription.error)) + text(host.errorFormatter.toHumanReadable(decimalDescription.error)) } } else -> { bottomSheetVerificationWaitingItem { id("waiting") - title(stringProvider.getString(R.string.please_wait)) + title(host.stringProvider.getString(R.string.please_wait)) } } } } private fun buildActions(state: VerificationEmojiCodeViewState) { + val host = this dividerItem { id("sep0") } @@ -131,27 +134,27 @@ class VerificationEmojiCodeController @Inject constructor( if (state.isWaitingFromOther) { bottomSheetVerificationWaitingItem { id("waiting") - title(stringProvider.getString(R.string.verification_request_waiting_for, state.otherUser?.getBestName() ?: "")) + title(host.stringProvider.getString(R.string.verification_request_waiting_for, state.otherUser?.getBestName() ?: "")) } } else { bottomSheetVerificationActionItem { id("ko") - title(stringProvider.getString(R.string.verification_sas_do_not_match)) - titleColor(colorProvider.getColor(R.color.vector_error_color)) + title(host.stringProvider.getString(R.string.verification_sas_do_not_match)) + titleColor(host.colorProvider.getColor(R.color.vector_error_color)) iconRes(R.drawable.ic_check_off) - iconColor(colorProvider.getColor(R.color.vector_error_color)) - listener { listener?.onDoNotMatchButtonTapped() } + iconColor(host.colorProvider.getColor(R.color.vector_error_color)) + listener { host.listener?.onDoNotMatchButtonTapped() } } dividerItem { id("sep1") } bottomSheetVerificationActionItem { id("ok") - title(stringProvider.getString(R.string.verification_sas_match)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.verification_sas_match)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_check_on) - iconColor(colorProvider.getColor(R.color.riotx_accent)) - listener { listener?.onMatchButtonTapped() } + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) + listener { host.listener?.onMatchButtonTapped() } } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt index 4b7452b511..e7a8058111 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt @@ -40,11 +40,12 @@ class VerificationQRWaitingController @Inject constructor( override fun buildModels() { val params = args ?: return + val host = this bottomSheetVerificationNoticeItem { id("notice") apply { - notice(stringProvider.getString(R.string.qr_code_scanned_verif_waiting_notice)) + notice(host.stringProvider.getString(R.string.qr_code_scanned_verif_waiting_notice)) } } @@ -55,7 +56,7 @@ class VerificationQRWaitingController @Inject constructor( bottomSheetVerificationWaitingItem { id("waiting") - title(stringProvider.getString(R.string.qr_code_scanned_verif_waiting, params.otherUserName)) + title(host.stringProvider.getString(R.string.qr_code_scanned_verif_waiting, params.otherUserName)) } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt index da382f75f1..d617b70c67 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt @@ -44,15 +44,16 @@ class VerificationQrScannedByOtherController @Inject constructor( override fun buildModels() { val state = viewState ?: return + val host = this bottomSheetVerificationNoticeItem { id("notice") apply { if (state.isMe) { - notice(stringProvider.getString(R.string.qr_code_scanned_self_verif_notice)) + notice(host.stringProvider.getString(R.string.qr_code_scanned_self_verif_notice)) } else { val name = state.otherUserMxItem?.getBestName() ?: "" - notice(stringProvider.getString(R.string.qr_code_scanned_by_other_notice, name)) + notice(host.stringProvider.getString(R.string.qr_code_scanned_by_other_notice, name)) } } } @@ -68,11 +69,11 @@ class VerificationQrScannedByOtherController @Inject constructor( bottomSheetVerificationActionItem { id("deny") - title(stringProvider.getString(R.string.qr_code_scanned_by_other_no)) - titleColor(colorProvider.getColor(R.color.vector_error_color)) + title(host.stringProvider.getString(R.string.qr_code_scanned_by_other_no)) + titleColor(host.colorProvider.getColor(R.color.vector_error_color)) iconRes(R.drawable.ic_check_off) - iconColor(colorProvider.getColor(R.color.vector_error_color)) - listener { listener?.onUserDeniesQrCodeScanned() } + iconColor(host.colorProvider.getColor(R.color.vector_error_color)) + listener { host.listener?.onUserDeniesQrCodeScanned() } } dividerItem { @@ -81,11 +82,11 @@ class VerificationQrScannedByOtherController @Inject constructor( bottomSheetVerificationActionItem { id("confirm") - title(stringProvider.getString(R.string.qr_code_scanned_by_other_yes)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.qr_code_scanned_by_other_yes)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_check_on) - iconColor(colorProvider.getColor(R.color.riotx_accent)) - listener { listener?.onUserConfirmsQrCodeScanned() } + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) + listener { host.listener?.onUserConfirmsQrCodeScanned() } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt index c7740e2ac5..ac063f2545 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt @@ -50,12 +50,13 @@ class VerificationRequestController @Inject constructor( override fun buildModels() { val state = viewState ?: return val matrixItem = viewState?.otherUserMxItem ?: return + val host = this if (state.selfVerificationMode) { if (state.hasAnyOtherSession) { bottomSheetVerificationNoticeItem { id("notice") - notice(stringProvider.getString(R.string.verification_open_other_to_verify)) + notice(host.stringProvider.getString(R.string.verification_open_other_to_verify)) } bottomSheetSelfWaitItem { @@ -75,12 +76,12 @@ class VerificationRequestController @Inject constructor( } bottomSheetVerificationActionItem { id("passphrase") - title(stringProvider.getString(R.string.verification_cannot_access_other_session)) - titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + title(host.stringProvider.getString(R.string.verification_cannot_access_other_session)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) subTitle(subtitle) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.onClickRecoverFromPassphrase() } + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { host.listener?.onClickRecoverFromPassphrase() } } } @@ -90,11 +91,11 @@ class VerificationRequestController @Inject constructor( bottomSheetVerificationActionItem { id("skip") - title(stringProvider.getString(R.string.skip)) - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + title(host.stringProvider.getString(R.string.skip)) + titleColor(host.colorProvider.getColor(R.color.riotx_destructive_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - listener { listener?.onClickSkip() } + iconColor(host.colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { host.listener?.onClickSkip() } } } else { val styledText = @@ -121,18 +122,18 @@ class VerificationRequestController @Inject constructor( is Uninitialized -> { bottomSheetVerificationActionItem { id("start") - title(stringProvider.getString(R.string.start_verification)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) - subTitle(stringProvider.getString(R.string.verification_request_start_notice)) + title(host.stringProvider.getString(R.string.start_verification)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) + subTitle(host.stringProvider.getString(R.string.verification_request_start_notice)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.onClickOnVerificationStart() } + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { host.listener?.onClickOnVerificationStart() } } } is Loading -> { bottomSheetVerificationWaitingItem { id("waiting") - title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + title(host.stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) } } is Success -> { @@ -140,12 +141,12 @@ class VerificationRequestController @Inject constructor( if (state.isMe) { bottomSheetVerificationWaitingItem { id("waiting") - title(stringProvider.getString(R.string.verification_request_waiting)) + title(host.stringProvider.getString(R.string.verification_request_waiting)) } } else { bottomSheetVerificationWaitingItem { id("waiting") - title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + title(host.stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) } } } @@ -160,12 +161,12 @@ class VerificationRequestController @Inject constructor( bottomSheetVerificationActionItem { id("wasnote") - title(stringProvider.getString(R.string.verify_new_session_was_not_me)) - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) - subTitle(stringProvider.getString(R.string.verify_new_session_compromized)) + title(host.stringProvider.getString(R.string.verify_new_session_was_not_me)) + titleColor(host.colorProvider.getColor(R.color.riotx_destructive_accent)) + subTitle(host.stringProvider.getString(R.string.verify_new_session_compromized)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.onClickOnWasNotMe() } + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { host.listener?.onClickOnWasNotMe() } } } } diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolRootController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolRootController.kt index 785e0140ac..ed0a58231e 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolRootController.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolRootController.kt @@ -34,25 +34,26 @@ class RoomDevToolRootController @Inject constructor( var interactionListener: DevToolsInteractionListener? = null override fun buildModels() { + val host = this genericButtonItem { id("explore") - text(stringProvider.getString(R.string.dev_tools_explore_room_state)) + text(host.stringProvider.getString(R.string.dev_tools_explore_room_state)) buttonClickAction(View.OnClickListener { - interactionListener?.processAction(RoomDevToolAction.ExploreRoomState) + host.interactionListener?.processAction(RoomDevToolAction.ExploreRoomState) }) } genericButtonItem { id("send") - text(stringProvider.getString(R.string.dev_tools_send_custom_event)) + text(host.stringProvider.getString(R.string.dev_tools_send_custom_event)) buttonClickAction(View.OnClickListener { - interactionListener?.processAction(RoomDevToolAction.SendCustomEvent(false)) + host.interactionListener?.processAction(RoomDevToolAction.SendCustomEvent(false)) }) } genericButtonItem { id("send_state") - text(stringProvider.getString(R.string.dev_tools_send_state_event)) + text(host.stringProvider.getString(R.string.dev_tools_send_state_event)) buttonClickAction(View.OnClickListener { - interactionListener?.processAction(RoomDevToolAction.SendCustomEvent(true)) + host.interactionListener?.processAction(RoomDevToolAction.SendCustomEvent(true)) }) } } diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt index e5b3fb737e..8f8b8257b1 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt @@ -32,6 +32,7 @@ class RoomDevToolSendFormController @Inject constructor( override fun buildModels(data: RoomDevToolViewState?) { val sendEventForm = (data?.displayMode as? RoomDevToolViewState.Mode.SendEventForm) ?: return + val host = this genericFooterItem { id("topSpace") @@ -41,10 +42,10 @@ class RoomDevToolSendFormController @Inject constructor( id("event_type") enabled(true) value(data.sendEventDraft?.type) - hint(stringProvider.getString(R.string.dev_tools_form_hint_type)) + hint(host.stringProvider.getString(R.string.dev_tools_form_hint_type)) showBottomSeparator(false) onTextChange { text -> - interactionListener?.processAction(RoomDevToolAction.CustomEventTypeChange(text)) + host.interactionListener?.processAction(RoomDevToolAction.CustomEventTypeChange(text)) } } @@ -53,10 +54,10 @@ class RoomDevToolSendFormController @Inject constructor( id("state_key") enabled(true) value(data.sendEventDraft?.stateKey) - hint(stringProvider.getString(R.string.dev_tools_form_hint_state_key)) + hint(host.stringProvider.getString(R.string.dev_tools_form_hint_state_key)) showBottomSeparator(false) onTextChange { text -> - interactionListener?.processAction(RoomDevToolAction.CustomEventStateKeyChange(text)) + host.interactionListener?.processAction(RoomDevToolAction.CustomEventStateKeyChange(text)) } } } @@ -65,10 +66,10 @@ class RoomDevToolSendFormController @Inject constructor( id("event_content") enabled(true) value(data.sendEventDraft?.content) - hint(stringProvider.getString(R.string.dev_tools_form_hint_event_content)) + hint(host.stringProvider.getString(R.string.dev_tools_form_hint_event_content)) showBottomSeparator(false) onTextChange { text -> - interactionListener?.processAction(RoomDevToolAction.CustomEventContentChange(text)) + host.interactionListener?.processAction(RoomDevToolAction.CustomEventContentChange(text)) } } } diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt index 38138474d9..25cd2f129f 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt @@ -35,6 +35,7 @@ class RoomStateListController @Inject constructor( var interactionListener: DevToolsInteractionListener? = null override fun buildModels(data: RoomDevToolViewState?) { + val host = this when (data?.displayMode) { RoomDevToolViewState.Mode.StateEventList -> { val stateEventsGroups = data.stateEvents.invoke().orEmpty().groupBy { it.getClearType() } @@ -42,17 +43,17 @@ class RoomStateListController @Inject constructor( if (stateEventsGroups.isEmpty()) { noResultItem { id("no state events") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { stateEventsGroups.forEach { entry -> genericItem { id(entry.key) title(entry.key) - description(stringProvider.getQuantityString(R.plurals.entries, entry.value.size, entry.value.size)) + description(host.stringProvider.getQuantityString(R.plurals.entries, entry.value.size, entry.value.size)) itemClickAction(GenericItem.Action("view").apply { perform = Runnable { - interactionListener?.processAction(RoomDevToolAction.ShowStateEventType(entry.key)) + host.interactionListener?.processAction(RoomDevToolAction.ShowStateEventType(entry.key)) } }) } @@ -64,7 +65,7 @@ class RoomStateListController @Inject constructor( if (stateEvents.isEmpty()) { noResultItem { id("no state events") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { stateEvents.forEach { stateEvent -> @@ -80,13 +81,13 @@ class RoomStateListController @Inject constructor( title(span { +"Type: " span { - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) text = "\"${stateEvent.type}\"" textStyle = "normal" } +"\nState Key: " span { - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) text = stateEvent.stateKey.let { "\"$it\"" } textStyle = "normal" } @@ -94,7 +95,7 @@ class RoomStateListController @Inject constructor( description(contentJson) itemClickAction(GenericItem.Action("view").apply { perform = Runnable { - interactionListener?.processAction(RoomDevToolAction.ShowStateEvent(stateEvent)) + host.interactionListener?.processAction(RoomDevToolAction.ShowStateEvent(stateEvent)) } }) } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt index 55c11f3a50..25cb3ce8dd 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt @@ -74,6 +74,7 @@ class DiscoverySettingsController @Inject constructor( } private fun buildConsentSection(data: DiscoverySettingsState) { + val host = this settingsSectionTitleItem { id("idConsentTitle") titleResId(R.string.settings_discovery_consent_title) @@ -86,10 +87,10 @@ class DiscoverySettingsController @Inject constructor( } settingsButtonItem { id("idConsentButton") - colorProvider(colorProvider) + colorProvider(host.colorProvider) buttonTitleId(R.string.settings_discovery_consent_action_revoke) buttonStyle(ButtonStyle.DESTRUCTIVE) - buttonClickListener { listener?.onTapUpdateUserConsent(false) } + buttonClickListener { host.listener?.onTapUpdateUserConsent(false) } } } else { settingsInfoItem { @@ -98,15 +99,16 @@ class DiscoverySettingsController @Inject constructor( } settingsButtonItem { id("idConsentButton") - colorProvider(colorProvider) + colorProvider(host.colorProvider) buttonTitleId(R.string.settings_discovery_consent_action_give_consent) - buttonClickListener { listener?.onTapUpdateUserConsent(true) } + buttonClickListener { host.listener?.onTapUpdateUserConsent(true) } } } } private fun buildIdentityServerSection(data: DiscoverySettingsState) { val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) + val host = this settingsSectionTitleItem { id("idServerTitle") @@ -121,22 +123,22 @@ class DiscoverySettingsController @Inject constructor( if (data.identityServer() != null && data.termsNotSigned) { settingsInfoItem { id("idServerFooter") - helperText(stringProvider.getString(R.string.settings_agree_to_terms, identityServer)) + helperText(host.stringProvider.getString(R.string.settings_agree_to_terms, identityServer)) showCompoundDrawable(true) - itemClickListener(View.OnClickListener { listener?.openIdentityServerTerms() }) + itemClickListener(View.OnClickListener { host.listener?.openIdentityServerTerms() }) } settingsButtonItem { id("seeTerms") - colorProvider(colorProvider) - buttonTitle(stringProvider.getString(R.string.open_terms_of, identityServer)) - buttonClickListener { listener?.openIdentityServerTerms() } + colorProvider(host.colorProvider) + buttonTitle(host.stringProvider.getString(R.string.open_terms_of, identityServer)) + buttonClickListener { host.listener?.openIdentityServerTerms() } } } else { settingsInfoItem { id("idServerFooter") showCompoundDrawable(false) if (data.identityServer() != null) { - helperText(stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServer)) + helperText(host.stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServer)) } else { helperTextResId(R.string.settings_discovery_identity_server_info_none) } @@ -145,13 +147,13 @@ class DiscoverySettingsController @Inject constructor( settingsButtonItem { id("change") - colorProvider(colorProvider) + colorProvider(host.colorProvider) if (data.identityServer() == null) { buttonTitleId(R.string.add_identity_server) } else { buttonTitleId(R.string.change_identity_server) } - buttonClickListener { listener?.onTapChangeIdentityServer() } + buttonClickListener { host.listener?.onTapChangeIdentityServer() } } if (data.identityServer() != null) { @@ -161,15 +163,16 @@ class DiscoverySettingsController @Inject constructor( } settingsButtonItem { id("remove") - colorProvider(colorProvider) + colorProvider(host.colorProvider) buttonTitleId(R.string.disconnect_identity_server) buttonStyle(ButtonStyle.DESTRUCTIVE) - buttonClickListener { listener?.onTapDisconnectIdentityServer() } + buttonClickListener { host.listener?.onTapDisconnectIdentityServer() } } } } private fun buildEmailsSection(emails: Async>) { + val host = this settingsSectionTitleItem { id("emails") titleResId(R.string.settings_discovery_emails_title) @@ -190,7 +193,7 @@ class DiscoverySettingsController @Inject constructor( if (emails().isEmpty()) { settingsInfoItem { id("emailsEmpty") - helperText(stringProvider.getString(R.string.settings_discovery_no_mails)) + helperText(host.stringProvider.getString(R.string.settings_discovery_no_mails)) } } else { emails().forEach { buildEmail(it) } @@ -202,6 +205,7 @@ class DiscoverySettingsController @Inject constructor( private fun buildEmail(pidInfo: PidInfo) { buildThreePid(pidInfo) + val host = this if (pidInfo.isShared is Fail) { buildSharedFail(pidInfo) } else if (pidInfo.isShared() == SharedState.BINDING_IN_PROGRESS) { @@ -210,14 +214,14 @@ class DiscoverySettingsController @Inject constructor( is Loading -> settingsInformationItem { id("info${pidInfo.threePid.value}") - colorProvider(colorProvider) - message(stringProvider.getString(R.string.settings_discovery_confirm_mail, pidInfo.threePid.value)) + colorProvider(host.colorProvider) + message(host.stringProvider.getString(R.string.settings_discovery_confirm_mail, pidInfo.threePid.value)) } is Fail -> settingsInformationItem { id("info${pidInfo.threePid.value}") - colorProvider(colorProvider) - message(stringProvider.getString(R.string.settings_discovery_confirm_mail_not_clicked, pidInfo.threePid.value)) + colorProvider(host.colorProvider) + message(host.stringProvider.getString(R.string.settings_discovery_confirm_mail_not_clicked, pidInfo.threePid.value)) textColorId(R.color.riotx_destructive_accent) } is Success -> Unit /* Cannot happen */ @@ -236,6 +240,7 @@ class DiscoverySettingsController @Inject constructor( } private fun buildMsisdnSection(msisdns: Async>) { + val host = this settingsSectionTitleItem { id("msisdn") titleResId(R.string.settings_discovery_msisdn_title) @@ -257,7 +262,7 @@ class DiscoverySettingsController @Inject constructor( if (msisdns().isEmpty()) { settingsInfoItem { id("no_msisdn") - helperText(stringProvider.getString(R.string.settings_discovery_no_msisdn)) + helperText(host.stringProvider.getString(R.string.settings_discovery_no_msisdn)) } } else { msisdns().forEach { buildMsisdn(it) } @@ -267,6 +272,7 @@ class DiscoverySettingsController @Inject constructor( } private fun buildMsisdn(pidInfo: PidInfo) { + val host = this val phoneNumber = pidInfo.threePid.getFormattedValue() buildThreePid(pidInfo, phoneNumber) @@ -289,19 +295,19 @@ class DiscoverySettingsController @Inject constructor( } settingsEditTextItem { id("msisdnVerification${pidInfo.threePid.value}") - descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber)) + descriptionText(host.stringProvider.getString(R.string.settings_text_message_sent, phoneNumber)) errorText(errorText) inProgress(pidInfo.finalRequest is Loading) interactionListener(object : SettingsEditTextItem.Listener { override fun onValidate() { - val code = codes[pidInfo.threePid] + val code = host.codes[pidInfo.threePid] if (pidInfo.threePid is ThreePid.Msisdn && code != null) { - listener?.sendMsisdnVerificationCode(pidInfo.threePid, code) + host.listener?.sendMsisdnVerificationCode(pidInfo.threePid, code) } } override fun onTextChange(text: String) { - codes[pidInfo.threePid] = text + host.codes[pidInfo.threePid] = text } }) } @@ -310,11 +316,12 @@ class DiscoverySettingsController @Inject constructor( } private fun buildThreePid(pidInfo: PidInfo, title: String = pidInfo.threePid.value) { + val host = this settingsTextButtonSingleLineItem { id(pidInfo.threePid.value) title(title) - colorProvider(colorProvider) - stringProvider(stringProvider) + colorProvider(host.colorProvider) + stringProvider(host.stringProvider) when (pidInfo.isShared) { is Loading -> { buttonIndeterminate(true) @@ -322,9 +329,9 @@ class DiscoverySettingsController @Inject constructor( is Fail -> { buttonType(ButtonType.NORMAL) buttonStyle(ButtonStyle.DESTRUCTIVE) - buttonTitle(stringProvider.getString(R.string.global_retry)) + buttonTitle(host.stringProvider.getString(R.string.global_retry)) iconMode(IconMode.ERROR) - buttonClickListener { listener?.onTapRetryToRetrieveBindings() } + buttonClickListener { host.listener?.onTapRetryToRetrieveBindings() } } is Success -> when (pidInfo.isShared()) { SharedState.SHARED, @@ -333,9 +340,9 @@ class DiscoverySettingsController @Inject constructor( checked(pidInfo.isShared() == SharedState.SHARED) switchChangeListener { _, checked -> if (checked) { - listener?.onTapShare(pidInfo.threePid) + host.listener?.onTapShare(pidInfo.threePid) } else { - listener?.onTapRevoke(pidInfo.threePid) + host.listener?.onTapRevoke(pidInfo.threePid) } } } @@ -353,32 +360,34 @@ class DiscoverySettingsController @Inject constructor( } private fun buildSharedFail(pidInfo: PidInfo) { + val host = this settingsInformationItem { id("info${pidInfo.threePid.value}") - colorProvider(colorProvider) + colorProvider(host.colorProvider) textColorId(R.color.vector_error_color) message((pidInfo.isShared as? Fail)?.error?.message ?: "") } } private fun buildContinueCancel(threePid: ThreePid) { + val host = this settingsContinueCancelItem { id("bottom${threePid.value}") continueOnClick { when (threePid) { is ThreePid.Email -> { - listener?.checkEmailVerification(threePid) + host.listener?.checkEmailVerification(threePid) } is ThreePid.Msisdn -> { - val code = codes[threePid] + val code = host.codes[threePid] if (code != null) { - listener?.sendMsisdnVerificationCode(threePid, code) + host.listener?.sendMsisdnVerificationCode(threePid, code) } } } } cancelOnClick { - listener?.cancelBinding(threePid) + host.listener?.cancelBinding(threePid) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsController.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsController.kt index a406803bbb..655b3dced6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsController.kt @@ -44,7 +44,7 @@ class BreadcrumbsController @Inject constructor( override fun buildModels() { val safeViewState = viewState ?: return - + val host = this // Add a ZeroItem to avoid automatic scroll when the breadcrumbs are updated from another client zeroItem { id("top") @@ -57,7 +57,7 @@ class BreadcrumbsController @Inject constructor( breadcrumbsItem { id(it.roomId) hasTypingUsers(it.typingUsers.isNotEmpty()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) matrixItem(it.toMatrixItem()) unreadNotificationCount(it.notificationCount) showHighlighted(it.highlightCount > 0) @@ -65,7 +65,7 @@ class BreadcrumbsController @Inject constructor( hasDraft(it.userDrafts.isNotEmpty()) itemClickListener( DebouncedClickListener({ _ -> - listener?.onBreadcrumbClicked(it.roomId) + host.listener?.onBreadcrumbClicked(it.roomId) }) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index f661aa5ba9..74bd168d09 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -61,15 +61,16 @@ class SearchResultController @Inject constructor( override fun buildModels(data: SearchViewState?) { data ?: return + val host = this val searchItems = buildSearchResultItems(data) if (data.hasMoreResult) { loadingItem { // Always use a different id, because we can be notified several times of visibility state changed - id("loadMore${idx++}") + id("loadMore${host.idx++}") onVisibilityStateChanged { _, _, visibilityState -> if (visibilityState == VisibilityState.VISIBLE) { - listener?.loadMore() + host.listener?.loadMore() } } } @@ -78,12 +79,12 @@ class SearchResultController @Inject constructor( // All returned results by the server has been filtered out and there is no more result noResultItem { id("noResult") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { noResultItem { id("noMoreResult") - text(stringProvider.getString(R.string.no_more_results)) + text(host.stringProvider.getString(R.string.no_more_results)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index b67527c24c..a0f87b9749 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -456,9 +456,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ { + val host = this@TimelineEventController return onVisibilityStateChanged { _, _, visibilityState -> if (visibilityState == VisibilityState.VISIBLE) { - callback?.onLoadMore(direction) + host.callback?.onLoadMore(direction) } } } @@ -498,8 +499,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec * Return true if added */ private fun LoadingItem_.addWhenLoading(direction: Timeline.Direction): Boolean { - val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false - addIf(shouldAdd, this@TimelineEventController) + val host = this@TimelineEventController + val shouldAdd = host.timeline?.hasMoreToLoad(direction) ?: false + addIf(shouldAdd, host) return shouldAdd } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index dcbc2c3293..e7c48739fc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -61,19 +61,20 @@ class MessageActionsEpoxyController @Inject constructor( var listener: MessageActionsEpoxyControllerListener? = null override fun buildModels(state: MessageActionState) { + val host = this // Message preview val date = state.timelineEvent()?.root?.originServerTs val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL) bottomSheetMessagePreviewItem { id("preview") - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) matrixItem(state.informationData.matrixItem) - movementMethod(createLinkMovementMethod(listener)) - imageContentRenderer(imageContentRenderer) - data(state.timelineEvent()?.buildImageContentRendererData(dimensionConverter.dpToPx(66))) - userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) } - body(state.messageBody.linkify(listener)) - bodyDetails(eventDetailsFormatter.format(state.timelineEvent()?.root)) + movementMethod(createLinkMovementMethod(host.listener)) + imageContentRenderer(host.imageContentRenderer) + data(state.timelineEvent()?.buildImageContentRendererData(host.dimensionConverter.dpToPx(66))) + userClicked { host.listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) } + body(state.messageBody.linkify(host.listener)) + bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)) time(formattedDate) } @@ -94,14 +95,14 @@ class MessageActionsEpoxyController @Inject constructor( bottomSheetSendStateItem { id("send_state") showProgress(true) - text(stringProvider.getString(R.string.event_status_sending_message)) + text(host.stringProvider.getString(R.string.event_status_sending_message)) } } else if (sendState == SendState.SENT) { bottomSheetSendStateItem { id("send_state") showProgress(false) drawableStart(R.drawable.ic_message_sent) - text(stringProvider.getString(R.string.event_status_sent_message)) + text(host.stringProvider.getString(R.string.event_status_sent_message)) } } @@ -110,7 +111,7 @@ class MessageActionsEpoxyController @Inject constructor( bottomSheetSendStateItem { id("e2e_clear") showProgress(false) - text(stringProvider.getString(R.string.unencrypted)) + text(host.stringProvider.getString(R.string.unencrypted)) drawableStart(R.drawable.ic_shield_warning_small) } } @@ -119,7 +120,7 @@ class MessageActionsEpoxyController @Inject constructor( bottomSheetSendStateItem { id("e2e_unverified") showProgress(false) - text(stringProvider.getString(R.string.encrypted_unverified)) + text(host.stringProvider.getString(R.string.encrypted_unverified)) drawableStart(R.drawable.ic_shield_warning_small) } } @@ -137,12 +138,12 @@ class MessageActionsEpoxyController @Inject constructor( bottomSheetQuickReactionsItem { id("quick_reaction") - fontProvider(fontProvider) + fontProvider(host.fontProvider) texts(state.quickStates()?.map { it.reaction }.orEmpty()) selecteds(state.quickStates.invoke().map { it.isSelected }) listener(object : BottomSheetQuickReactionsItem.Listener { override fun didSelect(emoji: String, selected: Boolean) { - listener?.didSelectMenuAction(EventSharedAction.QuickReact(state.eventId, emoji, selected)) + host.listener?.didSelectMenuAction(EventSharedAction.QuickReact(state.eventId, emoji, selected)) } }) } @@ -168,7 +169,7 @@ class MessageActionsEpoxyController @Inject constructor( textRes(action.titleRes) showExpand(action is EventSharedAction.ReportContent) expanded(state.expendedReportContentMenu) - listener(View.OnClickListener { listener?.didSelectMenuAction(action) }) + listener(View.OnClickListener { host.listener?.didSelectMenuAction(action) }) destructive(action.destructive) } @@ -184,7 +185,7 @@ class MessageActionsEpoxyController @Inject constructor( subMenuItem(true) iconRes(actionReport.iconResId) textRes(actionReport.titleRes) - listener(View.OnClickListener { listener?.didSelectMenuAction(actionReport) }) + listener(View.OnClickListener { host.listener?.didSelectMenuAction(actionReport) }) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index c82dd70e54..870cff0f4d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -47,6 +47,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController() { override fun buildModels(state: ViewEditHistoryViewState) { + val host = this when (state.editList) { is Incomplete -> { genericLoaderItem { @@ -56,7 +57,8 @@ class ViewEditHistoryEpoxyController(private val context: Context, is Fail -> { genericFooterItem { id("failure") - text(context.getString(R.string.unknown_error)) + // FIXME Should use stringprovider + text(host.context.getString(R.string.unknown_error)) } } is Success -> { @@ -66,10 +68,12 @@ class ViewEditHistoryEpoxyController(private val context: Context, } private fun renderEvents(sourceEvents: List, isOriginalReply: Boolean) { + val host = this if (sourceEvents.isEmpty()) { genericItem { id("footer") - title(context.getString(R.string.no_message_edits_found)) + // TODO use a stringProvider + title(host.context.getString(R.string.no_message_edits_found)) } } else { var lastDate: Calendar? = null @@ -83,7 +87,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, // need to display header with day genericItemHeader { id(evDate.hashCode()) - text(dateFormatter.format(evDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER)) + text(host.dateFormatter.format(evDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER)) } } lastDate = evDate @@ -127,7 +131,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, } genericItem { id(timelineEvent.eventId) - title(dateFormatter.format(timelineEvent.originServerTs, DateFormatKind.EDIT_HISTORY_ROW)) + title(host.dateFormatter.format(timelineEvent.originServerTs, DateFormatKind.EDIT_HISTORY_ROW)) description(spannedDiff ?: body) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt index d0bf69da3b..202f68879d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt @@ -38,6 +38,7 @@ class ViewReactionsEpoxyController @Inject constructor( var listener: Listener? = null override fun buildModels(state: DisplayReactionsViewState) { + val host = this when (state.mapReactionKeyToMemberList) { is Incomplete -> { genericLoaderItem { @@ -47,7 +48,7 @@ class ViewReactionsEpoxyController @Inject constructor( is Fail -> { genericFooterItem { id("failure") - text(stringProvider.getString(R.string.unknown_error)) + text(host.stringProvider.getString(R.string.unknown_error)) } } is Success -> { @@ -55,9 +56,9 @@ class ViewReactionsEpoxyController @Inject constructor( reactionInfoSimpleItem { id(it.eventId) timeStamp(it.timestamp) - reactionKey(emojiCompatWrapper.safeEmojiSpanify(it.reactionKey)) + reactionKey(host.emojiCompatWrapper.safeEmojiSpanify(it.reactionKey)) authorDisplayName(it.authorName ?: it.authorId) - userClicked { listener?.didSelectUser(it.authorId) } + userClicked { host.listener?.didSelectUser(it.authorId) } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt index b6536fbd93..7b828ea090 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt @@ -37,25 +37,26 @@ class RoomWidgetsController @Inject constructor( var listener: Listener? = null override fun buildModels(widgets: List) { + val host = this if (widgets.isEmpty()) { genericFooterItem { id("empty") - text(stringProvider.getString(R.string.room_no_active_widgets)) + text(host.stringProvider.getString(R.string.room_no_active_widgets)) } } else { widgets.forEach { roomWidgetItem { id(it.widgetId) widget(it) - widgetClicked { listener?.didSelectWidget(it) } + widgetClicked { host.listener?.didSelectWidget(it) } } } } genericButtonItem { id("addIntegration") - text(stringProvider.getString(R.string.room_manage_integrations)) - textColor(colorProvider.getColor(R.color.riotx_accent)) - buttonClickAction(View.OnClickListener { listener?.didSelectManageWidgets() }) + text(host.stringProvider.getString(R.string.room_manage_integrations)) + textColor(host.colorProvider.getColor(R.color.riotx_accent)) + buttonClickAction(View.OnClickListener { host.listener?.didSelectManageWidgets() }) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt index d4e062d1e4..cef8fa2d26 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt @@ -33,11 +33,12 @@ class RoomListFooterController @Inject constructor( var listener: RoomListListener? = null override fun buildModels(data: RoomListViewState?) { + val host = this when (data?.displayMode) { RoomListDisplayMode.FILTERED -> { filteredRoomFooterItem { id("filter_footer") - listener(listener) + listener(host.listener) currentFilter(data.roomFilter) } } @@ -45,7 +46,7 @@ class RoomListFooterController @Inject constructor( if (userPreferencesProvider.shouldShowLongClickOnRoomHelp()) { helpFooterItem { id("long_click_help") - text(stringProvider.getString(R.string.help_long_click_on_room_for_more_options)) + text(host.stringProvider.getString(R.string.help_long_click_on_room_for_more_options)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt index ebacdbd1eb..739b601f7d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt @@ -38,20 +38,21 @@ class RoomListQuickActionsEpoxyController @Inject constructor( override fun buildModels(state: RoomListQuickActionsState) { val roomSummary = state.roomSummary() ?: return + val host = this val showAll = state.mode == RoomListActionsArgs.Mode.FULL if (showAll) { // Preview, favorite, settings bottomSheetRoomPreviewItem { id("room_preview") - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) matrixItem(roomSummary.toMatrixItem()) - stringProvider(stringProvider) + stringProvider(host.stringProvider) izLowPriority(roomSummary.isLowPriority) izFavorite(roomSummary.isFavorite) - settingsClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Settings(roomSummary.roomId)) } - favoriteClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Favorite(roomSummary.roomId)) } - lowPriorityClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.LowPriority(roomSummary.roomId)) } + settingsClickListener { host.listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Settings(roomSummary.roomId)) } + favoriteClickListener { host.listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Favorite(roomSummary.roomId)) } + lowPriorityClickListener { host.listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.LowPriority(roomSummary.roomId)) } } // Notifications @@ -72,6 +73,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor( } private fun RoomListQuickActionsSharedAction.toBottomSheetItem(index: Int, roomNotificationState: RoomNotificationState? = null) { + val host = this@RoomListQuickActionsEpoxyController val selected = when (this) { is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> roomNotificationState == RoomNotificationState.ALL_MESSAGES_NOISY is RoomListQuickActionsSharedAction.NotificationsAll -> roomNotificationState == RoomNotificationState.ALL_MESSAGES @@ -85,7 +87,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor( iconRes(iconResId) textRes(titleRes) destructive(this@toBottomSheetItem.destructive) - listener(View.OnClickListener { listener?.didSelectMenuAction(this@toBottomSheetItem) }) + listener(View.OnClickListener { host.listener?.didSelectMenuAction(this@toBottomSheetItem) }) } } diff --git a/vector/src/main/java/im/vector/app/features/login/terms/PolicyController.kt b/vector/src/main/java/im/vector/app/features/login/terms/PolicyController.kt index e786b103a2..fb2ec877ce 100644 --- a/vector/src/main/java/im/vector/app/features/login/terms/PolicyController.kt +++ b/vector/src/main/java/im/vector/app/features/login/terms/PolicyController.kt @@ -28,16 +28,17 @@ class PolicyController @Inject constructor() : TypedEpoxyController) { + val host = this data.forEach { entry -> policyItem { id(entry.localizedFlowDataLoginTerms.policyName) checked(entry.checked) title(entry.localizedFlowDataLoginTerms.localizedName) - subtitle(homeServer) + subtitle(host.homeServer) - clickListener(View.OnClickListener { listener?.openPolicy(entry.localizedFlowDataLoginTerms) }) + clickListener(View.OnClickListener { host.listener?.openPolicy(entry.localizedFlowDataLoginTerms) }) checkChangeListener { _, isChecked -> - listener?.setChecked(entry.localizedFlowDataLoginTerms, isChecked) + host.listener?.setChecked(entry.localizedFlowDataLoginTerms, isChecked) } } } diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultController.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultController.kt index 8c510f568a..ac015ad1f0 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultController.kt @@ -45,19 +45,20 @@ class EmojiSearchResultController @Inject constructor( override fun buildModels(data: EmojiSearchResultViewState?) { val results = data?.results ?: return + val host = this if (results.isEmpty()) { if (data.query.isEmpty()) { // display 'Type something to find' genericFooterItem { id("type.query.item") - text(stringProvider.getString(R.string.reaction_search_type_hint)) + text(host.stringProvider.getString(R.string.reaction_search_type_hint)) } } else { // Display no search Results genericFooterItem { id("no.results.item") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } } else { @@ -66,9 +67,9 @@ class EmojiSearchResultController @Inject constructor( emojiSearchResultItem { id(it.name) emojiItem(it) - emojiTypeFace(emojiTypeface) + emojiTypeFace(host.emojiTypeface) currentQuery(data.query) - onClickListener(listener) + onClickListener(host.listener) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt index b71b494948..32c87bffd4 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt @@ -42,6 +42,7 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri var callback: Callback? = null override fun buildModels(viewState: PublicRoomsViewState) { + val host = this val publicRooms = viewState.publicRooms val unknownRoomItem = viewState.buildUnknownRoomIfNeeded() @@ -51,7 +52,7 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri // No result noResultItem { id("noResult") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { publicRooms.forEach { @@ -71,7 +72,7 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri } onVisibilityStateChanged { _, _, visibilityState -> if (visibilityState == VisibilityState.VISIBLE) { - callback?.loadMore() + host.callback?.loadMore() } } } @@ -81,15 +82,16 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri if (viewState.asyncPublicRoomsRequest is Fail) { errorWithRetryItem { id("error") - text(errorFormatter.toHumanReadable(viewState.asyncPublicRoomsRequest.error)) - listener { callback?.loadMore() } + text(host.errorFormatter.toHumanReadable(viewState.asyncPublicRoomsRequest.error)) + listener { host.callback?.loadMore() } } } } private fun buildPublicRoom(publicRoom: PublicRoom, viewState: PublicRoomsViewState) { + val host = this publicRoomItem { - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) id(publicRoom.roomId) matrixItem(publicRoom.toMatrixItem()) roomAlias(publicRoom.getPrimaryAlias()) @@ -107,10 +109,10 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri joinState(joinState) joinListener { - callback?.onPublicRoomJoin(publicRoom) + host.callback?.onPublicRoomJoin(publicRoom) } globalListener { - callback?.onPublicRoomClicked(publicRoom, joinState) + host.callback?.onPublicRoomClicked(publicRoom, joinState) } } } @@ -124,13 +126,14 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri isRoomId -> MatrixItem.RoomItem(roomIdOrAlias) else -> null } + val host = this@PublicRoomsController return roomItem?.let { UnknownRoomItem_().apply { id(roomIdOrAlias) matrixItem(it) - avatarRenderer(this@PublicRoomsController.avatarRenderer) + avatarRenderer(host.avatarRenderer) globalListener { - callback?.onUnknownRoomClicked(roomIdOrAlias) + host.callback?.onUnknownRoomClicked(roomIdOrAlias) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index e08f383512..efb54650b8 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -45,12 +45,13 @@ class CreateRoomController @Inject constructor( } private fun buildForm(viewState: CreateRoomViewState, enableFormElement: Boolean) { + val host = this formEditableAvatarItem { id("avatar") enabled(enableFormElement) imageUri(viewState.avatarUri) - clickListener { listener?.onAvatarChange() } - deleteListener { listener?.onAvatarDelete() } + clickListener { host.listener?.onAvatarChange() } + deleteListener { host.listener?.onAvatarDelete() } } settingsSectionTitleItem { id("nameSection") @@ -60,10 +61,10 @@ class CreateRoomController @Inject constructor( id("name") enabled(enableFormElement) value(viewState.roomName) - hint(stringProvider.getString(R.string.create_room_name_hint)) + hint(host.stringProvider.getString(R.string.create_room_name_hint)) onTextChange { text -> - listener?.onNameChange(text) + host.listener?.onNameChange(text) } } settingsSectionTitleItem { @@ -74,10 +75,10 @@ class CreateRoomController @Inject constructor( id("topic") enabled(enableFormElement) value(viewState.roomTopic) - hint(stringProvider.getString(R.string.create_room_topic_hint)) + hint(host.stringProvider.getString(R.string.create_room_topic_hint)) onTextChange { text -> - listener?.onTopicChange(text) + host.listener?.onTopicChange(text) } } settingsSectionTitleItem { @@ -87,13 +88,13 @@ class CreateRoomController @Inject constructor( formSwitchItem { id("public") enabled(enableFormElement) - title(stringProvider.getString(R.string.create_room_public_title)) - summary(stringProvider.getString(R.string.create_room_public_description)) + title(host.stringProvider.getString(R.string.create_room_public_title)) + summary(host.stringProvider.getString(R.string.create_room_public_description)) switchChecked(viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) showDivider(viewState.roomVisibilityType !is CreateRoomViewState.RoomVisibilityType.Public) listener { value -> - listener?.setIsPublic(value) + host.listener?.setIsPublic(value) } } if (viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) { @@ -104,11 +105,11 @@ class CreateRoomController @Inject constructor( value(viewState.roomVisibilityType.aliasLocalPart) homeServer(":" + viewState.homeServerName) errorMessage( - roomAliasErrorFormatter.format( + host.roomAliasErrorFormatter.format( (((viewState.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError) ) onTextChange { value -> - listener?.setAliasLocalPart(value) + host.listener?.setAliasLocalPart(value) } } } else { @@ -116,43 +117,43 @@ class CreateRoomController @Inject constructor( formSwitchItem { id("encryption") enabled(enableFormElement) - title(stringProvider.getString(R.string.create_room_encryption_title)) + title(host.stringProvider.getString(R.string.create_room_encryption_title)) summary( if (viewState.hsAdminHasDisabledE2E) { - stringProvider.getString(R.string.settings_hs_admin_e2e_disabled) + host.stringProvider.getString(R.string.settings_hs_admin_e2e_disabled) } else { - stringProvider.getString(R.string.create_room_encryption_description) + host.stringProvider.getString(R.string.create_room_encryption_description) } ) switchChecked(viewState.isEncrypted) listener { value -> - listener?.setIsEncrypted(value) + host.listener?.setIsEncrypted(value) } } } formAdvancedToggleItem { id("showAdvanced") - title(stringProvider.getString(if (viewState.showAdvanced) R.string.hide_advanced else R.string.show_advanced)) + title(host.stringProvider.getString(if (viewState.showAdvanced) R.string.hide_advanced else R.string.show_advanced)) expanded(!viewState.showAdvanced) - listener { listener?.toggleShowAdvanced() } + listener { host.listener?.toggleShowAdvanced() } } if (viewState.showAdvanced) { formSwitchItem { id("federation") enabled(enableFormElement) - title(stringProvider.getString(R.string.create_room_disable_federation_title, viewState.homeServerName)) - summary(stringProvider.getString(R.string.create_room_disable_federation_description)) + title(host.stringProvider.getString(R.string.create_room_disable_federation_title, viewState.homeServerName)) + summary(host.stringProvider.getString(R.string.create_room_disable_federation_description)) switchChecked(viewState.disableFederation) showDivider(false) - listener { value -> listener?.setDisableFederation(value) } + listener { value -> host.listener?.setDisableFederation(value) } } } formSubmitButtonItem { id("submit") enabled(enableFormElement) buttonTitleId(R.string.create_room_action_create) - buttonClickListener { listener?.submit() } + buttonClickListener { host.listener?.submit() } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt index 2dd3b509a8..75e9807bd0 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt @@ -38,6 +38,7 @@ class RoomDirectoryPickerController @Inject constructor(private val stringProvid var index = 0 override fun buildModels(viewState: RoomDirectoryPickerViewState) { + val host = this val asyncThirdPartyProtocol = viewState.asyncThirdPartyRequest when (asyncThirdPartyProtocol) { @@ -56,24 +57,25 @@ class RoomDirectoryPickerController @Inject constructor(private val stringProvid is Fail -> { errorWithRetryItem { id("error") - text(errorFormatter.toHumanReadable(asyncThirdPartyProtocol.error)) - listener { callback?.retry() } + text(host.errorFormatter.toHumanReadable(asyncThirdPartyProtocol.error)) + listener { host.callback?.retry() } } } } } private fun buildDirectory(roomDirectoryData: RoomDirectoryData) { + val host = this roomDirectoryItem { - id(index++) + id(host.index++) directoryName(roomDirectoryData.displayName) val description = when { roomDirectoryData.includeAllNetworks -> - stringProvider.getString(R.string.directory_server_all_rooms_on_server, roomDirectoryData.displayName) + host.stringProvider.getString(R.string.directory_server_all_rooms_on_server, roomDirectoryData.displayName) "Matrix" == roomDirectoryData.displayName -> - stringProvider.getString(R.string.directory_server_native_rooms, roomDirectoryData.displayName) + host.stringProvider.getString(R.string.directory_server_native_rooms, roomDirectoryData.displayName) else -> null } @@ -83,7 +85,7 @@ class RoomDirectoryPickerController @Inject constructor(private val stringProvid includeAllNetworks(roomDirectoryData.includeAllNetworks) globalListener { - callback?.onRoomDirectoryClicked(roomDirectoryData) + host.callback?.onRoomDirectoryClicked(roomDirectoryData) } } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index a692eebe40..88a0518cf9 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -99,6 +99,7 @@ class RoomMemberProfileController @Inject constructor( private fun buildSecuritySection(state: RoomMemberProfileViewState) { // Security buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) + val host = this if (state.isRoomEncrypted) { if (state.userMXCrossSigningInfo != null) { @@ -152,7 +153,7 @@ class RoomMemberProfileController @Inject constructor( genericFooterItem { id("verify_footer") - text(stringProvider.getString(R.string.room_profile_encrypted_subtitle)) + text(host.stringProvider.getString(R.string.room_profile_encrypted_subtitle)) centered(false) } } @@ -170,7 +171,7 @@ class RoomMemberProfileController @Inject constructor( } else { genericFooterItem { id("verify_footer_not_encrypted") - text(stringProvider.getString(R.string.room_profile_not_encrypted_subtitle)) + text(host.stringProvider.getString(R.string.room_profile_not_encrypted_subtitle)) centered(false) } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt index e0730d55f1..48eaa7ba6f 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt @@ -50,16 +50,15 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: var interactionListener: InteractionListener? = null override fun buildModels(data: DeviceListViewState?) { - if (data == null) { - return - } + data ?: return + val host = this when (data.cryptoDevices) { Uninitialized -> { } is Loading -> { loadingItem { id("loading") - loadingText(stringProvider.getString(R.string.loading)) + loadingText(host.stringProvider.getString(R.string.loading)) } } is Success -> { @@ -77,11 +76,11 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: style(ItemStyle.BIG_TEXT) titleIconResourceId(if (allGreen) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) title( - stringProvider.getString( + host.stringProvider.getString( if (allGreen) R.string.verification_profile_verified else R.string.verification_profile_warning ) ) - description(stringProvider.getString(R.string.verification_conclusion_ok_notice)) + description(host.stringProvider.getString(R.string.verification_conclusion_ok_notice)) } if (vectorPreferences.developerMode()) { @@ -92,13 +91,13 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: genericItem { id("sessions") style(ItemStyle.BIG_TEXT) - title(stringProvider.getString(R.string.room_member_profile_sessions_section_title)) + title(host.stringProvider.getString(R.string.room_member_profile_sessions_section_title)) } if (deviceList.isEmpty()) { // Can this really happen? genericFooterItem { id("empty") - text(stringProvider.getString(R.string.search_no_results)) + text(host.stringProvider.getString(R.string.search_no_results)) } } else { // Build list of device with status @@ -107,14 +106,14 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: id(device.deviceId) titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) apply { - if (vectorPreferences.developerMode()) { + if (host.vectorPreferences.developerMode()) { val seq = span { +(device.displayName() ?: device.deviceId) +"\n" span { text = "(${device.deviceId})" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - textSize = dimensionConverter.spToPx(14) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = host.dimensionConverter.spToPx(14) } } title(seq) @@ -123,17 +122,17 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: } } value( - stringProvider.getString( + host.stringProvider.getString( if (device.isVerified) R.string.trusted else R.string.not_trusted ) ) valueColorInt( - colorProvider.getColor( + host.colorProvider.getColor( if (device.isVerified) R.color.riotx_positive_accent else R.color.riotx_destructive_accent ) ) itemClickAction(View.OnClickListener { - interactionListener?.onDeviceSelected(device) + host.interactionListener?.onDeviceSelected(device) }) } } @@ -142,7 +141,7 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: is Fail -> { errorWithRetryItem { id("error") - text(stringProvider.getString(R.string.room_member_profile_failed_to_get_devices)) + text(host.stringProvider.getString(R.string.room_member_profile_failed_to_get_devices)) listener { // TODO } @@ -152,6 +151,7 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: } private fun addDebugInfo(data: DeviceListViewState) { + val host = this data.memberCrossSigningKey?.masterKey()?.let { genericItemWithValue { id("msk") @@ -161,8 +161,8 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: +"Master Key:\n" span { text = it.unpaddedBase64PublicKey ?: "" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - textSize = dimensionConverter.spToPx(12) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = host.dimensionConverter.spToPx(12) } } ) @@ -177,8 +177,8 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: +"User Key:\n" span { text = it.unpaddedBase64PublicKey ?: "" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - textSize = dimensionConverter.spToPx(12) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = host.dimensionConverter.spToPx(12) } } ) @@ -193,8 +193,8 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: +"Self Signed Key:\n" span { text = it.unpaddedBase64PublicKey ?: "" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - textSize = dimensionConverter.spToPx(12) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = host.dimensionConverter.spToPx(12) } } ) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt index 4c28acd904..f12f17ae14 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -44,6 +44,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi var interactionListener: InteractionListener? = null override fun buildModels(data: DeviceListViewState?) { + val host = this data?.selectedDevice?.let { val isVerified = it.trustLevel?.isVerified() == true genericItem { @@ -51,7 +52,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi style(ItemStyle.BIG_TEXT) titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) title( - stringProvider.getString( + host.stringProvider.getString( if (isVerified) R.string.verification_profile_verified else R.string.verification_profile_warning ) ) @@ -59,16 +60,16 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi genericFooterItem { id("desc") centered(false) - textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) apply { if (isVerified) { // TODO FORMAT - text(stringProvider.getString(R.string.verification_profile_device_verified_because, + text(host.stringProvider.getString(R.string.verification_profile_device_verified_because, data.userItem?.displayName ?: "", data.userItem?.id ?: "")) } else { // TODO what if mine - text(stringProvider.getString(R.string.verification_profile_device_new_signing, + text(host.stringProvider.getString(R.string.verification_profile_device_new_signing, data.userItem?.displayName ?: "", data.userItem?.id ?: "")) } @@ -84,8 +85,8 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi +(it.displayName() ?: "") span { text = " (${it.deviceId})" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - textSize = dimensionConverter.spToPx(14) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = host.dimensionConverter.spToPx(14) } } ) @@ -95,18 +96,18 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi genericFooterItem { id("warn") centered(false) - textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - text(stringProvider.getString(R.string.verification_profile_device_untrust_info)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + text(host.stringProvider.getString(R.string.verification_profile_device_untrust_info)) } bottomSheetVerificationActionItem { id("verify") - title(stringProvider.getString(R.string.cross_signing_verify_by_emoji)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.cross_signing_verify_by_emoji)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_accent)) + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) listener { - interactionListener?.onVerifyManually(it) + host.interactionListener?.onVerifyManually(it) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index 10e6dceebe..1b6f7f2328 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -62,9 +62,8 @@ class RoomProfileController @Inject constructor( } override fun buildModels(data: RoomProfileViewState?) { - if (data == null) { - return - } + data ?: return + val host = this val roomSummary = data.roomSummary() ?: return // Topic @@ -83,7 +82,7 @@ class RoomProfileController @Inject constructor( } override fun onUrlLongClicked(url: String): Boolean { - callback?.onUrlInTopicLongClicked(url) + host.callback?.onUrlInTopicLongClicked(url) return true } })) @@ -100,7 +99,7 @@ class RoomProfileController @Inject constructor( genericFooterItem { id("e2e info") centered(false) - text(stringProvider.getString(learnMoreSubtitle)) + text(host.stringProvider.getString(learnMoreSubtitle)) } buildEncryptionAction(data.actionPermissions, roomSummary) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt index 0b695031c5..6edeeedca3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt @@ -77,34 +77,36 @@ class RoomAliasController @Inject constructor( } private fun buildRoomDirectoryVisibility(data: RoomAliasViewState) { + val host = this when (data.roomDirectoryVisibility) { Uninitialized -> Unit is Loading -> Unit is Success -> { formSwitchItem { id("roomVisibility") - title(stringProvider.getString(R.string.room_alias_publish_to_directory, data.homeServerName)) + title(host.stringProvider.getString(R.string.room_alias_publish_to_directory, data.homeServerName)) showDivider(false) switchChecked(data.roomDirectoryVisibility() == RoomDirectoryVisibility.PUBLIC) listener { if (it) { - callback?.setRoomDirectoryVisibility(RoomDirectoryVisibility.PUBLIC) + host.callback?.setRoomDirectoryVisibility(RoomDirectoryVisibility.PUBLIC) } else { - callback?.setRoomDirectoryVisibility(RoomDirectoryVisibility.PRIVATE) + host.callback?.setRoomDirectoryVisibility(RoomDirectoryVisibility.PRIVATE) } } } } is Fail -> { errorWithRetryItem { - text(stringProvider.getString(R.string.room_alias_publish_to_directory_error, - errorFormatter.toHumanReadable(data.roomDirectoryVisibility.error))) + text(host.stringProvider.getString(R.string.room_alias_publish_to_directory_error, + host.errorFormatter.toHumanReadable(data.roomDirectoryVisibility.error))) } } } } private fun buildPublishInfo(data: RoomAliasViewState) { + val host = this buildProfileSection( stringProvider.getString(R.string.room_alias_published_alias_title) ) @@ -120,8 +122,8 @@ class RoomAliasController @Inject constructor( profileActionItem { id("canonical") title(data.canonicalAlias) - subtitle(stringProvider.getString(R.string.room_alias_published_alias_main)) - listener { callback?.openAliasDetail(canonicalAlias) } + subtitle(host.stringProvider.getString(R.string.room_alias_published_alias_main)) + listener { host.callback?.openAliasDetail(canonicalAlias) } } } @@ -143,7 +145,7 @@ class RoomAliasController @Inject constructor( profileActionItem { id("alt_$idx") title(altAlias) - listener { callback?.openAliasDetail(altAlias) } + listener { host.callback?.openAliasDetail(altAlias) } } } } @@ -154,14 +156,15 @@ class RoomAliasController @Inject constructor( } private fun buildPublishManuallyForm(data: RoomAliasViewState) { + val host = this when (data.publishManuallyState) { RoomAliasViewState.AddAliasState.Hidden -> Unit RoomAliasViewState.AddAliasState.Closed -> { settingsButtonItem { id("publishManually") - colorProvider(colorProvider) + colorProvider(host.colorProvider) buttonTitleId(R.string.room_alias_published_alias_add_manually) - buttonClickListener { callback?.toggleManualPublishForm() } + buttonClickListener { host.callback?.toggleManualPublishForm() } } } is RoomAliasViewState.AddAliasState.Editing -> { @@ -169,29 +172,30 @@ class RoomAliasController @Inject constructor( id("publishManuallyEdit") value(data.publishManuallyState.value) showBottomSeparator(false) - hint(stringProvider.getString(R.string.room_alias_address_hint)) + hint(host.stringProvider.getString(R.string.room_alias_address_hint)) inputType(InputType.TYPE_CLASS_TEXT) onTextChange { text -> - callback?.setNewAlias(text) + host.callback?.setNewAlias(text) } } settingsContinueCancelItem { id("publishManuallySubmit") - continueText(stringProvider.getString(R.string.room_alias_published_alias_add_manually_submit)) - continueOnClick { callback?.addAlias() } - cancelOnClick { callback?.toggleManualPublishForm() } + continueText(host.stringProvider.getString(R.string.room_alias_published_alias_add_manually_submit)) + continueOnClick { host.callback?.addAlias() } + cancelOnClick { host.callback?.toggleManualPublishForm() } } } } } private fun buildLocalInfo(data: RoomAliasViewState) { + val host = this buildProfileSection( stringProvider.getString(R.string.room_alias_local_address_title) ) settingsInfoItem { id("localInfo") - helperText(stringProvider.getString(R.string.room_alias_local_address_subtitle, data.homeServerName)) + helperText(host.stringProvider.getString(R.string.room_alias_local_address_subtitle, data.homeServerName)) } when (val localAliases = data.localAliases) { @@ -211,7 +215,7 @@ class RoomAliasController @Inject constructor( profileActionItem { id("loc_$idx") title(localAlias) - listener { callback?.openAliasDetail(localAlias) } + listener { host.callback?.openAliasDetail(localAlias) } } } } @@ -219,7 +223,7 @@ class RoomAliasController @Inject constructor( is Fail -> { errorWithRetryItem { id("alt_error") - text(errorFormatter.toHumanReadable(localAliases.error)) + text(host.errorFormatter.toHumanReadable(localAliases.error)) } } } @@ -229,14 +233,15 @@ class RoomAliasController @Inject constructor( } private fun buildAddLocalAlias(data: RoomAliasViewState) { + val host = this when (data.newLocalAliasState) { RoomAliasViewState.AddAliasState.Hidden -> Unit RoomAliasViewState.AddAliasState.Closed -> { settingsButtonItem { id("newLocalAliasButton") - colorProvider(colorProvider) + colorProvider(host.colorProvider) buttonTitleId(R.string.room_alias_local_address_add) - buttonClickListener { callback?.toggleLocalAliasForm() } + buttonClickListener { host.callback?.toggleLocalAliasForm() } } } is RoomAliasViewState.AddAliasState.Editing -> { @@ -245,16 +250,16 @@ class RoomAliasController @Inject constructor( value(data.newLocalAliasState.value) homeServer(":" + data.homeServerName) showBottomSeparator(false) - errorMessage(roomAliasErrorFormatter.format((data.newLocalAliasState.asyncRequest as? Fail)?.error as? RoomAliasError)) + errorMessage(host.roomAliasErrorFormatter.format((data.newLocalAliasState.asyncRequest as? Fail)?.error as? RoomAliasError)) onTextChange { value -> - callback?.setNewLocalAliasLocalPart(value) + host.callback?.setNewLocalAliasLocalPart(value) } } settingsContinueCancelItem { id("newLocalAliasSubmit") - continueText(stringProvider.getString(R.string.action_add)) - continueOnClick { callback?.addLocalAlias() } - cancelOnClick { callback?.toggleLocalAliasForm() } + continueText(host.stringProvider.getString(R.string.action_add)) + continueOnClick { host.callback?.addLocalAlias() } + cancelOnClick { host.callback?.toggleLocalAliasForm() } } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt index 157037c13d..b030afd503 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt @@ -73,12 +73,13 @@ class RoomAliasBottomSheetController @Inject constructor() : TypedEpoxyControlle } private fun RoomAliasBottomSheetSharedAction.toBottomSheetItem(index: Int) { + val host = this@RoomAliasBottomSheetController return bottomSheetActionItem { id("action_$index") iconRes(iconResId) textRes(titleRes) destructive(this@toBottomSheetItem.destructive) - listener(View.OnClickListener { listener?.didSelectMenuAction(this@toBottomSheetItem) }) + listener(View.OnClickListener { host.listener?.didSelectMenuAction(this@toBottomSheetItem) }) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt index 2a0c787a7a..6c2fac98d8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt @@ -52,6 +52,7 @@ class RoomBannedMemberListController @Inject constructor( override fun buildModels(data: RoomBannedMemberListViewState?) { val bannedList = data?.bannedMemberSummaries?.invoke() ?: return + val host = this val quantityString = stringProvider.getQuantityString(R.plurals.room_settings_banned_users_count, bannedList.size, bannedList.size) @@ -74,7 +75,7 @@ class RoomBannedMemberListController @Inject constructor( profileMatrixItemWithProgress { id(roomMember.userId) matrixItem(roomMember.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) apply { if (actionInProgress) { inProgress(true) @@ -83,7 +84,7 @@ class RoomBannedMemberListController @Inject constructor( inProgress(false) editable(true) clickListener { _ -> - callback?.onUnbanClicked(roomMember) + host.callback?.onUnbanClicked(roomMember) } } } @@ -92,7 +93,7 @@ class RoomBannedMemberListController @Inject constructor( between = { _, roomMemberBefore -> dividerItem { id("divider_${roomMemberBefore.userId}") - color(dividerColor) + color(host.dividerColor) } } ) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index eda461de14..a256d99175 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -55,6 +55,7 @@ class RoomMemberListController @Inject constructor( override fun buildModels(data: RoomMemberListViewState?) { data ?: return + val host = this roomMemberSummaryFilter.filter = data.filter @@ -93,17 +94,17 @@ class RoomMemberListController @Inject constructor( profileMatrixItem { id(roomMember.userId) matrixItem(roomMember.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) clickListener { _ -> - callback?.onRoomMemberClicked(roomMember) + host.callback?.onRoomMemberClicked(roomMember) } } }, between = { _, roomMemberBefore -> dividerItem { id("divider_${roomMemberBefore.userId}") - color(dividerColor) + color(host.dividerColor) } } ) @@ -111,7 +112,7 @@ class RoomMemberListController @Inject constructor( // Display the threepid invite after the regular invite dividerItem { id("divider_threepidinvites") - color(dividerColor) + color(host.dividerColor) } buildThreePidInvites(data) @@ -130,6 +131,7 @@ class RoomMemberListController @Inject constructor( } private fun buildThreePidInvites(data: RoomMemberListViewState) { + val host = this data.threePidInvites() ?.filter { it.content.toModel() != null } ?.join( @@ -139,10 +141,10 @@ class RoomMemberListController @Inject constructor( profileMatrixItem { id("3pid_$idx") matrixItem(content.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) editable(data.actionsPermissions.canRevokeThreePidInvite) clickListener { _ -> - callback?.onThreePidInviteClicked(event) + host.callback?.onThreePidInviteClicked(event) } } } @@ -150,7 +152,7 @@ class RoomMemberListController @Inject constructor( between = { idx, _ -> dividerItem { id("divider3_$idx") - color(dividerColor) + color(host.dividerColor) } } ) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt index fcc1354542..c510e0501b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt @@ -88,6 +88,7 @@ class RoomPermissionsController @Inject constructor( } override fun buildModels(data: RoomPermissionsViewState?) { + val host = this buildProfileSection( stringProvider.getString(R.string.room_permissions_title) ) @@ -97,17 +98,18 @@ class RoomPermissionsController @Inject constructor( else -> { loadingItem { id("loading") - loadingText(stringProvider.getString(R.string.loading)) + loadingText(host.stringProvider.getString(R.string.loading)) } } } } private fun buildPermissions(data: RoomPermissionsViewState, content: PowerLevelsContent) { + val host = this val editable = data.actionPermissions.canChangePowerLevels settingsInfoItem { id("notice") - helperText(stringProvider.getString(if (editable) R.string.room_permissions_notice else R.string.room_permissions_notice_read_only)) + helperText(host.stringProvider.getString(if (editable) R.string.room_permissions_notice else R.string.room_permissions_notice_read_only)) } // Useful permissions @@ -116,9 +118,9 @@ class RoomPermissionsController @Inject constructor( // Toggle formAdvancedToggleItem { id("showAdvanced") - title(stringProvider.getString(if (data.showAdvancedPermissions) R.string.hide_advanced else R.string.show_advanced)) + title(host.stringProvider.getString(if (data.showAdvancedPermissions) R.string.hide_advanced else R.string.show_advanced)) expanded(!data.showAdvancedPermissions) - listener { callback?.toggleShowAllPermissions() } + listener { host.callback?.toggleShowAllPermissions() } } // Advanced permissions diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index b12a6d52eb..24836bc504 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -62,6 +62,7 @@ class RoomSettingsController @Inject constructor( override fun buildModels(data: RoomSettingsViewState?) { val roomSummary = data?.roomSummary?.invoke() ?: return + val host = this formEditableAvatarItem { id("avatar") @@ -69,7 +70,7 @@ class RoomSettingsController @Inject constructor( when (val avatarAction = data.avatarAction) { RoomSettingsViewState.AvatarAction.None -> { // Use the current value - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) } @@ -78,8 +79,8 @@ class RoomSettingsController @Inject constructor( is RoomSettingsViewState.AvatarAction.UpdateAvatar -> imageUri(avatarAction.newAvatarUri) } - clickListener { callback?.onAvatarChange() } - deleteListener { callback?.onAvatarDelete() } + clickListener { host.callback?.onAvatarChange() } + deleteListener { host.callback?.onAvatarDelete() } } buildProfileSection( @@ -90,10 +91,10 @@ class RoomSettingsController @Inject constructor( id("name") enabled(data.actionPermissions.canChangeName) value(data.newName ?: roomSummary.displayName) - hint(stringProvider.getString(R.string.room_settings_name_hint)) + hint(host.stringProvider.getString(R.string.room_settings_name_hint)) onTextChange { text -> - callback?.onNameChanged(text) + host.callback?.onNameChanged(text) } } @@ -101,10 +102,10 @@ class RoomSettingsController @Inject constructor( id("topic") enabled(data.actionPermissions.canChangeTopic) value(data.newTopic ?: roomSummary.topic) - hint(stringProvider.getString(R.string.room_settings_topic_hint)) + hint(host.stringProvider.getString(R.string.room_settings_topic_hint)) onTextChange { text -> - callback?.onTopicChanged(text) + host.callback?.onTopicChanged(text) } } @@ -134,10 +135,10 @@ class RoomSettingsController @Inject constructor( // add guest access option? formSwitchItem { id("guest_access") - title(stringProvider.getString(R.string.room_settings_guest_access_title)) + title(host.stringProvider.getString(R.string.room_settings_guest_access_title)) switchChecked(guestAccess == GuestAccess.CanJoin) listener { - callback?.onToggleGuestAccess() + host.callback?.onToggleGuestAccess() } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileController.kt index f2c0ad2a9d..70240752e2 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileController.kt @@ -49,16 +49,17 @@ class UploadsFileController @Inject constructor( override fun buildModels(data: RoomUploadsViewState?) { data ?: return + val host = this buildFileItems(data.fileEvents) if (data.hasMore) { loadingItem { // Always use a different id, because we can be notified several times of visibility state changed - id("loadMore${idx++}") + id("loadMore${host.idx++}") onVisibilityStateChanged { _, _, visibilityState -> if (visibilityState == VisibilityState.VISIBLE) { - listener?.loadMore() + host.listener?.loadMore() } } } @@ -66,24 +67,25 @@ class UploadsFileController @Inject constructor( } private fun buildFileItems(fileEvents: List) { + val host = this fileEvents.forEach { uploadEvent -> uploadsFileItem { id(uploadEvent.eventId) title(uploadEvent.contentWithAttachmentContent.body) - subtitle(stringProvider.getString(R.string.uploads_files_subtitle, + subtitle(host.stringProvider.getString(R.string.uploads_files_subtitle, uploadEvent.senderInfo.disambiguatedDisplayName, - dateFormatter.format(uploadEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME))) + host.dateFormatter.format(uploadEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME))) listener(object : UploadsFileItem.Listener { override fun onItemClicked() { - listener?.onOpenClicked(uploadEvent) + host.listener?.onOpenClicked(uploadEvent) } override fun onDownloadClicked() { - listener?.onDownloadClicked(uploadEvent) + host.listener?.onDownloadClicked(uploadEvent) } override fun onShareClicked() { - listener?.onShareClicked(uploadEvent) + host.listener?.onShareClicked(uploadEvent) } }) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsMediaController.kt index f8dff345bb..a57d3c45fc 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsMediaController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -60,16 +60,17 @@ class UploadsMediaController @Inject constructor( override fun buildModels(data: RoomUploadsViewState?) { data ?: return + val host = this buildMediaItems(data.mediaEvents) if (data.hasMore) { squareLoadingItem { // Always use a different id, because we can be notified several times of visibility state changed - id("loadMore${idx++}") + id("loadMore${host.idx++}") onVisibilityStateChanged { _, _, visibilityState -> if (visibilityState == VisibilityState.VISIBLE) { - listener?.loadMore() + host.listener?.loadMore() } } } @@ -77,17 +78,18 @@ class UploadsMediaController @Inject constructor( } private fun buildMediaItems(mediaEvents: List) { + val host = this mediaEvents.forEach { uploadEvent -> when (uploadEvent.contentWithAttachmentContent.msgType) { MessageType.MSGTYPE_IMAGE -> { val data = uploadEvent.toImageContentRendererData() ?: return@forEach uploadsImageItem { id(uploadEvent.eventId) - imageContentRenderer(imageContentRenderer) + imageContentRenderer(host.imageContentRenderer) data(data) listener(object : UploadsImageItem.Listener { override fun onItemClicked(view: View, data: ImageContentRenderer.Data) { - listener?.onOpenImageClicked(view, data) + host.listener?.onOpenImageClicked(view, data) } }) } @@ -96,11 +98,11 @@ class UploadsMediaController @Inject constructor( val data = uploadEvent.toVideoContentRendererData() ?: return@forEach uploadsVideoItem { id(uploadEvent.eventId) - imageContentRenderer(imageContentRenderer) + imageContentRenderer(host.imageContentRenderer) data(data) listener(object : UploadsVideoItem.Listener { override fun onItemClicked(view: View, data: VideoContentRenderer.Data) { - listener?.onOpenVideoClicked(view, data) + host.listener?.onOpenVideoClicked(view, data) } }) } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt index 6425256929..a98cc35ac2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt @@ -42,18 +42,19 @@ class CrossSigningSettingsController @Inject constructor( override fun buildModels(data: CrossSigningSettingsViewState?) { if (data == null) return + val host = this when { data.xSigningKeyCanSign -> { genericItem { id("can") titleIconResourceId(R.drawable.ic_shield_trusted) - title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) + title(host.stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) } genericButtonItem { id("Reset") - text(stringProvider.getString(R.string.reset_cross_signing)) + text(host.stringProvider.getString(R.string.reset_cross_signing)) buttonClickAction(DebouncedClickListener({ - interactionListener?.didTapInitializeCrossSigning() + host.interactionListener?.didTapInitializeCrossSigning() })) } } @@ -61,13 +62,13 @@ class CrossSigningSettingsController @Inject constructor( genericItem { id("trusted") titleIconResourceId(R.drawable.ic_shield_custom) - title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) + title(host.stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) } genericButtonItem { id("Reset") - text(stringProvider.getString(R.string.reset_cross_signing)) + text(host.stringProvider.getString(R.string.reset_cross_signing)) buttonClickAction(DebouncedClickListener({ - interactionListener?.didTapInitializeCrossSigning() + host.interactionListener?.didTapInitializeCrossSigning() })) } } @@ -75,27 +76,27 @@ class CrossSigningSettingsController @Inject constructor( genericItem { id("enable") titleIconResourceId(R.drawable.ic_shield_black) - title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted)) + title(host.stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted)) } genericButtonItem { id("Reset") - text(stringProvider.getString(R.string.reset_cross_signing)) + text(host.stringProvider.getString(R.string.reset_cross_signing)) buttonClickAction(DebouncedClickListener({ - interactionListener?.didTapInitializeCrossSigning() + host.interactionListener?.didTapInitializeCrossSigning() })) } } else -> { genericItem { id("not") - title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled)) + title(host.stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled)) } genericPositiveButtonItem { id("Initialize") - text(stringProvider.getString(R.string.initialize_cross_signing)) + text(host.stringProvider.getString(R.string.initialize_cross_signing)) buttonClickAction(DebouncedClickListener({ - interactionListener?.didTapInitializeCrossSigning() + host.interactionListener?.didTapInitializeCrossSigning() })) } } @@ -112,8 +113,8 @@ class CrossSigningSettingsController @Inject constructor( +"Master Key:\n" span { text = it.unpaddedBase64PublicKey ?: "" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - textSize = dimensionConverter.spToPx(12) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = host.dimensionConverter.spToPx(12) } } ) @@ -128,8 +129,8 @@ class CrossSigningSettingsController @Inject constructor( +"User Key:\n" span { text = it.unpaddedBase64PublicKey ?: "" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - textSize = dimensionConverter.spToPx(12) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = host.dimensionConverter.spToPx(12) } } ) @@ -144,8 +145,8 @@ class CrossSigningSettingsController @Inject constructor( +"Self Signed Key:\n" span { text = it.unpaddedBase64PublicKey ?: "" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) - textSize = dimensionConverter.spToPx(12) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = host.dimensionConverter.spToPx(12) } } ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt index b66204a9a4..930ad54b7e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt @@ -80,6 +80,7 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( val isMine = data.isMine val currentSessionIsTrusted = data.accountCrossSigningIsTrusted Timber.v("handleE2EWithCrossSigning $isMine, $cryptoDeviceInfo, $shield") + val host = this if (isMine) { if (currentSessionIsTrusted) { @@ -87,8 +88,8 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( id("trust${cryptoDeviceInfo.deviceId}") style(ItemStyle.BIG_TEXT) titleIconResourceId(shield) - title(stringProvider.getString(R.string.encryption_information_verified)) - description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) + title(host.stringProvider.getString(R.string.encryption_information_verified)) + description(host.stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) } } else if (data.canVerifySession) { // You need to complete security, only if there are other session(s) available, or if 4S contains secrets @@ -96,11 +97,11 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( id("trust${cryptoDeviceInfo.deviceId}") style(ItemStyle.BIG_TEXT) titleIconResourceId(shield) - title(stringProvider.getString(R.string.crosssigning_verify_this_session)) + title(host.stringProvider.getString(R.string.crosssigning_verify_this_session)) if (data.hasOtherSessions) { - description(stringProvider.getString(R.string.confirm_your_identity)) + description(host.stringProvider.getString(R.string.confirm_your_identity)) } else { - description(stringProvider.getString(R.string.confirm_your_identity_quad_s)) + description(host.stringProvider.getString(R.string.confirm_your_identity_quad_s)) } } } @@ -116,16 +117,16 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( id("trust${cryptoDeviceInfo.deviceId}") style(ItemStyle.BIG_TEXT) titleIconResourceId(shield) - title(stringProvider.getString(R.string.encryption_information_verified)) - description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) + title(host.stringProvider.getString(R.string.encryption_information_verified)) + description(host.stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) } } else { genericItem { id("trust${cryptoDeviceInfo.deviceId}") titleIconResourceId(shield) style(ItemStyle.BIG_TEXT) - title(stringProvider.getString(R.string.encryption_information_not_verified)) - description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) + title(host.stringProvider.getString(R.string.encryption_information_not_verified)) + description(host.stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) } } } @@ -145,12 +146,12 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( } bottomSheetVerificationActionItem { id("completeSecurity") - title(stringProvider.getString(R.string.crosssigning_verify_this_session)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.crosssigning_verify_this_session)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_accent)) + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) listener { - callback?.onAction(DevicesAction.CompleteSecurity) + host.callback?.onAction(DevicesAction.CompleteSecurity) } } } else if (!isMine) { @@ -165,6 +166,7 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( } private fun handleE2EInLegacy(data: DeviceVerificationInfoBottomSheetViewState, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) { + val host = this // ==== Legacy val isMine = data.isMine @@ -174,16 +176,16 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( id("trust${cryptoDeviceInfo.deviceId}") style(ItemStyle.BIG_TEXT) titleIconResourceId(shield) - title(stringProvider.getString(R.string.encryption_information_verified)) - description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) + title(host.stringProvider.getString(R.string.encryption_information_verified)) + description(host.stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) } } else { genericItem { id("trust${cryptoDeviceInfo.deviceId}") titleIconResourceId(shield) style(ItemStyle.BIG_TEXT) - title(stringProvider.getString(R.string.encryption_information_not_verified)) - description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) + title(host.stringProvider.getString(R.string.encryption_information_not_verified)) + description(host.stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) } } @@ -203,29 +205,30 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( } bottomSheetVerificationActionItem { id("verify${cryptoDeviceInfo.deviceId}") - title(stringProvider.getString(R.string.verification_verify_device)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.verification_verify_device)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_accent)) + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) listener { - callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId)) + host.callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId)) } } } } private fun addVerifyActions(cryptoDeviceInfo: CryptoDeviceInfo) { + val host = this dividerItem { id("verifyDiv") } bottomSheetVerificationActionItem { id("verify_text") - title(stringProvider.getString(R.string.cross_signing_verify_by_text)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.cross_signing_verify_by_text)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_accent)) + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) listener { - callback?.onAction(DevicesAction.VerifyMyDeviceManually(cryptoDeviceInfo.deviceId)) + host.callback?.onAction(DevicesAction.VerifyMyDeviceManually(cryptoDeviceInfo.deviceId)) } } dividerItem { @@ -233,17 +236,18 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( } bottomSheetVerificationActionItem { id("verify_emoji") - title(stringProvider.getString(R.string.cross_signing_verify_by_emoji)) - titleColor(colorProvider.getColor(R.color.riotx_accent)) + title(host.stringProvider.getString(R.string.cross_signing_verify_by_emoji)) + titleColor(host.colorProvider.getColor(R.color.riotx_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_accent)) + iconColor(host.colorProvider.getColor(R.color.riotx_accent)) listener { - callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId)) + host.callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId)) } } } private fun addGenericDeviceManageActions(data: DeviceVerificationInfoBottomSheetViewState, deviceId: String) { + val host = this // Offer delete session if not me if (!data.isMine) { // Add the delete option @@ -252,12 +256,12 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( } bottomSheetVerificationActionItem { id("delete") - title(stringProvider.getString(R.string.settings_active_sessions_signout_device)) - titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + title(host.stringProvider.getString(R.string.settings_active_sessions_signout_device)) + titleColor(host.colorProvider.getColor(R.color.riotx_destructive_accent)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconColor(host.colorProvider.getColor(R.color.riotx_destructive_accent)) listener { - callback?.onAction(DevicesAction.Delete(deviceId)) + host.callback?.onAction(DevicesAction.Delete(deviceId)) } } } @@ -268,17 +272,18 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( } bottomSheetVerificationActionItem { id("rename") - title(stringProvider.getString(R.string.rename)) - titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + title(host.stringProvider.getString(R.string.rename)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) listener { - callback?.onAction(DevicesAction.PromptRename(deviceId)) + host.callback?.onAction(DevicesAction.PromptRename(deviceId)) } } } private fun handleNonE2EDevice(data: DeviceVerificationInfoBottomSheetViewState) { + val host = this val info = data.deviceInfo.invoke() ?: return genericItem { id("info${info.deviceId}") @@ -288,7 +293,7 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( genericFooterItem { id("infoCrypto${info.deviceId}") - text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info)) + text(host.stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info)) } info.deviceId?.let { addGenericDeviceManageActions(data, it) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesController.kt index ca2fea89d3..a3c7ffaa8f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesController.kt @@ -61,6 +61,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor } private fun buildDevicesModels(state: DevicesViewState) { + val host = this when (val devices = state.devices) { is Loading, is Uninitialized -> @@ -70,8 +71,8 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor is Fail -> errorWithRetryItem { id("error") - text(errorFormatter.toHumanReadable(devices.error)) - listener { callback?.retry() } + text(host.errorFormatter.toHumanReadable(devices.error)) + listener { host.callback?.retry() } } is Success -> buildDevicesList(devices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted) @@ -82,6 +83,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor myDeviceId: String, legacyMode: Boolean, currentSessionCrossTrusted: Boolean) { + val host = this devices .firstOrNull { it.deviceInfo.deviceId == myDeviceId @@ -90,21 +92,21 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor // Current device genericItemHeader { id("current") - text(stringProvider.getString(R.string.devices_current_device)) + text(host.stringProvider.getString(R.string.devices_current_device)) } deviceItem { id("myDevice${deviceInfo.deviceId}") legacyMode(legacyMode) trustedSession(currentSessionCrossTrusted) - dimensionConverter(dimensionConverter) - colorProvider(colorProvider) - detailedMode(vectorPreferences.developerMode()) + dimensionConverter(host.dimensionConverter) + colorProvider(host.colorProvider) + detailedMode(host.vectorPreferences.developerMode()) deviceInfo(deviceInfo) currentDevice(true) e2eCapable(true) - lastSeenFormatted(dateFormatter.format(deviceInfo.lastSeenTs, DateFormatKind.DEFAULT_DATE_AND_TIME)) - itemClickAction { callback?.onDeviceClicked(deviceInfo) } + lastSeenFormatted(host.dateFormatter.format(deviceInfo.lastSeenTs, DateFormatKind.DEFAULT_DATE_AND_TIME)) + itemClickAction { host.callback?.onDeviceClicked(deviceInfo) } trusted(DeviceTrustLevel(currentSessionCrossTrusted, true)) } @@ -126,7 +128,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor if (devices.size > 1) { genericItemHeader { id("others") - text(stringProvider.getString(R.string.devices_other_devices)) + text(host.stringProvider.getString(R.string.devices_other_devices)) } devices @@ -140,12 +142,12 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor id("device$idx") legacyMode(legacyMode) trustedSession(currentSessionCrossTrusted) - dimensionConverter(dimensionConverter) - colorProvider(colorProvider) - detailedMode(vectorPreferences.developerMode()) + dimensionConverter(host.dimensionConverter) + colorProvider(host.colorProvider) + detailedMode(host.vectorPreferences.developerMode()) deviceInfo(deviceInfo) currentDevice(false) - itemClickAction { callback?.onDeviceClicked(deviceInfo) } + itemClickAction { host.callback?.onDeviceClicked(deviceInfo) } e2eCapable(cryptoInfo != null) trusted(cryptoInfo?.trustLevel) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt index 13d7e0f396..8f4e36b9a1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt @@ -43,11 +43,12 @@ class AccountDataEpoxyController @Inject constructor( override fun buildModels(data: AccountDataViewState?) { if (data == null) return + val host = this when (data.accountData) { is Loading -> { loadingItem { id("loading") - loadingText(stringProvider.getString(R.string.loading)) + loadingText(host.stringProvider.getString(R.string.loading)) } } is Fail -> { @@ -61,7 +62,7 @@ class AccountDataEpoxyController @Inject constructor( if (dataList.isEmpty()) { genericFooterItem { id("noResults") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { dataList.forEach { accountData -> @@ -69,10 +70,10 @@ class AccountDataEpoxyController @Inject constructor( id(accountData.type) title(accountData.type) itemClickAction(DebouncedClickListener({ - interactionListener?.didTap(accountData) + host.interactionListener?.didTap(accountData) })) itemLongClickAction(View.OnLongClickListener { - interactionListener?.didLongTap(accountData) + host.interactionListener?.didLongTap(accountData) true }) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt index 603c67d074..666b0c3712 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt @@ -54,10 +54,11 @@ class GossipingTrailPagedEpoxyController @Inject constructor( var interactionListener: InteractionListener? = null override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> { + val host = this val event = item ?: return GenericItem_().apply { id(currentPosition) } return GenericItem_().apply { id(event.hashCode()) - itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } }) + itemClickAction(GenericItem.Action("view").apply { perform = Runnable { host.interactionListener?.didTap(event) } }) title( if (event.isEncrypted()) { "${event.getClearType()} [encrypted]" @@ -67,7 +68,7 @@ class GossipingTrailPagedEpoxyController @Inject constructor( ) description( span { - +vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME) + +host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME) span("\nfrom: ") { textStyle = "bold" } @@ -98,7 +99,7 @@ class GossipingTrailPagedEpoxyController @Inject constructor( val content = event.getClearContent().toModel() if (event.mxDecryptionResult == null) { span("**Failed to Decrypt** ${event.mCryptoError}") { - textColor = colorProvider.getColor(R.color.vector_error_color) + textColor = host.colorProvider.getColor(R.color.vector_error_color) } } span("\nsessionId:") { @@ -157,7 +158,7 @@ class GossipingTrailPagedEpoxyController @Inject constructor( +"${content?.requestingDeviceId}" } else if (event.getClearType() == EventType.ENCRYPTED) { span("**Failed to Decrypt** ${event.mCryptoError}") { - textColor = colorProvider.getColor(R.color.vector_error_color) + textColor = host.colorProvider.getColor(R.color.vector_error_color) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt index c2bdcfbd04..3c90a45237 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt @@ -40,6 +40,7 @@ class IncomingKeyRequestPagedController @Inject constructor( var interactionListener: InteractionListener? = null override fun buildItemModel(currentPosition: Int, item: IncomingRoomKeyRequest?): EpoxyModel<*> { + val host = this val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) } return GenericItem_().apply { @@ -51,7 +52,7 @@ class IncomingKeyRequestPagedController @Inject constructor( textStyle = "bold" } span("${roomKeyRequest.userId}") - +vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME) + +host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME) span("\nsessionId:") { textStyle = "bold" } diff --git a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt index 514311315d..3217756a82 100644 --- a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt @@ -46,6 +46,7 @@ class HomeserverSettingsController @Inject constructor( override fun buildModels(data: HomeServerSettingsViewState?) { data ?: return + val host = this buildHeader(data) buildCapabilities(data) @@ -58,8 +59,8 @@ class HomeserverSettingsController @Inject constructor( is Fail -> errorWithRetryItem { id("error") - text(errorFormatter.toHumanReadable(federationVersion.error)) - listener { callback?.retry() } + text(host.errorFormatter.toHumanReadable(federationVersion.error)) + listener { host.callback?.retry() } } is Success -> buildFederationVersion(federationVersion()) @@ -101,6 +102,7 @@ class HomeserverSettingsController @Inject constructor( } private fun buildCapabilities(data: HomeServerSettingsViewState) { + val host = this settingsSectionTitleItem { id("uploadTitle") titleResId(R.string.settings_server_upload_size_title) @@ -113,7 +115,7 @@ class HomeserverSettingsController @Inject constructor( if (limit == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) { helperTextResId(R.string.settings_server_upload_size_unknown) } else { - helperText(stringProvider.getString(R.string.settings_server_upload_size_content, "${limit / 1048576L} MB")) + helperText(host.stringProvider.getString(R.string.settings_server_upload_size_content, "${limit / 1048576L} MB")) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersController.kt b/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersController.kt index 8080f2e032..ce84366ef3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersController.kt @@ -46,18 +46,19 @@ class IgnoredUsersController @Inject constructor(private val stringProvider: Str } private fun buildIgnoredUserModels(users: List) { + val host = this if (users.isEmpty()) { noResultItem { id("empty") - text(stringProvider.getString(R.string.no_ignored_users)) + text(host.stringProvider.getString(R.string.no_ignored_users)) } } else { users.forEach { user -> userItem { id(user.userId) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) matrixItem(user.toMatrixItem()) - itemClickAction { callback?.onUserIdClicked(user.userId) } + itemClickAction { host.callback?.onUserIdClicked(user.userId) } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt index effb593add..55d0ed7c3f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt @@ -40,35 +40,36 @@ class LocalePickerController @Inject constructor( @ExperimentalStdlibApi override fun buildModels(data: LocalePickerViewState?) { val list = data?.locales ?: return + val host = this profileSectionItem { id("currentTitle") - title(stringProvider.getString(R.string.choose_locale_current_locale_title)) + title(host.stringProvider.getString(R.string.choose_locale_current_locale_title)) } localeItem { id(data.currentLocale.toString()) title(VectorLocale.localeToLocalisedString(data.currentLocale).safeCapitalize(data.currentLocale)) - if (vectorPreferences.developerMode()) { + if (host.vectorPreferences.developerMode()) { subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) } - clickListener { listener?.onUseCurrentClicked() } + clickListener { host.listener?.onUseCurrentClicked() } } profileSectionItem { id("otherTitle") - title(stringProvider.getString(R.string.choose_locale_other_locales_title)) + title(host.stringProvider.getString(R.string.choose_locale_other_locales_title)) } when (list) { is Incomplete -> { loadingItem { id("loading") - loadingText(stringProvider.getString(R.string.choose_locale_loading_locales)) + loadingText(host.stringProvider.getString(R.string.choose_locale_loading_locales)) } } is Success -> if (list().isEmpty()) { noResultItem { id("noResult") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { list() @@ -77,10 +78,10 @@ class LocalePickerController @Inject constructor( localeItem { id(it.toString()) title(VectorLocale.localeToLocalisedString(it).safeCapitalize(it)) - if (vectorPreferences.developerMode()) { + if (host.vectorPreferences.developerMode()) { subtitle(VectorLocale.localeToLocalisedStringInfo(it)) } - clickListener { listener?.onLocaleClicked(it) } + clickListener { host.listener?.onLocaleClicked(it) } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt index 2d111e4424..679f406832 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt @@ -27,11 +27,12 @@ class PushGateWayController @Inject constructor( ) : TypedEpoxyController() { override fun buildModels(data: PushGatewayViewState?) { + val host = this data?.pushGateways?.invoke()?.let { pushers -> if (pushers.isEmpty()) { genericFooterItem { id("footer") - text(stringProvider.getString(R.string.settings_push_gateway_no_pushers)) + text(host.stringProvider.getString(R.string.settings_push_gateway_no_pushers)) } } else { pushers.forEach { @@ -44,7 +45,7 @@ class PushGateWayController @Inject constructor( } ?: run { genericFooterItem { id("loading") - text(stringProvider.getString(R.string.loading)) + text(host.stringProvider.getString(R.string.loading)) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt index a8a1ab2e17..c0119ed3be 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt @@ -27,6 +27,7 @@ class PushRulesController @Inject constructor( ) : TypedEpoxyController() { override fun buildModels(data: PushRulesViewState?) { + val host = this data?.let { it.rules.forEach { pushRuleItem { @@ -37,7 +38,7 @@ class PushRulesController @Inject constructor( } ?: run { genericFooterItem { id("footer") - text(stringProvider.getString(R.string.settings_push_rules_no_rules)) + text(host.stringProvider.getString(R.string.settings_push_rules_no_rules)) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt index cf811f1611..cb2cb3382f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt @@ -71,6 +71,7 @@ class ThreePidsSettingsController @Inject constructor( override fun buildModels(data: ThreePidsSettingsViewState?) { if (data == null) return + val host = this if (data.uiState is ThreePidsSettingsUiState.Idle) { currentInputValue = "" @@ -80,7 +81,7 @@ class ThreePidsSettingsController @Inject constructor( is Loading -> { loadingItem { id("loading") - loadingText(stringProvider.getString(R.string.loading)) + loadingText(host.stringProvider.getString(R.string.loading)) } } is Fail -> { @@ -97,13 +98,14 @@ class ThreePidsSettingsController @Inject constructor( } private fun buildThreePids(list: List, data: ThreePidsSettingsViewState) { + val host = this val splited = list.groupBy { it is ThreePid.Email } val emails = splited[true].orEmpty() val msisdn = splited[false].orEmpty() settingsSectionTitleItem { id("email") - title(stringProvider.getString(R.string.settings_emails)) + title(host.stringProvider.getString(R.string.settings_emails)) } emails.forEach { buildThreePid("email ", it) } @@ -116,7 +118,7 @@ class ThreePidsSettingsController @Inject constructor( if (pendingList.isEmpty() && emails.isEmpty()) { noResultItem { id("noEmail") - text(stringProvider.getString(R.string.settings_emails_empty)) + text(host.stringProvider.getString(R.string.settings_emails_empty)) } } @@ -127,15 +129,15 @@ class ThreePidsSettingsController @Inject constructor( ThreePidsSettingsUiState.Idle -> genericButtonItem { id("addEmail") - text(stringProvider.getString(R.string.settings_add_email_address)) - textColor(colorProvider.getColor(R.color.riotx_accent)) - buttonClickAction(View.OnClickListener { interactionListener?.addEmail() }) + text(host.stringProvider.getString(R.string.settings_add_email_address)) + textColor(host.colorProvider.getColor(R.color.riotx_accent)) + buttonClickAction(View.OnClickListener { host.interactionListener?.addEmail() }) } is ThreePidsSettingsUiState.AddingEmail -> { settingsEditTextItem { id("addingEmail") inputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) - hint(stringProvider.getString(R.string.medium_email)) + hint(host.stringProvider.getString(R.string.medium_email)) if (data.editTextReinitiator.isTrue()) { value("") requestFocus(true) @@ -143,18 +145,18 @@ class ThreePidsSettingsController @Inject constructor( errorText(data.uiState.error) interactionListener(object : SettingsEditTextItem.Listener { override fun onValidate() { - interactionListener?.doAddEmail(currentInputValue) + host.interactionListener?.doAddEmail(host.currentInputValue) } override fun onTextChange(text: String) { - currentInputValue = text + host.currentInputValue = text } }) } settingsContinueCancelItem { id("contAddingEmail") - continueOnClick { interactionListener?.doAddEmail(currentInputValue) } - cancelOnClick { interactionListener?.cancelAdding() } + continueOnClick { host.interactionListener?.doAddEmail(host.currentInputValue) } + cancelOnClick { host.interactionListener?.cancelAdding() } } } is ThreePidsSettingsUiState.AddingPhoneNumber -> Unit @@ -162,7 +164,7 @@ class ThreePidsSettingsController @Inject constructor( settingsSectionTitleItem { id("msisdn") - title(stringProvider.getString(R.string.settings_phone_numbers)) + title(host.stringProvider.getString(R.string.settings_phone_numbers)) } msisdn.forEach { buildThreePid("msisdn ", it) } @@ -175,7 +177,7 @@ class ThreePidsSettingsController @Inject constructor( if (pendingList.isEmpty() && msisdn.isEmpty()) { noResultItem { id("noMsisdn") - text(stringProvider.getString(R.string.settings_phone_number_empty)) + text(host.stringProvider.getString(R.string.settings_phone_number_empty)) } } @@ -186,20 +188,20 @@ class ThreePidsSettingsController @Inject constructor( ThreePidsSettingsUiState.Idle -> genericButtonItem { id("addMsisdn") - text(stringProvider.getString(R.string.settings_add_phone_number)) - textColor(colorProvider.getColor(R.color.riotx_accent)) - buttonClickAction(View.OnClickListener { interactionListener?.addMsisdn() }) + text(host.stringProvider.getString(R.string.settings_add_phone_number)) + textColor(host.colorProvider.getColor(R.color.riotx_accent)) + buttonClickAction(View.OnClickListener { host.interactionListener?.addMsisdn() }) } is ThreePidsSettingsUiState.AddingEmail -> Unit is ThreePidsSettingsUiState.AddingPhoneNumber -> { settingsInfoItem { id("addingMsisdnInfo") - helperText(stringProvider.getString(R.string.login_msisdn_notice)) + helperText(host.stringProvider.getString(R.string.login_msisdn_notice)) } settingsEditTextItem { id("addingMsisdn") inputType(InputType.TYPE_CLASS_PHONE) - hint(stringProvider.getString(R.string.medium_phone_number)) + hint(host.stringProvider.getString(R.string.medium_phone_number)) if (data.editTextReinitiator.isTrue()) { value("") requestFocus(true) @@ -207,34 +209,36 @@ class ThreePidsSettingsController @Inject constructor( errorText(data.uiState.error) interactionListener(object : SettingsEditTextItem.Listener { override fun onValidate() { - interactionListener?.doAddMsisdn(currentInputValue) + host.interactionListener?.doAddMsisdn(host.currentInputValue) } override fun onTextChange(text: String) { - currentInputValue = text + host.currentInputValue = text } }) } settingsContinueCancelItem { id("contAddingMsisdn") - continueOnClick { interactionListener?.doAddMsisdn(currentInputValue) } - cancelOnClick { interactionListener?.cancelAdding() } + continueOnClick { host.interactionListener?.doAddMsisdn(host.currentInputValue) } + cancelOnClick { host.interactionListener?.cancelAdding() } } } }.exhaustive } private fun buildThreePid(idPrefix: String, threePid: ThreePid) { + val host = this threePidItem { id(idPrefix + threePid.value) // TODO Add an icon for emails // iconResId(if (threePid is ThreePid.Msisdn) R.drawable.ic_phone else null) title(threePid.getFormattedValue()) - deleteClickListener { interactionListener?.deleteThreePid(threePid) } + deleteClickListener { host.interactionListener?.deleteThreePid(threePid) } } } private fun buildPendingThreePid(data: ThreePidsSettingsViewState, idPrefix: String, threePid: ThreePid) { + val host = this threePidItem { id(idPrefix + threePid.value) // TODO Add an icon for emails @@ -246,43 +250,43 @@ class ThreePidsSettingsController @Inject constructor( is ThreePid.Email -> { settingsInformationItem { id("info" + idPrefix + threePid.value) - message(stringProvider.getString(R.string.account_email_validation_message)) - colorProvider(colorProvider) + message(host.stringProvider.getString(R.string.account_email_validation_message)) + colorProvider(host.colorProvider) } settingsContinueCancelItem { id("cont" + idPrefix + threePid.value) - continueOnClick { interactionListener?.continueThreePid(threePid) } - cancelOnClick { interactionListener?.cancelThreePid(threePid) } + continueOnClick { host.interactionListener?.continueThreePid(threePid) } + cancelOnClick { host.interactionListener?.cancelThreePid(threePid) } } } is ThreePid.Msisdn -> { settingsInformationItem { id("info" + idPrefix + threePid.value) - message(stringProvider.getString(R.string.settings_text_message_sent, threePid.getFormattedValue())) - colorProvider(colorProvider) + message(host.stringProvider.getString(R.string.settings_text_message_sent, threePid.getFormattedValue())) + colorProvider(host.colorProvider) } settingsEditTextItem { id("msisdnVerification${threePid.value}") inputType(InputType.TYPE_CLASS_NUMBER) - hint(stringProvider.getString(R.string.settings_text_message_sent_hint)) + hint(host.stringProvider.getString(R.string.settings_text_message_sent_hint)) if (data.msisdnValidationReinitiator[threePid]?.isTrue() == true) { value("") } - errorText(getCodeError(data, threePid)) + errorText(host.getCodeError(data, threePid)) interactionListener(object : SettingsEditTextItem.Listener { override fun onValidate() { - interactionListener?.submitCode(threePid, currentCodes[threePid] ?: "") + host.interactionListener?.submitCode(threePid, host.currentCodes[threePid] ?: "") } override fun onTextChange(text: String) { - currentCodes[threePid] = text + host.currentCodes[threePid] = text } }) } settingsContinueCancelItem { id("cont" + idPrefix + threePid.value) - continueOnClick { interactionListener?.submitCode(threePid, currentCodes[threePid] ?: "") } - cancelOnClick { interactionListener?.cancelThreePid(threePid) } + continueOnClick { host.interactionListener?.submitCode(threePid, host.currentCodes[threePid] ?: "") } + cancelOnClick { host.interactionListener?.cancelThreePid(threePid) } } } } diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt index 35c6de24c7..fd35bf11a4 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt @@ -37,6 +37,7 @@ class IncomingShareController @Inject constructor(private val roomSummaryItemFac var callback: Callback? = null override fun buildModels(data: IncomingShareViewState) { + val host = this if (data.sharedData == null || data.filteredRoomSummaries is Incomplete) { loadingItem { id("loading") @@ -47,7 +48,7 @@ class IncomingShareController @Inject constructor(private val roomSummaryItemFac if (roomSummaries.isNullOrEmpty()) { noResultItem { id("no_result") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { roomSummaries.forEach { roomSummary -> diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index 89fa4a982a..76f0fd7fd2 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -65,20 +65,21 @@ class SoftLogoutController @Inject constructor( } private fun buildHeader(state: SoftLogoutViewState) { + val host = this loginHeaderItem { id("header") } loginTitleItem { id("title") - text(stringProvider.getString(R.string.soft_logout_title)) + text(host.stringProvider.getString(R.string.soft_logout_title)) } loginTitleSmallItem { id("signTitle") - text(stringProvider.getString(R.string.soft_logout_signin_title)) + text(host.stringProvider.getString(R.string.soft_logout_signin_title)) } loginTextItem { id("signText1") - text(stringProvider.getString(R.string.soft_logout_signin_notice, + text(host.stringProvider.getString(R.string.soft_logout_signin_notice, state.homeServerUrl.toReducedUrl(), state.userDisplayName, state.userId)) @@ -86,12 +87,13 @@ class SoftLogoutController @Inject constructor( if (state.hasUnsavedKeys) { loginTextItem { id("signText2") - text(stringProvider.getString(R.string.soft_logout_signin_e2e_warning_notice)) + text(host.stringProvider.getString(R.string.soft_logout_signin_e2e_warning_notice)) } } } private fun buildForm(state: SoftLogoutViewState) { + val host = this when (state.asyncHomeServerLoginFlowRequest) { is Incomplete -> { loadingItem { @@ -101,8 +103,8 @@ class SoftLogoutController @Inject constructor( is Fail -> { loginErrorWithRetryItem { id("errorRetry") - text(errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error)) - listener { listener?.retry() } + text(host.errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error)) + listener { host.listener?.retry() } } } is Success -> { @@ -110,21 +112,21 @@ class SoftLogoutController @Inject constructor( LoginMode.Password -> { loginPasswordFormItem { id("passwordForm") - stringProvider(stringProvider) + stringProvider(host.stringProvider) passwordShown(state.passwordShown) submitEnabled(state.submitEnabled) - onPasswordEdited { listener?.passwordEdited(it) } - errorText((state.asyncLoginAction as? Fail)?.error?.let { errorFormatter.toHumanReadable(it) }) - passwordRevealClickListener { listener?.revealPasswordClicked() } - forgetPasswordClickListener { listener?.forgetPasswordClicked() } - submitClickListener { password -> listener?.signinSubmit(password) } + onPasswordEdited { host.listener?.passwordEdited(it) } + errorText((state.asyncLoginAction as? Fail)?.error?.let { host.errorFormatter.toHumanReadable(it) }) + passwordRevealClickListener { host.listener?.revealPasswordClicked() } + forgetPasswordClickListener { host.listener?.forgetPasswordClicked() } + submitClickListener { password -> host.listener?.signinSubmit(password) } } } is LoginMode.Sso -> { loginCenterButtonItem { id("sso") - text(stringProvider.getString(R.string.login_signin_sso)) - listener { listener?.signinFallbackSubmit() } + text(host.stringProvider.getString(R.string.login_signin_sso)) + listener { host.listener?.signinFallbackSubmit() } } } is LoginMode.SsoAndPassword -> { @@ -132,8 +134,8 @@ class SoftLogoutController @Inject constructor( LoginMode.Unsupported -> { loginCenterButtonItem { id("fallback") - text(stringProvider.getString(R.string.login_signin)) - listener { listener?.signinFallbackSubmit() } + text(host.stringProvider.getString(R.string.login_signin)) + listener { host.listener?.signinFallbackSubmit() } } } LoginMode.Unknown -> Unit // Should not happen @@ -143,18 +145,19 @@ class SoftLogoutController @Inject constructor( } private fun buildClearDataSection() { + val host = this loginTitleSmallItem { id("clearDataTitle") - text(stringProvider.getString(R.string.soft_logout_clear_data_title)) + text(host.stringProvider.getString(R.string.soft_logout_clear_data_title)) } loginTextItem { id("clearDataText") - text(stringProvider.getString(R.string.soft_logout_clear_data_notice)) + text(host.stringProvider.getString(R.string.soft_logout_clear_data_notice)) } loginRedButtonItem { id("clearDataSubmit") - text(stringProvider.getString(R.string.soft_logout_clear_data_submit)) - listener { listener?.clearData() } + text(host.stringProvider.getString(R.string.soft_logout_clear_data_submit)) + listener { host.listener?.clearData() } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index 8f48c3c2f1..51d46b5c5e 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -59,6 +59,7 @@ class SpaceSummaryController @Inject constructor( override fun buildModels() { val nonNullViewState = viewState ?: return + val host = this buildGroupModels( nonNullViewState.asyncSpaces(), nonNullViewState.selectedGroupingMethod, @@ -74,30 +75,30 @@ class SpaceSummaryController @Inject constructor( genericItemHeader { id("legacy_groups") - text(stringProvider.getString(R.string.groups_header)) - textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + text(host.stringProvider.getString(R.string.groups_header)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) } // add home for communities nonNullViewState.myMxItem.invoke()?.let { mxItem -> groupSummaryItem { - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) id("all_communities") - matrixItem(mxItem.copy(displayName = stringProvider.getString(R.string.group_all_communities))) + matrixItem(mxItem.copy(displayName = host.stringProvider.getString(R.string.group_all_communities))) selected(nonNullViewState.selectedGroupingMethod is RoomGroupingMethod.ByLegacyGroup && nonNullViewState.selectedGroupingMethod.group() == null) - listener { callback?.onGroupSelected(null) } + listener { host.callback?.onGroupSelected(null) } } } nonNullViewState.legacyGroups.forEach { groupSummary -> groupSummaryItem { - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) id(groupSummary.groupId) matrixItem(groupSummary.toMatrixItem()) selected(nonNullViewState.selectedGroupingMethod is RoomGroupingMethod.ByLegacyGroup && nonNullViewState.selectedGroupingMethod.group()?.groupId == groupSummary.groupId) - listener { callback?.onGroupSelected(groupSummary) } + listener { host.callback?.onGroupSelected(groupSummary) } } } } @@ -108,10 +109,11 @@ class SpaceSummaryController @Inject constructor( rootSpaces: List?, expandedStates: Map, homeCount: RoomAggregateNotificationCount) { + val host = this spaceBetaHeaderItem { id("beta_header") clickAction(View.OnClickListener { - callback?.sendFeedBack() + host.callback?.sendFeedBack() }) } @@ -120,13 +122,13 @@ class SpaceSummaryController @Inject constructor( summaries?.filter { it.membership == Membership.INVITE } ?.forEach { spaceSummaryItem { - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) id(it.roomId) matrixItem(it.toMatrixItem()) countState(UnreadCounterBadgeView.State(1, true)) selected(false) - description(stringProvider.getString(R.string.you_are_invited)) - listener { callback?.onSpaceInviteSelected(it) } + description(host.stringProvider.getString(R.string.you_are_invited)) + listener { host.callback?.onSpaceInviteSelected(it) } } } @@ -134,7 +136,7 @@ class SpaceSummaryController @Inject constructor( id("space_home") selected(selected is RoomGroupingMethod.BySpace && selected.space() == null) countState(UnreadCounterBadgeView.State(homeCount.totalCount, homeCount.isHighlight)) - listener { callback?.onSpaceSelected(null) } + listener { host.callback?.onSpaceSelected(null) } } rootSpaces @@ -149,15 +151,15 @@ class SpaceSummaryController @Inject constructor( val expanded = expandedStates[groupSummary.roomId] == true spaceSummaryItem { - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) id(groupSummary.roomId) hasChildren(hasChildren) expanded(expanded) matrixItem(groupSummary.toMatrixItem()) selected(isSelected) - onMore { callback?.onSpaceSettings(groupSummary) } - listener { callback?.onSpaceSelected(groupSummary) } - toggleExpand { callback?.onToggleExpand(groupSummary) } + onMore { host.callback?.onSpaceSettings(groupSummary) } + listener { host.callback?.onSpaceSelected(groupSummary) } + toggleExpand { host.callback?.onToggleExpand(groupSummary) } countState( UnreadCounterBadgeView.State( groupSummary.notificationCount, @@ -176,7 +178,7 @@ class SpaceSummaryController @Inject constructor( spaceAddItem { id("create") - listener { callback?.onAddSpaceSelected() } + listener { host.callback?.onAddSpaceSelected() } } } @@ -184,6 +186,7 @@ class SpaceSummaryController @Inject constructor( expandedStates: Map, selected: RoomGroupingMethod, info: SpaceChildInfo, currentDepth: Int, maxDepth: Int) { + val host = this if (currentDepth >= maxDepth) return val childSummary = summaries?.firstOrNull { it.roomId == info.childRoomId } ?: return // does it have children? @@ -194,15 +197,15 @@ class SpaceSummaryController @Inject constructor( val isSelected = selected is RoomGroupingMethod.BySpace && childSummary.roomId == selected.space()?.roomId subSpaceSummaryItem { - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) id(childSummary.roomId) hasChildren(!subSpaces.isNullOrEmpty()) selected(isSelected) expanded(expanded) - onMore { callback?.onSpaceSettings(childSummary) } + onMore { host.callback?.onSpaceSettings(childSummary) } matrixItem(childSummary.toMatrixItem()) - listener { callback?.onSpaceSelected(childSummary) } - toggleExpand { callback?.onToggleExpand(childSummary) } + listener { host.callback?.onSpaceSelected(childSummary) } + toggleExpand { host.callback?.onToggleExpand(childSummary) } indent(currentDepth) countState( UnreadCounterBadgeView.State( diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt index 8a87ff3473..a8b85c9887 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -36,23 +36,24 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( // var shouldForceFocusOnce = true override fun buildModels(data: CreateSpaceState?) { + val host = this genericFooterItem { id("info_help_header") style(ItemStyle.TITLE) text( if (data?.spaceType == SpaceType.Public) { - stringProvider.getString(R.string.create_spaces_room_public_header, data.name) + host.stringProvider.getString(R.string.create_spaces_room_public_header, data.name) } else { - stringProvider.getString(R.string.create_spaces_room_private_header) + host.stringProvider.getString(R.string.create_spaces_room_private_header) } ) - textColor(colorProvider.getColorFromAttribute(R.attr.riot_primary_text_color)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.riot_primary_text_color)) } genericFooterItem { id("info_help") text( - stringProvider.getString( + host.stringProvider.getString( if (data?.spaceType == SpaceType.Public) { R.string.create_spaces_room_public_header_desc } else { @@ -60,7 +61,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( } ) ) - textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)) } val firstRoomName = data?.defaultRooms?.get(0) @@ -69,11 +70,11 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( enabled(true) value(firstRoomName) singleLine(true) - hint(stringProvider.getString(R.string.create_room_name_section)) + hint(host.stringProvider.getString(R.string.create_room_name_section)) endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) showBottomSeparator(false) onTextChange { text -> - listener?.onNameChange(0, text) + host.listener?.onNameChange(0, text) } } @@ -83,11 +84,11 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( enabled(true) value(secondRoomName) singleLine(true) - hint(stringProvider.getString(R.string.create_room_name_section)) + hint(host.stringProvider.getString(R.string.create_room_name_section)) endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) showBottomSeparator(false) onTextChange { text -> - listener?.onNameChange(1, text) + host.listener?.onNameChange(1, text) } } @@ -97,11 +98,11 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( enabled(true) value(thirdRoomName) singleLine(true) - hint(stringProvider.getString(R.string.create_room_name_section)) + hint(host.stringProvider.getString(R.string.create_room_name_section)) endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) showBottomSeparator(false) onTextChange { text -> - listener?.onNameChange(2, text) + host.listener?.onNameChange(2, text) } // onBind { _, view, _ -> // if (shouldForceFocusOnce diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt index d46ae36275..6ab35d3bf6 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt @@ -37,13 +37,14 @@ class SpaceDetailEpoxyController @Inject constructor( // var shouldForceFocusOnce = true override fun buildModels(data: CreateSpaceState?) { + val host = this genericFooterItem { id("info_help") text( if (data?.spaceType == SpaceType.Public) { - stringProvider.getString(R.string.create_spaces_details_public_header) + host.stringProvider.getString(R.string.create_spaces_details_public_header) } else { - stringProvider.getString(R.string.create_spaces_details_private_header) + host.stringProvider.getString(R.string.create_spaces_details_private_header) } ) } @@ -52,17 +53,17 @@ class SpaceDetailEpoxyController @Inject constructor( id("avatar") enabled(true) imageUri(data?.avatarUri) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) matrixItem(data?.name?.let { MatrixItem.RoomItem("!", it, null).takeIf { !it.displayName.isNullOrBlank() } }) - clickListener { listener?.onAvatarChange() } - deleteListener { listener?.onAvatarDelete() } + clickListener { host.listener?.onAvatarChange() } + deleteListener { host.listener?.onAvatarDelete() } } formEditTextItem { id("name") enabled(true) value(data?.name) - hint(stringProvider.getString(R.string.create_room_name_hint)) + hint(host.stringProvider.getString(R.string.create_room_name_hint)) singleLine(true) showBottomSeparator(false) errorMessage(data?.nameInlineError) @@ -76,7 +77,7 @@ class SpaceDetailEpoxyController @Inject constructor( // } // } onTextChange { text -> - listener?.onNameChange(text) + host.listener?.onNameChange(text) } } @@ -84,11 +85,11 @@ class SpaceDetailEpoxyController @Inject constructor( id("topic") enabled(true) value(data?.topic) - hint(stringProvider.getString(R.string.create_space_topic_hint)) + hint(host.stringProvider.getString(R.string.create_space_topic_hint)) showBottomSeparator(false) textSizeSp(16) onTextChange { text -> - listener?.onTopicChange(text) + host.listener?.onTopicChange(text) } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt index 28d410529e..734c4e3261 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -55,6 +55,7 @@ class SpaceDirectoryController @Inject constructor( var listener: InteractionListener? = null override fun buildModels(data: SpaceDirectoryState?) { + val host = this val results = data?.spaceSummaryApiResult if (results is Incomplete) { @@ -70,13 +71,13 @@ class SpaceDirectoryController @Inject constructor( tintIcon(false) text( span { - span(stringProvider.getString(R.string.spaces_no_server_support_title)) { + span(host.stringProvider.getString(R.string.spaces_no_server_support_title)) { textStyle = "bold" - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary) + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_primary) } +"\n\n" - span(stringProvider.getString(R.string.spaces_no_server_support_description)) { - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + span(host.stringProvider.getString(R.string.spaces_no_server_support_description)) { + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) } } ) @@ -84,8 +85,8 @@ class SpaceDirectoryController @Inject constructor( } else { errorWithRetryItem { id("api_err") - text(errorFormatter.toHumanReadable(failure)) - listener { listener?.retry() } + text(host.errorFormatter.toHumanReadable(failure)) + listener { host.listener?.retry() } } } } else { @@ -98,7 +99,7 @@ class SpaceDirectoryController @Inject constructor( if (flattenChildInfo.isEmpty()) { genericFooterItem { id("empty_footer") - stringProvider.getString(R.string.no_result_placeholder) + host.stringProvider.getString(R.string.no_result_placeholder) } } else { flattenChildInfo.forEach { info -> @@ -108,23 +109,23 @@ class SpaceDirectoryController @Inject constructor( spaceChildInfoItem { id(info.childRoomId) matrixItem(MatrixItem.RoomItem(info.childRoomId, info.name, info.avatarUrl)) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) topic(info.topic) memberCount(info.activeMemberCount ?: 0) space(isSpace) loading(isLoading) buttonLabel( - if (isJoined) stringProvider.getString(R.string.action_open) - else stringProvider.getString(R.string.join) + if (isJoined) host.stringProvider.getString(R.string.action_open) + else host.stringProvider.getString(R.string.join) ) apply { if (isSpace) { - itemClickListener(View.OnClickListener { listener?.onSpaceChildClick(info) }) + itemClickListener(View.OnClickListener { host.listener?.onSpaceChildClick(info) }) } else { - itemClickListener(View.OnClickListener { listener?.onRoomClick(info) }) + itemClickListener(View.OnClickListener { host.listener?.onRoomClick(info) }) } } - buttonClickListener(View.OnClickListener { listener?.onButtonClick(info) }) + buttonClickListener(View.OnClickListener { host.listener?.onButtonClick(info) }) } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt index dffb09529b..22c148ac54 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt @@ -88,6 +88,7 @@ class AddRoomListController @Inject constructor( } override fun addModels(models: List>) { + val host = this val filteredModel = if (ignoreRooms == null) { models } else { @@ -100,7 +101,7 @@ class AddRoomListController @Inject constructor( add( RoomCategoryItem_().apply { id("header") - title(sectionName ?: "") + title(host.sectionName ?: "") expanded(true) } ) @@ -108,7 +109,7 @@ class AddRoomListController @Inject constructor( add( GenericPillItem_().apply { id("sub_header") - text(subHeaderText) + text(host.subHeaderText) imageRes(R.drawable.ic_info) } ) @@ -123,15 +124,16 @@ class AddRoomListController @Inject constructor( } override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { + val host = this if (item == null) return RoomSelectionPlaceHolderItem_().apply { id(currentPosition) } return RoomSelectionItem_().apply { id(item.roomId) matrixItem(item.toMatrixItem()) avatarRenderer(this@AddRoomListController.avatarRenderer) space(item.roomType == RoomType.SPACE) - selected(selectedItems[item.roomId] ?: false) + selected(host.selectedItems[item.roomId] ?: false) itemClickListener(DebouncedClickListener({ - listener?.onItemSelected(item) + host.listener?.onItemSelected(item) })) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt index a94a2d9242..5e3d6ab6d0 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt @@ -43,6 +43,7 @@ class SpaceManageRoomsController @Inject constructor( private val matchFilter = SpaceChildInfoMatchFilter() override fun buildModels(data: SpaceManageRoomViewState?) { + val host = this val roomListAsync = data?.childrenInfo if (roomListAsync is Incomplete) { loadingItem { id("loading") } @@ -51,8 +52,8 @@ class SpaceManageRoomsController @Inject constructor( if (roomListAsync is Fail) { errorWithRetryItem { id("Api Error") - text(errorFormatter.toHumanReadable(roomListAsync.error)) - listener { listener?.retry() } + text(host.errorFormatter.toHumanReadable(roomListAsync.error)) + listener { host.listener?.retry() } } return } @@ -70,12 +71,12 @@ class SpaceManageRoomsController @Inject constructor( roomManageSelectionItem { id(childInfo.childRoomId) matrixItem(childInfo.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) suggested(childInfo.suggested ?: false) space(childInfo.roomType == RoomType.SPACE) selected(data.selectedRooms.contains(childInfo.childRoomId)) itemClickListener(DebouncedClickListener({ - listener?.toggleSelection(childInfo) + host.listener?.toggleSelection(childInfo) })) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt index de51805472..614f6f92c8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt @@ -61,6 +61,7 @@ class SpaceSettingsController @Inject constructor( override fun buildModels(data: RoomSettingsViewState?) { val roomSummary = data?.roomSummary?.invoke() ?: return + val host = this formEditableSquareAvatarItem { id("avatar") @@ -68,7 +69,7 @@ class SpaceSettingsController @Inject constructor( when (val avatarAction = data.avatarAction) { RoomSettingsViewState.AvatarAction.None -> { // Use the current value - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) } @@ -77,8 +78,8 @@ class SpaceSettingsController @Inject constructor( is RoomSettingsViewState.AvatarAction.UpdateAvatar -> imageUri(avatarAction.newAvatarUri) } - clickListener { callback?.onAvatarChange() } - deleteListener { callback?.onAvatarDelete() } + clickListener { host.callback?.onAvatarChange() } + deleteListener { host.callback?.onAvatarDelete() } } buildProfileSection( @@ -89,10 +90,10 @@ class SpaceSettingsController @Inject constructor( id("name") enabled(data.actionPermissions.canChangeName) value(data.newName ?: roomSummary.displayName) - hint(stringProvider.getString(R.string.create_room_name_hint)) + hint(host.stringProvider.getString(R.string.create_room_name_hint)) showBottomSeparator(false) onTextChange { text -> - callback?.onNameChanged(text) + host.callback?.onNameChanged(text) } } @@ -100,10 +101,10 @@ class SpaceSettingsController @Inject constructor( id("topic") enabled(data.actionPermissions.canChangeTopic) value(data.newTopic ?: roomSummary.topic) - hint(stringProvider.getString(R.string.create_space_topic_hint)) + hint(host.stringProvider.getString(R.string.create_space_topic_hint)) showBottomSeparator(false) onTextChange { text -> - callback?.onTopicChanged(text) + host.callback?.onTopicChanged(text) } } @@ -122,11 +123,11 @@ class SpaceSettingsController @Inject constructor( formSwitchItem { id("isPublic") enabled(data.actionPermissions.canChangeJoinRule) - title(stringProvider.getString(R.string.make_this_space_public)) + title(host.stringProvider.getString(R.string.make_this_space_public)) switchChecked(isPublic) listener { value -> - callback?.setIsPublic(value) + host.callback?.setIsPublic(value) } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt index 71be3690a1..e5d3978a70 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt @@ -58,6 +58,7 @@ class SpacePeopleListController @Inject constructor( } override fun buildModels(data: RoomMemberListViewState?) { + val host = this val memberSummaries = data?.roomMemberSummaries?.invoke() if (memberSummaries == null) { loadingItem { id("loading") } @@ -72,7 +73,7 @@ class SpacePeopleListController @Inject constructor( if (filtered.isNotEmpty()) { dividerItem { id("divider_type_${memberEntry.first.titleRes}") - color(dividerColor) + color(host.dividerColor) } } foundCount += filtered.size @@ -82,15 +83,15 @@ class SpacePeopleListController @Inject constructor( profileMatrixItemWithPowerLevel { id(roomMember.userId) matrixItem(roomMember.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) .apply { val pl = memberEntry.first.toPowerLevelLabel() if (memberEntry.first == RoomMemberListCategories.INVITE) { powerLevelLabel( span { - span(stringProvider.getString(R.string.invited)) { - textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + span(host.stringProvider.getString(R.string.invited)) { + textColor = host.colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) textStyle = "bold" // fontFamily = "monospace" } @@ -100,10 +101,10 @@ class SpacePeopleListController @Inject constructor( powerLevelLabel( span { span(" $pl ") { - backgroundColor = colorProvider.getColor(R.color.notification_accent_color) - paddingTop = dimensionConverter.dpToPx(2) - paddingBottom = dimensionConverter.dpToPx(2) - textColor = colorProvider.getColor(R.color.white) + backgroundColor = host.colorProvider.getColor(R.color.notification_accent_color) + paddingTop = host.dimensionConverter.dpToPx(2) + paddingBottom = host.dimensionConverter.dpToPx(2) + textColor = host.colorProvider.getColor(R.color.white) textStyle = "bold" // fontFamily = "monospace" } @@ -115,14 +116,14 @@ class SpacePeopleListController @Inject constructor( } clickListener { _ -> - listener?.onSpaceMemberClicked(roomMember) + host.listener?.onSpaceMemberClicked(roomMember) } } }, between = { _, roomMemberBefore -> dividerItem { id("divider_${roomMemberBefore.userId}") - color(dividerColor) + color(host.dividerColor) } } ) @@ -135,22 +136,22 @@ class SpacePeopleListController @Inject constructor( title( span { +"\n" - +stringProvider.getString(R.string.no_result_placeholder) + +host.stringProvider.getString(R.string.no_result_placeholder) } ) description( span { - +stringProvider.getString(R.string.looking_for_someone_not_in_space, data.roomSummary.invoke()?.displayName ?: "") + +host.stringProvider.getString(R.string.looking_for_someone_not_in_space, data.roomSummary.invoke()?.displayName ?: "") +"\n" span("Invite them") { - textColor = colorProvider.getColorFromAttribute(R.attr.colorAccent) + textColor = host.colorProvider.getColorFromAttribute(R.attr.colorAccent) textStyle = "bold" } } ) itemClickAction(GenericItem.Action("invite").apply { perform = Runnable { - listener?.onInviteToSpaceSelected() + host.listener?.onInviteToSpaceSelected() } }) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt index 8f2e7379c4..e15f404cbf 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewController.kt @@ -37,11 +37,12 @@ class SpacePreviewController @Inject constructor( var interactionListener: InteractionListener? = null override fun buildModels(data: SpacePreviewState?) { + val host = this val memberCount = data?.spaceInfo?.invoke()?.memberCount ?: 0 spaceTopSummaryItem { id("info") - formattedMemberCount(stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)) + formattedMemberCount(host.stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)) topic(data?.spaceInfo?.invoke()?.topic ?: data?.topic ?: "") } @@ -49,7 +50,7 @@ class SpacePreviewController @Inject constructor( if (result.isNotEmpty()) { genericItemHeader { id("header_rooms") - text(stringProvider.getString(R.string.rooms)) + text(host.stringProvider.getString(R.string.rooms)) } buildChildren(result, 0) @@ -57,6 +58,7 @@ class SpacePreviewController @Inject constructor( } private fun buildChildren(children: List, depth: Int) { + val host = this children.forEach { child -> if (child.isSubSpace == true) { @@ -66,7 +68,7 @@ class SpacePreviewController @Inject constructor( title(child.name) depth(depth) avatarUrl(child.avatarUrl) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) } when (child.children) { is Loading -> { @@ -87,7 +89,7 @@ class SpacePreviewController @Inject constructor( topic(child.topic ?: "") avatarUrl(child.avatarUrl) memberCount(TextUtils.formatCountToShortDecimal(child.memberCount ?: 0)) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) } } // when (child) { diff --git a/vector/src/main/java/im/vector/app/features/terms/TermsController.kt b/vector/src/main/java/im/vector/app/features/terms/TermsController.kt index 0b0088e05e..e80d28bf8e 100644 --- a/vector/src/main/java/im/vector/app/features/terms/TermsController.kt +++ b/vector/src/main/java/im/vector/app/features/terms/TermsController.kt @@ -36,6 +36,7 @@ class TermsController @Inject constructor( override fun buildModels(data: ReviewTermsViewState?) { data ?: return + val host = this when (data.termsList) { is Incomplete -> { @@ -46,8 +47,8 @@ class TermsController @Inject constructor( is Fail -> { errorWithRetryItem { id("errorRetry") - text(errorFormatter.toHumanReadable(data.termsList.error)) - listener { listener?.retry() } + text(host.errorFormatter.toHumanReadable(data.termsList.error)) + listener { host.listener?.retry() } } } is Success -> buildTerms(data.termsList.invoke()) @@ -55,6 +56,7 @@ class TermsController @Inject constructor( } private fun buildTerms(termsList: List) { + val host = this settingsSectionTitleItem { id("header") titleResId(R.string.widget_integration_review_terms) @@ -63,12 +65,12 @@ class TermsController @Inject constructor( termItem { id(term.url) name(term.name) - description(description) + description(host.description) checked(term.accepted) - clickListener(View.OnClickListener { listener?.review(term) }) + clickListener(View.OnClickListener { host.listener?.review(term) }) checkChangeListener { _, isChecked -> - listener?.setChecked(term, isChecked) + host.listener?.setChecked(term, isChecked) } } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt index 90fb828663..5fe78cffdf 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt @@ -51,36 +51,37 @@ class UserListController @Inject constructor(private val session: Session, override fun buildModels() { val currentState = state ?: return + val host = this // Build generic items if (currentState.searchTerm.isBlank()) { if (currentState.showInviteActions()) { actionItem { id(R.drawable.ic_share) - title(stringProvider.getString(R.string.invite_friends)) + title(host.stringProvider.getString(R.string.invite_friends)) actionIconRes(R.drawable.ic_share) clickAction(View.OnClickListener { - callback?.onInviteFriendClick() + host.callback?.onInviteFriendClick() }) } } if (currentState.showContactBookAction) { actionItem { id(R.drawable.ic_baseline_perm_contact_calendar_24) - title(stringProvider.getString(R.string.contacts_book_title)) + title(host.stringProvider.getString(R.string.contacts_book_title)) actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24) clickAction(View.OnClickListener { - callback?.onContactBookClick() + host.callback?.onContactBookClick() }) } } if (currentState.showInviteActions()) { actionItem { id(R.drawable.ic_qr_code_add) - title(stringProvider.getString(R.string.qr_code)) + title(host.stringProvider.getString(R.string.qr_code)) actionIconRes(R.drawable.ic_qr_code_add) clickAction(View.OnClickListener { - callback?.onUseQRCode() + host.callback?.onUseQRCode() }) } } @@ -109,12 +110,13 @@ class UserListController @Inject constructor(private val session: Session, } private fun buildKnownUsers(currentState: UserListViewState, selectedUsers: List) { + val host = this currentState.knownUsers() ?.filter { it.userId != session.myUserId } ?.let { userList -> userListHeaderItem { id("known_header") - header(stringProvider.getString(R.string.direct_room_user_list_known_title)) + header(host.stringProvider.getString(R.string.direct_room_user_list_known_title)) } if (userList.isEmpty()) { @@ -127,9 +129,9 @@ class UserListController @Inject constructor(private val session: Session, id(item.userId) selected(isSelected) matrixItem(item.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) clickListener { _ -> - callback?.onItemClick(item) + host.callback?.onItemClick(item) } } } @@ -137,6 +139,7 @@ class UserListController @Inject constructor(private val session: Session, } private fun buildDirectoryUsers(directoryUsers: List, selectedUsers: List, searchTerms: String, ignoreIds: List) { + val host = this val toDisplay = directoryUsers .filter { !ignoreIds.contains(it.userId) && it.userId != session.myUserId } @@ -145,7 +148,7 @@ class UserListController @Inject constructor(private val session: Session, } userListHeaderItem { id("suggestions") - header(stringProvider.getString(R.string.direct_room_user_list_suggestions_title)) + header(host.stringProvider.getString(R.string.direct_room_user_list_suggestions_title)) } if (toDisplay.isEmpty()) { renderEmptyState() @@ -156,9 +159,9 @@ class UserListController @Inject constructor(private val session: Session, id(user.userId) selected(isSelected) matrixItem(user.toMatrixItem()) - avatarRenderer(avatarRenderer) + avatarRenderer(host.avatarRenderer) clickListener { _ -> - callback?.onItemClick(user) + host.callback?.onItemClick(user) } } } @@ -172,16 +175,18 @@ class UserListController @Inject constructor(private val session: Session, } private fun renderEmptyState() { + val host = this noResultItem { id("noResult") - text(stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder)) } } private fun renderFailure(failure: Throwable) { + val host = this errorWithRetryItem { id("error") - text(errorFormatter.toHumanReadable(failure)) + text(host.errorFormatter.toHumanReadable(failure)) } } From 9a9edd979db3af1a534089a1e75ae54e310ac122 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 11:05:52 +0200 Subject: [PATCH 104/202] Change related to Epoxy 4.6.0 upgrade - step 1.1: handle DSL restriction, other error --- .../roomprofile/members/RoomMemberListController.kt | 6 +----- .../app/features/spaces/people/SpacePeopleListController.kt | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index a256d99175..5f070818a0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -140,7 +140,7 @@ class RoomMemberListController @Inject constructor( ?.let { content -> profileMatrixItem { id("3pid_$idx") - matrixItem(content.toMatrixItem()) + matrixItem(MatrixItem.UserItem("@", displayName = content.displayName)) avatarRenderer(host.avatarRenderer) editable(data.actionsPermissions.canRevokeThreePidInvite) clickListener { _ -> @@ -157,8 +157,4 @@ class RoomMemberListController @Inject constructor( } ) } - - private fun RoomThirdPartyInviteContent.toMatrixItem(): MatrixItem { - return MatrixItem.UserItem("@", displayName = displayName) - } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt index e5d3978a70..eb237510f1 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt @@ -86,7 +86,7 @@ class SpacePeopleListController @Inject constructor( avatarRenderer(host.avatarRenderer) userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) .apply { - val pl = memberEntry.first.toPowerLevelLabel() + val pl = host.toPowerLevelLabel(memberEntry.first) if (memberEntry.first == RoomMemberListCategories.INVITE) { powerLevelLabel( span { @@ -158,8 +158,8 @@ class SpacePeopleListController @Inject constructor( } } - private fun RoomMemberListCategories.toPowerLevelLabel(): String? { - return when (this) { + private fun toPowerLevelLabel(categories: RoomMemberListCategories): String? { + return when (categories) { RoomMemberListCategories.ADMIN -> stringProvider.getString(R.string.power_level_admin) RoomMemberListCategories.MODERATOR -> stringProvider.getString(R.string.power_level_moderator) else -> null From 8114d52d7dfac6c10bae579339016239f31616d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 11:43:42 +0200 Subject: [PATCH 105/202] Change related to Epoxy 4.6.0 upgrade - step 2: small API change --- .../home/room/detail/RoomMessageTouchHelperCallback.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt index 41f386c606..25b2685ffb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -70,10 +70,10 @@ class RoomMessageTouchHelperCallback(private val context: Context, private val minShowDistance = convertToPx(20) private val triggerDelta = convertToPx(20) - override fun onSwiped(viewHolder: EpoxyViewHolder?, direction: Int) { + override fun onSwiped(viewHolder: EpoxyViewHolder, direction: Int) { } - override fun onMove(recyclerView: RecyclerView?, viewHolder: EpoxyViewHolder?, target: EpoxyViewHolder?): Boolean { + override fun onMove(recyclerView: RecyclerView, viewHolder: EpoxyViewHolder, target: EpoxyViewHolder): Boolean { return false } From 3d174b0a259bcd5b5315a22ba862652b745a559a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 11:54:19 +0200 Subject: [PATCH 106/202] Cleanup and properly inject the controller --- .../edithistory/ViewEditHistoryBottomSheet.kt | 9 ++----- .../ViewEditHistoryEpoxyController.kt | 24 ++++++++++--------- .../edithistory/ViewEditHistoryViewModel.kt | 8 +++---- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt index 095cc4754a..63dfe97d7e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt @@ -30,24 +30,19 @@ import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetGenericListWithTitleBinding import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.app.features.html.EventHtmlRenderer import javax.inject.Inject /** * Bottom sheet displaying list of edits for a given event ordered by timestamp */ -class ViewEditHistoryBottomSheet : +class ViewEditHistoryBottomSheet: VectorBaseBottomSheetDialogFragment() { private val viewModel: ViewEditHistoryViewModel by fragmentViewModel(ViewEditHistoryViewModel::class) @Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory - @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer - - private val epoxyController by lazy { - ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) - } + @Inject lateinit var epoxyController: ViewEditHistoryEpoxyController override fun injectWith(injector: ScreenComponent) { injector.inject(this) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 870cff0f4d..c3c53084ed 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -15,9 +15,7 @@ */ package im.vector.app.features.home.room.detail.timeline.edithistory -import android.content.Context import android.text.Spannable -import androidx.core.content.ContextCompat import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete @@ -25,6 +23,8 @@ import com.airbnb.mvrx.Success import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericItemHeader @@ -38,13 +38,17 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply import org.matrix.android.sdk.internal.session.room.send.TextContent import java.util.Calendar +import javax.inject.Inject /** * Epoxy controller for edit history list */ -class ViewEditHistoryEpoxyController(private val context: Context, - val dateFormatter: VectorDateFormatter, - val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController() { +class ViewEditHistoryEpoxyController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, + private val eventHtmlRenderer: EventHtmlRenderer, + private val dateFormatter: VectorDateFormatter +) : TypedEpoxyController() { override fun buildModels(state: ViewEditHistoryViewState) { val host = this @@ -57,8 +61,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, is Fail -> { genericFooterItem { id("failure") - // FIXME Should use stringprovider - text(host.context.getString(R.string.unknown_error)) + text(host.stringProvider.getString(R.string.unknown_error)) } } is Success -> { @@ -72,8 +75,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, if (sourceEvents.isEmpty()) { genericItem { id("footer") - // TODO use a stringProvider - title(host.context.getString(R.string.no_message_edits_found)) + title(host.stringProvider.getString(R.string.no_message_edits_found)) } } else { var lastDate: Calendar? = null @@ -110,14 +112,14 @@ class ViewEditHistoryEpoxyController(private val context: Context, diff_match_patch.Operation.DELETE -> { span { text = it.text.replace("\n", " ") - textColor = ContextCompat.getColor(context, R.color.vector_error_color) + textColor = colorProvider.getColor(R.color.vector_error_color) textDecorationLine = "line-through" } } diff_match_patch.Operation.INSERT -> { span { text = it.text - textColor = ContextCompat.getColor(context, R.color.vector_success_color) + textColor = colorProvider.getColor(R.color.vector_success_color) } } else -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index af814f5856..5732326f2e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -25,7 +25,6 @@ import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -37,10 +36,9 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import timber.log.Timber import java.util.UUID -class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted - initialState: ViewEditHistoryViewState, - val session: Session, - val dateFormatter: VectorDateFormatter +class ViewEditHistoryViewModel @AssistedInject constructor( + @Assisted initialState: ViewEditHistoryViewState, + private val session: Session ) : VectorViewModel(initialState) { private val roomId = initialState.roomId From 566369cccd5fcf71cc3ba2fb70d36bd9f582da15 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 12:03:47 +0200 Subject: [PATCH 107/202] Split long lines --- ...eysBackupSettingsRecyclerViewController.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index e214437232..1a33d2fdc4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -35,9 +35,11 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersion import java.util.UUID import javax.inject.Inject -class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider, - private val vectorPreferences: VectorPreferences, - private val session: Session) : TypedEpoxyController() { +class KeysBackupSettingsRecyclerViewController @Inject constructor( + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, + private val session: Session +) : TypedEpoxyController() { var listener: Listener? = null @@ -130,7 +132,8 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s if (data.keysBackupVersionTrust()?.usable == false) { description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) } else { - description(host.stringProvider.getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup)) + description(host.stringProvider + .getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup)) } } @@ -204,10 +207,12 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s endIconResourceId(R.drawable.e2e_verified) } else { if (isDeviceVerified) { - description(host.stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)) + description(host.stringProvider + .getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)) endIconResourceId(R.drawable.e2e_verified) } else { - description(host.stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)) + description(host.stringProvider + .getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)) endIconResourceId(R.drawable.e2e_warning) } } @@ -215,9 +220,11 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s // Invalid signature endIconResourceId(R.drawable.e2e_warning) if (isDeviceVerified) { - description(host.stringProvider.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)) + description(host.stringProvider + .getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)) } else { - description(host.stringProvider.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)) + description(host.stringProvider + .getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)) } } } From 25f7f29d9415f86f46663958182fa27f6e8ad11c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 14:28:24 +0200 Subject: [PATCH 108/202] Implement a workaround to render and in the timeline (#1817) --- CHANGES.md | 1 + .../vector/app/features/html/SpanUtilsTest.kt | 96 +++++++++++++++++++ .../timeline/factory/MessageItemFactory.kt | 9 +- .../detail/timeline/item/MessageTextItem.kt | 22 ++++- .../im/vector/app/features/html/SpanUtils.kt | 41 ++++++++ 5 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt create mode 100644 vector/src/main/java/im/vector/app/features/html/SpanUtils.kt diff --git a/CHANGES.md b/CHANGES.md index e0baf71e5c..0c7b3d2e44 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ Bugfix 🐛: - Space Invite by link not always displayed for public space (#3345) - Wrong copy in share space bottom sheet (#3346) - Fix a problem with database migration on nightly builds (#3335) + - Implement a workaround to render <del> and <u> in the timeline (#1817) Translations 🗣: - diff --git a/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt b/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt new file mode 100644 index 0000000000..2ede20a07d --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.html + +import android.graphics.Color +import android.os.Build +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.text.style.StrikethroughSpan +import android.text.style.UnderlineSpan +import im.vector.app.InstrumentedTest +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SpanUtilsTest : InstrumentedTest { + + private val spanUtils = SpanUtils() + + @Test + fun canUseTextFutureString() { + spanUtils.canUseTextFuture("test").shouldBeTrue() + } + + @Test + fun canUseTextFutureCharSequenceOK() { + spanUtils.canUseTextFuture(SpannableStringBuilder().append("hello")).shouldBeTrue() + } + + @Test + fun canUseTextFutureCharSequenceWithSpanOK() { + val string = SpannableString("Text with strikethrough, underline, red spans") + string.setSpan(ForegroundColorSpan(Color.RED), 36, 39, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + spanUtils.canUseTextFuture(string) shouldBeEqualTo true + } + + @Test + fun canUseTextFutureCharSequenceWithSpanKOStrikethroughSpan() { + val string = SpannableString("Text with strikethrough, underline, red spans") + string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + spanUtils.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed() + } + + @Test + fun canUseTextFutureCharSequenceWithSpanKOUnderlineSpan() { + val string = SpannableString("Text with strikethrough, underline, red spans") + string.setSpan(UnderlineSpan(), 25, 34, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + spanUtils.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed() + } + + @Test + fun canUseTextFutureCharSequenceWithSpanKOBoth() { + val string = SpannableString("Text with strikethrough, underline, red spans") + string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + string.setSpan(UnderlineSpan(), 25, 34, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + spanUtils.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed() + } + + @Test + fun canUseTextFutureCharSequenceWithSpanKOAll() { + val string = SpannableString("Text with strikethrough, underline, red spans") + string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + string.setSpan(UnderlineSpan(), 25, 34, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + string.setSpan(ForegroundColorSpan(Color.RED), 36, 39, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + spanUtils.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed() + } + + private fun trueIfAlwaysAllowed() = Build.VERSION.SDK_INT < Build.VERSION_CODES.P +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 63770e4538..7b9601ad33 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -60,6 +60,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.html.CodeVisitor import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor +import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer @@ -109,6 +110,7 @@ class MessageItemFactory @Inject constructor( private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, private val pillsPostProcessorFactory: PillsPostProcessor.Factory, + private val spanUtils: SpanUtils, private val session: Session) { // TODO inject this properly? @@ -420,6 +422,7 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { + val canUseTextFuture = spanUtils.canUseTextFuture(body) val linkifiedBody = body.linkify(callback) return MessageTextItem_().apply { @@ -431,6 +434,7 @@ class MessageItemFactory @Inject constructor( } } .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) + .canUseTextFuture(canUseTextFuture) .searchForPills(isFormatted) .previewUrlRetriever(callback?.getPreviewUrlRetriever()) .imageContentRenderer(imageContentRenderer) @@ -503,12 +507,14 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { + val htmlBody = messageContent.getHtmlBody() val formattedBody = span { - text = messageContent.getHtmlBody() + text = htmlBody textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) textStyle = "italic" } + val canUseTextFuture = spanUtils.canUseTextFuture(htmlBody) val message = formattedBody.linkify(callback) return MessageTextItem_() @@ -518,6 +524,7 @@ class MessageItemFactory @Inject constructor( .previewUrlCallback(callback) .attributes(attributes) .message(message) + .canUseTextFuture(canUseTextFuture) .highlighted(highlight) .movementMethod(createLinkMovementMethod(callback)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index 659a3d5460..86b83cbe47 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -40,6 +40,9 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var message: CharSequence? = null + @EpoxyAttribute + var canUseTextFuture: Boolean = true + @EpoxyAttribute var useBigFont: Boolean = false @@ -80,17 +83,26 @@ abstract class MessageTextItem : AbsMessageItem() { it.bind(holder.messageView) } } - val textFuture = PrecomputedTextCompat.getTextFuture( - message ?: "", - TextViewCompat.getTextMetricsParams(holder.messageView), - null) + val textFuture = if (canUseTextFuture) { + PrecomputedTextCompat.getTextFuture( + message ?: "", + TextViewCompat.getTextMetricsParams(holder.messageView), + null) + } else { + null + } super.bind(holder) holder.messageView.movementMethod = movementMethod renderSendState(holder.messageView, holder.messageView) holder.messageView.setOnClickListener(attributes.itemClickListener) holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) - holder.messageView.setTextFuture(textFuture) + + if (canUseTextFuture) { + holder.messageView.setTextFuture(textFuture) + } else { + holder.messageView.text = message + } } override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/html/SpanUtils.kt b/vector/src/main/java/im/vector/app/features/html/SpanUtils.kt new file mode 100644 index 0000000000..4e2c1c1a50 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/SpanUtils.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.html + +import android.os.Build +import android.text.Spanned +import android.text.style.StrikethroughSpan +import android.text.style.UnderlineSpan +import javax.inject.Inject + +class SpanUtils @Inject constructor() { + // Workaround for https://issuetracker.google.com/issues/188454876 + fun canUseTextFuture(charSequence: CharSequence): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + // On old devices, it works correctly + return true + } + + if (charSequence !is Spanned) { + return true + } + + return charSequence + .getSpans(0, charSequence.length, Any::class.java) + .all { it !is StrikethroughSpan && it !is UnderlineSpan } + } +} From ff1d8c310e30a184dc0916ded163184ea44490eb Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Thu, 25 Mar 2021 12:26:26 +0100 Subject: [PATCH 109/202] ci: add initial github actions pipelines --- .github/workflows/build.yml | 59 +++++++++++++++++++++++++++++++++++ .github/workflows/quality.yml | 46 +++++++++++++++++++++++++++ .github/workflows/tests.yml | 39 +++++++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/quality.yml create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..6df5bb7c70 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: Build + +on: + pull_request: {} + push: + branches: [master, develop] + +jobs: + debug: + name: Build debug APK (${{ matrix.target }}) + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/master' + strategy: + matrix: + target: [ Gplay, Fdroid ] + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Assemble ${{ matrix.target }} debug apk + run: ./gradlew clean lint${{ matrix.target }}Release assemble${{ matrix.target }}Debug --stacktrace + - name: Upload APKs + uses: actions/upload-artifact@v2 + with: + name: release-debug-${{ matrix.target }} + path: | + vector/build/outputs/apk/*/debug/*.apk + vector/build/reports/*.* + + gplay: + name: Build unsigned GPlay APK + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Assemble GPlay unsigned apk + run: ./gradlew clean assembleGplayRelease --stacktrace + - name: Upload APKs + uses: actions/upload-artifact@v2 + with: + name: release-unsigned-GPlay + path: | + vector/build/outputs/apk/*/debug/*.apk + +# TODO: add exodus checks diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000000..fe41dfde74 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,46 @@ +name: Code Quality Checks + +on: + pull_request: {} + push: + branches: [master, develop] + +jobs: + check: + name: Project Check Suite + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run code quality check suite + run: ./tools/check/check_code_quality.sh + + klint: + name: Kotlin Linter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run klint + run: | + curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.36.0/ktlint && chmod a+x ktlint + ./ktlint --android --experimental -v + + android-lint: + name: Android Linter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Lint analysis of the SDK + run: ./gradlew clean :matrix-sdk-android:lintRelease --stacktrace + - name: Upload reports + uses: actions/upload-artifact@v2 + with: + name: report-lint-andorid-sdk + path: matrix-sdk-android/build/reports/*.* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..d95b6bbc21 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,39 @@ +name: Test + +on: + pull_request: {} + push: + branches: [master, develop] + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Run unit tests + run: ./gradlew clean test --stacktrace -PallWarningsAsErrors=false + + android-tests: + name: Android Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Run android tests + run: ./gradlew clean assembleAndroidTest --stacktrace -PallWarningsAsErrors=false From 467d3e764ead68494d68a296af7c8b352448d915 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Thu, 25 Mar 2021 13:33:46 +0100 Subject: [PATCH 110/202] ci: do not fail fast on debug apk builds --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6df5bb7c70..3f9397165a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,7 @@ jobs: runs-on: ubuntu-latest if: github.ref != 'refs/heads/master' strategy: + fail-fast: false matrix: target: [ Gplay, Fdroid ] steps: From 55912ef49d2d2390bad7ca6156d0b841167b37e5 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Thu, 25 Mar 2021 13:53:35 +0100 Subject: [PATCH 111/202] ci: cache debug apk linting reports on failure --- .github/workflows/build.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f9397165a..0e5e637961 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,15 +24,23 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- + - name: Lint ${{ matrix.target }} release + run: ./gradlew clean lint${{ matrix.target }}Release --stacktrace + - name: Upload ${{ matrix.target }} linting report + uses: actions/upload-artifact@v2 + if: always() + with: + name: release-debug-linting-report-${{ matrix.target }} + path: | + vector/build/reports/*.* - name: Assemble ${{ matrix.target }} debug apk - run: ./gradlew clean lint${{ matrix.target }}Release assemble${{ matrix.target }}Debug --stacktrace - - name: Upload APKs + run: ./gradlew assemble${{ matrix.target }}Debug --stacktrace + - name: Upload ${{ matrix.target }} debug APKs uses: actions/upload-artifact@v2 with: - name: release-debug-${{ matrix.target }} + name: release-apk-debug-${{ matrix.target }} path: | vector/build/outputs/apk/*/debug/*.apk - vector/build/reports/*.* gplay: name: Build unsigned GPlay APK @@ -50,10 +58,10 @@ jobs: ${{ runner.os }}-gradle- - name: Assemble GPlay unsigned apk run: ./gradlew clean assembleGplayRelease --stacktrace - - name: Upload APKs + - name: Upload Gplay unsigned APKs uses: actions/upload-artifact@v2 with: - name: release-unsigned-GPlay + name: release-apk-unsigned-GPlay path: | vector/build/outputs/apk/*/debug/*.apk From 209a9b09c77dd0301170510fd1abdd2ef9e02975 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Thu, 25 Mar 2021 13:55:01 +0100 Subject: [PATCH 112/202] ci: fix typo in naming --- .github/workflows/quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index fe41dfde74..9cbb1f4d2e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -42,5 +42,5 @@ jobs: - name: Upload reports uses: actions/upload-artifact@v2 with: - name: report-lint-andorid-sdk + name: linting-report-android-sdk path: matrix-sdk-android/build/reports/*.* From f0adf29d121d76a0331b7a9c7287de73de7489f5 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 26 Mar 2021 21:19:48 +0100 Subject: [PATCH 113/202] ci: split out apk linting --- .github/workflows/build.yml | 13 ++----------- .github/workflows/quality.yml | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e5e637961..807cc5fa75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,9 @@ name: Build on: - pull_request: {} + pull_request: { } push: - branches: [master, develop] + branches: [ master, develop ] jobs: debug: @@ -24,15 +24,6 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - - name: Lint ${{ matrix.target }} release - run: ./gradlew clean lint${{ matrix.target }}Release --stacktrace - - name: Upload ${{ matrix.target }} linting report - uses: actions/upload-artifact@v2 - if: always() - with: - name: release-debug-linting-report-${{ matrix.target }} - path: | - vector/build/reports/*.* - name: Assemble ${{ matrix.target }} debug apk run: ./gradlew assemble${{ matrix.target }}Debug --stacktrace - name: Upload ${{ matrix.target }} debug APKs diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 9cbb1f4d2e..92e7a3e647 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1,9 +1,9 @@ name: Code Quality Checks on: - pull_request: {} + pull_request: { } push: - branches: [master, develop] + branches: [ master, develop ] jobs: check: @@ -44,3 +44,31 @@ jobs: with: name: linting-report-android-sdk path: matrix-sdk-android/build/reports/*.* + + apk-lint: + name: Lint APK (${{ matrix.target }}) + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/master' + strategy: + fail-fast: false + matrix: + target: [ Gplay, Fdroid ] + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Lint ${{ matrix.target }} release + run: ./gradlew clean lint${{ matrix.target }}Release --stacktrace + - name: Upload ${{ matrix.target }} linting report + uses: actions/upload-artifact@v2 + if: always() + with: + name: release-debug-linting-report-${{ matrix.target }} + path: | + vector/build/reports/*.* From bebd84d1f5470cb267970dc7ec6f2f66b2bce438 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 26 Mar 2021 21:20:06 +0100 Subject: [PATCH 114/202] ci: add integration tests --- .github/workflows/integration.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/integration.yml diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000000..2fd1c3732d --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,31 @@ +name: Integeration Test + +on: + pull_request: { } + push: + branches: [ master, develop ] + +jobs: + integration-tests: + name: Synapse Integration Tests + runs-on: ubuntu-latest + services: + synapse: + image: docker.io/matrixdotorg/synapse:latest + options: "--entrypoint tail" + ports: [ 8080, 8480, 8081, 8481, 8082, 8482 ] + steps: + - uses: actions/checkout@v2 + - name: Start synapse server + run: | + docker exec ${{ job.services.synapse.id }} bash -c 'curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | sed s/127.0.0.1/0.0.0.0/g | bash' + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Run unit tests + run: ./gradlew vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest From c39a8caaed287f4e6675609d3d8f50a8f9bef552 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 26 Mar 2021 21:23:43 +0100 Subject: [PATCH 115/202] ci: disable integration health checks --- .github/workflows/integration.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 2fd1c3732d..8990b0b2ef 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -12,7 +12,9 @@ jobs: services: synapse: image: docker.io/matrixdotorg/synapse:latest - options: "--entrypoint tail" + options: >- + --entrypoint=tail + --no-healthcheck ports: [ 8080, 8480, 8081, 8481, 8082, 8482 ] steps: - uses: actions/checkout@v2 From 62ca8d87dcc55d2c743f2180ed89ac636ca81ab3 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 26 Mar 2021 21:37:08 +0100 Subject: [PATCH 116/202] ci: setup synapse server manually --- .github/workflows/integration.yml | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 8990b0b2ef..7975fec451 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -9,18 +9,20 @@ jobs: integration-tests: name: Synapse Integration Tests runs-on: ubuntu-latest - services: - synapse: - image: docker.io/matrixdotorg/synapse:latest - options: >- - --entrypoint=tail - --no-healthcheck - ports: [ 8080, 8480, 8081, 8481, 8082, 8482 ] steps: - uses: actions/checkout@v2 - - name: Start synapse server - run: | - docker exec ${{ job.services.synapse.id }} bash -c 'curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | sed s/127.0.0.1/0.0.0.0/g | bash' + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- - uses: actions/cache@v2 with: path: | @@ -29,5 +31,12 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- + - name: Start synapse server + run: | + python3 -m venv .synapse + source .synapse/bin/activate + pip install synapse matrix-synapse + curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \ + | sed s/127.0.0.1/0.0.0.0/g | bash - name: Run unit tests run: ./gradlew vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest From 1fac4dfe3e4b567101a92ccae697b60261599196 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 26 Mar 2021 23:52:06 +0100 Subject: [PATCH 117/202] ci: clean up naming --- .github/workflows/build.yml | 2 +- .github/workflows/integration.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 807cc5fa75..7bcd3167fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build +name: APK Build on: pull_request: { } diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 7975fec451..67674886ac 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -7,7 +7,7 @@ on: jobs: integration-tests: - name: Synapse Integration Tests + name: Integration Tests (Synapse) runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -38,5 +38,5 @@ jobs: pip install synapse matrix-synapse curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \ | sed s/127.0.0.1/0.0.0.0/g | bash - - name: Run unit tests + - name: Run integration tests run: ./gradlew vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest From d926890c7957368d58a1db6d4ac89fffb10a710e Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Tue, 30 Mar 2021 13:52:19 +0200 Subject: [PATCH 118/202] ci: ignore warnings for integration tests --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 67674886ac..183a4142ad 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -39,4 +39,4 @@ jobs: curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \ | sed s/127.0.0.1/0.0.0.0/g | bash - name: Run integration tests - run: ./gradlew vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest + run: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest From 112a160725c0f0f86802bda4d83829ae0f048051 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Tue, 30 Mar 2021 15:11:55 +0200 Subject: [PATCH 119/202] ci: enable android emulator --- .github/workflows/integration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 183a4142ad..4cdcbc10f6 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -39,4 +39,5 @@ jobs: curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \ | sed s/127.0.0.1/0.0.0.0/g | bash - name: Run integration tests + uses: reactivecircus/android-emulator-runner@v2 run: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest From 33b210084262ac172ebaaf737e4e73faf5ee40d5 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Tue, 30 Mar 2021 15:14:26 +0200 Subject: [PATCH 120/202] ci: fix integration test config syntax --- .github/workflows/integration.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 4cdcbc10f6..8521970eb8 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -40,4 +40,5 @@ jobs: | sed s/127.0.0.1/0.0.0.0/g | bash - name: Run integration tests uses: reactivecircus/android-emulator-runner@v2 - run: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest + with: + script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest From 7db5c66f2ce8a2f506e287cd8412d9b02f1d3912 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Tue, 30 Mar 2021 15:16:56 +0200 Subject: [PATCH 121/202] ci: fix integration test config api-level --- .github/workflows/integration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 8521970eb8..5ebe119086 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -41,4 +41,5 @@ jobs: - name: Run integration tests uses: reactivecircus/android-emulator-runner@v2 with: + api-level: 29 script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest From 619e8cca3706523c398baa26c1f6c668d997cdce Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 May 2021 18:24:00 +0200 Subject: [PATCH 122/202] typo --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 5ebe119086..022da8bb94 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,4 +1,4 @@ -name: Integeration Test +name: Integration Test on: pull_request: { } From b5f7abda48c6c0d64ce468d2b662f435b3938e9f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 May 2021 18:25:34 +0200 Subject: [PATCH 123/202] master -> main --- .github/workflows/build.yml | 6 +++--- .github/workflows/integration.yml | 2 +- .github/workflows/quality.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7bcd3167fe..b6a16c085c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,13 +3,13 @@ name: APK Build on: pull_request: { } push: - branches: [ master, develop ] + branches: [ main, develop ] jobs: debug: name: Build debug APK (${{ matrix.target }}) runs-on: ubuntu-latest - if: github.ref != 'refs/heads/master' + if: github.ref != 'refs/heads/main' strategy: fail-fast: false matrix: @@ -36,7 +36,7 @@ jobs: gplay: name: Build unsigned GPlay APK runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v2 - uses: actions/cache@v2 diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 022da8bb94..94cd9912ed 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -3,7 +3,7 @@ name: Integration Test on: pull_request: { } push: - branches: [ master, develop ] + branches: [ main, develop ] jobs: integration-tests: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 92e7a3e647..a65e6b5dee 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -3,7 +3,7 @@ name: Code Quality Checks on: pull_request: { } push: - branches: [ master, develop ] + branches: [ main, develop ] jobs: check: @@ -48,7 +48,7 @@ jobs: apk-lint: name: Lint APK (${{ matrix.target }}) runs-on: ubuntu-latest - if: github.ref != 'refs/heads/master' + if: github.ref != 'refs/heads/main' strategy: fail-fast: false matrix: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d95b6bbc21..ee63afa33c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: Test on: pull_request: {} push: - branches: [master, develop] + branches: [main, develop] jobs: unit-tests: From fa6aaca67a71985d45d44319778cd8a75973b2c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 17:38:32 +0200 Subject: [PATCH 124/202] Fix some typo --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6a16c085c..85148a2632 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: jobs: debug: - name: Build debug APK (${{ matrix.target }}) + name: Build debug APKs (${{ matrix.target }}) runs-on: ubuntu-latest if: github.ref != 'refs/heads/main' strategy: @@ -29,12 +29,12 @@ jobs: - name: Upload ${{ matrix.target }} debug APKs uses: actions/upload-artifact@v2 with: - name: release-apk-debug-${{ matrix.target }} + name: vector-${{ matrix.target }}-debug path: | vector/build/outputs/apk/*/debug/*.apk - gplay: - name: Build unsigned GPlay APK + release: + name: Build unsigned GPlay APKs runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: @@ -52,8 +52,8 @@ jobs: - name: Upload Gplay unsigned APKs uses: actions/upload-artifact@v2 with: - name: release-apk-unsigned-GPlay + name: vector-gplay-release-unsigned path: | - vector/build/outputs/apk/*/debug/*.apk + vector/build/outputs/apk/*/release/*.apk # TODO: add exodus checks From 974c7ef8d98072004ad5f68f43056a6e8c3f25cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 17:45:47 +0200 Subject: [PATCH 125/202] Run test on API 21 and API 30, and disable rate limiting on Synapse --- .github/workflows/integration.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 94cd9912ed..c277739555 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -9,6 +9,9 @@ jobs: integration-tests: name: Integration Tests (Synapse) runs-on: ubuntu-latest + strategy: + matrix: + api-level: [21, 30] steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 @@ -36,10 +39,10 @@ jobs: python3 -m venv .synapse source .synapse/bin/activate pip install synapse matrix-synapse - curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \ + curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \ | sed s/127.0.0.1/0.0.0.0/g | bash - - name: Run integration tests + - name: Run integration tests on API ${{ matrix.api-level }} uses: reactivecircus/android-emulator-runner@v2 with: - api-level: 29 + api-level: ${{ matrix.api-level }} script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest From c437ed394df158c14a005998d2e50f81569bd278 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 17:48:12 +0200 Subject: [PATCH 126/202] No need to build Android test, it's done by integration workflow --- .github/workflows/tests.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee63afa33c..6e51368ce5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ on: jobs: unit-tests: - name: Unit Tests + name: Run Unit Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -21,19 +21,3 @@ jobs: ${{ runner.os }}-gradle- - name: Run unit tests run: ./gradlew clean test --stacktrace -PallWarningsAsErrors=false - - android-tests: - name: Android Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Run android tests - run: ./gradlew clean assembleAndroidTest --stacktrace -PallWarningsAsErrors=false From 9e795894be068f12374e3e7f746ac8d732a120a8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 May 2021 18:16:05 +0200 Subject: [PATCH 127/202] Change command to run connected tests --- .github/workflows/integration.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index c277739555..cb6f1b0e48 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -45,4 +45,5 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest + # script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest + script: ./gradlew -PallWarningsAsErrors=false connectedCheck From 82c50b7c1dc04ef419196d8bcfd485650c663cba Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 19 May 2021 18:19:49 +0200 Subject: [PATCH 128/202] Jitsi auth: introduce JitsiService and JWT token creation --- vector/build.gradle | 16 ++- .../java/im/vector/app/core/network/OkHttp.kt | 47 +++++++ .../java/im/vector/app/core/utils/Base32.kt | 28 ++++ .../features/call/conference/JitsiService.kt | 120 ++++++++++++++++++ .../call/conference/JitsiWellKnown.kt | 25 ++++ .../call/conference/jwt/JitsiJWTFactory.kt | 58 +++++++++ 6 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/network/OkHttp.kt create mode 100644 vector/src/main/java/im/vector/app/core/utils/Base32.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/conference/JitsiWellKnown.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt diff --git a/vector/build.gradle b/vector/build.gradle index a9a8ba0924..21c4e38de9 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -303,6 +303,7 @@ dependencies { def arch_version = '2.1.0' def lifecycle_version = '2.2.0' def rxbinding_version = '3.1.0' + def jjwt_version = '0.11.2' // Tests def kluent_version = '1.65' @@ -444,9 +445,9 @@ dependencies { // Jitsi implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') { - exclude group: 'com.google.firebase' - exclude group: 'com.google.android.gms' - exclude group: 'com.android.installreferrer' + exclude group: 'com.google.firebase' + exclude group: 'com.google.android.gms' + exclude group: 'com.android.installreferrer' } // QR-code @@ -460,6 +461,15 @@ dependencies { implementation 'im.dlg:android-dialer:1.2.5' + // JWT + api "io.jsonwebtoken:jjwt-api:$jjwt_version" + runtimeOnly "io.jsonwebtoken:jjwt-impl:$jjwt_version" + runtimeOnly("io.jsonwebtoken:jjwt-orgjson:$jjwt_version") { + exclude group: 'org.json', module: 'json' //provided by Android natively + } + implementation 'commons-codec:commons-codec:1.15' + + // TESTS testImplementation 'junit:junit:4.13.2' testImplementation "org.amshove.kluent:kluent-android:$kluent_version" diff --git a/vector/src/main/java/im/vector/app/core/network/OkHttp.kt b/vector/src/main/java/im/vector/app/core/network/OkHttp.kt new file mode 100644 index 0000000000..338ebab0b4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/network/OkHttp.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.network + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +suspend fun Call.await(): Response { + return suspendCancellableCoroutine { continuation -> + enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + continuation.resume(response) + } + + override fun onFailure(call: Call, e: IOException) { + if (continuation.isCancelled) return + continuation.resumeWithException(e) + } + }) + continuation.invokeOnCancellation { + try { + cancel() + } catch (ex: Throwable) { + //Ignore cancel exception + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/utils/Base32.kt b/vector/src/main/java/im/vector/app/core/utils/Base32.kt new file mode 100644 index 0000000000..4a42a252a1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/Base32.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.core.utils + +import org.apache.commons.codec.binary.Base32 + +fun String.toBase32String(padding: Boolean = true): String { + val base32 = Base32().encodeAsString(toByteArray()) + return if (padding) { + base32 + } else { + base32.replace("=", "") + } +} + diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt new file mode 100644 index 0000000000..c3632d282a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import im.vector.app.R +import im.vector.app.core.network.await +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.toBase32String +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.settings.VectorLocale +import okhttp3.Request +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.matrix.android.sdk.api.util.appendParamToUrl +import org.matrix.android.sdk.internal.di.MoshiProvider +import java.util.UUID +import javax.inject.Inject + +class JitsiService @Inject constructor( + private val session: Session, + private val rawService: RawService, + private val stringProvider: StringProvider) { + + companion object { + const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt" + } + + suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget { + // Build data for a jitsi widget + val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis() + val preferredJitsiDomain = tryOrNull { + rawService.getElementWellknown(session.myUserId) + ?.jitsiServer + ?.preferredDomain + } + val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) + val jitsiAuth = getJitsiAuth(jitsiDomain) + val confId = createConferenceId(roomId, jitsiAuth) + + // We use the default element wrapper for this widget + // https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md + // https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/WidgetUtils.ts#L469 + val url = buildString { + append("https://app.element.io/jitsi.html") + appendParamToUrl("confId", confId) + append("#conferenceDomain=\$domain") + append("&conferenceId=\$conferenceId") + append("&isAudioOnly=\$isAudioOnly") + append("&displayName=\$matrix_display_name") + append("&avatarUrl=\$matrix_avatar_url") + append("&userId=\$matrix_user_id") + append("&roomId=\$matrix_room_id") + append("&theme=\$theme") + if (jitsiAuth != null) { + append("&auth=$jitsiAuth") + } + } + val widgetEventContent = mapOf( + "url" to url, + "type" to WidgetType.Jitsi.legacy, + "data" to mapOf( + "conferenceId" to confId, + "domain" to jitsiDomain, + "isAudioOnly" to !withVideo, + "authenticationType" to jitsiAuth + ), + "creatorUserId" to session.myUserId, + "id" to widgetId, + "name" to "jitsi" + ) + + return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) + } + + private fun createConferenceId(roomId: String, jitsiAuth: String?): String { + return if (jitsiAuth == JITSI_OPEN_ID_TOKEN_JWT_AUTH) { + // Create conference ID from room ID + // For compatibility with Jitsi, use base32 without padding. + // More details here: + // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification + roomId.toBase32String(padding = false) + } else { + // Create a random conference ID + // Create a random enough jitsi conference id + // Note: the jitsi server automatically creates conference when the conference + // id does not exist yet + var widgetSessionId = UUID.randomUUID().toString() + if (widgetSessionId.length > 8) { + widgetSessionId = widgetSessionId.substring(0, 7) + } + roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale) + } + } + + private suspend fun getJitsiAuth(jitsiDomain: String): String? { + val request = Request.Builder().url("https://$jitsiDomain/.well-known/element/jitsi").build() + return tryOrNull { + val response = session.getOkHttpClient().newCall(request).await() + val json = response.body?.string() ?: return null + MoshiProvider.providesMoshi().adapter(JitsiWellKnown::class.java).fromJson(json)?.auth + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWellKnown.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWellKnown.kt new file mode 100644 index 0000000000..b18831f050 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWellKnown.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class JitsiWellKnown( + @Json(name = "auth") val auth: String +) diff --git a/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt new file mode 100644 index 0000000000..7e9458841a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference.jwt + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.Keys +import javax.inject.Inject + +class JitsiJWTFactory @Inject constructor() { + + /** + * Create a JWT token for jitsi openidtoken-jwt authentication + * See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification + */ + fun create(jitsiServerDomain: String, + openIdAccessToken: String, + roomId: String, + userAvatarUrl: String, + userDisplayName: String): String { + + // The secret key here is irrelevant, we're only using the JWT to transport data to Prosody in the Jitsi stack. + val key = Keys.secretKeyFor(SignatureAlgorithm.HS256) + val context = mapOf( + "user" to mapOf( + "name" to userDisplayName, + "avatar" to userAvatarUrl + ), + "matrix" to mapOf( + "token" to openIdAccessToken, + "room_id" to roomId + ) + ) + return Jwts.builder() + .setIssuer(jitsiServerDomain) + .setSubject(jitsiServerDomain) + .setAudience("https://$jitsiServerDomain") + // room is not used at the moment, a * works here. + .claim("room", "*") + .claim("context", context) + .signWith(key) + .compact() + } +} From ca2f671286c43f212ee1df90a3136bc1cc9c1218 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 19 May 2021 19:34:06 +0200 Subject: [PATCH 129/202] Jitsi auth: introduce openid token --- .../room/model/thirdparty/OpenIdToken.kt | 45 +++++++++++++++++++ .../session/thirdparty/ThirdPartyService.kt | 8 ++++ .../thirdparty/DefaultThirdPartyService.kt | 8 +++- .../session/thirdparty/GetOpenIdTokenTask.kt | 39 ++++++++++++++++ .../session/thirdparty/ThirdPartyAPI.kt | 13 ++++++ .../session/thirdparty/ThirdPartyModule.kt | 3 ++ 6 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/OpenIdToken.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetOpenIdTokenTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/OpenIdToken.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/OpenIdToken.kt new file mode 100644 index 0000000000..67b39b57c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/OpenIdToken.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class holds the response for openId request_token API + * See https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token + */ +@JsonClass(generateAdapter = true) +data class OpenIdToken( + /** + * Required. An access token the consumer may use to verify the identity of the person who generated the token. + * This is given to the federation API GET /openid/userinfo to verify the user's identity. + */ + @Json(name = "access_token") val accessToken: String, + /** + * Required. The string Bearer. + */ + @Json(name = "token_type") val tokenType: String, + /** + * Required. The homeserver domain the consumer should use when attempting to verify the user's identity. + */ + @Json(name = "matrix_server_name") val matrix_server_name: String, + /** + * Required. The number of seconds before this token expires and a new one must be generated. + */ + @Json(name = "expires_in") val expires_in: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt index 2ae4562b0b..708ff39c3a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api.session.thirdparty +import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser @@ -36,4 +37,11 @@ interface ThirdPartyService { * @param fields One or more custom fields that are passed to the AS to help identify the user. */ suspend fun getThirdPartyUser(protocol: String, fields: Map = emptyMap()): List + + /** + * Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix. + * The generated token is only valid for exchanging for user information from the federation API for OpenID. + */ + suspend fun getOpenIdToken(): OpenIdToken + } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt index 13829c400a..8634a20bba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt @@ -16,13 +16,15 @@ package org.matrix.android.sdk.internal.session.thirdparty +import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser import javax.inject.Inject internal class DefaultThirdPartyService @Inject constructor(private val getThirdPartyProtocolTask: GetThirdPartyProtocolsTask, - private val getThirdPartyUserTask: GetThirdPartyUserTask) + private val getThirdPartyUserTask: GetThirdPartyUserTask, + private val getOpenIdTokenTask: GetOpenIdTokenTask) : ThirdPartyService { override suspend fun getThirdPartyProtocols(): Map { @@ -36,4 +38,8 @@ internal class DefaultThirdPartyService @Inject constructor(private val getThird ) return getThirdPartyUserTask.execute(taskParams) } + + override suspend fun getOpenIdToken(): OpenIdToken { + return getOpenIdTokenTask.execute(Unit) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetOpenIdTokenTask.kt new file mode 100644 index 0000000000..e9d82d2a4c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetOpenIdTokenTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetOpenIdTokenTask : Task + +internal class DefaultGetOpenIdTokenTask @Inject constructor( + private val thirdPartyAPI: ThirdPartyAPI, + private val globalErrorReceiver: GlobalErrorReceiver, + @UserId private val userId: String +) : GetOpenIdTokenTask { + + override suspend fun execute(params: Unit): OpenIdToken { + return executeRequest(globalErrorReceiver) { + thirdPartyAPI.requestOpenIdToken(userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt index 2e03bc7a86..c4f17835ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.thirdparty +import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser import org.matrix.android.sdk.internal.network.NetworkConstants @@ -41,4 +42,16 @@ internal interface ThirdPartyAPI { @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}") suspend fun getThirdPartyUser(@Path("protocol") protocol: String, @QueryMap params: Map?): List + + /** + * Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix. + * The generated token is only valid for exchanging for user information from the federation API for OpenID. + * The access token generated is only valid for the OpenID API. It cannot be used to request another OpenID access token or call /sync, for example. + * + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") + suspend fun requestOpenIdToken(@Path("userId") userId: String): OpenIdToken + + } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt index d3acd7a9f3..62bcca4850 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt @@ -44,4 +44,7 @@ internal abstract class ThirdPartyModule { @Binds abstract fun bindGetThirdPartyUserTask(task: DefaultGetThirdPartyUserTask): GetThirdPartyUserTask + + @Binds + abstract fun bindGetOpenIdTokenTask(task: DefaultGetOpenIdTokenTask): GetOpenIdTokenTask } From 07b84bbff182f701f5c355e939d2c942d6fe284d Mon Sep 17 00:00:00 2001 From: Ville Ranki Date: Tue, 18 May 2021 15:08:13 +0000 Subject: [PATCH 130/202] Translated using Weblate (Finnish) Currently translated at 66.6% (12 of 18 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fi/ --- fastlane/metadata/android/fi/short_description.txt | 2 +- fastlane/metadata/android/fi/title.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/metadata/android/fi/short_description.txt b/fastlane/metadata/android/fi/short_description.txt index 64f35a7dff..0f5314071d 100644 --- a/fastlane/metadata/android/fi/short_description.txt +++ b/fastlane/metadata/android/fi/short_description.txt @@ -1 +1 @@ -Turvallista, hajautettua keskustelua ja VoIP-puheluita. Pidä tietosi turvassa. +Ryhmäviestin - salattua viestintää, ryhmäkeskusteluja ja videopuheluita diff --git a/fastlane/metadata/android/fi/title.txt b/fastlane/metadata/android/fi/title.txt index 8cda14e3c8..080d4020d0 100644 --- a/fastlane/metadata/android/fi/title.txt +++ b/fastlane/metadata/android/fi/title.txt @@ -1 +1 @@ -Element (aiemmin Riot.im) +Element - Turvallinen viestin From e80ca634ca15596ece3760a74869528990f1f544 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Mon, 17 May 2021 20:39:04 +0000 Subject: [PATCH 131/202] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (18 of 18 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/pt_BR/ --- .../android/pt-BR/changelogs/40100170.txt | 4 +- .../android/pt-BR/changelogs/40101000.txt | 4 +- .../android/pt-BR/changelogs/40101010.txt | 4 +- .../android/pt-BR/changelogs/40101020.txt | 2 + .../android/pt-BR/changelogs/40101030.txt | 2 + .../android/pt-BR/changelogs/40101040.txt | 2 + .../android/pt-BR/changelogs/40101050.txt | 2 + .../android/pt-BR/changelogs/40101060.txt | 2 + .../android/pt-BR/full_description.txt | 47 +++++++++++-------- .../android/pt-BR/short_description.txt | 2 +- fastlane/metadata/android/pt-BR/title.txt | 2 +- 11 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40101020.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40101030.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40101040.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40101050.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40101060.txt diff --git a/fastlane/metadata/android/pt-BR/changelogs/40100170.txt b/fastlane/metadata/android/pt-BR/changelogs/40100170.txt index 15081f160f..2292ddab98 100644 --- a/fastlane/metadata/android/pt-BR/changelogs/40100170.txt +++ b/fastlane/metadata/android/pt-BR/changelogs/40100170.txt @@ -1,2 +1,2 @@ -Principais mudanças nessa versão: correções de erros! -Registro de todas as alterações: https://github.com/vector-im/element-android/releases/tag/v1.0.17 +Principais alterações nesta versão: Correções de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101000.txt b/fastlane/metadata/android/pt-BR/changelogs/40101000.txt index 8138e376c6..8cdb122f14 100644 --- a/fastlane/metadata/android/pt-BR/changelogs/40101000.txt +++ b/fastlane/metadata/android/pt-BR/changelogs/40101000.txt @@ -1,2 +1,2 @@ -Principais mudanças nesta versão: Melhoria de VoIP (chamadas de áudio e vídeo em conversas) e correção de erros! -Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.1.0 +Principais alterações nesta versão: melhoramento de VoIP (chamadas de áudio e vídeo em DM) e correções de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101010.txt b/fastlane/metadata/android/pt-BR/changelogs/40101010.txt index 56f9c2955d..3a1128f229 100644 --- a/fastlane/metadata/android/pt-BR/changelogs/40101010.txt +++ b/fastlane/metadata/android/pt-BR/changelogs/40101010.txt @@ -1,2 +1,2 @@ -Principais mudanças nesta versão: melhoria de desempenho e correção de erros! -Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.1.1 +Principais alterações nesta versão: melhoramento de performance e correções de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101020.txt b/fastlane/metadata/android/pt-BR/changelogs/40101020.txt new file mode 100644 index 0000000000..7ee163e003 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40101020.txt @@ -0,0 +1,2 @@ +Principais alterações nesta versão: melhoramento de performance e correções de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101030.txt b/fastlane/metadata/android/pt-BR/changelogs/40101030.txt new file mode 100644 index 0000000000..e83058e014 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40101030.txt @@ -0,0 +1,2 @@ +Principais alterações nesta versão: melhoramento de performance e correções de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101040.txt b/fastlane/metadata/android/pt-BR/changelogs/40101040.txt new file mode 100644 index 0000000000..c58ede0161 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Principais alterações nesta versão: melhora de performance e correções de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101050.txt b/fastlane/metadata/android/pt-BR/changelogs/40101050.txt new file mode 100644 index 0000000000..5ab2dbee0d --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40101050.txt @@ -0,0 +1,2 @@ +Principais alterações nesta versão: correções quentes para 1.1.4 +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101060.txt b/fastlane/metadata/android/pt-BR/changelogs/40101060.txt new file mode 100644 index 0000000000..062b53d279 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40101060.txt @@ -0,0 +1,2 @@ +Principais alterações nesta versão: correções quentes para 1.1.5 +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt index 82b38b473c..b4fee53b4d 100644 --- a/fastlane/metadata/android/pt-BR/full_description.txt +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -1,30 +1,39 @@ -Element é um novo tipo de aplicativo de mensagens e colaboração que: +Element é tanto um mensageiro seguro como um app de colaboração de time de produtividade que é ideal para chats de grupo enquanto se trabalha remotamente. Este app de chat usa encriptação ponta-a-ponta para prover conferência de vídeo, compartilhamento de arquivo e chamadas de voz poderasos. -1. Coloca você no controle de sua privacidade; -2. Permite que você se comunique com qualquer pessoa na rede Matrix e, através de integrações, outros aplicativos como o Slack; -3. Protege você de anúncios, mineração de dados e de ecossistemas fechados; -4. Faz uso da criptografia de ponta a ponta, com autoverificação para confirmar outras pessoas. +As funções de Element incluem: +- Ferramentas de comunicação online avançadas +- Mensagens completamente encriptadas para permitir comunicação de corporação mais segura, até para pessoas trabalhando remotamente +- Chat descentralizado baseado na framework open source Matrix +- Compartilhamento de arquivo seguramente com dados encriptados enquanto se gerencia projetos +- Chats de vídeo com Voz sobre IP e compartilhamento de tela +- Integração fácil com suas ferramentas de colaboração online favoritas, ferramentas de gerenciamento de projetos, serviços de VoIP e outros apps de mensageria de time -Element é completamente diferente de outros aplicativos de mensagem e colaboração porque é descentralizado e código aberto. +Element é completamente diferente de outros apps de mensageria e colaboração. Ele opera em Matrix, uma rede aberta para mensageria segura e comunicação descentralizada. Ele permite auto-hospedagem para dar a usuárias(os) máxima propriedade e controle de seus dados e suas mensagens. -É possível hospedar um servidor ou escolher um servidor hospedeiro, para que você tenha privacidade, controle e que você seja o dono de seus dados e conversas. Com o Element, você possui acesso a uma rede aberta; então você não está preso falando somente com outros usuários no Element. O Element também é muito seguro. +Privacidade e mensageria encriptada +Element protege você de ads não-desejados, minagem de dados e jardins murados. Ele também assegura todos os seus dados, vídeo um-a-um e comunicação de voz através de encriptação ponta-a-ponta e verificação de dispositivo assinada cruzado. -Element é capaz de fazer tudo isso porque ele opera no Matrix, o padrão para comunicação aberta e descentralizada. +Element dá a você controle sobre sua privacidade enquanto permite a você se comunicar seguramente com qualquer pessoa na rede Matrix, ou outras ferramentas de colaboração ao se integrar com apps tais como Slack. -Element coloca você no controle ao permitir quem hospeda suas conversas. Neste aplicativo, você pode escolher hospedar de maneiras diferentes: +Element pode ser auto-hospedado +Para permitir mais controle de seus dados e conversas sensíveis, Element pode ser auto-hospedado ou você pode escolher qualquer host baseado em Matrix - o standard para comunicação open source e descentralizada. Element dá a você privacidade, conformidade de segurança e flexibilidade de integração. -1. Crie uma conta gratuita no servidor público matrix.org; -2. Hospede sua conta no seu servidor; -3. Entre em uma conta em um servidor customizado ao simplesmente se inscrever na plataforma de hospedagem Serviços Matrix Element. +Tenha posse de seus dados +Você decidade onde mannter seus dados e mensagens. Sem o risco de minagem de dados ou acesso de terceiros. -Por que escolher o Element? +Element põe você em controle de diferentes maneiras: +1. Tenha uma conta grátis no servidor público matrix.org hospedado pelos desenvolvedores Matrix, ou escolha de milhares de servidores públicos hospedados por pessoas se voluntariando +2. Auto-hospede sua conta ao rodar um servidor em sua própria infraestrutura de TI +3. Registre-se para uma conta num servidor personalizado ao simplesmente assinar a plataforma de hospedagem Element Matrix Services -SEJA O DONO DE SEUS DADOS: você decide onde manter seus dados e mensagens. Você é o dono deles e também os controla, não alguma mega corporação que minera os seus dados ou compartilha eles com terceiros. +Mensageria e colaboração abertos +Você pode fazer chat com qualquer pessoa na rede Matrix, caso ela esteja usando Element, um outro app de Matrix ou mesmo se ela estiver usando um app de mensagem diferente. -COLABORAÇÃO E MENSAGENS ABERTAS: você pode falar com qualquer outra pessoa na rede Matrix, não importa se ela está usando o Element ou algum outro aplicativo Matrix, ou até mesmo se ela está utilizando um sistema de mensagens diferente como o Slack, IRC ou XMPP. +Super seguro +Encriptação ponta-a-ponta real (somente aquelas/es na conversa podem decriptar mensagens), e verificação de dispositivo assinada cuzado. -SUPER-SEGURO: criptografia de ponta a ponta verdadeira (apenas aqueles na conversa podem descriptografar mensagens), e assinatura cruzada para verificar os dispositivos de participantes de conversas. +Comunicação e integração completas +Messageria, chamas de voz e vídeo, compartilhamento de arquivo, compartilhamento de tela e um monte de integrações, bots e widgets. Construa salas, comunidades, fique em contato e tenha as coisas feitas. -COMUNICAÇÃO COMPLETA: mensagens, chamadas de voz, chamadas de vídeo, compartilhamento de arquivos, compartilhamento de tela e um monte de integrações, robôs e widgets. Construa salas, comunidades, mantenha contato e faça seus projetos. - -NÃO IMPORTA ONDE VOCÊ ESTEJA: mantenha contato não importa onde você esteja com o histórico sincronizado de mensagens em todos os seus dispositivos e no navegador em https://app.element.io. +Continue de onde você parou +Fique em contato onde quer que você esteja com histórico de mensagem completamente sincronizado por todos os seus dispositivos e na web em https://app.element.io diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt index 853f629c30..f03b5347b9 100644 --- a/fastlane/metadata/android/pt-BR/short_description.txt +++ b/fastlane/metadata/android/pt-BR/short_description.txt @@ -1 +1 @@ -Conversas e chamadas seguras e descentralizadas. Mantenha seus dados protegidos. +Mensageiro de grupo - mensagens encriptadas, chat de grupo e chamadas de vídeo diff --git a/fastlane/metadata/android/pt-BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt index 5d2ae0c353..90dbcf1bba 100644 --- a/fastlane/metadata/android/pt-BR/title.txt +++ b/fastlane/metadata/android/pt-BR/title.txt @@ -1 +1 @@ -Element (o novo Riot.im) +Element - Mensageiro Seguro From 3d6fbf452fd309f65b7d0d2db4abc5c1ee091b93 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Wed, 19 May 2021 21:18:19 +0000 Subject: [PATCH 132/202] Translated using Weblate (Spanish) Currently translated at 91.0% (2234 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/es/ --- vector/src/main/res/values-es/strings.xml | 232 +++++++++++----------- 1 file changed, 121 insertions(+), 111 deletions(-) diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index b0357ab34c..076ca3ffed 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -28,7 +28,7 @@ todos los miembros de la sala. todos. desconocido (%s). - %1$s ha activado la encriptación de Extremo-a-Extremo (%2$s) + %1$s ha activado el cifrado Extremo-a-Extremo (%2$s) %1$s solicitó una conferencia de VozIP Conferencia de VozIP iniciada Conferencia de VozIP finalizada @@ -105,17 +105,17 @@ %1$s ha retirado la invitación de %2$s. Razón: %3$s %1$s ha añadido %2$s como alias de esta sala. - %1$s ha añadido %2$s como alias de esta sala. + %1$s han añadido %2$s como alias de esta sala. %1$s ha quitado %2$s como alias de esta sala. - %1$s ha quitado %2$s como alias de esta sala. + %1$s han quitado %2$s como alias de esta sala. %1$s ha establecido la dirección principal de esta sala a %2$s. %1$s ha permitido que los invitados se unan a la sala. %1$s ha impedido que los invitados se unan a la sala. - %1$s ha activado la encriptación extremo a extremo. - %1$s ha activado la encriptación de extremo a extremo (algoritmo no reconocido %2$s). + %1$s ha activado el cifrado Extremo-a-Extremo. + %1$s ha activado el cifrado Extremo-a-Extremo (algoritmo no reconocido %2$s). Enviaste una imagen. Enviaste un sticker. Tu invitación @@ -144,7 +144,7 @@ Respondiste la llamada. Terminaste la llamada. Hiciste visible el futuro historial de la %1$s - Has activado la encriptación de Extremo-a-Extremo (%1$s) + Has activado la cifrado Extremo-a-Extremo (%1$s) Has actualizado esta sala. Solicitaste una conferencia de VozIP Quitaste el nombre de la sala @@ -195,8 +195,8 @@ Quitaste la dirección principal de esta sala. Ha permitido que los invitados se unan a la sala. Ha impedido que los invitados se unan a la sala. - Tu has activado la encriptación de Extremo-a-Extremo. - Has activado la encriptación de Extremo-a-Extremo (algoritmo %1$s no reconocido). + Has activado el cifrado Extremo-a-Extremo. + Has activado el cifrado Extremo-a-Extremo (algoritmo %1$s no reconocido). Has impedido que invitados se unan a la sala. Has permitido a invitados unirse aquí. Te has ido. Razón: %1$s @@ -290,7 +290,7 @@ Reenviar Enlace Permanente Ver Fuente - Ver Fuente Desencriptada + Ver Fuente Descifrada Eliminar Renombrar Reportar contenido @@ -304,7 +304,7 @@ Necesitas permiso para invitar a iniciar una conferencia en esta sala No se puede iniciar la llamada Detalles de la sesión - No se admiten llamadas de conferencia en salas encriptadas + No se admiten llamadas de conferencia en salas cifradas Enviar de Todos Modos o Invitar @@ -566,7 +566,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua %s está escribiendo… %1$s y %2$s están escribiendo… %1$s y %2$s y otros están escribiendo… - Enviar un mensaje encriptado… + Enviar un mensaje cifrado… Enviar un mensaje (sin cifrar)… Se perdió la conexión con el servidor. Los mensajes no se enviaron. ¿%1$s o %2$s ahora? @@ -761,11 +761,11 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Direcciones Laboratorios Estas son funcionalidades experimentales que pueden romperse de maneras inesperadas. Utilizar con precaución. - Encriptación de Extremo a Extremo - La encriptación de Extremo a Extremo está activa - Necesitas cerrar sesión para poder habilitar la encriptación. + Cifrado Extremo-a-Extremo + El cifrado Extremo-a-Extremo está activo + Necesitas cerrar sesión para poder habilitar el cifrado. Cifrar solo a sesiones verificadas - Nunca enviar mensajes encriptados a sesiones sin verificar en esta sala desde esta sesión. + Nunca enviar mensajes cifrados a sesiones sin verificar en esta sala desde esta sesión. Esta sala no tiene direcciones locales Dirección nueva (ej. #foo:matrix.org) @@ -777,23 +777,23 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Dejar de Establecer como dirección principal Copiar ID de Sala Copiar Dirección de Sala - La encriptación está habilitada en esta sala. - La encriptación está deshabilitada en esta sala. - Habilitar encriptado + El cifrado está habilitado en esta sala. + El cifrado está deshabilitado en esta sala. + Habilitar cifrado \n(advertencia: ¡no se puede volver a deshabilitar!) Directorio %s estaba intentando cargar un momento específico en la línea de tiempo de esta sala pero no pudo encontrarlo. - Información de encriptación Extremo-a-Extremo + Información de cifrado Extremo-a-Extremo Información de eventos ID de Usuario Clave de identidad Curve25519 Clave de huella digital Ed25519 reclamada Algoritmo ID de Sesión - Error de desencriptación + Error de descifrado Información de la sesión emisora Nombre público Nombre público @@ -801,21 +801,21 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Clave de sesión Verificación Huella digital Ed25519 - Exportar claves de salas con encriptación Extremo-a-Extremo + Exportar claves de salas con cifrado Extremo-a-Extremo Exportar claves de sala Exportar las claves a un archivo local Exportar Ingresar frase de contraseña Confirmar frase de contraseña - Las claves de salas con encriptado de Extremo-a-Extremo se guardaron en \'%s\'. + Las claves de salas con cifrado Extremo-a-Extremo se guardaron en \'%s\'. \n \nAdvertencia: este archivo puede ser eliminado si la aplicación se desinstala. - Importar claves de salas con encriptación Extremo-a-Extremo + Importar claves de salas con cifrado Extremo-a-Extremo Importar claves de sala Importar las claves desde un archivo local Importar Cifrar solo a sesiones verificadas - Nunca enviar mensajes encriptados a sesiones sin verificar desde esta sesión. + Nunca enviar mensajes cifrados a sesiones sin verificar desde esta sesión. SIN Verificar Verificado Prohibido @@ -901,8 +901,8 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Añadir aplicaciones de Matrix Utilizar cámara nativa - Has añadido una nueva sesión \'%s\', que está solicitando claves de encriptación. - Tu sesión sin verificar \'%s\' está solicitando claves de encriptación. + Has añadido una nueva sesión \'%s\', que está solicitando claves de cifrado. + Tu sesión sin verificar \'%s\' está solicitando claves de cifrado. Iniciar verificación Compartir sin verificar Ignorar solicitud @@ -915,7 +915,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Desactivado Ruidoso - Mensaje encriptado + Mensaje cifrado Detalles de comunidad Cargando… Salir @@ -1057,7 +1057,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Borrar continuar con… Lo sentimos, no se encontró ninguna aplicación externa para completar esta acción. - Volver a solicitar las claves de encriptado de tus otras sesiones. + Volver a solicitar las claves de cifrado de tus otras sesiones. Solicitud de clave enviada. Privacidad de Notificaciones ${app_name} puede ejecutarse en segundo plano para gestionar tus notificaciones de forma segura y privada. Esto podría afectar la duración de la batería. @@ -1161,12 +1161,12 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Copia de seguridad de la clave Usar copia de seguridad de la clave La copia de seguridad de la clave no ha finalizado, por favor espere… - No quiero mis mensajes encriptados + No quiero mis mensajes cifrados Creando copia de seguridad de las claves… Usar copia de seguridad de la clave ¿Estás seguro\? Copia de seguridad - Perderá el acceso a sus mensajes encriptados si cierra sesión sin hacer una copia de seguridad de sus claves. + Perderá el acceso a sus mensajes cifrados si cierra sesión sin hacer una copia de seguridad de sus claves. Quedarse Saltar Hecho @@ -1199,14 +1199,14 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Comprueba ajustes Prueba de servicios Google Play APK de servicios de Google Play esta disponible y actualizado. - Al cerrar la sesión se perderán los mensajes encriptados + Al cerrar la sesión se perderán los mensajes cifrados ¿Estás seguro que quieres cerrar la sesión\? Está URL no es válida, por favor compruébala El diagnóstico base se ha completado con éxito. Si aun no recibes notificaciones, por favor mándanos un informe de error. Una o más pruebas han fallado, por favor prueba las soluciones propuestas. Una o más pruebas han fallado, por favor mándanos un informe de error para que podamos investigar. - Copia de seguridad en progreso. Si cierras sesión ahora perderás el acceso a tus mensajes encriptados. - La copia de seguridad debería estar activa ahora en todas tus sesiones para evitar la pérdida de acceso a tus mensajes encriptados. + Copia de seguridad en progreso. Si cierras sesión ahora perderás el acceso a tus mensajes cifrados. + La copia de seguridad debería estar activa ahora en todas tus sesiones para evitar la pérdida de acceso a tus mensajes cifrados. ${app_name} usa los servicios de Google Play para entregar mensajes Push pero no parece estar configurado correctamente: \n%1$s solucionar error con los Servicios de Google Play @@ -1231,10 +1231,10 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Borrar copia de seguridad nueva copia de seguridad Ese era yo - Nunca pierda mensajes encriptados - Configurar copia de seguridad de las claves de encriptado - Nunca pierdas mensajes encriptados - Nuevas claves de encriptación de mensajes + Nunca pierda mensajes cifrados + Configurar copia de seguridad de las claves de cifrado + Nunca pierdas mensajes cifrados + Nuevas claves de cifrado de mensajes Gestionar Copia de Seguridad Guardando copia de seguridad… Versión @@ -1293,7 +1293,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Compresión predeterminada Seleccionar Seleccionar - Recuperación de mensajes encriptados + Recuperación de mensajes cifrados Gestionar copia de seguridad clave %1$s: %2$d mensaje @@ -1330,8 +1330,8 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua La contraseña que has introducido es muy débil Por favor borra la contraseña si quieres que ${app_name} genere una clave de recuperación. No hay ninguna sesión de Matrix disponible - Nunca perder los mensajes encriptados - Los mensajes en salas encriptadas están asegurados con encriptación Extremo-a-Extremo. Solo los integrantes de la sala y tu podéis leer estos mensajes. + Nunca perder los mensajes cifrados + Los mensajes en salas cifradas están asegurados con cifrado Extremo-a-Extremo. Solo los integrantes de la sala y tu podéis leer estos mensajes. \n \nAsegúrate de guardar bien tus claves para evitar perderlas. (Avanzado) @@ -1346,7 +1346,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua (Avanzado) Establecer clave de recuperación Completado! Tus claves se están guardando. - Tu clave de recuperación es una red de seguridad - puedes usarla para recuperar el acceso a tus mensajes encriptados si olvidas tu contraseña. + Tu clave de recuperación es una red de seguridad - puedes usarla para recuperar el acceso a tus mensajes cifrados si olvidas tu contraseña. \nMantén tu clave de recuperación en algún lugar muy seguro como un administrador de contraseñas (o en una caja fuerte) Mantén tu clave de recuperación en algún lugar muy seguro como un administrador de contraseñas (o en una caja fuerte) Hecho @@ -1366,10 +1366,10 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Tus claves cifradas están siendo guardadas en segundo plano en tu servidor. La copia de seguridad inicial podría tardar varios minutos. Estás seguro\? Podrías perder el acceso a tus mensajes si te desconectas o pierdes este dispositivo. - Utiliza tu clave de recuperación para desbloquear tu historial de mensajes encriptados + Utiliza tu clave de recuperación para desbloquear tu historial de mensajes cifrados Utiliza tu clave de recuperación No sabes tu clave de recuperación\? puedes %s. - Utiliza tu clave de recuperación para desbloquear tu historial de mensajes encriptados + Utiliza tu clave de recuperación para desbloquear tu historial de mensajes cifrados Introduzca la clave de recuperación Mensaje de recuperación Has perdido tu clave de recuperación\? Puedes crear una nueva en ajustes. @@ -1413,14 +1413,14 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua \n%2$s Configuración de uso Origen predeterminado de medios - Configurar copia de seguridad de las claves de encriptación + Configurar copia de seguridad de las claves de cifrado Obteniendo una versión de copia de seguridad… La copia de seguridad tiene una firma valida de la sesión no verificada %s La copia de seguridad tiene una firma inválida de la sesión verificada %s La copia de seguridad tiene una firma inválida de la sesión no verificada %s Error al conseguir información de confianza para la copia de seguridad (%s). Para usar la copia de seguridad de la clave en esta sesión introduzca su contraseña o su clave de recuperación ahora. - Deseas borrar tus claves de encriptación guardadas en el servidor\? No podrás usar tu clave de recuperación para leer el historial de mensajes encriptados. + ¿Deseas borrar tus claves de cifrado guardadas en el servidor\? No podrás usar tu clave de recuperación para leer el historial de mensajes cifrados. Una nueva copia de seguridad de mensajes ha sido detectada. \n \nSi no ha establecido un nuevo método de recuperación, alguien podría estar intentando acceder a su cuenta. Cambie su contraseña y establezca un nuevo método de recuperación inmediatamente en ajustes. @@ -1428,11 +1428,11 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Reproducir sonido de cámara Verificar sesión ip desconocida - Una nueva sesión solicita claves de encriptación. + Una nueva sesión solicita claves de cifrado. \nSesión: %1$s \nVisto por última vez: %2$s \nSi no has iniciado sesión en otro dispositivo ignora esta solicitud. - Una sesión no verificada solicita claves de encriptación. + Una sesión no verificada solicita claves de cifrado. \nSesión: %1$s \nVisto por última vez: %2$s \nSi no has iniciado sesión en otro dispositivo ignora esta solicitud. @@ -1450,7 +1450,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Para más seguridad, te recomendamos que hagas esto en persona o por otros medios confiables. Empezar verificación Solicitud de verificación - Verifica esta sesión para marcarla de confianza. Marcar sesiones de otros como de confianza te da aún más tranquilidad cuando usas cifrado de Extremo-a-Extremo. + Verifica esta sesión para marcarla de confianza. Marcar sesiones de otros como de confianza te da aún más tranquilidad cuando usas cifrado Extremo-a-Extremo. Verificar esta sesión la marcará como confiable, y también marcará como confiable tu sesión para la contraparte. Verifica esta sesión confirmando los emojis que aparecen en la pantalla de la contraparte Verifica esta sesión confirmando que los siguietes números aparecen en la pantalla de la contraparte @@ -1459,7 +1459,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Esperando confirmación de la contraparte… ¡Verificado! Has verificado correctamente esta sesión. - Los mensajes con este usuario están encriptados de Extremo-a-Extremo y no son legibles por terceros. + Los mensajes con este usuario están cifrados Extremo-a-Extremo y no son legibles por terceros. Ok ¿No aparece nada\? No todas las aplicaciones cliente soportan verificación interactiva. Usa la verificación clásica. Usar verificación clásica. @@ -1747,7 +1747,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Mensajes no leídos Es tu conversación. Sé su dueño. Envía mensajes a personas o grupos - Mantén las conversaciones privadas con encriptación + Mantén conversaciones privadas con cifrado Extiende y personaliza tu experiencia Empieza Selecciona un servidor @@ -1869,7 +1869,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua ${app_name} puede fallar con más frecuencia cuando ocurre un error inesperado Antepone ¯\\_(ツ)_/¯ a un mensaje de texto sin formato Habilitar crifrado - Una vez habilitada, la encriptación no se puede deshabilitar. + Una vez habilitado, el cifrado no se puede deshabilitar. Su dominio de correo electrónico no está autorizado para registrarse en este servidor Inicio de sesión no confiable Coinciden @@ -1904,7 +1904,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Verificar %s Verificado %s Esperando por %s… - Los mensajes en esta sala no están encriptados de Extremo-a-Extremo. + Los mensajes en esta sala no están cifrados Extremo-a-Extremo. Seguridad Saber mas Mas @@ -1937,10 +1937,10 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Envía el mensaje dado en colores Línea de tiempo Editor de mensage - Encriptacion habilitada (end-to-end) … - Una vez habilitada, la encriptación no se puede deshabilitar. - Encriptar \? - Habilitar la encriptación + Habilitar cifrado Extremo-a-Extremo… + Una vez habilitado, el cifrado no se puede deshabilitar. + ¿Desea cifrar \? + Habilitar el cifrado Firma cruzada Firma cruzada no habilitada Sesiones Activas @@ -1959,7 +1959,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua No es confiable Inicializar Firmas Cruzadas Restablecer claves - Codigo QR + Código QR Correcto No Sin conexión @@ -1983,7 +1983,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Clave de mensaje Contraseña de la cuenta ¡Listo! - Encriptación habilitada + Cifrado habilitado Sala creada y configurada por usted. Esperando por %s… Ajuste de Notificaciones @@ -2035,7 +2035,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Configurar copia de seguridad segura Restablecer copia de seguridad segura Configurar en este dispositivo - Protéjase contra la pérdida de acceso a los mensajes y datos encriptados haciendo una copia de seguridad de las claves de encriptado en su servidor. + Protéjase contra la pérdida de acceso a los mensajes y datos cifrados haciendo una copia de seguridad de las claves de cifrado en su servidor. Genere una nueva llave de seguridad o establezca una nueva frase de seguridad para su copia de seguridad existente. Esto reemplazará su clave o frase actual. Las integraciones están deshabilitadas @@ -2057,7 +2057,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Sin widgets activos La clave de recuperación se ha guardada. Copia de seguridad segura - Protéjase contra la pérdida de acceso a mensajes y datos encriptados + Protéjase contra la pérdida de acceso a mensajes y datos cifrados Configurar copia de seguridad segura Mensaje borrado Se ha creado la sala, pero algunas invitaciones no se han enviado por el siguiente motivo: @@ -2078,7 +2078,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua No hiciste cambios Hiciste que la sala fuera pública para quien conozca el enlace. Hiciste la sala solo por invitación. - Únase gratis a millones en el servidor público más grande + Únase gratis a millones de personas en el mayor servidor público Continuar con SSO Dirección de servicios de Element Matrix Ingrese la dirección del servidor que desea utilizar @@ -2087,7 +2087,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Email Nueva contraseña ¡Advertencia! - Cambiar su contraseña restablecerá cualquier clave de encriptado de Extremo-a-Extremo en todas sus sesiones, haciendo ilegible el historial de chat encriptado. Configure la Copia de seguridad de claves o exporte las claves de su sala desde otra sesión antes de restablecer su contraseña. + Cambiar su contraseña restablecerá cualquier clave de cifrado Extremo-a-Extremo en todas sus sesiones, haciendo ilegible el historial de chat cifrado. Configure la Copia de seguridad de claves o exporte las claves de su sala desde otra sesión antes de restablecer su contraseña. Seguir Este correo electrónico no está vinculado a ninguna cuenta Revisa tu correo @@ -2114,7 +2114,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Numero de teléfono (opcional) Siguiente Confirmar número de teléfono - Acabamos de mandar un codigo a %1$s. Ingréselo a continuación para verificar que es usted. + Acabamos de mandar un código a %1$s. Introdúzcalo a continuación para verificar su identidad. Introduzca el código Enviar de nuevo Siguiente @@ -2167,18 +2167,18 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Estás desconectado Registrarse El administrador de su servidor privado (%1$s) ha cerrado la sesión de su cuenta %2$s (%3$s). - Inicie sesión para recuperar las claves de encriptación almacenadas exclusivamente en este dispositivo. Los necesita para leer todos sus mensajes seguros en cualquier dispositivo. + Inicie sesión para recuperar las claves de cifrado almacenadas exclusivamente en este dispositivo. Los necesita para leer todos sus mensajes seguros en cualquier dispositivo. Registrarse Contraseña Borrar datos personales - Advertencia: sus datos personales (incluidas las claves de encriptación) todavía están almacenadas en este dispositivo. + Advertencia: sus datos personales (incluidas las claves de cifrado) todavía están almacenadas en este dispositivo. \n \nBórrelo si terminó de usar este dispositivo o si desea iniciar sesión en otra cuenta. Borrar todos los datos Borrar datos ¿Borrar todos los datos almacenados actualmente en este dispositivo\? \nVuelva a iniciar sesión para acceder a los datos y mensajes de su cuenta. - Perderás el acceso a los mensajes seguros a menos que inicies sesión para recuperar tus claves de encriptación. + Perderás el acceso a los mensajes seguros a menos que inicies sesión para recuperar tus claves de cifrado. Borrar datos La sesión actual es para el usuario %1$s y usted proporciona las credenciales para el usuario %2$s. Esto no está suportado por ${app_name}. \nPrimero borre los datos, luego inicie sesión nuevamente con otra cuenta. @@ -2193,19 +2193,19 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Para mayor seguridad, verifique %s verificando un código único en ambos dispositivos. \n \nPara máxima seguridad, hágalo en persona. - Los mensajes de esta sala están encriptados de Extremo-a-Extremo. + Los mensajes de esta sala están cifrados Extremo-a-Extremo. \n -\nSus mensajes están protegidos con candados y solo usted y el destinatario tienen las claves únicas para desbloquearlos. +\nSus mensajes están protegidos y sólo usted y el destinatario tienen las claves únicas para descifrarlos. Esta sesión no puede compartir esta verificación con sus otras sesiones. \nLa verificación se guardará localmente y se compartirá en una versión futura de la aplicación. Envía el emote dado coloreado como un arcoíris - Una vez habilitado, la encriptación de una sala no se puede deshabilitar. Los mensajes enviados en una sala encriptada no pueden ser vistos por el servidor, solo por los participantes de la sala. Habilitar la encriptación puede impedir que muchos bots y puentes funcionen correctamente. + Una vez habilitado, el cifrado de una sala no se puede deshabilitar. Los mensajes enviados en una sala cifrada no pueden ser vistos por el servidor, solo por los participantes de la sala. Habilitar el cifrado puede impedir que muchos bots y puentes funcionen correctamente. Para estar seguro, verifique %s comprobando un código de un solo uso. Para estar seguro, hágalo en persona o use otra forma de comunicarse. Compare los emoji únicos, asegurándose de que aparezcan en el mismo orden. Compare el código con el que se muestra en la pantalla del otro usuario. - Los mensajes con este usuario están encriptados de extremo a extremo y no pueden ser leídos por terceros. - Su nueva sesión ahora está verificada. Tiene acceso a sus mensajes encriptados y otros usuarios lo verán como de confianza. + Los mensajes con este usuario están cifrados Extremo-a-Extremo y no pueden ser leídos por terceros. + Su nueva sesión ahora está verificada. Tiene acceso a sus mensajes cifrados y otros usuarios lo verán como de confianza. La firma cruzada está habilitada \n Claves privadas en el dispositivo. La firma cruzada está habilitada @@ -2213,15 +2213,15 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua \nNo se conocen las claves privadas La firma cruzada está habilitada. \nLas claves no son de confianza - El administrador de su servidor ha desactivado la encriptación de Extremo-a-Extremo de forma predeterminada en salas privadas y mensajes directos. + El administrador de su servidor ha desactivado el cifrado Extremo-a-Extremo de forma predeterminada en salas privadas y mensajes directos. No hay información criptográfica disponible Esta sesión es confiable para mensajería segura porque usted la verificó: - Verifique esta sesión para marcarla como confiable y otorgarle acceso a mensajes encriptados. Si no inició sesión en esta sesión, su cuenta puede verse comprometida: + Verifique esta sesión para marcarla como confiable y otorgarle acceso a mensajes cifrados. Si no inició sesión en esta sesión, su cuenta puede verse comprometida: %d sesión activa %d sesiones activas - Utilice una sesión existente para verificar ésta, otorgándole acceso a los mensajes encriptados. + Utilice una sesión existente para verificar ésta, otorgándole acceso a los mensajes cifrados. Esta sesión es de confianza para mensajería segura porque %1$s (%2$s) la ha verificado: %1$s (%2$s) iniciado sesión con una nueva sesión: Hasta que este usuario confíe en esta sesión, los mensajes enviados hacia y desde ella se etiquetan con advertencias. Alternativamente, puede verificarlo manualmente. @@ -2250,10 +2250,10 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Evento eliminado por el usuario, motivo: %1$s Evento moderado por el administrador de la sala, motivo: %1$s Solicitudes clave - Desbloquear el historial de mensajes encriptados - Utilice esta sesión para verificar su nuevo, otorgándole acceso a mensajes encriptados. - Si cancela, no podrá leer mensajes encriptados en este dispositivo y otros usuarios no confiarán en él - Si cancela, no podrá leer mensajes encriptados en su nuevo dispositivo y otros usuarios no confiarán en él + Desbloquear el historial de mensajes cifrados + Utilice esta sesión para verificar su nuevo, otorgándole acceso a mensajes cifrados. + Si cancela, no podrá leer mensajes cifrados en este dispositivo y otros usuarios no confiarán en él + Si cancela, no podrá leer mensajes cifrados en su nuevo dispositivo y otros usuarios no confiarán en él No verificarás %1$s (%2$s) si cancelas ahora. Comience de nuevo en su perfil de usuario. Uno de los siguientes puede verse comprometido: \n @@ -2268,7 +2268,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Generar una clave de mensaje Confirmar %s Ingrese su %s para continuar. - Proteja y desbloquee los mensajes encriptados y confíe en %s. + Proteja y desbloquee los mensajes cifrados y confíe en %s. Ingrese su %s nuevamente para confirmarlo. No use la contraseña de su cuenta. Ingrese una frase de seguridad que solo usted conozca, que se usa para proteger secretos en su servidor. @@ -2287,34 +2287,34 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Configuración de copia de seguridad de claves Tus %2$s y %1$s ahora están configurados. \n -\n¡Mantenlos a salvo! Los necesitará para desbloquear mensajes encriptados y proteger la información si pierde todas sus sesiones activas. +\n¡Mantenlos a salvo! Los necesitará para desbloquear mensajes cifrados y proteger la información si pierde todas sus sesiones activas. Imprímelo y guárdalo en un lugar seguro Guárdelo en una llave USB o unidad de respaldo Cópielo en su almacenamiento personal en la nube No puedes hacer eso desde el móvil - Establecer una frase de contraseña de recuperación le permite proteger y desbloquear mensajes encriptados y de confianza. + Establecer una Frase de Recuperación le permite proteger y desbloquear mensajes cifrados y de confianza. \n -\nSi no desea establecer una contraseña de mensaje, genere una clave de mensaje. - Establecer una frase de contraseña de recuperación le permite proteger y desbloquear mensajes encriptados y de confianza. - Si cancela ahora, puede perder mensajes y datos encriptados si pierde el acceso a sus inicios de sesión. +\nSi no desea establecer una Contraseña de Mensaje, genere una Clave de Mensaje en su lugar. + Establecer una Frase de Recuperación le permite proteger y desbloquear mensajes cifrados y de confianza. + Si cancela ahora, puede perder mensajes y datos cifrados si pierde el acceso a sus inicios de sesión. \n -\nTambién puede configurar la Copia de seguridad segura y administrar sus claves en Configuración. - Los mensajes de esta sala están encriptados de Extremo-a-Extremo. Obtenga más información y verifique a los usuarios en su perfil. - Encriptación no habilitada - La encriptación usada por esta sala no es compatible +\nTambién puede configurar la Copia de Seguridad Segura y administrar sus claves en Configuración. + Los mensajes de esta sala están cifrados Extremo-a-Extremo. Obtenga más información y verifique a los usuarios en su perfil. + Cifrado no habilitado + El cifrado usado por esta sala no es compatible %s creado y configurado la sala. ¡Casi ahí! ¿El otro dispositivo muestra el mismo escudo\? ¡Casi ahí! Esperando confirmación… No se pudieron importar las claves Mensajes que contienen @room - Mensajes encriptados en chats 1:1 - Mensajes encriptados en chats de grupo + Mensajes cifrados en conversaciones personales + Mensajes cifrados en chats de grupo Cuando las salas son actualizadas Solucionar problemas Envía un mensaje como texto estándar, sin interpretarlo como Markdown Nombre de usuario y / o contraseña incorrectos. La contraseña ingresada comienza o termina con espacios, verifíquela. Esta cuenta ha sido desactivada. - Mejora de encriptación disponible + Mejora de cifrado disponible Habilitar la firma cruzada Verifíquese a usted mismo y a los demás para mantener sus chats seguros Entrar %s @@ -2341,7 +2341,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua ${app_name} Web \n${app_name} de escritorio ${app_name} iOS -\${app_name} Android +\n${app_name} Android u otro cliente Matrix con capacidad de firma cruzada Utilice la última versión de ${app_name} en sus otros dispositivos: Obliga a descartar la sesión de grupo saliente actual en una sala cifrada @@ -2351,15 +2351,15 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Seleccione su clave de recuperación o introdúzcala manualmente escribiéndola o pegándola desde su portapapeles La copia de seguridad no se pudo descifrar con esta clave de recuperación: verifique que ingresó la clave de recuperación correcta. No se pudo acceder al almacenamiento seguro - Sin encriptar - Encriptado por un dispositivo no verificado + Sin cifrar + Cifrado por un dispositivo no verificado Revise dónde inició sesión Verifique todas sus sesiones para asegurarse de que su cuenta y sus mensajes estén seguros Verifique el nuevo inicio de sesión accediendo a su cuenta: %1$s Verificar manualmente por texto Verificación interactiva por emoji - Confirme su identidad verificando este inicio de sesión de una de sus otras sesiones, otorgándole acceso a los mensajes encriptados. - Confirme su identidad verificando este inicio de sesión, otorgándole acceso a los mensajes encriptados. + Confirme su identidad verificando este inicio de sesión de una de sus otras sesiones, otorgándole acceso a los mensajes cifrados. + Confirme su identidad verificando este inicio de sesión, otorgándole acceso a los mensajes cifrados. Marcar como de confianza No pudimos crear tu DM. Marque los usuarios que desea invitar y vuelva a intentarlo. Primero acepta los términos del servidor de identidad en la configuración. @@ -2380,7 +2380,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Enciende la cámara Configurar copia de seguridad segura Respaldo seguro - Protéjase contra la pérdida de acceso a los mensajes y datos encriptados haciendo una copia de seguridad de las claves de encriptación en su servidor. + Protéjase contra la pérdida de acceso a los mensajes y datos cifrados haciendo una copia de seguridad de las claves de cifrado en su servidor. Preparar Usa una llave de seguridad Genere una clave de seguridad para almacenar en un lugar seguro, como un administrador de contraseñas o una caja fuerte. @@ -2400,11 +2400,11 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua No puedes acceder a este mensaje Esperando este mensaje, esto puede tardar un poco No se puede descifrar - Debido a la encriptación de Extremo-a-Extremo, es posible que deba esperar a que llegue el mensaje de alguien porque las claves de encriptación no se le enviaron correctamente. + Debido al cifrado Extremo-a-Extremo, es posible que deba esperar a que llegue el mensaje de alguien porque las claves de cifrado no se le enviaron correctamente. No puede acceder a este mensaje porque ha sido bloqueado por el remitente No puede acceder a este mensaje porque el remitente no confía en su sesión No puede acceder a este mensaje porque el remitente no envió las claves a propósito - Esperando al historial de encriptación + Esperando al historial de cifrado ¡Nos complace anunciar que hemos cambiado de nombre! Tu aplicación está actualizada y accediste a tu cuenta. Guardar la clave de recuperación en Agregar desde mi directorio telefónico @@ -2430,14 +2430,14 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua ¿Olvidó su PIN\? No se puede abrir una sala en la que está prohibido. No puedo encontrar esta sala. Asegúrate de que exista. - Los mensajes en esta sala están encriptados punto-a-punto. + Los mensajes en esta sala están cifrados Extremo-a-Extremo. Mensaje directo Salir Preferencias - Los mensajes aquí están encriptados de Extremo-a-Extremo. + Los mensajes aquí están cifrados Extremo-a-Extremo. \n -\nTus mensajes están asegurados con un candado. Solo tú y tú destinatario tenéis las llaves especiales para desencriptarlos. - Los mensajes aquí no están encriptados de Extremo-a-Extremo. +\nTus mensajes están asegurados con un candado. Solo tú y tú destinatario tenéis las llaves especiales para descifrarlos. + Los mensajes aquí no están cifrados Extremo-a-Extremo. Botones de Bot Encuesta Eliminar de baja prioridad @@ -2454,7 +2454,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua La aplicación está recibiendo PUSH La aplicación está esperando al PUSH Probar Push - Búsquedas en salas encriptadas todavía no están soportadas. + Todavía no se puede hacer búsquedas en salas cifradas. Filtrar usuarios excluidos Enviar la historia de peticiones de claves compartidas No hay más resultados @@ -2499,7 +2499,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Tú has configurado como sólo con invitación. %1$s ha configurado como sólo con invitación. Añadir imagen de - Mostrar la historia completa en salas encriptadas + Mostrar la historia completa en salas cifradas Ajustes de la sala Tema Tema de la sala (opcional) @@ -2538,27 +2538,27 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Añadir No tiene permisos para encryptar esta sala. No es un ID matrix valido - Mi codigo - Compartir mi codigo + Mi código + Compartir mi código Escanear QR 🔐️ Unirme a ${app_name} Hey, contactame por ${app_name}:%s Adicionar persona - Compartir ese codigo para que puedan contactarloa usted. + Comparte este código para que puedan contactar contigo. Crear una converzacion por ID Matrix Crear una converzacion escanenado QR Adicionar botón en el redactor de mensajes para abrir el teclado emoji Mostrar teclado emoji Reciente - Adicionar por codigo QR + Añadir mediante código QR Buscar por nick o ID - Codigo QR + Código QR Sugerencias Contactos Usuarios conocidos Mostrar efectos de chat Para leer el código QR , necesita dar permisos de acceso a su cámara. - Invitar contactos + Invitar amigos Modifique los roles y privilegios de la sala. Sala no pública. No puede acceder sin una invitacion. Actualizar sala @@ -2589,4 +2589,14 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Esperar Continuar Volver + Los espacios son una manera de agrupar salas y personas por trabajo, diversión o lo que quieras. + ¡Bienvenido a los Espacios! + Únete a un Espacio + Crea un Espacio + Añade un Espacio + Crea un Espacio + Crear un espacio + Los Espacios son una nueva forma de agrupar salas y personas + Sincronización inicial: +\nEsperando respuesta del servidor… \ No newline at end of file From 8da8d448235e5b0e4ed4bf8676c9e25d52982a7e Mon Sep 17 00:00:00 2001 From: Ville Ranki Date: Tue, 18 May 2021 15:23:09 +0000 Subject: [PATCH 133/202] Translated using Weblate (Finnish) Currently translated at 77.1% (1894 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fi/ --- vector/src/main/res/values-fi/strings.xml | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 36d87488da..28a576613e 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -2230,4 +2230,35 @@ MEDIA Asetit palvelimen käyttäjäoikeuslistan tähän huoneeseen. %s asetti palvelimen käyttäjäoikeuslistan tähän huoneeseen. + Asetit huoneen pääosoitteeksi %1$s. + Lisäsit %1$s ja poistit %2$s huoneen osoitteista. + + Poistit %1$s huoneen osoitteista. + Poistitt %1$s huoneen osoitteista. + + + Lisäsit %1$s osoitteeksi tähän huoneeseen. + Lisäsit %1$s osoitteiksi tähän huoneeseen. + + Peruit %1$s:n kutsun. Syynä: %2$s + Viesti lähetetty + Ensimmäinen synkronointi: +\nLadataan tietoja… + Ensimmäinen synkronointi: +\nOdotetaan palvelimen vastausta. . . + + %1$s, %2$s, %3$s ja %4$d muu + %1$s, %2$s, %3$s ja %4$d muuta + + %1$s %2$s:sta %3$s:n + %1$s muutti käyttäjän %2$s oikeustasoa. + Muutit käyttäjän %1$s oikeustasoa. + Muokkasit videopuhelua + %1$s muokkasi videopuhelua + Lopetit videopuhelun + %1$s lopetti videopuhelun + Aloitit videopuhelun + %1$s aloitti videopuhelun + Muutit huoneen palvelimien käyttäjäoikeuslistaa. + %s muutti huoneen palvelimien käyttäjäoikeuslistaa. \ No newline at end of file From c423c270f78930f545e07546a1f1daf7f649edd0 Mon Sep 17 00:00:00 2001 From: AmeliMeow Date: Wed, 19 May 2021 15:05:47 +0000 Subject: [PATCH 134/202] Translated using Weblate (Lithuanian) Currently translated at 4.4% (108 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/lt/ --- vector/src/main/res/values-lt/strings.xml | 76 +++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/vector/src/main/res/values-lt/strings.xml b/vector/src/main/res/values-lt/strings.xml index 6eb730da63..b18595388c 100644 --- a/vector/src/main/res/values-lt/strings.xml +++ b/vector/src/main/res/values-lt/strings.xml @@ -32,4 +32,80 @@ %1$s išėjo iš kambario Jūs prisijungėte %1$s prisijungė + Pranešimai + Versija + Normalūs + Įjungti + Įjungti + Tel. numeris + E. paštas + Versija + Nustatymai + Nustatymai + Atsijungti + Ieškoti + Priežastis + Ignoruoti + Atblokuoti + Užblokuoti + Pakviesti + Prisijungę + Atsijungę + Sukurti + Sinchronizuojama… + Atmesti + Peržiūra + Prisijungti + Pašalinti + Tęsti + NE + TAIP + Informacija + Skambinama… + Skambutis + Skambučiai + Šiandien + Jūs pakvietėte %1$s + %1$s pakvietė %2$s + Jūs išsiuntėte pakvietimą %1$s prisijungti prie kambario + Jūs atnaujinote savo profilį %1$s + %1$s atnaujino savo profilį %2$s + Žinutę pašalino %1$s [Priežastis: %2$s] + Žinutė pašalinta [Priežastis: %1$s] + Žinutę pašalino %1$s + Žinutė pašalinta + Jūs pašalinote kambario nuotrauką + %1$s pašalino kambario nuotrauka + Jūs pašalinote kambario tema + %1$s pašalino kambario temą + Jūs pašalinote kambario pavadinimą + %1$s pašalino kambario pavadinimą + VoIP konferencija baigta + VoIP konferencija pradėta + Jūs paprašėte VoIP konferencijos + 🎉 Visiem serveriam yra uždrausta dalyvauti! Šiuo kambariu nebegalima naudotis. + Jūs atnaujinote šį kambarį. + %s atnaujino šį kambarį. + Jūs įjungėte ištisinį šifravimą (%1$s) + %1$s įjungė ištisinį šifravimą (%2$s) + nežinomas (%s). + bet kas. + Visi kambario dalyviai. + Visi kambario dalyviai, nuo tada, kai jie yra pakviesti. + %s baigė skambutį. + Jūs atsiliepėte į skambutį. + %s atsiliepė į skambutį. + %s pradėjo balso skambutį. + Jūs pradėjote vaizdo skambutį. + %s pradėjo vaizdo skambutį. + %1$s pakeitė kambario pavadinimą į %2$s + Jūs pakeitėte kambario nuotrauka + Jūs pakeitėte temą į %1$s + %1$s pakeitė temą į: %2$s + Jūs pakeitėte savo vardą iš %1$s į %2$s + %1$s pakeitė savo vardą iš %2$s į %3$s + Jūs pakeitėte savo vardą į %1$s + %1$s pakeitė savo vardą į %2$s + Jūs pakeitėte savo profilio nuotrauką + %1$s pakeitė savo profilio nuotrauką \ No newline at end of file From ade2f0c06512d55ffd8a56ba60c747f2ba65885f Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 19 May 2021 01:12:52 +0000 Subject: [PATCH 135/202] Translated using Weblate (Ukrainian) Currently translated at 94.4% (17 of 18 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/ --- fastlane/metadata/android/uk/changelogs/40101040.txt | 2 ++ fastlane/metadata/android/uk/changelogs/40101050.txt | 2 ++ fastlane/metadata/android/uk/changelogs/40101060.txt | 2 ++ fastlane/metadata/android/uk/short_description.txt | 2 +- fastlane/metadata/android/uk/title.txt | 2 +- 5 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/uk/changelogs/40101040.txt create mode 100644 fastlane/metadata/android/uk/changelogs/40101050.txt create mode 100644 fastlane/metadata/android/uk/changelogs/40101060.txt diff --git a/fastlane/metadata/android/uk/changelogs/40101040.txt b/fastlane/metadata/android/uk/changelogs/40101040.txt new file mode 100644 index 0000000000..663133a99c --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: поліпшення швидкодії та виправлення помилок! +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/uk/changelogs/40101050.txt b/fastlane/metadata/android/uk/changelogs/40101050.txt new file mode 100644 index 0000000000..00f44f2606 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40101050.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: виправлення для 1.1.4 +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/uk/changelogs/40101060.txt b/fastlane/metadata/android/uk/changelogs/40101060.txt new file mode 100644 index 0000000000..8a4ae2a4d2 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40101060.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: виправлення для 1.1.5 +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt index 116f6f95b7..fa42cce81e 100644 --- a/fastlane/metadata/android/uk/short_description.txt +++ b/fastlane/metadata/android/uk/short_description.txt @@ -1 +1 @@ -Захищене децентралізоване листування та дзвінки. Тримайте ваші дані в безпеці. +Груповий месенджер — зашифровані повідомлення, групові бесіди та відеовиклики diff --git a/fastlane/metadata/android/uk/title.txt b/fastlane/metadata/android/uk/title.txt index 88e3f7c573..0a170676ff 100644 --- a/fastlane/metadata/android/uk/title.txt +++ b/fastlane/metadata/android/uk/title.txt @@ -1 +1 @@ -Element (раніше Riot.im) +Element — Безпечний месенджер From ad0cd981838c96c9870516d6b6508b0a9e2f602a Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 17 May 2021 19:32:41 +0000 Subject: [PATCH 136/202] Translated using Weblate (Swedish) Currently translated at 100.0% (2454 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/ --- vector/src/main/res/values-sv/strings.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index 64b67bcc3d..d88d0072f3 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -2752,4 +2752,23 @@ Gå med iallafall Gå med i utrymme Skapa utrymme + Hantera rum och utrymmen + Markera som inte föreslaget + Markera som föreslaget + Föreslagna + Gör det här rummet offentligt + Hantera rum + Letar du efter någon inte i %s\? + %s bjuder in dig + Det här rummet är offentligt + Skicka media med originalstorlek + + Skicka video med originalstorlek + Skicka videor med originalstorlek + + Filen är för stor för att ladda upp. + Komprimerar video %d%% + Komprimerar bild… + Använd som förval och fråga inte igen + Fråga alltid \ No newline at end of file From 93d0fc0035f8b916e964ec6fedd890f771dafa1c Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Tue, 18 May 2021 21:23:36 +0000 Subject: [PATCH 137/202] Translated using Weblate (Portuguese (Brazil)) Currently translated at 96.8% (2376 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/ --- vector/src/main/res/values-pt-rBR/strings.xml | 2182 +++++++++-------- 1 file changed, 1099 insertions(+), 1083 deletions(-) diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index b46bacebca..f55453707f 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -1,224 +1,224 @@ %1$s: %2$s - %1$s enviou uma foto. + %1$s enviou uma imagem. convite de %s %1$s convidou %2$s %1$s convidou você - %1$s entrou na sala + %1$s juntou-se à sala %1$s saiu da sala - %1$s recusou o convite - %1$s removeu %2$s - %1$s removeu o banimento de %2$s + %1$s rejeitou o convite + %1$s expulsou %2$s + %1$s desbaniu %2$s %1$s baniu %2$s - %1$s desfez o convite a %2$s - %1$s alterou a foto de perfil - %1$s definiu o nome e sobrenome como %2$s - %1$s alterou o nome e sobrenome de %2$s para %3$s - %1$s removeu o nome e sobrenome (era %2$s) - %1$s alterou a descrição para: %2$s - %1$s alterou o nome da sala para: %2$s - %s iniciou uma chamada de vídeo. - %s iniciou uma chamada de voz. - %s aceitou a chamada. - %s encerrou a chamada. + %1$s retirou o convite de %2$s + %1$s mudou seu avatar + %1$s definiu seu nome de exibição para %2$s + %1$s mudou seu nome de exibição de %2$s para %3$s + %1$s removeu seu nome de exibição (era %2$s) + %1$s mudou o tópico para: %2$s + %1$s mudou o nome da sala para: %2$s + %s começou uma chamada de vídeo. + %s começou uma chamada de voz. + %s atendeu a chamada. + %s terminou a chamada. %1$s deixou o histórico futuro da sala visível para %2$s - todos os participantes da sala, a partir do momento em que foram convidados. - todos os participantes da sala, a partir do momento em que entraram nela. - todos os participantes da sala. + todos os membros da sala, do ponto que foram convidados. + todos os mebros da sala, do ponto que se juntaram. + todos os membros da sala. qualquer pessoa. - desconhecido (%s). - %1$s ativou a criptografia de ponta a ponta (%2$s) - %1$s deseja iniciar uma chamada em grupo - Chamada em grupo iniciada - Chamada em grupo encerrada - (a foto de perfil também foi alterada) + desconhecida (%s). + %1$s ativou a encriptação ponta-a-ponta (%2$s) + %1$s requisitou uma conferência de VoIP + Conferência de VoIP começou + Conferência de VoIP terminou + (avatar mudou também) %1$s removeu o nome da sala - %1$s removeu a descrição da sala - %1$s atualizou o perfil %2$s - %1$s enviou um convite para %2$s entrar na sala + %1$s removeu o tópico da sala + %1$s atualizou seu perfil %2$s + %1$s enviou um convite para %2$s para se juntar à sala %1$s aceitou o convite para %2$s - ** Não foi possível descriptografar: %s ** - O aparelho do remetente não nos enviou as chaves para esta mensagem. + ** Incapaz de decriptar: %s ** + O dispositivo do(a) enviador(a) não nos enviou as chaves para esta mensagem. Não foi possível redigir - Não foi possível enviar a mensagem - O envio da imagem falhou + Não foi possível enviar mensagem + Upload de imagem falhou - Erro de conexão à internet - Erro no servidor Matrix + Erro de rede + Erro de Matrix - Atualmente, não é possível entrar novamente em uma sala vazia. + Atualmente não é possível se rejuntar a uma sala vazia. - Endereço de e-mail + Endereço de email Número de telefone - %1$s enviou uma figurinha. + %1$s enviou um sticker. Convite de %s - Convite para sala + Convite de Sala %1$s e %2$s Sala vazia %1$s e 1 outro %1$s e %2$d outros - Você enviou uma foto. - Você enviou uma figurinha. + Você enviou uma imagem. + Você enviou um sticker. Seu convite %1$s criou a sala Você criou a sala Você convidou %1$s - Você entrou na sala + Você juntou-se à sala Você saiu da sala - Você recusou o convite - Você removeu %1$s - Você removeu o banimento de %1$s + Você rejeitou o convite + Você expulsou %1$s + Você desbaniu %1$s Você baniu %1$s - Você desfez o convite a %1$s - Você alterou a sua foto de perfil - Você definiu o seu nome e sobrenome como %1$s - Você alterou o seu nome e sobrenome de %1$s para %2$s - Você removeu o seu nome e sobrenome (era %1$s) - Você alterou a descrição para: %1$s - %1$s alterou a foto da sala - Você alterou a foto da sala - Você alterou o nome da sala para: %1$s - Você iniciou uma chamada de vídeo. - Você iniciou uma chamada de voz. + Você retirou o convite de %1$s + Você mudou seu avatar + Você definiu seu nome de exibição para %1$s + Você mudou seu nome de exibição de %1$s para %2$s + Você removeu seu nome de exibição (era %1$s) + Você mudou o tópico para: %1$s + %1$s mudou o avatar da sala + Você mudou o avatar da sala + Você mudou o nome da sala para: %1$s + Você começou uma chamada de vídeo. + Você começou uma chamada de voz. %s enviou dados para configurar a chamada. Você enviou dados para configurar a chamada. - Você aceitou a chamada. - Você encerrou a chamada. + Você atendeu a chamada. + Você terminou a chamada. Você deixou o histórico futuro da sala visível para %1$s - Você ativou a criptografia de ponta a ponta (%1$s) - %s atualizou esta sala. - Você atualizou esta sala. - Você solicitou uma chamada em grupo + Você ativou a encriptação ponta-a-ponta (%1$s) + %s fez o upgrade desta sala. + Você fez o upgrade desta sala. + Você requisitou uma conferência de VoIP Você removeu o nome da sala - Você removeu a descrição da sala - %1$s removeu a foto da sala - Você removeu a foto da sala - Mensagem apagada - Mensagem apagada por %1$s - Mensagem apagada [motivo: %1$s] - Mensagem apagada por %1$s [motivo: %2$s] - Você atualizou o seu perfil %1$s - Você enviou um convite para %1$s entrar na sala - %1$s cancelou o convite a %2$s para entrar na sala - Você cancelou o convite a %1$s para entrar na sala + Você removeu o tópico da sala + %1$s removeu o avatar da sala + Você removeu o avatar da sala + Mensagem removida + Mensagem removida por %1$s + Mensagem removida [razão: %1$s] + Mensagem removida por %1$s [razão: %2$s] + Você atualizou seu perfil %1$s + Você enviou um convite para %1$s para se juntar à sala + %1$s revogou o convite para %2$s para entrar na sala + Você revogou o convite para %1$s para entrar na sala Você aceitou o convite para %1$s - %1$s adicionou o widget %2$s - Você adicionou o widget %1$s - %1$s removeu o widget %2$s - Você removeu o widget %1$s - %1$s editou o widget %2$s - Você editou o widget %1$s - Administrador - Moderador - Padrão + %1$s adicionou widget %2$s + Você adicionou widget %1$s + %1$s removeu widget %2$s + Você removeu widget %1$s + %1$s modificou widget %2$s + Você modificou widget %1$s + Admin + Moderador(a) + Default Personalizado (%1$d) Personalizado - Você alterou o nível de permissão de %1$s. - %1$s alterou o nível de permissão de %2$s. + Você mudou o nível de poder de %1$s. + %1$s mudou o nível de poder de %2$s. %1$s de %2$s para %3$s - Primeira sincronização: -\nImportando a conta… - Primeira sincronização: -\nImportando as chaves de criptografia - Primeira sincronização: -\nImportando as salas - Primeira sincronização: -\nImportando as salas em que você entrou - Primeira sincronização: -\nImportando as salas em que você foi convidado - Primeira sincronização: -\nImportando as salas em que você saiu - Primeira sincronização: -\nImportando as comunidades - Primeira sincronização: -\nImportando os dados da conta + Sinc Inicial: +\nImportando conta… + Sinc Inicial: +\nImportando crypto + Sinc Inicial: +\nImportando Salas + Sinc Inicial: +\nImportando Salas Que Você Se Juntou + Sinc Inicial: +\nImportando Salas Que Você Foi Convidada(o) + Sinc Inicial: +\nImportando Salas de Que Você Saiu + Sinc Inicial: +\nImportando Comunidades + Sinc Inicial: +\nImportando Dados de Conta Enviando mensagem… Limpar a fila de envio - Convite de %1$s. Motivo: %2$s - O seu convite. Motivo: %1$s - %1$s convidou %2$s. Motivo: %3$s - Você convidou %1$s. Motivo: %2$s - %1$s convidou você. Motivo: %2$s - %1$s entrou na sala. Motivo: %2$s - Você entrou na sala. Motivo: %1$s - %1$s saiu da sala. Motivo: %2$s - Você saiu da sala. Motivo: %1$s - %1$s recusou o convite. Motivo: %2$s - Você recusou o convite. Motivo: %1$s - %1$s removeu %2$s. Motivo: %3$s - Você removeu %1$s. Motivo: %2$s - %1$s removeu o banimento de %2$s. Motivo: %3$s - Você removeu o banimento de %1$s. Motivo: %2$s - %1$s baniu %2$s. Motivo: %3$s - Você baniu %1$s. Motivo: %2$s - %1$s enviou um convite para %2$s entrar na sala. Motivo: %3$s - Você enviou um convite para %1$s entrar na sala. Motivo: %2$s - %1$s revogou o convite para %2$s entrar na sala. Motivo: %3$s - Você revogou o convite para %1$s entrar na sala. Motivo: %2$s - %1$s aceitou o convite para %2$s. Motivo: %3$s - Você aceitou o convite para %1$s. Motivo: %2$s - %1$s desfez o convite de %2$s. Motivo: %3$s - Você desfez o convite de %1$s. Motivo: %2$s + Convite de %1$s. Razão: %2$s + Seu convite. Razão: %1$s + %1$s convidou %2$s. Razão: %3$s + Você convidou %1$s. Razão: %2$s + %1$s convidou você. Razão: %2$s + %1$s juntou-se à sala. Razão: %2$s + Você juntou-se à sala. Razão: %1$s + %1$s saiu da sala. Razão: %2$s + Você saiu da sala. Razão: %1$s + %1$s rejeitou o convite. Razão: %2$s + Você rejeitou o convite. Razão: %1$s + %1$s expulsou %2$s. Razão: %3$s + Você expulsou %1$s. Razão: %2$s + %1$s desbaniu %2$s. Razão: %3$s + Você desbaniu %1$s. Razão: %2$s + %1$s baniu %2$s. Razão: %3$s + Você baniu %1$s. Razão: %2$s + %1$s enviou um convite para %2$s para entrar na sala. Razão: %3$s + Você enviou um convite para %1$s para entrar na sala. Razão: %2$s + %1$s revogou o convite para %2$s para entrar na sala. Razão: %3$s + Você revogou o convite para %1$s para entrar na sala. Razão: %2$s + %1$s aceitou o convite para %2$s. Razão: %3$s + Você aceitou o convite para %1$s. Razão: %2$s + %1$s retirou o convite de %2$s. Razão: %3$s + Você retirou o convite de %1$s. Razão: %2$s - %1$s adicionou %2$s como um endereço desta sala. - %1$s adicionou %2$s como endereços desta sala. + %1$s adicionou %2$s como um endereço para esta sala. + %1$s adicionou %2$s como endereços para esta sala. - Você adicionou %1$s como um endereço desta sala. - Você adicionou %1$s como endereços desta sala. + Você adicionou %1$s como um endereço para esta sala. + Você adicionou %1$s como endereços para esta sala. - %1$s removeu %2$s como um endereço desta sala. - %1$s removeu %2$s como endereços desta sala. + %1$s removeu %2$s como um endereço para esta sala. + %1$s removeu %2$s como endereços para esta sala. - Você removeu %1$s como um endereço desta sala. - Você removeu %1$s como endereços desta sala. + Você removeu %1$s como um endereço para esta sala. + Você removeu %1$s como endereços para esta sala. - %1$s adicionou %2$s e removeu %3$s como endereços desta sala. - Você adicionou %1$s e removeu %2$s como endereços desta sala. - %1$s definiu o endereço principal desta sala como %2$s. - Você definiu o endereço principal desta sala como %1$s. - %1$s removeu o endereço principal desta sala. - Você removeu o endereço principal desta sala. - %1$s permitiu que convidados entrem na sala. - Você permitiu que convidados entrem na sala. - %1$s impediu que convidados entrassem na sala. - Você impediu que convidados entrassem na sala. - %1$s ativou a criptografia de ponta a ponta. - Você ativou a criptografia de ponta a ponta. - %1$s ativou a criptografia de ponta a ponta (algoritmo não reconhecido %2$s). - Você ativou a criptografia de ponta a ponta (algoritmo não reconhecido %1$s). - Você impediu que desconhecidos entrem na sala. - %1$s impediu que desconhecidos entrem na sala. - Você permitiu que desconhecidos entrem aqui. - %1$s permitiu que desconhecidos entrem aqui. - Você saiu. Motivo: %1$s - %1$s saiu. Motivo: %2$s - Você entrou. Motivo: %1$s - %1$s entrou. Motivo: %2$s - Você cancelou o convite para %1$s - %1$s cancelou o convite para %2$s + %1$s adicionou %2$s e removeu %3$s como endereços para esta sala. + Você adicionou %1$s e removeu %2$s como endereços para esta sala. + %1$s definiu o endereço principal para esta sala para %2$s. + Você definiu o endereço principal para esta sala para %1$s. + %1$s removeu o endereço principal para esta sala. + Você removeu o endereço principal para esta sala. + %1$s tem permitido que visitas se juntem à sala. + Você tem permitido que visitas se juntem à sala. + %1$s tem prevenido visitas de se juntarem à sala. + Você tem prevenido visitas de se juntarem à sala. + %1$s ativou encriptação ponta-a-ponta. + Você ativou encriptação ponta-a-ponta. + %1$s ativou encriptação ponta-a-ponta (algoritmo irreconhecido %2$s). + Você ativou encriptação ponta-a-ponta (algoritmo irreconhecido %1$s). + Você tem prevenido visitas de se juntarem à sala. + %1$s tem prevenido visitas de se juntarem à sala. + Você tem permitido que visitas se juntem aqui. + %1$s tem permitido que visitas se juntem aqui. + Você saiu. Razão: %1$s + %1$s saiu. Razão: %2$s + Você juntou-se. Razão: %1$s + %1$s juntou-se. Razão: %2$s + Você revogou o convite para %1$s + %1$s revogou o convite para %2$s Você convidou %1$s %1$s convidou %2$s - Você atualizou esta sala. - %s atualizou esta sala. - Você definiu que as mensagens enviadas a partir do presente momento estarão disponíveis para %1$s - %1$s definiu que as mensagens enviadas a partir do presente momento estarão disponíveis para %2$s + Você fez o upgrade aqui. + %s fez o upgrade aqui. + Você deixou mensagens futuras vísiveis para %1$s + %1$s deixou mensagens futuras visíveis para %2$s Você saiu da sala %1$s saiu da sala - Você entrou - %1$s entrou - Você criou a sala - %1$s criou a sala + Você juntou-se + %1$s juntou-se + Você criou a discussão + %1$s criou a discussão Sala vazia (era %s) %1$s, %2$s, %3$s e %4$d outro @@ -226,24 +226,24 @@ %1$s, %2$s, %3$s e %4$s %1$s, %2$s e %3$s - 🎉 Todos os servidores estão proibidos de participar! Esta sala não pode mais ser usada. - Nenhuma alteração. - • Servidores correspondentes aos IP literais agora estão banidos. - • Servidores correspondentes aos IP literais agora estão permitidos. - • Servidores correspondentes à %s foram removidos da lista de permitidos. - • Servidores correspondentes à %s agora são permitidos. - • Servidores correspondente à %s foram removidos da lista de banidos. - • Servidores correspondentes à %s foram banidos. - Você alterou a lista de controle de acesso (ACL) do servidor para esta sala. - %s alterou a lista de controle de acesso (ACL) do servidor para esta sala. - • Servidores correspondentes aos IP literais estão banidos. - • Servidores correspondentes aos IP literais estão permitidos. - • Servidores correspondentes à %s estão permitidos. - • Servidores correspondentes à %s estão banidos. - Você definiu a lista de controle de acesso (ACL) do servidor para esta sala. - %s definiu a lista de controle de acesso (ACL) do servidor para esta sala. - Você alterou os endereços alternativos desta sala. - %1$s alterou os endereços alternativos desta sala. + 🎉 Todos os servidores estão banidos de participar! Esta sala não pode mais ser usada. + Nenhuma mudança. + • Servidores correspondendo a literais de IP estão agora banidos. + • Servidores correspondendo a literais de IP estão agora permitidos. + • Servidores correspondendo a %s foram removidos da lista de permitidos. + • Servidores correspondendo a %s estão agora permitidos. + • Servidores correspondendo a %s foram removidos da lista de banimento. + • Servidores correspondendo a %s estão agora banidos. + Você mudou as LCAs do servidor para esta sala. + %s mudou as LCAs do servidor para esta sala. + • Servidores correspondendo a literais de IP estão banidos. + • Servidores correspondendo a literais de IP estão permitidos. + • Servidores correspondendo a %s estão permitidos. + • Servidores correspondendo a %s estão banidos. + Você definiu as LCAs do servidor para esta sala. + %s definiu as LCAs do servidor para esta sala. + Você mudou os endereços alternativos para esta sala. + %1$s mudou os endereços alternativos para esta sala. Você removeu o endereço alternativo %1$s para esta sala. Você removeu os endereços alternativos %1$s para esta sala. @@ -260,74 +260,74 @@ %1$s adicionou o endereço alternativo %2$s para esta sala. %1$s adicionou os endereços alternativos %2$s para esta sala. - Você alterou os endereços desta sala. - %1$s alterou os endereços desta sala. - Você alterou os endereços principal e alternativos desta sala. - %1$s alterou os endereços principal e alternativos desta sala. - Você modificou a chamada de vídeo - Chamada de vídeo modificada por %1$s - Você encerrou a chamada de vídeo - Chamada de vídeo encerrada por %1$s - Você começou uma chamada de vídeo - Chamada de vídeo iniciada por %1$s + Você mudou os endereços para esta sala. + %1$s mudou os endereços para esta sala. + Você mudou os endereços principal e alternativos para esta sala. + %1$s mudou os endereços principal e alternativos para esta sala. + Você modificou conferência de vídeo + Conferência de vídeo modificada por %1$s + Você terminou conferência de vídeo + Conferência de vídeo terminada por %1$s + Você começou conferência de vídeo + Conferência de vídeo começada por %1$s Mensagens Sala Configurações - Detalhes do participante + Detalhes de Membros Histórico Aceitar - Recusar - Encerrar + Declinar + Desligar OK Cancelar Salvar - Sair da sala + Sair Enviar Reenviar - Apagar + Remover Citar Compartilhar - Depois + Mais Tarde Encaminhar - Link - Ver código-fonte - Ver código-fonte descriptografado - Apagar + Permalink + Ver Fonte + Ver Fonte Decriptada + Deletar Renomear - Denunciar conteúdo - Chamada em andamento - Chamada em grupo em andamento. -\nEntre com %1$s ou %2$s + Reportar conteúdo + Chamada ativa + Chamada de conferência em curso. +\nJunte-se como %1$s ou %2$s Voz Vídeo - Não foi possível iniciar a chamada, tente mais tarde - Devido à falta de permissões, alguns recursos podem estar faltando… - Você precisa de permissão para convidar, para iniciar uma chamada em grupo nesta sala - Não foi possível iniciar a chamada - Informações da sessão - Não há suporte para chamadas em grupo em salas criptografadas - Enviar mesmo assim + Não é possível começar a chamada, por favor tente mais tarde + Devido a permissões faltando, alguns recursos podem estar faltando… + Você precisa de permissão para convidar para começar uma conferência nesta sala + Não é possível começar chamada + Informação da sessão + Chamadas de conferência não são suportadas em salas encriptadas + Enviar Mesmo Assim ou Convidar - Sair - Chamada de voz - Chamada de vídeo - Busca global + Fazer signout + Chamada de Voz + Chamada de Vídeo + Pesquisa global Marcar tudo como lido Histórico - Responder + Responder rápido Abrir Fechar - Copiado para a área de transferência + Copiado para clipboard Desativar Confirmação - Atenção + Aviso - Início + Home Favoritos Pessoas Salas @@ -342,112 +342,112 @@ Conversas Agenda de endereços local - Apenas contatos na Matrix + Contatos de Matrix somente Nenhuma conversa - Você não permitiu que o ${app_name} acesse seus contatos locais + Você não permitiu que ${app_name} acesse seus contatos locais Nenhum resultado Salas - Lista de salas + Diretório de salas Nenhuma sala Nenhuma sala pública disponível %d usuário %d usuários - Enviar registros - Enviar registros da falha - Enviar recorte de tela - Relatar um erro - Por favor, descreva o erro. O que você fez\? O que você esperava ocorrer\? O que aconteceu\? - Descreva o seu problema aqui - Para diagnosticar problemas, os registros deste cliente serão enviados juntos deste relatório de erros. Este relatório de erros, incluindo os registros e recortes de tela, não serão visíveis publicamente. Se você prefere enviar apenas o texto acima, por favor, desmarque: - Você parece estar agitando o celular rapidamente. Gostaria de enviar um relatório de erro\? - O relatório de erro foi enviado com êxito - Falha ao enviar o relatório de erro (%s) - Andamento (%s%%) - O aplicativo encerrou inesperadamente da última vez. Gostaria de abrir a tela de relatórios de erros\? + Enviar logs + Enviar crash logs + Enviar screenshot + Reportar bug + Por favor descreva o bug. O que você fez\? O que você esperava que acontecese\? O que aconteceu na verdade\? + Descreva seu problema aqui + A fim de diagnosticar problemas, logs deste cliente serão enviados com este reporte de bug. Este reporte de bug, incluindo os logs e o screenshot, não será visível publicamente. Se você prefere somente enviar o texto acima, por favor desmarque: + Você parece estar agitando o celular em frustração. Gostaria de abrir a tela de reporte de bug\? + O reporte de bug tem sido enviado com sucesso + O reporte de bug falhou para enviar (%s) + Progresso (%s%%) + O aplicativo crashou da última vez. Gostaria de abrir a tela de reporte de crash\? Enviar para - Leitura: - Entrar na sala - Nome de usuário - Criar conta - Entrar - Sair - Endereço do servidor principal - Endereço do servidor de identidade + Lido + Juntar-se a Sala + Nome de Usuário + Criar Conta + Fazer Login + Fazer Signout + URL de Servidor de Casa + URL de Servidor de Identidade Pesquisar - Iniciar nova conversa - Iniciar chamada de voz - Iniciar chamada de vídeo + Começar Novo Chat + Começar Chamada de Voz + Começar Chamada de Vídeo Enviar arquivos - Tirar uma foto ou gravar vídeo + Tirar foto ou vídeo - Entrar - Criar conta - Enviar + Fazer login + Criar Conta + Submeter Pular - Enviar e-mail para redefinir senha - Voltar à tela de login - E-mail ou nome de usuário + Enviar Email de Reset + Retornar a tela de login + Email ou nome de usuário Senha Nova senha Nome de usuário - Endereço de e-mail - Endereço de e-mail (opcional) + Endereço de email + Endereço de email (opcional) Número de telefone Número de telefone (opcional) - Repita a senha - Confirme sua nova senha - Nome de usuário e/ou senha incorretos - Nomes de usuário só podem conter letras, números, pontos, hifens e sublinhados - Senha muita curta (mínimo de 6 caracteres) - Falta a senha - Este não parece ser um endereço de e-mail válido - Este não parece ser um número de telefone válido - Este e-mail já está cadastrado. - Falta o endereço de e-mail - Falta o número de telefone - Falta o endereço de e-mail ou o número de telefone + Repetir senha + Confirmar sua nova senha + Nome de usuário e/ou senha incorreta(s) + Nomes de usuário só podem conter letras, números, pontos, hifens e underscores + Senha curta demais (mín 6) + Senha faltando + Isto não parece com um endereço de email válido + Isto não parece com um número de telefone válido + Este endereço de email já está definido. + Endereço de email faltando + Número de telefone faltando + Endereço de email ou número de telefone faltando Token inválido - As senhas não correspondem - Esqueceu sua senha? - Use opções para servidor personalizado (avançado) - Por favor, verifique o seu e-mail para continuar a inscrição - Atualmente, registrar-se com e-mail e número de telefone ao mesmo tempo não é possível. Apenas o número de telefone será levado em consideração. + Senhas não batem + Esqueceu senha\? + Usar opções de servidor personalizado (avançado) + Por favor cheque seu email para continuar registro + Registro com email e número de telefone de vez não é suportado ainda até que a api exista. Somente o número de telefone será levado em consideração. \n -\nNo entanto, você pode adicionar o endereço de e-mail ao seu perfil nas configurações. - Este servidor local quer se certificar de que você não é um robô - Nome de usuário indisponível - Servidor principal: - Servidor de identidade: - Eu verifiquei o meu endereço de e-mail - Para redefinir sua senha, digite o endereço de e-mail vinculado à sua conta: - O e-mail vinculado à sua conta precisa ser informado. - Uma nova senha precisa ser inserida. - Um e-mail foi enviado para %s. Após clicar no link contido no e-mail, clique abaixo. - Falha ao confirmar o endereço de e-mail: certifique-se de clicar no link do e-mail - Sua senha foi alterada. +\nVocê pode adicionar seu email a seu perfil em configurações. + Este Servidor de Casa gostaria de assegurar que você não é um robô + Nome de usuário em uso + Servidor de Casa: + Servidor de Identidade: + Eu tenho verificado meu endereço de email + Para resettar sua senha, entre o endereço de email linkado a sua conta: + O endereço de email linkado a sua conta deve ser entrado. + Uma nova senha deve ser entrada. + Um email tem sido enviado para %s. Uma vez que tenha seguido o link que ele contém, clique abaixo. + Falha ao verificar endereço de email: assegure-se que clicou no link no email + Sua senha tem sido resettada. \n -\nVocê foi desconectado de todas as sessões e não receberá mais notificações. Para reativar as notificações, faça login novamente em cada aparelho. +\nVocê tem sido feito logout de todas as sessões e não vai mais receber notificações push. Para reativar notificações, faça re-login em cada dispositivo. - O endereço precisa começar com http[s]:// - Não foi possível fazer login: erro de rede - Não foi possível fazer login - Não foi possível criar conta: erro de rede - Não foi possível criar conta - Não foi possível criar conta: falha na verificação de posse do e-mail - Por favor, digite um endereço válido - Nome de usuário ou senha inválido + URL deve começar com http[s]:// + Incapaz de fazer login: Erro de rede + Incapaz de fazer login + Incapaz de registrar: Erro de rede + Incapaz de registrar + Incapaz de registrar: falha de posse de email + Por favor entre um URL válido + Nome de usuário/senha inválida(o) O token de acesso especificado não foi reconhecido - JSON mal formatado - Não contem um JSON válido - Muitas solicitações foram enviadas - Este nome de usuário já está em uso - O link no e-mail ainda não foi clicado + JSON malformado + Não continha JSON válido + Requisições demais tem sido enviadas + Este nome de usuário já é usado + O link de email que não tem sido clicado ainda - Lista de confirmações de leitura + Lista de Recibos de Leitura Enviar como @@ -457,7 +457,7 @@ Pequeno Cancelar o download? - Cancelar o envio\? + Cancelar o upload\? %d s %1$dm %2$ds @@ -465,272 +465,273 @@ Hoje Nome da sala - Descrição da sala + Tópico da sala - Chamada aceita - Iniciando chamada… - Chamada encerrada + Chamada conectada + Chamada conectando… + Chamada terminada Chamando… - Recebendo chamada - Recebendo chamada de vídeo - Recebendo chamada de voz - Chamada em andamento… - A pessoa não atendeu a chamada. - A conexão à mídia falhou - Não foi possível iniciar a câmera - chamada atendida em outro lugar + Chamada Recebendo + Chamada de Vídeo Recebendo + Chamada de Voz Recebendo + Chamada Em Progresso… + O lado remoto falhou ao atender. + Conexão de Mídia Falhou + Não é possível inicializar a câmera + chamada atendida em algum outro lugar - Tirar uma foto ou gravar vídeo + Tirar uma foto ou um vídeo Não é possível gravar vídeo Informação - ${app_name} precisa de permissão para acessar sua galeria de fotos e vídeos para enviar e salvar anexos. + ${app_name} precisa de permissão para acessar sua biblioteca de fotos e vídeos para enviar e salvar anexos. \n -\nPor favor, permita o acesso na próxima tela para poder enviar arquivos do seu celular. - ${app_name} necessita permissão para acessar sua câmera para poder tirar fotos e fazer chamadas de vídeo. +\nPor favor permita acesso no próximo pop-up para ser capaz de enviar arquivos do seu celular. + ${app_name} precisa de permissão para acessar sua câmera para tirar fotos e chamadas de vídeo. " \n -\nPor favor, permita o acesso na próxima tela para fazer a chamada." - ${app_name} necessita permissão para acessar seu microfone para realizar chamadas de áudio. +\nPor favor permita acesso no próximo pop-up para ser capaz de fazer a chamada." + ${app_name} precisa de permissão para acessar seu microfone para performar chamadas de áudio. " \n -\nPor favor, permita o acesso na próxima tela para fazer a chamada." - ${app_name} necessita permissão para acessar sua câmera e seu microfone para fazer chamadas de vídeo. +\nPor favor permita acesso no próximo pop-up para ser capaz de fazer a chamada." + ${app_name} precisa de permissão para acessar sua câmera e seu microfone para performar chamadas de vídeo. \n -\nPor favor, permita o acesso na próxima tela para fazer a chamada. - ${app_name} precisa de permissão para acessar os seus contatos para poder encontrar outros usuários a partir de seus e-mails e números de telefone. Se você concordar em usar a sua lista de contatos para esse propósito, permita o acesso na próxima janela pop-up. - ${app_name} precisa de permissão para acessar os seus contatos para poder encontrar outros usuários a partir de seus e-mails e números de telefone. +\nPor favor permita acesso no próximo pop-up para ser capaz de fazer a chamada. + ${app_name} pode checar seu livro de endereços para achar outras(os) usuárias(os) de Matrix baseado em seus email e números de telefone. Se você concorda em compartilhar seu livro de endereços para este propósito, por favor permita acesso no próximo pop-up. + ${app_name} pode checar seu livro de endereços para encontrar outras(os) usuásias(os) de Matrix baseado em seus email e números de telefone. \n -\nVocê concorda em usar a sua lista de contatos para esse propósito\? - Desculpe. A ação não foi realizada, por falta de permissão +\nVocê concorda em compartilhar seu livro de endereços para este propósito\? + Desculpe. Ação não performada, devido a permissões faltando Salvo - Salvar nos downloads? + Salvar em downloads\? SIM NÃO Continuar - Apagar - Entrar - Visualizar - Recusar + Remover + Juntar-se + Previsualizar + Rejeitar - Ir para a primeira mensagem não lida. + Pular para primeira mensagem não-lida. - Você foi convidada(o) a entrar nesta sala por %s - Este convite foi enviado a %s, que não está associado com esta conta. -\nVocê pode querer fazer login com uma conta diferente, ou adicionar este e-mail à sua conta. - Você está tentando acessar %s. Quer entrar na sala para poder participar da conversa? + Você tem sido convidada(o) a juntar-se a esta sala por %s + Este convite foi enviado a %s, que não está associada(o) com esta conta. +\nVocê pode desejar fazer login com uma conta diferente, ou adicionar este email a sua conta. + Você está tentando acessar %s. Gostaria de se juntar a fim de participar da discussão\? uma sala - Esta é uma pré-visualização desta sala. Interações com esta sala estão desativadas. + Esta é uma previsualização desta sala. Interações de sala têm sido desativadas. - Nova conversa - Adicionar uma pessoa - 1 participante + Novo Chat + Adicionar membro + 1 membro Sair da sala - Tem certeza de que deseja sair da sala\? - Deseja remover %s desta conversa\? + Você tem certeza que quer sair da sala\? + Você tem certeza que quer remover %s deste chat\? Criar Online Offline Ocioso - FERRAMENTAS DE ADMINISTRAÇÃO + FERRAMENTAS DE ADMIN CHAMADA - Conversas + Mensagens Diretas SESSÕES Convidar - Sair da sala + Sair desta sala Remover desta sala - Banir da sala - Remover banimento - Redefinir como usuário normal - Tornar moderador + Banir + Desbanir + Resettar a usuária(o) normal + Tornar moderador(a) Tornar admin - Bloquear - Desbloquear - ID de usuário, nome e sobrenome ou e-mail + Ignorar + Designorar + ID de usuário, Nome ou email Mencionar - Mostrar lista de sessões - Você não poderá desfazer esta alteração, já que você está promovendo este usuário para ter o mesmo nível de permissões que você. -\nTem certeza\? - "Você tem certeza que quer convidar %s para esta conversa?" + Mostrar Lista de Sessões + Você não vai ser capaz de desfazer esta mudança já que você está promovendo a(o) usuária(o) para ter o mesmo nível de poder que você. +\nVocê tem certeza\? + Você tem certeza que quer convidar %s para este chat\? Convidar por ID CONTATOS LOCAIS (%d) - Apenas usuários Matrix - Convidar pessoa por sua ID - Por favor, digite um ou mais endereços de e-mail ou ID Matrix - E-mail ou ID Matrix + Usuárias(os) Matrix somente + Convidar usuária(o) por ID + Por favor entre um ou mais endereços de email ou ID Matrix + Email ou ID Matrix Pesquisar %s está digitando… %1$s & %2$s estão digitando… - %1$s & %2$s & outros estão digitando… - Digite uma mensagem criptografada… - Digite uma mensagem (não criptografada)… - A conexão com o servidor se perdeu. + %1$s & %2$s & outras(os) estão digitando… + Envie uma mensagem encriptada… + Envie uma mensagem (não-encriptada)… + Conectividade ao servidor tem sido perdida. Mensagens não enviadas. %1$s ou %2$s agora? - Mensagens não enviadas por causa da presença de sessões desconhecidas. %1$s ou %2$s agora\? + Mensagens não enviadas devido a sessões desconhecidas estarem presentes. %1$s ou %2$s agora\? Reenviar todas Cancelar todas - Reenviar mensagens não enviadas - Apagar mensagens não enviadas + Reenviar mensagens não-enviadas + Deletar mensagens não-enviadas Arquivo não encontrado - Você não tem permissão para digitar nesta sala + Você não tem permissão para postar nesta sala Confiar Não confiar - Sair da Conta - Bloquear + Fazer logout + Ignorar Impressão digital (%s): - Não foi possível confirmar a identidade do servidor remoto. - Isso pode significar que alguém está interceptando suas mensagens de forma maliciosa, ou então o seu celular não confia no certificado fornecido pelo servidor remoto. - Se o administrador do servidor disse que isso era esperado, verifique se a impressão digital abaixo é a mesma que a impressão digital que ele forneceu a você. - Você tinha um certificado confiável para o seu telefone, mas ele mudou. Isso é ALTAMENTE INCOMUM. É recomendável que você NÃO ACEITE este novo certificado. - O certificado foi alterado de um anteriormente confiável para um que não é confiável. O servidor pode ter renovado seu certificado. Entre em contato com o administrador do servidor para obter a impressão digital esperada. - Apenas aceite o certificado se o administrador do servidor publicou uma impressão digital que é idêntica a que está acima. + Não foi possível verificar identidade de servidor remoto. + Isto poderia significar que alguém está maliciosamente interceptando seu tráfico, ou que seu celular não confia no certificado provido pelo servidor remoto. + Se o(a) administrador(a) do servidor tem dito que isto é esperado, assegure que a impressão digital abaixo corresponde à impressão digital provida por ele(a). + O certificado tem mudado de um que era confiado por seu telefone. Isto é ALTAMENTE INCOMUM. É recomendado que você NÃO ACEITE este novo certificado. + O certificado tem sido mudado de um previamente confiado para um que não é confiado. O servidor pode ter renovado seu certificado. Conacte o(a) administrador(a) do servidor para a impressão digital esperada. + Somente aceite o certificado se o(a) administrador(a) do servidor tem publicado uma impressão digital que bate com a acima. - Detalhes da sala + Detalhes da Sala Pessoas Arquivos Configurações - ID mal formatado. Precisa ser um endereço de e-mail ou um ID Matrix, como \'@participantelocal:dominio\' - CONVIDADOS - ENTRARAM + ID malformado. Devia ser um endereço de email ou um ID Matrix como \'@partlocal:dominio\' + CONVIDADAS(OS) + SE JUNTARAM - Motivo de denunciar este conteúdo - Deseja ocultar todas as mensagens deste usuário\? + Razão por reportar este conteúdo + Você quer esconder todas as mensagens deste usuário\? \n -\nEsta ação irá reiniciar o aplicativo e poderá demorar um pouco. - Cancelar envio - Cancelar download +\nNote que esta ação irá reiniciar o app e pode levar algum tempo. + Cancelar Upload + Cancelar Download Pesquisar - Pesquisar participantes da sala + Filtrar membros da sala Nenhum resultado SALAS MENSAGENS PESSOAS ARQUIVOS - ENTRAR - LISTA PÚBLICA + JUNTAR-SE + DIRETÓRIO FAVORITOS SALAS BAIXA PRIORIDADE CONVITES - Iniciar conversa + Começar chat Criar sala - Entrar na sala - Entrar em uma sala - Digite o ide ou o apelido de uma sala + Juntar-se a sala + Juntar-se a uma sala + Digite um id de sala ou um alias de sala - Pesquisar na lista pública - Buscando na lista… + Navegar diretório + Pesquisando diretório… Favoritar - Despriorizar - Conversa direta - Sair da conversa + Des-prioritizar + Chat Direto + Sair de Conversa Esquecer Mensagens Configurações Versão - Termos e condições - Licenças de terceiros - Direito autoral + Termos & condições + Notas de terceiros + Direito de autor Política de privacidade - Foto de perfil - Nome e sobrenome - E-mail - Adicionar endereço de e-mail + Imagem de Perfil + Nome de Exibição + Email + Adicionar endereço de email Telefone Adicionar número de telefone - Mostrar informações do aplicativo nas configurações do sistema. - Informações sobre o aplicativo - Receba notificações de novas mensagens - Ativar notificações nesta sessão - Acender a tela por 3 segundos - Mensagens em conversas individuais - Mensagens em salas - Quando eu for convidada(o) a uma sala - Recebendo chamada - Mensagens enviadas por bots - Sincronização em segundo plano - Ativar a sincronização em segundo plano - Tempo expirado na solicitação de sincronização - Demora entre cada solicitação + Mostrar info de aplicativo nas configurações de sistema. + Info de aplicativo + Ativar notificações para esta conta + Ativar notificações para esta sessão + Ligar a tela por 3 segundos + Mnsgns em chats um-a-um + Mnsgns em chats de grupo + Quando eu sou convidada(o) a uma sala + Convites de chamada + Mensagens enviadas por bot + Sincronização no background + Ativar sinc no background + Timeout de requisição de sinc + Delay entre casa Sinc Versão - Versão do olm - Termos e condições - Licenças de terceiros - Direitos autorais + versão de olm + Termos & condições + Notas de terceiros + Direitos de autor Política de privacidade - Limpar cache e recarregar + Limpar cache - Configurações de usuário + Configurações de usuária(o) Notificações - Usuários bloqueados - Outros - Avançado + Usuárias(os) ignoradas(os) + Outras + Avançadas Criptografia - Aparelhos notificados + Alvos de Notificação Contatos locais Permissão de acesso a contatos - País da agenda de contatos - Página inicial + País de livro de telefone + Dsiplay home Fixar salas com notificações perdidas - Fixar salas com mensagens não lidas + Fixar salas com mensagens não-lidas Sessões - Detalhes da sessão + Informação de sessão ID - Nome - Nome público do aparelho - Visto por último às - %1$s em %2$s - Esta operação exige autenticação adicional.\nPara continuar, digite sua senha. + Nome Público + Atualizar Nome Público + Visto por último + %1$s @ %2$s + Esta operação requer autenticação adicional. +\nPara continuar, por favor entre sua senha. Autenticação Senha: - Enviar - Conectado como - Servidor Principal (Home Server) - Servidor de identidade - Confirmação pendente - Por favor, verifique o seu e-mail e clique no link que está lá. Feito isso, clique em continuar. - Não não foi possível confirmar o seu endereço de e-mail. Por favor, verifique o seu e-mail e clique no link que ele contém. Feito isso, clique em continuar. - Este endereço de e-mail já está em uso. - Este endereço de e-mail não foi encontrado. + Submeter + Feito login como + Servidor de Casa + Servidor de Identidade + Verificação Pendente + Por favor cheque seu email e clique no link que ele contém. Uma vez que isto for feito, clique em continuar. + Incapaz de verificar endereço de email. Por favor cheque seu email e clique no link que ele contém. Uma vez que isto for feito, clique em continuar. + Este endereço de email já está em uso. + Este endereço de email não foi encontrado. Este número de telefone já está em uso. - Alterar senha + Mudar senha Senha atual - Nova senha - Confirme a nova senha - Não consegui atualizar a senha - Sua senha foi atualizada + Senha nova + Confirmar senha nova + Falha ao atualizar senha + Sua senha tem sido atualizada Mostrar todas as mensagens de %s\? \n -\nNote que esta ação irá reiniciar o aplicativo e pode levar algum tempo. - Deseja deixar de notificar este aparelho\? - Deseja remover o %1$s %2$s\? +\nNote que esta ação vai reiniciar o app e pode levar algum tempo. + Você tem certeza que quer remover este alvo de notificação\? + Você tem certeza que quer remover o %1$s %2$s\? Escolha um país País - Por favor, escolha um país + Por favor escolha um país Número de telefone Número de telefone inválido para o país selecionado - Confirmação do telefone - "Nós enviamos um SMS com um código de ativação. Por favor, digite este código abaixo." - Digite o código de ativação - Erro ao validar o seu número de telefone + Verificação de telefone + Nós temos enviado um SMS com um código de ativação. Por favor entre este código abaixo. + Entre um código de ativação + Erro enquanto validando seu número de telefone Código - Foto da sala - Nome da sala - Descrição - Etiqueta da sala + Foto da Sala + Nome da Sala + Tópico + Etiqueta da Sala Etiquetada como: Favoritar @@ -738,124 +739,124 @@ Nenhuma Acesso e visibilidade - Exibir esta sala na lista pública de salas - Acesso à sala - Legibilidade do histórico da sala - Quem pode ler o histórico de mensagens? + Listar esta sala em diretório de salas + Acesso a Sala + Legibilidade de Histórico da Sala + Quem pode ler o histórico\? Quem pode acessar esta sala? Qualquer pessoa - Apenas participantes (a partir do momento em que esta opção foi escolhida) - Apenas participantes (desde que foram convidados) - Apenas participantes (desde que entraram na sala) + Membros somente (desde o ponto no tempo de selecionar esta opção) + Membros somente (desde que eles foram convidados) + Membros somente (desde que eles se juntaram) - Para fazer um link para uma sala, ela precisa ter um endereço. - Apenas as pessoas que foram convidadas + Para linkar a uma sala ela deve ter um endereço. + Somente pessoas que têm sido convidadas Qualquer pessoa que saiba o link da sala, exceto visitantes Qualquer pessoa que saiba o link da sala, incluindo visitantes - Usuários banidos + Usuárias(os) banidas(os) - Avançado + Avançadas ID interno desta sala Endereços - Laboratórios - Estes são recursos experimentais que podem quebrar de forma inesperada. Use com cautela. - Criptografia de ponta a ponta - A criptografia de ponta a ponta está ativada - Você precisa desconectar para poder ativar a criptografia. - Criptografar apenas para sessões confirmadas - Nunca enviar mensagens criptografadas para sessões não confirmadas nesta sala, a partir desta sessão. + Labs + Estes são recursos experimentais que podem quebrar de maneiras inesperadas. Use com cuidado. + Encriptação Ponta-a-Ponta + Encriptação Ponta-a-Ponta está ativa + Você precisa fazer logout para ser capaz de ativar a encriptação. + Encriptar para sessões verificadas somente + Nunca enviar mensagens encriptadas para sessões não-confirmadas nesta sala desta sessão. - Esta sala não tem endereços locais - Novo endereço (p.ex: #foo:matrix.org") - Formato inválido de alias + Esta sala não tem nenhum endereço local + Novo endereço (e.g. #foo:matrix.org) + Formato de alias inválido \'%s\' não é um formato válido para um alias - Você não terá endereço principal especificado para esta sala. - Alertas do endereço principal + Você não vai ter nenhum endereço principal especificado para esta sala. + Avisos de endereço principal Definir como endereço principal - Retirar este endereço como principal - Copiar o ID desta sala - Copiar o endereço desta sala - A criptografia está ativada nesta sala. - A criptografia está desativada nesta sala. - Ativar criptografia· -\n(atenção: não é possível desativar depois!) + Des-definir como endereço principal + Copiar ID de Sala + Copiar Endereço da Sala + Encriptação está ativada nesta sala. + Encriptação está desativada nesta sala. + Ativar encriptação +\n(atenção: não pode ser desativado de novo!) - Lista + Diretório - %s tentou carregar um trecho específico da conversa desta sala, mas não conseguiu. + %s estava tentando carregar um ponto específico na timeline desta sala mas foi incapaz de encontrá-lo. - Informação sobre a criptografia de ponta a ponta + Informação de encriptação ponta-a-ponta Informação do evento - ID de usuária/o - Chave de identidade curve25519 - Chave de impressão digital ed25519 reivindicada + Id de usuária(o) + Chave de identidade Curve25519 + Chave de impressão digital Ed25519 clamada Algoritmo ID de Sessão - Erro de descriptografia - Informação sobre a sessão do remetente - Nome público do aparelho - Nome - ID da sessão + Erro de decriptação + Informação de sessão do(a) enviador(a) + Nome público + Nome público + ID de sessão Chave da sessão - Confirmação - Impressão digital ed25519 - Exportar chaves de sala E2E + Verificação + Impressão digital Ed25519 + Exportar chaves de sala PAP Exportar chaves de sala Exportar as chaves para um arquivo local Exportar - Digite a frase secreta - Confirme a frase secreta - As chaves E2E da sala foram salvas em \'%s\'. + Entrar frasepasse + Confirmar frasepasse + As chaves de sala PAP têm sido salvas em \'%s\'. \n -\nAtenção: este arquivo poderá ser apagado se o aplicativo for desinstalado. - Importar chaves de sala E2E +\nAviso: este arquivo pode ser deletado se o aplicativo for desinstalado. + Importar chaves de sala PAP Importar chaves de sala Importar as chaves de um arquivo local Importar - Criptografar apenas para sessões confirmadas - Nunca enviar mensagens criptografadas para sessões não confirmadas, a partir desta sessão. - Não confirmado - Confirmado + Encriptar para sessões confirmadas somente + Nunca enviar mensagens encriptadas para sessões não-verificadas desta sessão. + Não Verificada + Verificada Na lista negra sessão desconhecida nenhuma - Confirmar - Marcar como não confirmado + Verificar + Desverificar Colocar na lista negra Retirar da lista negra - Confirmar sessão - Compare as seguintes informações com aquelas na sessão do outro usuário e confirme: - Se não corresponderem, a segurança da sua comunicação pode estar comprometida. - Eu confirmo que as chaves são iguais + Verificar sessão + Confirme ao comparar o seguinte com as Configurações de Usuária(o) em sua outra sessão: + Se não baterem, a segurança de sua comunicação pode estar comprometida. + Eu verifico que as chaves batem - Esta sala contém sessões desconhecidas - Esta sala contém sessões desconhecidas que não foram confirmadas. -\nIsso significa que não há garantia de que estas sessões realmente pertencem aos usuários identificados. -\nRecomendamos que você faça o processo de confirmação para cada sessão desconhecida antes de continuar, mas você pode reenviar a mensagem sem confirmar os aparelhos, se preferir. + Sala contém sessões desconhecidas + Esta sala contém sessões desconhecidas que não têm sido verificadas. +\nIsto significa que não há nenhuma garantia que as sessões pertencem às(aos) usuárias(os) às(aos) quais elas clamam pertencer. +\nNós recomendamos que você passe pelo processo de verificação para cada sessão antes de continuar, mas você pode reenviar a mensagem sem verificar se você preferir. \n -\nSessões desconhecidas nesta sala: +\nSessões desconhecidas: - Escolha uma lista pública de salas + Selecione um diretório de salas O servidor pode estar indisponível ou sobrecarregado - Digite um servidor local, a partir do qual serão listadas as salas públicas - Endereço do servidor principal - Todas as salas com o servidor %s - Todas as salas nativas em %s + Digite um servidor de casa para de onde listar salas públicas + URL de servidorcasa + Todas as salas em servidor %s + Todas as salas nativas de %s - Pesquisar no histórico + Pesquisar por histórico Offline - Lista de usuários - LISTA DE USUÁRIOS (%s) - Iniciar com o sistema - Esvaziar o cache de mídia + Diretório de usuários + DIRETÓRIO DE USUÁRIOS (%s) + Começar em boot + Limpar cache de mídia Manter mídia - Mostrar a hora para todas as mensagens + Mostrar timestamps para todas as mensagens Modo de economia de dados - Aparência - Idioma - Escolha o idioma + Interface de usuária(o) + Língua + Escolher língua 3 dias 1 semana 1 mês @@ -869,112 +870,112 @@ Maior Ainda maior Gigantesco - Tema claro - Tema escuro - Tema preto + Tema Claro + Tema Escuro + Tema Preto Sincronizando… - Escutando eventos - Notificações com som + À escuta por eventos + Notificações barulhentas Notificações silenciosas - Relatar um erro - Detalhes da comunidade + Reporte de bug + Detalhes de Comunidade Carregando… Sair Comunidades Filtrar nomes de comunidades Convidar Comunidades - Nenhuma comunidade - Tem certeza de que deseja iniciar uma nova conversa com %s\? - Tem certeza de que deseja iniciar uma chamada de voz\? - Tem certeza de que deseja iniciar uma chamada de vídeo\? + Nenhum grupo + Você tem certeza que quer começar um novo chat com %s\? + Você tem certeza que quer começar uma chamada de voz\? + Você tem certeza que quer começar uma chamada de vídeo\? Tirar foto - Gravar vídeo - Lista de comunidades - Chamada de voz - Banir o usuário resultará em sua remoção desta sala, impedindo-o de entrar nela novamente. - Todas as mensagens novas (com som) - Todas as mensagens novas - Apenas @menções - Silenciar - Adicionar o Element na tela inicial + Tirar vídeo + Lista de Grupos + Chamar + Banir usuária(o) vai expulsá-la(o) desta sala e preveni-la(o) de se juntar de novo. + Todas as mensagens (barulhento) + Todas as mensagens + Menções somente + Mudo + Adicionar a tela de Início Som de notificação - Mensagens que contenham o meu nome e sobrenome - Mensagens que contenham o meu nome de usuário - Visualização prévia do endereço - Mostrar a hora no formato de 12 horas - Vibrar ao mencionar um usuário - Estatísticas de uso - Ícone + Mnsgns contendo meu nome de exibição + Mnsgns contando meu nome de usuário + Previsualização de URL emlinha + Mostrar timestamps em formato de 12 horas + Vibrar ao mencionar um(a) usuário(a) + Analítica + Flair Notificações - Esta sala não está mostrando ícones de nenhuma comunidade - Nova ID da comunidade (p.ex: +foo:matrix.org) - ID de comunidade inválido - \'%s\' não é um ID de comunidade válido - Você necessita ter permissões para poder gerenciar os widgets desta sala - A criação do widget falhou - Crie chamadas em grupo com jitsi - Você tem certeza que quer apagar este widget desta sala? + Esta sala não está mostrando flair para nenhuma comunidade + Nova ID de comunidade (e.g. +foo:matrix.org) + ID de comunidade inválida + \'%s\' não é uma ID de comunidade válida + Você precisa de permissão para gerenciar widgets nesta sala + Criação de widget tem falhado + Criar chamadas de conferência com jitsi + Você tem certeza que quer deletar o widget desta sala\? - Impossível criar o widget. - O envio do pedido falhou. - O nível de permissão precisa ser um número inteiro positivo. + Incapaz de criar widget. + Falha ao enviar requisição. + Nível de poder deve ser um inteiro positivo. Você não está nesta sala. - Você não tem permissões para fazer isso nesta sala. - O pedido veio sem o room_id. - O pedido veio sem o user_id. - A sala %s não está visível. - Adicionar aplicativos Matrix - Usar a câmera nativa + Você não tem permissão para fazer isso nesta sala. + room_id faltando em requisição. + user_id faltando em requisição. + Sala %s não está visível. + Adicionar apps Matrix + Usar câmera nativa - Você adicionou uma nova sessão \'%s\', que está solicitando as chaves de criptografia. - Sua sessão não confirmada \'%s\' está solicitando as chaves de criptografia. - Iniciar a confirmação - Compartilhar sem confirmar - Ignorar a solicitação + Você adicionou uma nova sessão \'%s\', que está requisitando chaves de encriptação. + Sua sessão não-verificada \'%s\' está requisitando chaves de encriptação. + Começar verificação + Compartilhar sem verificar + Ignorar requisição - Atenção! - Chamadas em grupo estão em desenvolvimento, portanto podem não ser estáveis. + Aviso! + Chamamento de conferência está em desenvolvimento e pode não ser estável. Erro de comando - Comando desconhecido: %s + Comando irreconhecido: %s - Desativado - Ativado com som - Mensagem criptografada + Desativada + Barulhenta + Mensagem encriptada Criar - Criar comunidade + Criar Comunidade Nome da comunidade Exemplo - ID da comunidade + ID de comunidade exemplo - Início + Home Pessoas Salas - Sem ninguém + Nenhum(a) usuário(a) Salas - Entrou - em que foi convidada/o - Pesquisar participantes da comunidade - Filtrar salas da comunidade - O(A) administrador(a) desta comunidade não definiu uma descrição longa da mesma. - Você foi removido da sala %1$s por %2$s - Você foi banido da sala %1$s devido à %2$s - Motivo: %1$s - Entrar novamente - Esquecer a sala + Juntou-se + Convidada(o) + Filtrar membros do grupo + Filtrar salas do grupo + O(a) administrador(a) da comunidade não tem provido uma descrição longa para esta comunidade. + Você foi expulsa(o) de %1$s por %2$s + Você foi banida(o) de %1$s por %2$s + Razão: %1$s + Rejuntar-se + Esquecer sala Ações Abrir cabeçalho Sincronizando… - %d participante ativo - %d participantes ativos + %d membro ativo + %d membros ativos - %d participante - %d participantes + %d membro + %d membros %d nova mensagem @@ -989,17 +990,17 @@ %1$s salas encontradas para %2$s - %d alteração na filiação - %d alterações na filiação + %d mudança de filiação + %d mudanças de filiação - Lista de participantes + Listar membros - %d mensagem não lida - %d mensagens não lidas + %d mensagem notificada não-lida + %d mensagens notificadas não-lidas - %d mensagem não lida - %d mensagens não lidas + %d mensagem notificada não-lida + %d mensagens notificadas não-lidas %d sala @@ -1011,32 +1012,32 @@ %d widgets ativos - Foto de perfil para a confirmação de leitura - Foto de perfil para avisos - Foto de perfil - Agite rapidamente para relatar um erro + Avatar de recibo + Avatar de nota + Avatar + Agite com raiva para reportar bug Normal Privacidade reduzida - O app necessita de permissão para rodar em segundo plano - Enviar uma figurinha + O app precisa de permissão para rodar no background + Enviar um sticker Licenças de terceiros - Baixar + Fazer Download Falar Limpar - Devido à falta de permissões, essa ação não é possível. - Alertas do sistema - Se possível, escreva a descrição em inglês, por favor. - Enviar áudio - Enviar uma figurinha - No momento, você não tem nenhum pacote de figurinhas ativado. + Devido a permissões faltando, esta ação não é possível. + Alertas de Sistema + Se possível, por favor escreva a descrição em Inglês. + Enviar voz + Enviar sticker + Atualmente você não tem nenhum pacote de stickers ativado. \n -\nQuer adicionar alguns agora\? +\nAdicionar alguns agora\? continuar com… - Desculpe, nenhum aplicativo externo foi encontrado para concluir esta ação. - Solicitar novamente as chaves de criptografia das suas outras sessões.Pedir novamente as chaves de criptografia de seus outros dispositivos. + Desculpe, nenhum aplicativo externo foi encontrado para completar esta ação. + Re-requisitar chaves de encriptação de suas outras sessões. Requisição de chave enviada. Requisição enviada - Por favor, inicie o ${app_name} em outro aparelho que possa descriptografar a mensagem, de modo que ele possa enviar as chaves para esta sessão. + Por favor lance ${app_name} num outro dispositivo que possa decriptar a mensagem para que ele possa enviar as chaves para esta sessão. %ds %ds @@ -1050,189 +1051,189 @@ %dh - %d dia - %d dias + %dd + %dd %1$s agora - há %1$s %2$s + %1$s %2$s atrás "%1$s, " %1$s e %2$s %1$s %2$s - Digite sua resposta criptografada… - Digite sua resposta (não criptografada)… + Envie uma resposta encriptada… + Envie uma resposta (não-encriptada)… - %d selecionado - %d selecionados + %d selecionada + %d selecionadas - Privacidade das notificações - • As notificações são enviadas pelo \"Google Cloud Messaging\" - • As notificações contém apenas metadados - • O conteúdo das mensagens da notificação é armazenado de forma segura diretamente no servidor Matrix - • As notificações contém metadados e os dados da mensagem - • As notificações não exibirão o conteúdo da mensagem - Pré-visualizar a mídia antes de enviá-la - Desativar minha conta + Privacidade de notificações + • Notificações são enviadas via Firebase Cloud Messaging + • Notificações somente contêm meta dados + • Conteúdo de mensagem da notificação é localizado seguramente direto do servidor de casa de Matrix + • Notificações contêm metadados e dados de mensagem + • Notificações não vão mostrar conteúdo de mensagem + Previsualizar mídia antes de enviar + Desativar conta Desativar minha conta - Privacidade das notificações - ${app_name} pode funcionar em segundo plano para gerenciar as suas notificações de forma segura e confidencial. Isso poderá impactar o uso da bateria. - Conceder a permissão - Escolha outra opção - Enviar dados de uso - ${app_name} coleta dados de uso anônimos para nos ajudar a melhorar o aplicativo. - Por favor, ative o envio de dados de uso para nos ajudar a melhorar o ${app_name}. + Privacidade de Notificação + ${app_name} pode rodar no background para gerenciar suas notificações seguramente e privadamente. Isto pode afetar uso de bateria. + Conceder permissão + Escolha um outra opção + Enviar dados de analítica + ${app_name} coleta analítica anônima para nos permitir melhorar o aplicativo. + Por favor ative analítica para nos ajudar a melhorar ${app_name}. Sim, eu quero ajudar! - Você não faz parte de alguma comunidade, no momento. - Escreva aqui… - Um parâmetro obrigatório está faltando. + Você não é atualmente um membro de quaisquer comunidades. + Digite aqui… + Um parâmetro requerido está faltando. Um parâmetro não é válido. - Use a tecla Enter do teclado para enviar a mensagem + Usar tecla enter de teclado para enviar mensagem Enviar mensagens de voz - Mostra a ação - Bane o usuário com a ID fornecida - Remove o banimento do usuário com a ID fornecida - Define o grau de poder de um(a) usuário(a) - Retira o nível de operador(a) do(a) usuário(a) com o ID fornecido - Convida a(o) usuária(o) com um dado ID para esta sala - Entra na sala com o alias fornecido - Deixa a sala - Define a descrição da sala - Remove o usuário com o ID fornecido - Altera o seu nome e sobrenome - Ativar/Desativar a formatação de texto - Reparar a gestão de aplicativos Matrix + Exibe ação + Bane usuária(o) com id dada + Desbane usuária(o) com id dada + Define nível de poder de um(a) usuário(a) + Desopa usuária(o) com id dada + Convida usuária(o) com id dada para esta sala + Junta-se a sala com alias dado + Sair de sala + Definir o tópico da sala + Expulsa a(o) usuária(o) com id dada + Muda seu apelido de exibição + Ativar/Desativar markdown + Para consertar gerenciamento de Apps Matrix - %d participante - %d participantes + %d membro + %d membros %d sala %d salas - Para continuar usando o Servidor de Base %1$s, você precisa revisar e aceitar os termos e condições de uso. + Para continuar usando o servidorcasa %1$s você deve revisar e aceitar os termos e condições. Revisar agora - Desativar minha conta - Isso tornará sua conta permanentemente inutilizável. Você não conseguirá efetuar login e ninguém poderá registrar novamente o mesmo ID de usuário. Isso fará com que sua conta saia de todas as salas das quais está participando e removerá os detalhes de sua conta do servidor de identidade. Esta ação é irreversível. + Desativar Conta + Isto vai fazer sua conta permanentemente inusável. Você não vai ser capaz de fazer login, e ninguém vai poder re-registrar o mesmo ID de usuária(o). Isto vai causar sua conta sair de todas as salas em que ela está participando, e vai remover detalhes de sua conta de seu servidor de identidade. Esta ação é irreversível. \n -\nDesativar sua conta não faz com que, por padrão, suas mensagens enviadas sejam apagadas. Se você deseja que suas mensagens também sejam apagadas, marque a opção abaixo. +\nDesativar sua conta não nos causa por padrão esquecer mensagens que você tem enviado. Se você gostaria que nós esqueçamos suas mensagens, por favor marque a caixa abaixo. \n -\nA visibilidade de mensagens na Matrix é semelhante a um e-mail. O fato de apagarmos suas mensagens significa que suas mensagens enviadas não serão compartilhadas com nenhum usuário novo ou ainda não registrado, mas os usuários registrados que já tiveram acesso a essas mensagens ainda terão acesso uma cópia delas. - Quando minha conta for desativada, apague todas as mensagens que eu enviei (Atenção: isso fará com que futuros usuários tenham uma visão incompleta das conversas) - Para continuar, entre com sua senha: - Desativar minha conta - Por favor, digite sua senha. - Esta sala foi substituída e não está mais ativa +\nVisibilidade de mensagem em Matrix é similar a email. Nós esquecermos suas mensagens significa que mensagens que você tem enviado não vão ser compartilhadas com nenhum usuária(o) nova(o) ou não-registrada(o), mas usuárias(os) registradas(os) que já têm acesso a estas mensagens vão ainda ter acesso à cópia delas(es). + Por favor esqueça todas as mensagens que eu tenho enviado quando minha conta for desativada (Aviso: isto vai causar usuárias/os futuras/os terem uma visão incompleta de conversas) + Para continuar, por favor entre sua senha: + Desativar Conta + Por favor entre sua senha. + Esta sala tem sido substituída e não está mais ativa A conversa continua aqui - Esta sala é a continuação de outra conversa - Clique aqui para ver as mensagens anteriores - Limite de recursos excedido - Entre em contato com o(a) administrador(a) - entre em contato com o administrador do seu serviço - Este servidor local excedeu um dos seus limites de recursos, portanto alguns usuários não conseguirão fazer login. - Este servidor local excedeu um de seus limites de recursos. - Este homeserver atingiu o seu limite mensal de usuários ativos, portanto alguns usuários não conseguirão fazer login. - Este homeserver atingiu o seu limite mensal de usuários ativos. - Por favor, %s para que este limite seja aumentado. - Por favor, %s para seguir usando este serviço. - Crie uma frase secreta para criptografar as chaves exportadas. Você precisará inserir essa frase secreta para importar chaves criptografadas. + Esta sala é uma continuação de uma outra conversa + Clique aqui para ver mensagens mais antigas + Limite de Recurso Excedido + Contactar Administrador(a) + contacte o(a) administrador(a) de seu serviço + Este servidorcasa tem excedido um de seus limites de recurso, então algumas(ns) usuária(os) não vão ser capazes de fazer login. + Este servidorcasa tem excedido um de seus limites de recurso. + Este servidorcasa tem atingido seu limite de Usuárias(os) Mensalmente Ativos então algumas(ns) usuárias(os) não vão ser capazes de fazer login. + Este servidorcasa tem atingido seu limite de Usuárias(os) Mensalmente Ativas(os). + Por favor %s para ter este limite aumentado. + Por favor %s para continuar usando este serviço. + Por favor crie uma frasepasse para encriptar as chaves exportadas. Você vai precisar entrar a mesma frasepasse para ser capaz de importar as chaves. Aceitar Erro - Por favor, revise e aceite as políticas deste servidor local: + Por favor revise e aceite as políticas deste servidor de casa: Chamadas - Use o toque padrão do ${app_name} para chamadas recebidas - Toque de chamadas recebidas - Selecione o toque de chamadas: - Motivo + Usar toque default de ${app_name} para chamadas recebendo + Toque de chamada recebendo + Selecione toque para chamadas: + Razão Versão %s - Resolver problemas nas notificações + Resolver Problemas de Notificação Diagnóstico de resolução de problemas - Executar Testes - Executando… (%1$d of %2$d) - O diagnóstico básico está ok. Se você ainda não recebe notificações, por favor relate um erro para nos ajudar a investigar. - Um ou mais testes falharam, tente as correções sugeridas. - Um ou mais testes falharam, por favor relate um erro para nos ajudar a investigar. - Configurações do sistema - Notificações estão ativadas nas configurações do sistema. + Rodar Testes + Rodando… (%1$d of %2$d) + Diagnóstico básico está OK. Se você ainda não recebe notificações, por favor submita um reporte de bug para nos ajudar a investigar. + Um ou mais testes têm falhado, tente correção(ões) sugerida(s). + Um ou mais testes têm falhado, por favor submita um reporte de bug para nos ajudar a investigar. + Configurações de Sistema. + Notificações estão ativadas nas configurações de sistema. Notificações estão desativadas nas configurações do sistema. -\nPor favor, revise as configurações do sistema. - Abra as Configurações - Configurações da conta +\nPor favor cheque configurações de sistema. + Abrir Configurações + Configurações de Conta. Notificações estão ativadas para sua conta. Notificações estão desativadas para sua conta. -\nPor favor, revise as configurações da conta. +\nPor favor cheque configurações de conta. Ativar - Configurações da sessão - Notificações estão ativadas nesta sessão. - Remover da sala - Notificações não estão ativadas nesta sessão. -\nPor favor, revise as configurações do ${app_name}. + Configurações de Sessão. + Notificações estão ativadas para esta sessão. + Expulsar + Notificações não estão ativadas para esta sessão. +\nPor favor cheque as configurações de ${app_name}. Ativar - Versão do Google Play Services - Google Play Services APK está disponível e atualizado. - ${app_name} usa Google Play Services para entregar mensagens push, mas isto não parece estar configurado corretamente: + Checagem de Play Services + APK de Google Play Services está disponível e atualizado. + ${app_name} usa Google Play Services para entregar mensagens push mas não parece estar configurado corretamente: \n%1$s Consertar Play Services - Token do Firebase + Token de Firebase Token FCM recuperado com sucesso: \n%1$s - Ligar mesmo assim - Falha ao recuperar o token do FCM: + Ligar Mesmo Assim + Falha ao recuperar token do FCM: \n%1$s - Registro do token do Firebase - Token FCM registrado com sucesso no HomeServer. - Falha ao registrar o token FCM no servidor local: + Registro de Token + Token FCM registrado com sucesso a ServidorCasa. + Falha ao registrar token FCM a ServidorCasa: \n%1$s Serviço de Notificações - Serviço de Notificações está em execução. - Serviço de Notificações não está em execução. + Serviço de Notificações está rodando. + Serviço de Notificações não está rodando. \nTente reiniciar o aplicativo. - Iniciar Serviço - O serviço foi encerrado e reiniciado automaticamente. - Falha ao reiniciar Serviço - Começar na inicialização - O serviço iniciará quando o aparelho for reiniciado. - O serviço não iniciará enquanto o aparelho não for reiniciado. Você não receberá notificações até que o ${app_name} for aberto ao menos uma vez. - Iniciar com o sistema - Revisar restrições de segundo plano - Otimização de bateria - ${app_name} não é afetado pela Otimização de Bateria. - Reinicialização Automática do Serviço de Notificações + Começar Serviço + Serviço foi matado e reiniciado automaticamente. + Falha ao reiniciar serviço + Começar em boot + Serviço vai começar quando o dispositivo for reiniciado. + Serviço não vai começar quando o dispositivo for reiniciado, você não vai receber notificações até que ${app_name} tenha sido aberto uma vez. + Ativar Começar em boot + Checar restrições de background + Otimização de Bateria + ${app_name} não é afetado por Otimização de Bateria. + Auto-Reinício de Serviço de Notificações Desativar restrições - Ignorar a otimização - Prévia de links dentro do chat quando seu homeserver suporta este recurso. - Permitir que saibam quando eu estiver digitando - Deixe outros usuários saberem quando você estiver digitando. - Mostrar confirmações de leitura dos outros usuários - Clique nas confirmações de leitura para obter mais informações. - Mostrar eventos de entrada e saída de sala - Convites, remoções e banimentos não são afetados. - Mostrar eventos da conta - Mostrar alterações de foto de perfil e de nome e sobrenome. - Conexão em segundo plano - ${app_name} precisa manter um baixo impacto na conexão em segundo plano para ter notificações confiáveis. -\nNa próxima tela, você será solicitado a permitir que o ${app_name} funcione sempre em segundo plano. Por favor, aceite. - Restrições de segundo plano estão desativadas para o ${app_name}. Este teste deve ser realizado usando dados móveis (sem Wi-Fi). + Ignorar Otimização + Previsualize links dentro do chat quando seu servidor de casa suporta este recurso. + Enviar notificações de digitação + Deixe outras(os) usuárias(os) saberem quando você está digitando. + Mostrar recibos de leitura + Clique nos recibos de leitura para uma lista detalhada. + Mostrar eventos de juntar-se e sair + Convites, expulsões e bans são desafetados. + Mostrar eventos de conta + Inclui mudanças de avatar e nome de exibição. + Conexão no Background + ${app_name} precisa manter uma conexão no background de baixo impacto a fim de ter notificações confiáveis. +\nNa próxima tela você vai ser instigado a permitir que o ${app_name} rode sempre em background, por favor aceite. + Restrições de background estão desativadas para ${app_name}. Este teste devia ser rodado usando dados móveis (sem Wi-Fi). \n%1$s - Restrições em segundo plano estão ativadas para o ${app_name}. -\nO aplicativo funciona bastante restringido enquanto está em segundo plano, o que pode afetar as notificações. + Restrições de background estão ativadas para ${app_name}. +\nTrabalho que o app tenta fazer vai ser agressivamente restringido enquando ele está em background, e isto pode afetar notificações. \n%1$s - Formatação de texto - Formatar o texto das mensagens a serem enviadas. Por exemplo: inserir asteriscos antes e depois do texto, mostrará o texto em itálico. + Formatação Markdown + Formate mensagens usando sintaxe markdown antes que elas são enviadas. Isto permite formatação avançada tal como usar asteriscos para exibir texto itálico. Conceder permissão - Ocorreu um erro ao confirmar seu endereço de e-mail. + Um erro ocorreu enquanto verificando seu endereço de email. Senha - Ocorreu um erro ao confirmar seu número de telefone. - Informação adicional: %s - Inicie a câmera do sistema em vez da câmera personalizada. - Esta opção requer um aplicativo de terceiros para gravar mensagens de voz. - O comando \"%s\" precisa de mais parâmetros ou alguns parâmetros estão incorretos. - A formatação de texto foi ativada. - A formatação de texto foi desativada. - Aumente o desempenho apenas carregando os participantes da sala na primeira exibição. - Seu servidor principal ainda não suporta o carregamento Lazy de participantes da sala. Tente depois. - Desculpe, ocorreu um erro + Um erro ocorreu enquanto confirmando seu número de telefone. + Info adicional: %s + Começar a câmera de sistema em vez da tela de câmera personalizada. + Esta opção requer um aplicativo de terceiro para gravar as mensagens. + O comando \"%s\" precisa de mais parâmetros, ou alguns parâmetros estão incorretos. + Markdown tem sido ativado. + Markdown tem sido desativado. + Aumenta performance ao somente carregar membros de salas em primeira visualização. + Seu servidorcasa não suporta carregamento preguiçoso de membros de salas ainda. Tente mais tarde. + Desculpe, um erro ocorreu expandir colapsar - Mostrar a área de informações + Mostrar a área de info Sempre Para mensagens e erros Somente para erros @@ -1240,177 +1241,177 @@ %1$s: %2$s +%d %d+ - Nenhum APK do Google Play Services válido foi encontrado. Notificações podem não funcionar corretamente. - Se um usuário deixar um aparelho desconectado por um período de tempo, com a tela desligada, o aparelho entrará no modo Soneca. Isso impede que os aplicativos acessem a rede, adiando seus trabalhos, sincronizações e alarmes padrão. - Criar frase secreta - A frase secreta está errada - Carregamento Lazy dos participantes das salas - Chamada de vídeo em andamento… - Backup da chave - Usar Backup da chave - Backup de chaves não está concluído. Por favor, aguarde… - Você perderá suas mensagens criptografadas se sair agora - Backup de chave em andamento. Se você sair agora, perderá o acesso às suas mensagens criptografadas. - O backup de chave deve estar ativado em todas as suas sessões, para evitar perder o acesso às suas mensagens criptografadas. - Não quero minhas mensagens criptografadas - Fazendo backup das chaves… - Use o backup de chave + Nenhum APK de Google Play Services válido encontrado. Notificações podem não funcionar apropriadamente. + Se um(a) usuário(a) deixa um dispositivo fora do plug e parado por um período de tempo, com a tela desligada, o aparelho entra em modo Doze. Isto previne apps de acessar a rede e adiar seus trabalhos, sincs e alarmes padrões. + Criar frasepasse + Frasepasse não bate + Carregar preguiçoso membros das salas + Chamada de Vídeo em Progresso… + Backup de Chave + Usar Backup de Chave + Backup de chaves não está terminado, por favor espere… + Você vai perder suas mensagens encriptadas se fizer signout agora + Backup de chave em progresso. Se você fizer signout agora você vai perder acesso a suas mensagens encriptadas. + Backup de Chave Seguro devia estar ativo em todas as suas sessões para evitar perder o acesso a suas mensagens encriptadas. + Eu não quero minhas mensagens encriptadas + Fazendo backup de chaves… + Usar Backup de Chave Tem certeza\? - Fazer o backup - Você perderá o acesso às suas mensagens criptografadas, a menos que faça backup das suas chaves antes de sair. + Fazer backup + Você vai perder acesso a suas mensagens encriptadas a menos que faça backup de suas chaves antes de fazer signout. Assinatura Ficar Pular - Fechar + Feito Abortar - Deseja mesmo sair\? - Configurar notificações - Configurar notificações por evento - Configurações personalizadas - Observe que alguns tipos de mensagens estão configurados para serem silenciosos (produzirão uma notificação sem som). - Algumas notificações estão desativadas nas suas configurações personalizadas. - Falha ao carregar regras personalizadas, tente novamente. - Verifique as configurações + Você tem certeza que quer fazer sign out\? + Configurações de Notificações Avançadas + Importância de notificações por evento + Configurações Personalizadas. + Observe que alguns tipos de mensagens estão definidos para serem silenciosos (vão produzir uma notificação sem nenhum som). + Algumas notificações estão desativadas em suas configurações personalizadas. + Falha ao carregar regras personalizadas, por favor retente. + Checar Configurações [%1$s] -\nEste erro está fora do controle do ${app_name} e, de acordo com o Google, esse erro indica que o aparelho tem muitos aplicativos registrados com FCM. O erro só ocorre nos casos em que há números extremos de aplicativos, portanto, isso não deve afetar o usuário comum. - Bloquear - Entre com o login único - Este endereço não está acessível. Por favor, verifique-o - Seu aparelho está usando um protocolo de segurança TLS desatualizado, vulnerável a ataques. Para sua segurança, você não poderá se conectar +\nEste erro está fora de controle de ${app_name} e de acordo com Google, este erro indica que o dispositivo tem apps demais registrados com FCM. O erro somente ocorre em casos onde há números extremos de apps, então isso não devia afetar a(o) usuária(o) média(o). + Ignorar + Fazer sign-in com sign-on único + Este URL não é alcançável, por favor cheque-o + Seu dispositivo está usando um protocolo de segurança TLS desatualizado, vulnerável a ataque, para sua segurança você não vai ser capaz de se conectar [%1$s] -\nEste erro está fora de controle da ${app_name}. Isso pode ocorrer por vários motivos. Talvez funcione se você tentar novamente mais tarde. Você também pode verificar se o uso de dados do Google Play Service está restrito nas configurações do sistema, ou se o relógio do seu aparelho está correto. O erro também pode ocorrer em ROMs personalizadas. - Iniciando o serviço +\nEste erro está fora de controle de ${app_name}. Isto pode ocorrer por várias razões. Talvez vai funcionar se você retentar mais tarde, você também pode checar que Google Play Service não está restrito em uso de dados nas configurações de sistema, ou que o relógio do seu dispositivo está correto, ou ele pode acontecem em ROM personalizada. + Inicializando serviço [%1$s] -\nEste erro está fora de controle do ${app_name}. Não há conta do Google no celular. Por favor, abra o gerenciador de contas e adicione uma conta do Google. +\nEste erro está fora de controle de ${app_name}. Não há uma conta de Google no celular. Por favor abra o gerenciador de contas e adicione uma conta de Google. Adicionar Conta - Configurar notificações com som - Configurar notificações de chamada - Configurar notificações silenciosas - Escolha a cor do LED, vibração, som… + Configurar Notificações Barulhentas + Configurar Notificações de Chamada + Configurar Notificações Silenciosas + Escolher cor de LED, vibração, som… Gerenciamento de Chaves de Criptografia - Enviar mensagem com a tecla enter - Confirmar sessão - Nenhuma + Enviar mensagem com enter + Verificar sessão + Nenhum Revogar Desconectar Revisar - Recusar + Declinar Marcar como lida - Nenhum servidor de identidade está configurado. - A chamada falhou por conta de má configuração no servidor - Reproduzir + Nenhum servidor de identidade configurado. + Chamada falhou devido a servidor configurado errado + Tocar Pausar Descartar Copiar - Pronto + Sucesso Notificações - Peça ao administrador do seu servidor principal (%1$s) que configure um servidor TURN para que as chamadas funcionem de maneira confiável. + Por favor peça ao administrador do seu servidor de casa (%1$s) para confugurar um servidor TURN a fim que chamadas funcionem confiavelmente. \n -\nAlternativamente, você pode tentar usar o servidor público em %2$s. No entanto, ele não é tão confiável e compartilhará o seu IP com esse servidor. Você também pode configurar isso nas Configurações. +\nAlternativamente, você pode tentar usar o servidor público em %2$s, mas isto não vai set tão confiável, e ele vai compartilhar seu endereço de IP com esse servidor. Você também pode gerir isto em Configurações. Tente usar %s - Não pergunte novamente - A chamada falhou + Não me pergunte de novo + Chamada ${app_name} Falhou Falha ao estabelecer conexão em tempo real. -\nPor favor, peça ao administrador do seu servidor para configurar um servidor TURN, de modo que as chamadas funcionem de maneira estável. - Selecione a caixa de som +\nPor favor peça ao administrador do seu servidor de casa para configurar um servidor TURN a fim que chamadas funcionem confiavelmente. + Selecionar Dispositivo de Som Celular - Alto-falante - Fone de ouvido - Fone de ouvido sem fio - Trocar a câmera + Falante + Auscultadores + Auscultadores Semfio + Trocar Câmera Frontal Traseira - Desativar vídeo de alta definição - Ativar vídeo de alta definição - Defina um e-mail para recuperação da conta. Posteriormente, você pode permitir que as pessoas encontrem você através dele. - Defina um número de telefone. Posteriormente, você pode permitir que as pessoas encontrem você através dele. - Defina um e-mail para a recuperação da conta. Posteriormente, um endereço de e-mail ou número de telefone pode ser usado para ser encontrado por outras pessoas. - Defina um e-mail para recuperação da conta. Posteriormente, você pode permitir que as pessoas encontrem você através dele, ou através do número de telefone. - Este não é um endereço de servidor Matrix válido - Não foi possível acessar um servidor local neste endereço. Por favor, verifique-o - Erro de SSL: a identidade da pessoa não foi confirmada. + Desativar HD + Ativar HD + Defina um email para recuperação de conta, e mais tarde para ser opcionalmente discobertável por pessoas que conhecem você. + Defina um telefone, e mais tarde para ser opcionalmente descobertável por pessoas que conhecem você. + Defina um email para recuperação de conta. Use mais tarde email ou telefone para ser opcionalmente descobertável por pessoas que conhecem você. + Defina um email para recuperação de conta. Use mais tarde email ou telefone para ser opcionalmente descobertável por pessoas que conhecem você. + Isto não é um endereço de servidor Matrix válido + Não é possível alcançar um servidor de casa neste URL, por favor cheque-o + Erro de SSL: a identidade da(o) peer não tem sido verificada. Erro de SSL. - Permitir a assistência do servidor de chamadas reserva - Permitir a assistência do servidor de chamadas reserva %s quando seu servidor não oferecer este serviço (seu endereço IP será transmitido quando você ligar) - Chamada em andamento (%s) - Retornar à chamada - Adicione um servidor de identidade nas suas configurações para executar esta ação. + Permitir servidor fallback de assistência de chamadas + Vai usar %s como assistência quando seu servidor de casa não oferece um (seu endereço de IP vai ser compartilhado durante uma chamada) + Chamada Ativa (%s) + Retornar a chamada + Adicione um servidor de identidade em suas configurações para performar esta ação. Cancelar convite - Reduzir privilégios\? - Você não poderá desfazer essa alteração, já que está reduzido seus privilégios. Se você for a última pessoa nesta sala, será impossível recuperar a permissão atual. - Reduzir privilégio - Bloquear usuário - Ao bloquear este usuário, as mensagens dele serão ocultadas de você em todas as salas. + Rebaixar-se\? + Você não vai pode desfazer esta mudança já que está se rebaixando, se você for a(o) última(o) usuária(o) privilegiada(o) na sala vai ser impossível recuperar privilégios. + Rebaixar + Ignorar usuária(o) + Ignorar esta(e) usuária(o) vai remover as mensagens dela(e) das salas que vocês compartilham. \n -\nVocê pode reverter esta ação a qualquer momento nas configurações. - Desbloquear usuário - Desbloquear este usuário mostrará todas as mensagens dele novamente. +\nVocê pode reverter esta ação a qualquer momento nas configurações gerais. + Designorar usuária(o) + Designorar esta(e) usuária(o) vai mostrar todas as mensagens dela(e) de novo. Cancelar convite - Tem certeza que quer cancelar o convite para este usuário\? - Remover usuário - Motivo da remoção - esta ação removerá o usuário desta sala. + Você tem certeza que quer cancelar o convite para esta(e) usuária(o)\? + Expulsar usuária(o) + Razão de expulsão + expulsar usuária(o) vai removê-la(o) desta sala. \n -\nPara impedir que o usuário nunca mais entre novamente, você precisará bani-lo. - Banir usuário - Motivo do banimento - Remover banimento do usuário - Remover o banimento do usuário permitirá que ele entre novamente na sala. +\nPara preveni-la(o) de se juntar de novo, você devia bani-la(o) em vez disso. + Banir usuária(o) + Razão de ban + Desbanir usuária(o) + Desbanir usuária(o) vai permitir que ela(e) se junte à sala de novo. Confirme sua senha - Não pode fazer isto pelo ${app_name} app - Autenticação exigida - O aplicativo não precisa de se conectar ao servidor em segundo plano, isto deve reduzir a utilização da bateria - Sincronização em segundo plano + Você não pode fazer isto de ${app_name} celular + Autenticação é requerida + O app não precisa de se conectar ao ServidorCasa no background, isto deveria reduzir uso de bateria + Modo Sinc no Background Optimizado para bateria - ${app_name} sincronizará em segundo plano para preservar os recursos limitados do aparelho (bateria). -\nDependendo do estado dos recursos do seu aparelho, a sincronização pode ser adiada pelo sistema operacional. - Optimizado em tempo real - O ${app_name} sincronizará periodicamente em segundo plano, no momento estabelecido (configurável). -\nIsso afetará o uso de dados e da bateria. Haverá uma notificação permanente informando que o ${app_name} está sincronizando. - Sem sincronização em segundo plano - Você não será notificado sobre mensagens recebidas quando o ${app_name} estiver em segundo plano. - Não foi possível actualizar a configuração. - Intervalo de sincronização preferido + ${app_name} vai sincar em background de maneira que preserva recursos limitados do dispositivo (bateria). +\nDependendo do estado de recurso de seu dispositivo, a sinc pode ser adiada pelo sistema operacional. + Optimizado para tempo real + ${app_name} vai sincar em background periodicamente em tempo preciso (configurável). +\nIsto vai impactar uso de rádio e bateria, vai ter uma notificação permanente exibida declarando que ${app_name} está à escuta por eventos. + Sem sinc no background + Você não vai ser notificada(o) sobre mensagens recebendo quando o app está em background. + Falha ao atualizar configurações. + Intervalo de Sinc Preferido %s -\nA sincronização pode ser adiada dependendo dos recursos (bateria) ou do estado do aparelho (modo de suspensão). +\nA sinc pode ser adiada dependendo dos recursos (bateria) ou estado do dispositivo (sono). Integrações - Use o Gerenciador de Integrações para gerenciar bots, integrações, widgets e pacotes de figurinhas. -\nO Gerenciador de Integrações recebe dados de configuração e pode modificar widgets, enviar convites para salas e definir níveis de permissão em seu nome. - Botão enter do teclado enviará a mensagem em vez de adicionar uma quebra de linha - Backup online + Use um Gerenciador de Integração para gerenciar bots, bridges, widgets e pacotes de stickers. +\nGerenciadores de Integração recebem dados de configuração, e podem modificar widgets, enviar convites de sala e definir níveis de poder em seu nome. + Botão enter do teclado suave vai enviar mensagem em vez de adicionar uma quebra de linha + Backup Seguro Gerenciar - Configurar backup online - Restaurar backup online - Configurar neste aparelho - Prevenir contra a perda de acesso a mensagens e dados encriptados, guardando as chaves de encriptação no seu servidor. - Gerar uma nova Chave de Segurança ou definir uma nova Frase de Segurança para o seu backup existente. - Isto irá substituir a sua Chave ou Frase actual. - Encontrar + Configurar Backup Seguro + Resettar Backup Seguro + Configurar neste dispositivo + Salvaguarde-se contra perder acesso a mensagens & dados encriptados ao fazer backup de chaves de encriptação em seu servidor. + Gere uma nova Chave de Segurança ou defina uma nova Frase de Segurança para seu backup existente. + Isto vai substituir sua Chave ou Frase atual. + Descoberta Gerencie suas configurações de descoberta. - O modo de baixo uso de dados desativa a atualização de presença e a animação de que alguém está digitando. + Modo de economia de dados aplica um filtro específico para que atualizações de presença e notificações de digitação sejam filtrados fora. Permitir integrações - Gerenciador de Integrações - As integrações estão desativadas - Ative \'Permitir integrações\' nas Configurações para fazer isso. + Gerenciador de Integração + Integrações estão desativadas + Ative \'Permitir integrações\' em Configurações para fazer isto. Atualizar Senha A senha não é válida - As senhas não são iguais - Média - Compressão predefinida + Senhas não batem + Mídia + Compressão default Escolher - Fonte de mídia por padrão + Fonte de mídia default Escolher - Tocar o som da câmera + Tocar som de obturador - %d usuário banido - %d usuários banidos + %d usuária(o) banida(o) + %d usuárias(os) banidas(os) - O nome público (visível para as pessoas com quem você se comunica) - O nome público de uma sessão é visível para as pessoas com quem você se comunica + Nome público (visível para pessoas com quem você se comunica) + O nome público de uma sessão é visível para pessoas com quem você se comunica Chaves exportadas com sucesso - Recuperação de mensagens criptografadas - Configurar backup das chaves - IP desconhecido + Recuperação de Mensagens Encriptadas + Gerenciar Backup de Chaves + ip desconhecido %1$s: %2$d mensagem %1$s: %2$d mensagens @@ -1424,121 +1425,121 @@ Novas Mensagens Novo Convite Eu - ** Não foi possível enviar - por favor abra a sala + ** Não foi possível enviar - por favor abrir sala %1$s: %2$s %1$s: %2$s %3$s VISTA - Widgets Ativos + Widgets ativos Widget - Carregar widget - Widget adicionado por: - A sua utilização pode definir cookies e compartilhar dados com %s: - A sua utilização pode compartilhar dados com %s: + Carregar Widget + Este widget foi adicionado por: + Usá-lo pode definir cookies e compartilhar dados com %s: + Usá-lo pode compartilhar dados com %s: Falha ao carregar widget. \n%s Recarregar widget - Abrir no navegador - Remover para mim - O seu nome e sobrenome - Link da sua foto de perfil - Sua ID de usuário + Abrir em browser + Revogar acesso para mim + Seu nome de exibição + URL de seu avatar + Sua ID de usuária(o) Seu tema - ID do widget - ID da sala - Desculpe, as chamadas em grupo com o Jitsi não são suportadas em aparelhos antigos (com versões do Android anteriores a 6.0) - Este wigdet deseja utilizar os seguintes recursos: + ID de widget + ID de sala + Desculpe, chamadas de conferência com Jitsi não são suportadas em dispositivos antigos (dispositivos SO Android abaixo de 6.0) + Este wigdet quer usar os seguintes recursos: Permitir - Bloquear Tudo + Bloquear Todos Usar a câmera Usar o microfone - Ler as mídias protegidos por DRM - O gerenciador de integrações não está configurado. - Para continuar, você precisa aceitar os termos de serviço. - Uma nova sessão está solicitando chaves de criptografia. + Ler Mídia protegida por DRM + Nenhum gerenciador de integração configurado. + Para continuar você precisa aceitar os Termos deste serviço. + Uma nova sessão está requisitando chaves de encriptação. \nNome da sessão: %1$s -\nVisto por último às: %2$s -\nSe você não fez login em outra sessão, ignore essa solicitação. - Uma nova sessão está solicitando chaves de criptografia. +\nVisto por último: %2$s +\nSe você não fez login numa outra sessão, ignore esta requisição. + Uma sessão não-verificada está requisitando chaves de encriptação. \nNome da sessão: %1$s -\nVisto por último às: %2$s -\nSe você não fez login em outra sessão, ignore essa solicitação. - Confirmar +\nVisto por último: %2$s +\nSe você não fez login numa outra sessão, ignore esta requisição. + Verificar Compartilhar - Pedido de compartilhamento das Chaves + Requisição de Compartilhamento de Chaves Ignorar - Ativado - Digite o nome de usuário. - Digite a sua frase secreta - A frase secreta é muito fraca - Por favor, apague a frase secreta se desejar que o ${app_name} gere uma chave de recuperação. + Silenciosa + Por favor entre um nome de usuário. + Por favor entre sua frasepasse + Frasepasse é fraca demais + Por favor delete a frasepasse se quiser que ${app_name} gere uma chave de recuperação. Nenhuma sessão Matrix disponível - Nunca perca mensagens criptografadas - As mensagens em salas criptografadas são protegidas com a criptografia de ponta a ponta. Somente você e o(s) destinatário(s) têm as chaves para ler essas mensagens. -\n -\nFaça backup de suas chaves para evitar perdê-las. - Comece a fazer o backup de chave + Nunca perca mensagens encriptadas + Mensagens em salas encriptadas são asseguradas com a encriptação ponta-a-ponta. Somente você e a/o(s) recipiente(s) têm as chaves para ler estas mensagens. +\n +\nFaça seguramente backup de suas chaves para evitar perdê-las. + Começar a usar Backup de Chaves (Avançado) - Exportar as chaves manualmente - Proteja seu backup com uma frase secreta. - Armazenaremos uma cópia criptografada de suas chaves em nosso servidor. Proteja seu backup com uma frase secreta para mantê-lo seguro. -\n -\nPara segurança máxima, a frase secreta deve ser diferente da senha da sua conta. - Criar frase secreta - Criando backup - Ou proteja seu backup com uma chave de recuperação, salvando-a em algum lugar seguro. - (Avançado) Configurar a chave de recuperação - Parabéns ! - O backup das suas chaves está sendo feito. - Sua chave de recuperação é uma rede de proteção - você pode usá-la para restaurar o acesso às suas mensagens criptografadas se você esquecer sua frase de recuperação. + Exportar chaves manualmente + Assegure seu backup com uma Frasepasse. + Nós vamos armazenar uma cópia encriptada de suas chaves em seu servidorcasa. Proteja seu backup com uma frasepasse para mantê-lo seguro. +\n +\nPara segurança máxima, esta deve ser diferente da senha de sua conta. + Definir Frasepasse + Criando Backup + Ou, assegure seu backup com uma Chave de Recuperação, salvando-a em algum lugar seguro. + (Avançado) Configurar com Chave de Recuperação + Sucesso ! + Backup de suas chaves está sendo feito. + Sua chave de recuperação é uma rede de proteção - você pode usá-la para restaurar acesso a suas mensagens encriptadas se você esquecer sua frasepasse. \nMantenha a sua chave de recuperação em algum lugar muito seguro, como um gerenciador de senhas (ou um cofre) Mantenha sua chave de recuperação em algum lugar muito seguro, como um gerenciador de senhas (ou um cofre) - Fechar - Já fiz uma cópia - Salvar chave de recuperação + Feito + Eu tenho feito uma cópia + Salvar Chave de Recuperação Compartilhar - Salvar como um arquivo - A chave de recuperação foi guardada para \"%s\". + Salvar como Arquivo + A chave de recuperação tem sido salva em \"%s\". \n -\nAtenção: este arquivo pode ser removido se o aplicativo for desinstalado. - A chave de recuperação foi guardada. - Já existe um backup no seu servidor - Parece que você já configurou um backup de chaves em outra sessão. Deseja substituí-lo por um novo backup\? +\nAviso: este arquivo pode ser deletado se o aplicativo for desinstalado. + A chave de recuperação tem sido salva. + Um backup já existe em seu ServidorCasa + Parece que você já tem configurado um backup de chaves numa outra sessão. Você quer substituí-lo pelo que está criando\? Substituir Parar - Faça uma cópia + Por favor faça uma cópia Compartilhar chave de recuperação com… - Gerando Chave de Recuperação usando a frase secreta, este processo pode levar alguns segundos. - Chave de recuperação + Gerando Chave de Recuperação usando a frasepasse, este processo pode levar muitos segundos. + Chave de Recuperação Erro inesperado - Começando o backup - O backup de suas chaves está sendo feito. O primeiro backup pode demorar vários minutos. + Backup Começado + Backup de suas chaves está sendo feito. O backup inicial poderia levar muitos minutos. Você tem certeza\? - Você pode perder o acesso às suas mensagens se você sair do Element ou perder este aparelho. - Obtendo a versão do backup… - Use sua frase secreta de recuperação para desbloquear seu histórico de mensagens seguras + Você pode perder acesso a suas mensagens se você fizer logout ou perder este dispositivo. + Obtendo versão de backup… + Use sua frasepasse de recuperação para destrancar seu histórico de mensagens encriptadas use sua chave de recuperação - Não sabe sua frase secreta de recuperação, você pode %s. - Use a sua Chave de Recuperação para desbloquear o seu histórico de mensagens criptografadas - Digite a chave de recuperação + Não sabe sua frasepasse de recuperação, você pode %s. + Use a sua Chave de Recuperação para destrancar seu histórico de mensagens encriptadas + Entar Chave de Recuperação Recuperação de Mensagem - Perdeu sua chave de recuperação\? Você pode configurar uma nova nas configurações. - O backup não pôde ser descriptografado com essa frase secreta: verifique se você digitou corretamente a frase secreta de recuperação. - Erro de rede: verifique a sua conexão e tente de novo, por favor. - Restaurando o backup: - Processando a chave de recuperação… - Baixando as chaves… - Importando as chaves… - Desbloquear Histórico - Digite uma chave de recuperação - O backup não pôde ser descriptografado com essa chave de recuperação: verifique se você digitou a chave de recuperação correta. - Backup restaurado %s ! + Perdeu sua chave de recuperação\? Você pode configurar uma nova em configurações. + Backup não pôde ser decriptado com esta frasepasse: por favor verifique que você entrou a frasepasse de recuperação correta. + Erro de rede: por favor verifique sua conexão e retente. + Restaurando backup: + Computando chave de recuperação… + Fazendo download de chaves… + Importando chaves… + Destrancar Histórico + Por favor entre uma chave de recuperação + Backup não pôde ser decriptado com esta chave de recuperação: por favor verifique que você entrou a chave de recuperação correta. + Backup Restaurado %s ! - O backup restaurou %d chave. - O backup restaurou %d chaves. + Restaurou um backup com %d chave. + Restaurou um backup com %d chaves. - %d nova chave foi adicionada a esta sessão. - %d novas chaves foram adicionadas a esta sessão. + %d nova chave tem sido adicionada a esta sessão. + %d novas chaves têm sido adicionadas a esta sessão. Não foi possível obter a versão mais recente das chaves de recuperação (%s). A sessão criptografada não está activa @@ -2388,22 +2389,22 @@ Ativar o PIN Se você quiser redefinir seu PIN, toque no Esqueci o PIN para sair e redefinir-lo. Confirme o PIN para desativar o PIN - Evitar chamadas acidentais - Confirmarei a intenção de iniciar uma chamada - Você não tem permissão para iniciar uma chamada em grupo nesta sala - Uma chamada em grupo já está em andamento! - Iniciar chamada de vídeo - Iniciar chamada de voz - As chamadas utilizam as políticas de segurança e permissão do Jitsi. Todas as pessoas que estão atualmente na sala verão um convite para participar enquanto sua chamada estiver acontecendo. - Você não pode iniciar uma chamada com você mesmo - Você não pode iniciar uma chamada consigo mesmo. Aguarde os participantes aceitarem o convite - Falha ao adicionar o widget - Falha ao remover o widget + Prevenir chamada acidental + Pedir por confirmação antes de começar uma chamada + Você não tem permissão para começar uma chamada de conferência nesta sala + Uma conferência já está em progresso! + Começar reunião de vídeo + Começar reunião de áudio + Reuniões usam políticas de segurança e permissão de Jitsi. Todas as pessoas atualmente na sala vão ver um convite para se juntarem enquanto sua reunião estiver acontecendo. + Você não pode começar uma chamada com você mesma(o) + Você não pode começar uma chamada com você mesma(o), espere pelas(os) participantes aceitarem convite + Falha ao adicionar widget + Falha ao remover widget %1$d/%2$d chave importada com êxito. %1$d/%2$d chaves importadas com êxito. - Gerenciar integrações + Gerenciar Integrações Nenhum widget ativo A sala foi criada, mas alguns convites não foram enviados pelo seguinte motivo: \n @@ -2418,28 +2419,28 @@ Atenção! Última tentativa restante antes de você ser desconectada/o! Muitos erros, você foi desconectada/o - Você não tem permissão para iniciar uma chamada nesta sala - Nenhum número de telefone foi adicionado à sua conta - Endereços de e-mail - Nenhum e-mail foi adicionado à sua conta + Você não tem permissão para começar uma chamada nesta sala + Nenhum número de telefone tem sido adicionado a sua conta + Endereços de email + Nenhum email tem sido adicionado a sua conta Números de telefone Remover %s\? - Certifique-se de ter clicado no link do e-mail que enviamos para você. + Assegure-se que você tem clicado no link no email que enviamos para você. %d segundo %d segundos - Mostrar quando alguém for convidado/entrar/sair/banido e mostrar alterações de foto de perfil e de nome e sobrenome. - E-mails e números de telefone - Editar e-mails e números de telefone vinculados à sua conta Matrix + Inclui eventos de convite/juntar-se/saiu/expulsão/ban e mudanças de avatar/nome de exibição. + Emails e números de telefone + Gerenciar emails e números de telefone linkados a sua conta Matrix Código Por favor, use o formato internacional (o número de telefone precisa começar com \'+\') Confirme sua identidade verificando este login, concedendo a ele acesso a mensagens criptografadas. Não é possível carregar uma sala da qual você foi banido. Não foi possível encontrar esta sala. Certifique-se de que ela existe. O link não está correto - Este número de telefone já foi adicionado. - Mostrar eventos de status dos integrantes da sala + Este número de telefone já está definido. + Mostrar eventos de estado de membros da sala Votação Botões do bot Reagiu com: %s @@ -2480,22 +2481,22 @@ Exibir histórico completo em salas criptografadas %1$s e %2$s %1$s em %2$s e %3$s - Você clicou na notificação! - Clique na notificação, por favor. Se você não receber uma notificação, verifique as configurações do seu aparelho. - Exibição de notificações - Você está vendo a notificação! Clique aqui! - Houve uma falha ao receber a notificação. Reinstalar o aplicativo pode ser a solução. - O aplicativo está recebendo a notificação - O aplicativo está aguardando a notificação - Ainda não é possível buscar mensagens em salas criptografadas. - Você não tem permissão para iniciar uma chamada - Você não tem permissão para iniciar uma chamada em grupo - Redefinir + A notificação tem sido clicada! + Por favor clique na notificação. Se você não vê a notificação, por favor cheque as configurações de sistema. + Exibição de Notificações + Você está visualizando a notificação! Clique em mim! + Falha ao receber push. Solução podia ser reinstalar o aplicativo. + O aplicativo está recebendo PUSH + O aplicativo está esperando pelo PUSH + Pesquisar em salas encriptadas não é suportado ainda. + Você não tem permissão para começar uma chamada + Você não tem permissão para começar uma chamada de conferência + Resettar %1$s tornou a sala acessível somente com convite. Você definiu que a sala só receberá integrantes com convite. %s entrou. - Buscar usuários banidos - Testar o recebimento de notificações + Filtrar usuárias(os) banidas(os) + Testar Push %d convite %d convites @@ -2511,8 +2512,8 @@ Descrição Descrição da sala (opcional) Nome da sala - Enviar histórico de solicitações do compartilhamento de chaves - Não há mais resultados + Enviar histórico de requisições de compartilhamento de chaves + Mais nenhum resultado Exportar auditoria Enviar mensagem Mostrar mais @@ -2567,23 +2568,23 @@ Código QR Adicionar por código QR Pesquise por nome ou ID - Aceite a permissão para acessar seus contatos. - Para escanear um código QR, você precisa permitir o acesso à câmera. - Começar a conversar - Tornar público este endereço + Aceite permissão para acessar seus contatos. + Para scannear um QR code, você precisa permitir acesso a câmera. + Começar a Conversar + Publicar este endereço Adicionar um endereço local - Esta sala não tem endereços locais - Endereços locais - Remover o endereço \"%1$s\"\? + Esta sala não tem nenhum endereço local + Endereços Locais + Deletar o endereço \"%1$s\"\? Publicar Publicar um novo endereço manualmente - Outros endereços públicos: + Outros endereços publicados: Endereço principal Este é o endereço principal - Os endereços públicos podem ser usados por qualquer pessoa em qualquer servidor para entrar na sua sala. Para tornar público um endereço, primeiramente ele precisa ser definido como um endereço local. - Endereços públicos - Endereços da sala - Veja e gerencie os endereços desta sala e a visibilidade dela na lista de salas. + Endereços publicados podem ser usados por qualquer pessoa em qualquer servidor para se junta a sua sala. Para publicar um endereço, ele precisa ser definido como um endereço local primeiro. + Endereços Publicados + Endereços da Sala + Veja e gerencie endereços desta sala, e sua visibilidade no diretório de salas. Endereços da sala Adicionar Alterar o seu PIN atual @@ -2591,21 +2592,21 @@ Esta sala não pode ser visualizada. Você quer entrar nela\? Esta sala não está acessível no momento. \nTente novamente mais tarde, ou peça ao administrador da sala para verificar se você tem acesso. - Não foi possível saber a visibilidade atual da sala na lista de salas (%1$s). - Publicar esta sala na lista pública de salas de %1$s\? - Remover este endereço - Defina endereços para esta sala, tendo em vista que as pessoas possam encontrá-la por meio do servidor local (%1$s) - Novo endereço publicado (por exemplo, #apelido:servidor) + Incapaz de recuperar a visibilidade atual de diretório de salas (%1$s). + Publicar esta sala ao público no direitório de salas de %1$s\? + Despublicar este endereço + Defina endereços para esta sala para que usuárias(os) possam encontrar esta sala através de eu servidor de casa (%1$s) + Novo endereço publicado (e.g. #alias:servidor) Nenhum outro endereço publicado ainda. Nenhum outro endereço publicado ainda, adicione um abaixo. - Tornar pública esta sala na lista de salas de %1$s\? - Remover o endereço \"%1$s\"\? - Acesso à sala - Alterar quem pode ler o histórico só se aplicará às futuras mensagens nesta sala. A visibilidade do histórico existente não será alterada. - Remover + Publicar esta sala ao público no diretório de salas de %1$s\? + Despublicar o endereço \"%1$s\"\? + Acesso a sala + Mundaças de quem pode ler o histórico só se vão aplicar a mensagens futuras nesta sala. A visibilidade do histórico existente vai ser inalterada. + Despublicar login único - Adicionar um botão no campo de texto para abrir o teclado de emojis - Notificar a todos + Adicionar um botão em compositor de mensagem para abrir teclado de emoji + Notificar todo mundo enviar neve ❄️ enviar confetes 🎉 Enviar a mensagem com neve @@ -2615,37 +2616,37 @@ Registrar-se com %s Continuar com %s Ou - Exibir o teclado de emojis - Use o comando /confetti ou envie uma mensagem contendo ❄️ ou 🎉 - Exibir efeitos em conversas - Alterar a descrição - Atualizar a sala + Mostrar teclado de emoji + Use comando /confetti ou envie uma mensagem contendo ❄️ ou 🎉 + Mostrar efeitos de chat + Mudar tópico + Fazer upgrade da sala Enviar eventos m.room.server_acl - Alterar permissões - Alterar o nome da sala - Alterar a visibilidade do histórico de alterações - Ativar a criptografia da sala - Alterar o endereço principal da sala - Alterar a foto da sala + Mudar permissões + Mudar nome da sala + Mudar visibilidade do histórico + Ativar encriptação da sala + Mudar endereço principal para a sala + Mudar avatar da sala Modificar widgets - Remover mensagens enviadas por outras pessoas - Banir usuários - Remover usuários - Alterar as configurações - Convidar usuários + Remover mensagens enviadas por outras(os) + Banir usuárias(os) + Expulsar usuárias(os) + Mudar configurações + Convidar usuárias(os) Enviar mensagens - Papel de usuário padrão - Você não tem permissão para atualizar os papéis de usuário necessários para alterar várias partes da sala - Selecione os papéis de usuário necessários para alterar várias partes da sala - Visualize e atualize os papéis de usuário necessários para alterar várias partes da sala. + Papel default + Você não tem permissão para atualizar os papéis necessários para mudar várias partes da sala + Selecione os papéis requeridos para mudar várias partes da sala + Visualize e atualize os papéis requeridos para mudar várias partes da sala. Permissões Permissões da sala - Esta sala não é pública. Você não poderá entrar novamente sem um convite. - Padrão do sistema + Esta sala não é pública. Você não vai ser capaz de se rejuntar sem um convite. + Default de Sistema Falha ao se autenticar O ${app_name} precisa que você insira suas credenciais para executar esta ação. Necessário autenticar-se novamente - Não autorizado, sem credenciais de autenticação válidas + Não autorizada(o), credenciais de autenticação válidas faltando Pessoas Ocorreu um erro ao transferir a chamada Transferir @@ -2670,11 +2671,11 @@ %1$s começou uma chamada Você começou uma chamada Falha ao fazer a autoverificação - Você pausou a chamada - %s pausou a chamada - Pausar + Você pôs a chamada em espera + %s pôs a chamada em espera + Pôr em espera Retomar - Voltar + Retornar Nível de confiança padrão Selecionado Vídeo @@ -2690,17 +2691,17 @@ Versão do servidor Nome do servidor Configurações da sala - Sair da chamada atual e mudar para a outra\? + Sair da conferência atual e trocar para a outra\? Versão da sala - Mostrar todas as salas na lista de salas, incluindo as salas com conteúdo sensível. - Mostrar salas com conteúdo sensível - Lista de salas + Mostrar todas as salas no diretório de salas, incluindo salas com conteúdo explícito. + Mostrar salas com conteúdo explícito + Diretório de salas Novo valor Alterar - Sincronização inicial: -\nBaixando dados… - Sincronização inicial: -\nAguardando resposta do servidor… + Sinc Inicial: +\nFazendo download de dados… + Sinc Inicial: +\nEsperando por resposta de servidor… Nível de confiança: confiável Nível de confiança: alerta Deseja mesmo excluir todas as mensagens não enviadas nesta sala\? @@ -2741,4 +2742,19 @@ %d entradas Limite do envio de arquivo do servidor + Qualquer pessoa num espaço com esta sala pode encontrar e juntar-se a ela. Somente admins desta sala podem adicioná-la a um espaço. + Espaços + Qualquer pessoa pode encontrar a sala e juntar-se + Pública + Somente pessoas convidadas podem encontrar e juntar-se + Privada + Configuração de acesso desconhecida (%s) + Qualquer pessoa que possa tocar na sala, membros pdem então aceitar ou rejeitar + Permitir visitantes se juntarem + Usar como default e não perguntar de novo + Sempre perguntar + Espaços + Convites + Salas Sugeridas + Mensagem enviada \ No newline at end of file From 0a6adad68087c36938a3d572a447dcf8f0734548 Mon Sep 17 00:00:00 2001 From: Julian Heinzel Date: Tue, 18 May 2021 23:20:40 +0000 Subject: [PATCH 138/202] Translated using Weblate (German) Currently translated at 100.0% (18 of 18 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/de/ --- .../metadata/android/de/full_description.txt | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt index 69343cf0e3..1334adf554 100644 --- a/fastlane/metadata/android/de/full_description.txt +++ b/fastlane/metadata/android/de/full_description.txt @@ -1,39 +1,39 @@ -Element ist mehr als ein sicherer Messenger. Es ist ein produktives Kolaborationsapp für das Team und eignet sich ideal für den Gruppenchat beim Arbeiten von zuhause aus. Mit eingebauter Ende-zu-Ende-Verschlüsselung ermöglicht Element umfangreiche und sichere Videokonferenzen, das Teilen von Dokumenten/Dateien und Videoanrufe. +Element ist einerseits ein sicherer Messenger, andererseits ideal geeignet für die produktive Zusammenarbeit mit dem Team im Homeoffice. Mit eingebauter Ende-zu-Ende-Verschlüsselung ermöglicht Element umfangreiche und sichere Videokonferenzen, das Teilen von Dateien sowie Sprachanrufe. -Element enthält folgende Funktionen: -- Fortgeschrittene Werkzeuge für die Online-Kommunikation -- Vollverschlüsselte Nachrichten um eine sichere Kommunikation innerhalb und außerhalb des Unternehmens zu ermöglichen -- Dezentralisierte Chats basierend auf das quelloffene Matrix-Framework -- Sichere und kontrollierte Dateienfreigabe durch verschlüsselte Daten beim verwalten von Projekten -- Videochats über VoIP und Bildschirmübertragung -- Einfache Einbindungen mit Ihren favorisierten Online-Kolaborationswerkzeugen, Projektverwaltungswerkzeugen, VoIP-Diensten und andere Kommunikationsapps für Ihren Team +Element bietet folgende Funktionen: +- Fortschrittliche Werkzeuge für die Online-Kommunikation +- Vollständig verschlüsselte Nachrichten, um eine sichere Kommunikation innerhalb und außerhalb von Unternehmen zu ermöglichen +- Dezentralisierte Chats, basierend auf dem quelloffenen Matrix-Framework +- Sichere und kontrollierte Dateifreigabe durch verschlüsselte Daten beim Verwalten von Projekten +- Videochats mit VoIP und Bildschirmübertragung +- Einfache Einbindung in Ihre bevorzugten Online-Kollaborations- und Projektverwaltungswerkzeuge, VoIP-Dienste und andere Kommunikationsapps für Ihr Team -Element unterscheidet sich deutlich von anderen Kommunikations- und Kollaborationsapps. Es läuft auf Matrix, ein offenes Netzwerk für eine sichere und dezentralisierte Kommunikation. Es erlaubt den Nutzern ihre eigenen Matrix-Dienste zu betreiben und gibt ihnen damit die vollständige Kontrolle und Besitz über ihre eigenen Daten und Nachrichten. +Element unterscheidet sich grundlegend von anderen Kommunikations- und Kollaborationsapps. Es läuft auf Matrix, einem offenen Netzwerk für sichere und dezentralisierte Kommunikation. Es erlaubt Nutzern ihre eigene Infrastruktur zu betreiben und gibt ihnen damit vollständige Kontrolle und Besitz über ihre eigenen Daten und Nachrichten. Privatsphäre/Datenschutz und verschlüsselte Kommunikation -Element schützt Ihnen vor unerwünschte Werbung, das Datenschürfen und geschlossene unentkommbare Dienste. Auch schützt es all Ihre Daten, Video und Sprachkommunikation unter vier Augen durch Ende-zu-Ende-Verschlüsselung und das Quersignieren von Geräten zur Verifizierung. +Element schützt Sie vor unerwünschter Werbung, dem Datenschürfen und abgeschlossenen Plattformen. Auch schützt es all Ihre Daten, Ihre Video- und Sprachkommunikation unter vier Augen, durch Ende-zu-Ende-Verschlüsselung und durch das Quersignieren von Geräten zur Verifizierung. -Element gibt Ihnen die Kontrolle über Ihre Privatsphäre, während es Ihnen ermöglicht mit jeden auf dem Matrix-Netzwerk oder andere geschäftliche Kollaborationswerkzeuge durch das Einbinden von Apps wie Slack sicher zu kommunizieren. +Element gibt Ihnen die Kontrolle über Ihre Privatsphäre und ermöglicht es Ihnen zugleich, mit jedem im Matrix-Netzwerk sicher zu kommunizieren - oder auch auf anderen geschäftlichen Kollaborationswerkzeugen, zum Beispiel durch das Einbinden von Apps wie Slack. Element kann man selber betreiben -Um mehr Kontrolle über Ihre sensiblen Daten und Konversationen zu ermöglichen, kann man Element selbst betreiben oder Sie wählen irgendeinen Matrix-basierten Dienst - der standard für quelloffene, dezentralisierte Kommunikation. Element gibt Ihnen Privatsphäre, Sicherheitskonformität und die Flexibilität zum Integrieren. +Um mehr Kontrolle über Ihre sensiblen Daten und Konversationen zu ermöglichen, kann man Element selbst betreiben, oder Sie wählen irgendeinen Matrix-basierten Dienst - der Standard für quelloffene, dezentralisierte Kommunikation. Element gibt Ihnen Privatsphäre, Sicherheitskonformität und Flexibilität für Integrationen. Besitzen Sie Ihre Daten -Sie entscheiden wo Sie Ihre Daten und Nachrichten aufbewahren, ohne das Risiko des Datenschürfens oder des Zugriffes Dritter. +Sie entscheiden, wo Sie Ihre Daten und Nachrichten aufbewahren - ohne das Datenschürfen oder den Zugriff Dritter zu riskieren. -Element gibt Ihnen die Kontrolle durch verschiedene Wege: -1. Kostenlos auf dem öffentlichen matrix.org Server registrieren, der von den Matrix-Entwicklern gehostet wird, oder wähle aus Tausenden von öffentlichen Servern, die von Freiwilligen gehostet werden -2. Einen Konto auf einem eigenen Server in der eigenen IT-Infrastruktur betreiben -3. Einen Konto auf einem benutzerdefinierten Server erstellen, zum Beispiel durch ein Abonnement bei Element Matrix Services (kurz EMS) +Element gibt Ihnen auf verschiedene Arten die Kontrolle: +1. Kostenlos auf dem öffentlichen matrix.org-Server registrieren, der von den Matrix-Entwicklern gehostet wird, oder wählen Sie aus Tausenden von öffentlichen Servern, die von Freiwilligen betrieben werden +2. Ein Konto auf einem eigenen Server in der eigenen IT-Infrastruktur betreiben +3. Einen Konto auf einem maßgeschneiderten Server erstellen, zum Beispiel durch ein Abonnement der Element Matrix Services (kurz EMS) Offene Kommunikation und Zusammenarbeit -Sie können mit jeden auf dem Matrix-Netzwerk chatten, egal ob sie Element, eine Matrix-App oder sogar eine andere Kommunikationsapp nutzen. +Sie können mit jedem im Matrix-Netzwerk chatten, egal ob ihr Kontakt Element, eine andere Matrix-App oder sogar eine völlig andere Anwendung nutzt. Super sicher -Reale Ende-zu-Ende-Verschlüsselung (nur die Personen in der Konversation können die Nachricht entschüsseln) und Quersignierung von Geräten zur Verifizierung. +Echte Ende-zu-Ende-Verschlüsselung (nur die Personen in der Unterhaltung können die Nachrichten entschüsseln), sowie die Quersignierung von Geräten zur Verifizierung. Vollständige Kommunikation und Integration -Kurznachrichten, Sprach- und Videoanrufe, kontrollierte Dateifreigaben, Bildschirmübertragungen und eine ganze Reihe an Integrationen, Bots and Widgets. Schaffe Räume, Gemeinschaften, bleibe auf dem Laufenden und erledige Sachen. +Kurznachrichten, Sprach- und Videoanrufe, Dateifreigaben, Bildschirmübertragungen und eine ganze Reihe an Integrationen, Bots and Widgets. Schaffen Sie Räume, Communities, bleiben Sie auf dem Laufenden und erledigen Sie Sachen. -Das Stehengelassene später wieder aufgreifen -Bleibe auf dem Laufenden, egal wo Sie sind, mit vollständig synchronisierter Nachrichtenverlauf quer über all Ihrer Geräte und im Netz auf https://app.element.io +Da Weitermachen, wo Sie aufgehört haben +Bleiben Sie in Kontakt, egal wo Sie sind, mit vollständig synchronisiertem Nachrichtenverlauf quer über all Ihre Geräte und im Netz auf https://app.element.io From 6961e54f1541106839e062a679dadcc03d0095ab Mon Sep 17 00:00:00 2001 From: Julian Heinzel Date: Tue, 18 May 2021 22:18:12 +0000 Subject: [PATCH 139/202] Translated using Weblate (German) Currently translated at 100.0% (2454 of 2454 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- vector/src/main/res/values-de/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index f37a501ae0..07915db3fe 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -2731,7 +2731,7 @@ Nicht gesendete Nachrichten löschen Fehlgeschlagen Willst du zu sendende Nachrichten zurückziehen\? - Alle fehgeschlagene Nachrichten löschen + Alle fehlgeschlagene Nachrichten löschen Senden der Nachricht gescheitert Wird gesendet Nachricht gesendet @@ -2792,8 +2792,8 @@ Ein privater Space um deine Räume zu organisieren Um einem bereits existierenden Space beizutreten, benötigst du eine Einladung. Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt. - Dein Privater space - Dein Öffentliches Space + Dein privater Space + Dein öffentlicher Space Betrete einen Space mit der angegebenen ID Beschreibung Erzeuge Space… From 4cf38951ef161ddcc89fed894b8dffecb9ffb97e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 11:02:36 +0200 Subject: [PATCH 140/202] Fix warning after bump appcompat from 1.2.0 to 1.3.0 --- .../lib/attachmentviewer/AttachmentViewerActivity.kt | 9 ++++++--- .../attachments/preview/AttachmentsPreviewFragment.kt | 7 +++++-- .../home/room/detail/composer/ComposerEditText.kt | 4 ++-- .../app/features/notifications/NotificationUtils.kt | 5 ++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt index 418b5b5cbb..f909418d6f 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt @@ -33,6 +33,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.GestureDetectorCompat import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.transition.TransitionManager @@ -124,9 +125,11 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi scaleDetector = createScaleGestureDetector() ViewCompat.setOnApplyWindowInsetsListener(views.rootContainer) { _, insets -> - overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom) - topInset = insets.systemWindowInsetTop - bottomInset = insets.systemWindowInsetBottom + val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + + overlayView?.updatePadding(top = systemBarsInsets.top, bottom = systemBarsInsets.bottom) + topInset = systemBarsInsets.top + bottomInset = systemBarsInsets.bottom insets } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index 9594f89a0e..0e46cb2c78 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -30,6 +30,7 @@ import android.view.ViewGroup import android.widget.Toast import androidx.core.net.toUri import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager @@ -172,12 +173,14 @@ class AttachmentsPreviewFragment @Inject constructor( view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION } ViewCompat.setOnApplyWindowInsetsListener(views.attachmentPreviewerBottomContainer) { v, insets -> - v.updatePadding(bottom = insets.systemWindowInsetBottom) + val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(bottom = systemBarsInsets.bottom) insets } ViewCompat.setOnApplyWindowInsetsListener(views.attachmentPreviewerToolbar) { v, insets -> + val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.updateLayoutParams { - topMargin = insets.systemWindowInsetTop + topMargin = systemBarsInsets.top } insets } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index 2257e5ee81..45c937ca5e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -41,8 +41,8 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib var callback: Callback? = null - override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { - val ic: InputConnection = super.onCreateInputConnection(editorInfo) + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { + val ic = super.onCreateInputConnection(editorInfo) ?: return null EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("*/*")) val callback = diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index e7cafc6a9b..35cc95f3dc 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -559,9 +559,8 @@ class NotificationUtils @Inject constructor(private val context: Context, NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply, stringProvider.getString(R.string.action_quick_reply), replyPendingIntent) .addRemoteInput(remoteInput) - .build()?.let { - addAction(it) - } + .build() + .let { addAction(it) } } } From b11e6a2b26f4fd4fe228020f0c7683e0c97d3594 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 13:53:51 +0200 Subject: [PATCH 141/202] ktlint --- .../vector/app/features/login2/LoginFragmentSignupUsername2.kt | 1 - .../java/im/vector/app/features/login2/LoginFragmentToAny2.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt index 00b06ed82d..ff6a218796 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt @@ -23,7 +23,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.autofill.HintConstants -import androidx.core.text.isDigitsOnly import androidx.core.view.isVisible import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index d865e16e35..892699723e 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -23,7 +23,6 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants -import androidx.core.text.isDigitsOnly import androidx.core.view.isVisible import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R From 4ba40b1d34894050610ad3b4bf8b092f048879e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 13:56:41 +0200 Subject: [PATCH 142/202] Disable LoginFlowV2 to merge it on develop --- CHANGES.md | 1 - vector/build.gradle | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 323eb53485..74b008bfc4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,6 @@ Features ✨: Improvements 🙌: - Add ability to install APK from directly from Element (#2381) - - Improve login/register flow (#1410, #2585, #3172) Bugfix 🐛: - Message states cosmetic changes (#3007) diff --git a/vector/build.gradle b/vector/build.gradle index 7b4abeb385..bc9cd8c1ca 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -138,8 +138,9 @@ android { resValue "string", "build_number", "\"${buildNumber}\"" // The two booleans must not have the same value. We need two values for the manifest - resValue "bool", "useLoginV1", "false" - resValue "bool", "useLoginV2", "true" + // LoginFlowV2 is disabled to be merged on develop (changelog: Improve login/register flow (#1410, #2585, #3172)) + resValue "bool", "useLoginV1", "true" + resValue "bool", "useLoginV2", "false" buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" From e7c5353240dd17131f907a80fd175d70561900d0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 15:48:22 +0200 Subject: [PATCH 143/202] PR merged after the release --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 372ac26196..a28b84410e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Bugfix 🐛: - Wrong copy in share space bottom sheet (#3346) - Fix a problem with database migration on nightly builds (#3335) - Implement a workaround to render <del> and <u> in the timeline (#1817) + - Make sure the SDK can retrieve the secret storage if the system is upgraded (#3304) Translations 🗣: - @@ -59,7 +60,6 @@ Bugfix 🐛: - Properly clean the back stack if the user cancel registration when waiting for email validation - Fix read marker visibility/position when filtering some events - Fix user invitation in case of restricted profile api (#3306) - - Make sure the SDK can retrieve the secret storage if the system is upgraded (#3304) SDK API changes ⚠️: - RegistrationWizard.createAccount() parameters are now all optional, following Matrix spec (#3205) From 9df11d6873dd4809fee5c58099449f9c79d32027 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 16:41:21 +0200 Subject: [PATCH 144/202] Configure towncrier tool --- tools/towncrier/template.md | 47 +++++++++++++++++++++++++++++++++++++ towncrier.toml | 7 ++++++ 2 files changed, 54 insertions(+) create mode 100644 tools/towncrier/template.md create mode 100644 towncrier.toml diff --git a/tools/towncrier/template.md b/tools/towncrier/template.md new file mode 100644 index 0000000000..fd160682b5 --- /dev/null +++ b/tools/towncrier/template.md @@ -0,0 +1,47 @@ +{% if top_line %} +{{ top_line }} +{{ top_underline * ((top_line)|length)}} +{% elif versiondata.name %} +{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }}) +{{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}} +{% else %} +{{ versiondata.version }} ({{ versiondata.date }}) +{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}} +{% endif %} +{% for section, _ in sections.items() %} +{% set underline = underlines[0] %}{% if section %}{{section}} +{{ underline * section|length }}{% set underline = underlines[1] %} +{% endif %} + +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section]%} +{% if definitions[category]['name'] == "Features" %} +Features ✨: +{% elif definitions[category]['name'] == "Bugfixes" %} +Bugfixes 🐛: +{% elif definitions[category]['name'] == "Deprecations and Removals" %} +SDK API changes ⚠️: +{% elif definitions[category]['name'] == "Improved Documentation" %} +Improved Documentation 📚: +{% elif definitions[category]['name'] == "Misc" %} +Other changes: +{% else %} +{{ definitions[category]['name'] }} +{% endif %} +{% if definitions[category]['showcontent'] %} +{% for text, values in sections[section][category].items() %} + - {{ text }} ({{ values|join(', ') }}) +{% endfor %} +{% else %} + - {{ sections[section][category]['']|join(', ') }} +{% endif %} +{% if sections[section][category]|length == 0 %} +No significant changes. +{% else %} +{% endif %} + +{% endfor %} +{% else %} +No significant changes. +{% endif %} +{% endfor %} diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 0000000000..09a927b77f --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,7 @@ +[tool.towncrier] +directory = "newsfragment" +filename = "CHANGES.md" +name = "Changes in Element" +# Note: there is a bug, if I use title_format, the title is printed twice +# title_format = "Changes in Element {version} ({project_date})" +template="tools/towncrier/template.md" From 6240910b9063a91959ad694dfc337a1f4cf1aaee Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 16:53:26 +0200 Subject: [PATCH 145/202] Configure towncrier tools - Update documentation --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CHANGES.md | 33 -------------------------------- CONTRIBUTING.md | 16 ++++++++++++++-- 3 files changed, 15 insertions(+), 36 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 34d7b40a88..501aa6784a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,6 +5,6 @@ - [ ] Changes has been tested on an Android device or Android emulator with API 21 - [ ] UI change has been tested on both light and dark themes - [ ] Pull request is based on the develop branch -- [ ] Pull request updates [CHANGES.md](https://github.com/vector-im/element-android/blob/develop/CHANGES.md) +- [ ] Pull request includes a new file under ./newsfragment. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog - [ ] Pull request includes screenshots or videos if containing UI changes - [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#sign-off) diff --git a/CHANGES.md b/CHANGES.md index a28b84410e..97beae1dbe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1379,36 +1379,3 @@ Changes in RiotX 0.1.0 (2019-07-11) First release! Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771 - - -======================================================= -+ TEMPLATE WHEN PREPARING A NEW RELEASE + -======================================================= - - -Changes in Element 1.1.X (2021-XX-XX) -=================================================== - -Features ✨: - - - -Improvements 🙌: - - - -Bugfix 🐛: - - - -Translations 🗣: - - - -SDK API changes ⚠️: - - - -Build 🧱: - - - -Test: - - - -Other changes: - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd32991051..399216ac4f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,9 +51,21 @@ If an issue does not exist yet, it may be relevant to open a new issue and let u This project is full Kotlin. Please do not write Java classes. -### CHANGES.md +### Changelog -Please add a line to the top of the file `CHANGES.md` describing your change. +Please create at least one file under ./newsfragment containing details about your change. Towncrier will be used when preparing the release. + +Towncrier says to use the PR number for the filename, but the issue number is also fine. + +Supported filename extensions are: + +- ``.feature``: Signifying a new feature in Element Android or in the Matrix SDK. +- ``.bugfix``: Signifying a bug fix. +- ``.doc``: Signifying a documentation improvement. +- ``.removal``: Signifying a deprecation or removal of public API. Can be used to notifying about API change in the Matrix SDK +- ``.misc``: A ticket has been closed, but it is not of interest to users. + +See https://github.com/twisted/towncrier#news-fragments if you need more details. ### Code quality From 86a861b77930d2291d2d6c3fd875986c4b3f8a55 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 16:54:41 +0200 Subject: [PATCH 146/202] Configure towncrier tools - start using it! --- newsfragment/3293.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragment/3293.misc diff --git a/newsfragment/3293.misc b/newsfragment/3293.misc new file mode 100644 index 0000000000..66de1440e7 --- /dev/null +++ b/newsfragment/3293.misc @@ -0,0 +1 @@ +Setup towncrier tool \ No newline at end of file From 4c24fe815cb3e6c0e8db2592930dbea2b3ebc501 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 16:55:30 +0200 Subject: [PATCH 147/202] Improve doc --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 399216ac4f..4b61777d3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ Supported filename extensions are: - ``.bugfix``: Signifying a bug fix. - ``.doc``: Signifying a documentation improvement. - ``.removal``: Signifying a deprecation or removal of public API. Can be used to notifying about API change in the Matrix SDK -- ``.misc``: A ticket has been closed, but it is not of interest to users. +- ``.misc``: A ticket has been closed, but it is not of interest to users. Note that in this case, the content of the file will not be output, but just the issue/PR number. See https://github.com/twisted/towncrier#news-fragments if you need more details. From 2462c871b29e7b3b1b2b65f3cbb8bde32c59697e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 17:14:35 +0200 Subject: [PATCH 148/202] Update the CI tool. --- tools/travis/check_pr.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/travis/check_pr.sh b/tools/travis/check_pr.sh index 4c0d9611f0..b797244e55 100755 --- a/tools/travis/check_pr.sh +++ b/tools/travis/check_pr.sh @@ -23,7 +23,7 @@ branch=${TRAVIS_BRANCH} # If not on develop, exit, else we cannot get the list of modified files # It is ok to check only when on develop branch if [[ "${branch}" -eq 'develop' ]]; then - echo "Check that the file 'CHANGES.md' has been modified" + echo "Check that a file has been added to /newsfragment" else echo "Not on develop branch" exit 0 @@ -37,9 +37,9 @@ listOfModifiedFiles=`git diff --name-only HEAD ${branch}` # echo ${listOfModifiedFiles} -if [[ ${listOfModifiedFiles} = *"CHANGES.md"* ]]; then - echo "CHANGES.md has been modified!" +if [[ ${listOfModifiedFiles} = *"newsfragment"* ]]; then + echo "A file has been added to /newsfragment!" else - echo "❌ Please add a line describing your change in CHANGES.md" + echo "❌ Please add a file describing your changes in /newsfragment. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog" exit 1 fi From e4b65053d4c983ae2832ebb9cc8df4570469f50e Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 20 May 2021 18:41:27 +0200 Subject: [PATCH 149/202] Jitsi auth: fix openId API --- .../sdk/internal/session/thirdparty/ThirdPartyAPI.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt index c4f17835ba..3e810a1a13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -19,8 +19,11 @@ package org.matrix.android.sdk.internal.session.thirdparty import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.QueryMap @@ -50,8 +53,10 @@ internal interface ThirdPartyAPI { * * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") - suspend fun requestOpenIdToken(@Path("userId") userId: String): OpenIdToken + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") + suspend fun requestOpenIdToken(@Path("userId") userId: String, + // We should post an empty body + @Body body: JsonDict = HashMap()): OpenIdToken } From 201f4c342a32492f4b61f9290e14d560451d9ead Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 May 2021 18:51:09 +0200 Subject: [PATCH 150/202] Fix lint issue --- vector/src/main/res/layout/composer_layout.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml index 4d65100a97..9de2053fc5 100644 --- a/vector/src/main/res/layout/composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -81,8 +81,8 @@ android:background="?android:attr/selectableItemBackground" android:contentDescription="@string/cancel" android:src="@drawable/ic_close_round" - android:tint="@color/riotx_notice" - tools:ignore="MissingConstraints" /> + app:tint="@color/riotx_notice" + tools:ignore="MissingConstraints,MissingPrefix" /> Date: Thu, 20 May 2021 18:53:56 +0200 Subject: [PATCH 151/202] Jitsi auth: fix some mistakes and gives the jwt to Jitsi --- .../session/thirdparty/ThirdPartyService.kt | 1 - .../session/thirdparty/ThirdPartyAPI.kt | 2 - .../java/im/vector/app/core/network/OkHttp.kt | 2 +- .../java/im/vector/app/core/utils/Base32.kt | 1 - .../java/im/vector/app/core/utils/UrlUtils.kt | 7 + .../call/conference/JitsiCallViewEvents.kt | 6 +- .../call/conference/JitsiCallViewModel.kt | 43 ++-- .../features/call/conference/JitsiService.kt | 64 +++++- .../JitsiWidgetPropertiesFactory.kt | 2 +- .../call/conference/VectorJitsiActivity.kt | 22 ++- .../call/conference/jwt/JitsiJWTFactory.kt | 27 +-- .../home/room/detail/RoomDetailViewModel.kt | 183 +++++++----------- vector/src/main/res/values/strings.xml | 1 + 13 files changed, 183 insertions(+), 178 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt index 708ff39c3a..28ac3832f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt @@ -43,5 +43,4 @@ interface ThirdPartyService { * The generated token is only valid for exchanging for user information from the federation API for OpenID. */ suspend fun getOpenIdToken(): OpenIdToken - } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt index 3e810a1a13..3c3f57a504 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -57,6 +57,4 @@ internal interface ThirdPartyAPI { suspend fun requestOpenIdToken(@Path("userId") userId: String, // We should post an empty body @Body body: JsonDict = HashMap()): OpenIdToken - - } diff --git a/vector/src/main/java/im/vector/app/core/network/OkHttp.kt b/vector/src/main/java/im/vector/app/core/network/OkHttp.kt index 338ebab0b4..1bc6621771 100644 --- a/vector/src/main/java/im/vector/app/core/network/OkHttp.kt +++ b/vector/src/main/java/im/vector/app/core/network/OkHttp.kt @@ -40,7 +40,7 @@ suspend fun Call.await(): Response { try { cancel() } catch (ex: Throwable) { - //Ignore cancel exception + // Ignore cancel exception } } } diff --git a/vector/src/main/java/im/vector/app/core/utils/Base32.kt b/vector/src/main/java/im/vector/app/core/utils/Base32.kt index 4a42a252a1..9f220e08eb 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Base32.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Base32.kt @@ -25,4 +25,3 @@ fun String.toBase32String(padding: Boolean = true): String { base32.replace("=", "") } } - diff --git a/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt b/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt index 095e01fa56..d292612e54 100644 --- a/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt @@ -38,6 +38,13 @@ internal fun String.ensureProtocol(): String { } } +/** + * Ensure string do not starts with "http" or "https" protocol. + */ +internal fun String.ensureNoProtocol(): String { + return removePrefix("https://").removePrefix("http://") +} + internal fun String.ensureTrailingSlash(): String { return when { isEmpty() -> this diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt index d41f758f52..c8d570a73f 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt @@ -20,12 +20,13 @@ import im.vector.app.core.platform.VectorViewEvents import org.jitsi.meet.sdk.JitsiMeetUserInfo sealed class JitsiCallViewEvents : VectorViewEvents { - data class StartConference( + data class JoinConference( val enableVideo: Boolean, val jitsiUrl: String, val subject: String, val confId: String, - val userInfo: JitsiMeetUserInfo + val userInfo: JitsiMeetUserInfo, + val token: String? ) : JitsiCallViewEvents() data class ConfirmSwitchingConference( @@ -33,5 +34,6 @@ sealed class JitsiCallViewEvents : VectorViewEvents { ) : JitsiCallViewEvents() object LeaveConference : JitsiCallViewEvents() + object FailJoiningConference: JitsiCallViewEvents() object Finish : JitsiCallViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt index 92dd2ebcd0..0fc85cb58c 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -27,24 +27,19 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.themes.ThemeProvider import io.reactivex.disposables.Disposable import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.jitsi.meet.sdk.JitsiMeetUserInfo import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.rx.asObservable -import java.net.URL class JitsiCallViewModel @AssistedInject constructor( @Assisted initialState: JitsiCallViewState, private val session: Session, - private val jitsiMeetPropertiesFactory: JitsiWidgetPropertiesFactory, - private val themeProvider: ThemeProvider + private val jitsiService: JitsiService ) : VectorViewModel(initialState) { @AssistedFactory @@ -55,7 +50,7 @@ class JitsiCallViewModel @AssistedInject constructor( private var currentWidgetObserver: Disposable? = null private val widgetService = session.widgetService() - private var confIsStarted = false + private var confIsJoined = false private var pendingArgs: VectorJitsiActivity.Args? = null init { @@ -63,7 +58,7 @@ class JitsiCallViewModel @AssistedInject constructor( } private fun observeWidget(roomId: String, widgetId: String) { - confIsStarted = false + confIsJoined = false currentWidgetObserver?.dispose() currentWidgetObserver = widgetService.getRoomWidgetsLive(roomId, QueryStringValue.Equals(widgetId), WidgetType.Jitsi.values()) .asObservable() @@ -74,10 +69,9 @@ class JitsiCallViewModel @AssistedInject constructor( setState { copy(widget = Success(jitsiWidget)) } - - if (!confIsStarted) { - confIsStarted = true - startConference(jitsiWidget) + if (!confIsJoined) { + confIsJoined = true + joinConference(jitsiWidget) } } else { setState { @@ -90,24 +84,15 @@ class JitsiCallViewModel @AssistedInject constructor( .disposeOnClear() } - private fun startConference(jitsiWidget: Widget) = withState { state -> - val me = session.getRoomMember(session.myUserId, state.roomId)?.toMatrixItem() - val userInfo = JitsiMeetUserInfo().apply { - displayName = me?.getBestName() - avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) } + private fun joinConference(jitsiWidget: Widget) = withState { state -> + viewModelScope.launch { + try { + val joinConference = jitsiService.joinConference(state.roomId, jitsiWidget, state.enableVideo) + _viewEvents.post(joinConference) + } catch (throwable: Throwable) { + _viewEvents.post(JitsiCallViewEvents.FailJoiningConference) + } } - val roomName = session.getRoomSummary(state.roomId)?.displayName - - val ppt = widgetService.getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme()) - ?.let { url -> jitsiMeetPropertiesFactory.create(url) } - - _viewEvents.post(JitsiCallViewEvents.StartConference( - enableVideo = state.enableVideo, - jitsiUrl = "https://${ppt?.domain}", - subject = roomName ?: "", - confId = ppt?.confId ?: "", - userInfo = userInfo - )) } override fun handle(action: JitsiCallViewActions) { diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt index c3632d282a..d49ffc1306 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -19,27 +19,38 @@ package im.vector.app.features.call.conference import im.vector.app.R import im.vector.app.core.network.await import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.ensureNoProtocol +import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.toBase32String +import im.vector.app.features.call.conference.jwt.JitsiJWTFactory import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.themes.ThemeProvider import okhttp3.Request +import org.jitsi.meet.sdk.JitsiMeetUserInfo import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.appendParamToUrl +import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.di.MoshiProvider +import java.net.URL import java.util.UUID import javax.inject.Inject class JitsiService @Inject constructor( private val session: Session, private val rawService: RawService, - private val stringProvider: StringProvider) { + private val stringProvider: StringProvider, + private val themeProvider: ThemeProvider, + private val jitsiWidgetPropertiesFactory: JitsiWidgetPropertiesFactory, + private val jitsiJWTFactory: JitsiJWTFactory) { companion object { const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt" + private const val JITSI_AUTH_KEY = "auth" } suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget { @@ -49,8 +60,9 @@ class JitsiService @Inject constructor( rawService.getElementWellknown(session.myUserId) ?.jitsiServer ?.preferredDomain + ?.ensureNoProtocol() } - val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) + val jitsiDomain = (preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain)) val jitsiAuth = getJitsiAuth(jitsiDomain) val confId = createConferenceId(roomId, jitsiAuth) @@ -79,7 +91,7 @@ class JitsiService @Inject constructor( "conferenceId" to confId, "domain" to jitsiDomain, "isAudioOnly" to !withVideo, - "authenticationType" to jitsiAuth + JITSI_AUTH_KEY to jitsiAuth ), "creatorUserId" to session.myUserId, "id" to widgetId, @@ -89,6 +101,49 @@ class JitsiService @Inject constructor( return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) } + suspend fun joinConference(roomId: String, jitsiWidget: Widget, enableVideo: Boolean): JitsiCallViewEvents.JoinConference { + val me = session.getRoomMember(session.myUserId, roomId)?.toMatrixItem() + val userDisplayName = me?.getBestName() + val userAvatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) } + val userInfo = JitsiMeetUserInfo().apply { + this.displayName = userDisplayName + this.avatar = userAvatar?.let { URL(it) } + } + val roomName = session.getRoomSummary(roomId)?.displayName + val properties = session.widgetService().getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme()) + ?.let { url -> jitsiWidgetPropertiesFactory.create(url) } ?: throw IllegalStateException() + + val token = if (jitsiWidget.isOpenIdJWTAuthenticationRequired()) { + getOpenIdJWTToken(roomId, properties.domain, userDisplayName ?: session.myUserId, userAvatar ?: "") + } else { + null + } + return JitsiCallViewEvents.JoinConference( + enableVideo = enableVideo, + jitsiUrl = properties.domain.ensureProtocol(), + subject = roomName ?: "", + confId = properties.confId ?: "", + userInfo = userInfo, + token = token + ) + } + + private fun Widget.isOpenIdJWTAuthenticationRequired(): Boolean { + return widgetContent.data[JITSI_AUTH_KEY] == JITSI_OPEN_ID_TOKEN_JWT_AUTH + } + + private suspend fun getOpenIdJWTToken(roomId: String, domain: String, userDisplayName: String, userAvatar: String): String { + val openIdToken = session.thirdPartyService().getOpenIdToken() + return jitsiJWTFactory.create( + homeServerName = session.sessionParams.homeServerUrl.ensureNoProtocol(), + jitsiServerDomain = domain, + openIdAccessToken = openIdToken.accessToken, + roomId = roomId, + userAvatarUrl = userAvatar, + userDisplayName = userDisplayName + ) + } + private fun createConferenceId(roomId: String, jitsiAuth: String?): String { return if (jitsiAuth == JITSI_OPEN_ID_TOKEN_JWT_AUTH) { // Create conference ID from room ID @@ -97,7 +152,6 @@ class JitsiService @Inject constructor( // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification roomId.toBase32String(padding = false) } else { - // Create a random conference ID // Create a random enough jitsi conference id // Note: the jitsi server automatically creates conference when the conference // id does not exist yet @@ -110,7 +164,7 @@ class JitsiService @Inject constructor( } private suspend fun getJitsiAuth(jitsiDomain: String): String? { - val request = Request.Builder().url("https://$jitsiDomain/.well-known/element/jitsi").build() + val request = Request.Builder().url("$jitsiDomain/.well-known/element/jitsi".ensureProtocol()).build() return tryOrNull { val response = session.getOkHttpClient().newCall(request).await() val json = response.body?.string() ?: return null diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt index 8014e01fb2..8ba8ec0c75 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt @@ -37,7 +37,7 @@ class JitsiWidgetPropertiesFactory @Inject constructor( .orEmpty() return JitsiWidgetProperties( - domain = configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain), + domain = (configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain)), confId = configs["conferenceId"], displayName = configs["displayName"], avatarUrl = configs["avatarUrl"] diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 3f2d52e9e7..15346422a6 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -25,6 +25,7 @@ import android.content.res.Configuration import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.localbroadcastmanager.content.LocalBroadcastManager @@ -86,8 +87,9 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee jitsiViewModel.observeViewEvents { when (it) { - is JitsiCallViewEvents.StartConference -> configureJitsiView(it) + is JitsiCallViewEvents.JoinConference -> configureJitsiView(it) is JitsiCallViewEvents.ConfirmSwitchingConference -> handleConfirmSwitching(it) + JitsiCallViewEvents.FailJoiningConference -> handleFailJoining() JitsiCallViewEvents.Finish -> finish() JitsiCallViewEvents.LeaveConference -> handleLeaveConference() }.exhaustive @@ -138,12 +140,18 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee } } - private fun configureJitsiView(startConference: JitsiCallViewEvents.StartConference) { + private fun handleFailJoining() { + Toast.makeText(this, getString(R.string.error_jitsi_join_conf), Toast.LENGTH_LONG).show() + finish() + } + + private fun configureJitsiView(joinConference: JitsiCallViewEvents.JoinConference) { val jitsiMeetConferenceOptions = JitsiMeetConferenceOptions.Builder() - .setVideoMuted(!startConference.enableVideo) - .setUserInfo(startConference.userInfo) + .setVideoMuted(!joinConference.enableVideo) + .setUserInfo(joinConference.userInfo) + .setToken(joinConference.token) .apply { - tryOrNull { URL(startConference.jitsiUrl) }?.let { + tryOrNull { URL(joinConference.jitsiUrl) }?.let { setServerURL(it) } } @@ -153,8 +161,8 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee .setFeatureFlag("add-people.enabled", false) .setFeatureFlag("video-share.enabled", false) .setFeatureFlag("call-integration.enabled", false) - .setRoom(startConference.confId) - .setSubject(startConference.subject) + .setRoom(joinConference.confId) + .setSubject(joinConference.subject) .build() jitsiMeetView?.join(jitsiMeetConferenceOptions) } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt index 7e9458841a..68475232c7 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt @@ -16,6 +16,7 @@ package im.vector.app.features.call.conference.jwt +import im.vector.app.core.utils.ensureProtocol import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.security.Keys @@ -27,28 +28,32 @@ class JitsiJWTFactory @Inject constructor() { * Create a JWT token for jitsi openidtoken-jwt authentication * See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification */ - fun create(jitsiServerDomain: String, - openIdAccessToken: String, - roomId: String, - userAvatarUrl: String, - userDisplayName: String): String { - + fun create(homeServerName: String, + jitsiServerDomain: String, + openIdAccessToken: String, + roomId: String, + userAvatarUrl: String, + userDisplayName: String): String { // The secret key here is irrelevant, we're only using the JWT to transport data to Prosody in the Jitsi stack. val key = Keys.secretKeyFor(SignatureAlgorithm.HS256) val context = mapOf( + "matrix" to mapOf( + "token" to openIdAccessToken, + "room_id" to roomId, + "server_name" to homeServerName + ), "user" to mapOf( "name" to userDisplayName, "avatar" to userAvatarUrl - ), - "matrix" to mapOf( - "token" to openIdAccessToken, - "room_id" to roomId ) ) + // As per Jitsi token auth, `iss` needs to be set to something agreed between + // JWT generating side and Prosody config. Since we have no configuration for + // the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense. return Jwts.builder() .setIssuer(jitsiServerDomain) .setSubject(jitsiServerDomain) - .setAudience("https://$jitsiServerDomain") + .setAudience(jitsiServerDomain.ensureProtocol()) // room is not used at the moment, a * works here. .claim("room", "*") .claim("context", context) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 3a9969b43c..44392309e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.CommandParser @@ -52,9 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsF import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory -import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorPreferences import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy @@ -68,7 +67,6 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -99,13 +97,11 @@ import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber -import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -115,7 +111,6 @@ class RoomDetailViewModel @AssistedInject constructor( private val stringProvider: StringProvider, private val rainbowGenerator: RainbowGenerator, private val session: Session, - private val rawService: RawService, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stickerPickerActionHandler: StickerPickerActionHandler, private val roomSummariesHolder: RoomSummariesHolder, @@ -123,6 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val callManager: WebRtcCallManager, private val chatEffectManager: ChatEffectManager, private val directRoomHelper: DirectRoomHelper, + private val jitsiService: JitsiService, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener { @@ -186,7 +182,7 @@ class RoomDetailViewModel @AssistedInject constructor( tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } } // Inform the SDK that the room is displayed - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { tryOrNull { session.onRoomDisplayed(initialState.roomId) } } callManager.addPstnSupportListener(this) @@ -267,67 +263,67 @@ class RoomDetailViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) - is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) - is RoomDetailAction.SendMedia -> handleSendMedia(action) - is RoomDetailAction.SendSticker -> handleSendSticker(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.EnterRegularMode -> handleEnterRegularMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) - is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) - is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailAction.ResendMessage -> handleResendEvent(action) - is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailAction.ReportContent -> handleReportContent(action) - is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) + is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) + is RoomDetailAction.SaveDraft -> handleSaveDraft(action) + is RoomDetailAction.SendMessage -> handleSendMessage(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.SendSticker -> handleSendSticker(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.EnterRegularMode -> handleEnterRegularMode(action) + is RoomDetailAction.EnterEditMode -> handleEditAction(action) + is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) + is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() - is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) - is RoomDetailAction.RequestVerification -> handleRequestVerification(action) - is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) - is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) - is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) - is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() - is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() - is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) - is RoomDetailAction.StartCall -> handleStartCall(action) - is RoomDetailAction.AcceptCall -> handleAcceptCall(action) - is RoomDetailAction.EndCall -> handleEndCall() - is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() - is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) - is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) - is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) - is RoomDetailAction.CancelSend -> handleCancel(action) - is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) - is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) - RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() - RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() - is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) - RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) - is RoomDetailAction.ShowRoomAvatarFullScreen -> { + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() + is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) + is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) + is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) + is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) + is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() + is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() + is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) + is RoomDetailAction.StartCall -> handleStartCall(action) + is RoomDetailAction.AcceptCall -> handleAcceptCall(action) + is RoomDetailAction.EndCall -> handleEndCall() + is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() + is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) + is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) + is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) + is RoomDetailAction.CancelSend -> handleCancel(action) + is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) + is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) + RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() + RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() + is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) + RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) + is RoomDetailAction.ShowRoomAvatarFullScreen -> { _viewEvents.post( RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) ) } - is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) - RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() - RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) + RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() + RoomDetailAction.ResendAll -> handleResendAll() }.exhaustive } @@ -437,57 +433,8 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) viewModelScope.launch(Dispatchers.IO) { - // Build data for a jitsi widget - val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis() - - // Create a random enough jitsi conference id - // Note: the jitsi server automatically creates conference when the conference - // id does not exist yet - var widgetSessionId = UUID.randomUUID().toString() - - if (widgetSessionId.length > 8) { - widgetSessionId = widgetSessionId.substring(0, 7) - } - val roomId: String = room.roomId - val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale) - - val preferredJitsiDomain = tryOrNull { - rawService.getElementWellknown(session.myUserId) - ?.jitsiServer - ?.preferredDomain - } - val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) - - // We use the default element wrapper for this widget - // https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md - // https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/WidgetUtils.ts#L469 - val url = buildString { - append("https://app.element.io/jitsi.html") - appendParamToUrl("confId", confId) - append("#conferenceDomain=\$domain") - append("&conferenceId=\$conferenceId") - append("&isAudioOnly=\$isAudioOnly") - append("&displayName=\$matrix_display_name") - append("&avatarUrl=\$matrix_avatar_url") - append("&userId=\$matrix_user_id") - append("&roomId=\$matrix_room_id") - append("&theme=\$theme") - } - val widgetEventContent = mapOf( - "url" to url, - "type" to WidgetType.Jitsi.legacy, - "data" to mapOf( - "conferenceId" to confId, - "domain" to jitsiDomain, - "isAudioOnly" to !action.withVideo - ), - "creatorUserId" to session.myUserId, - "id" to widgetId, - "name" to "jitsi" - ) - try { - val widget = session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) + val widget = jitsiService.createJitsiWidget(room.roomId, action.withVideo) _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) @@ -670,13 +617,13 @@ class RoomDetailViewModel @AssistedInject constructor( } when (itemId) { R.id.timeline_setting -> true - R.id.invite -> state.canInvite + R.id.invite -> state.canInvite R.id.open_matrix_apps -> true R.id.voice_call, - R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() - R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() - R.id.search -> true - R.id.dev_tools -> vectorPreferences.developerMode() + R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() + R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() + R.id.search -> true + R.id.dev_tools -> vectorPreferences.developerMode() else -> false } } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index aa85a52ec3..1fcd406364 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1675,6 +1675,7 @@ Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 6.0) + Sorry, an error occurred while trying to join the conference Leave the current conference and switch to the other one? This widget wants to use the following resources: From 2ca0397867cbbf9b9e3a0c08f4a44d0543982d1d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 20 May 2021 18:57:43 +0200 Subject: [PATCH 152/202] Update CHANGES --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0fcfabf2ed..343196ffea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,10 @@ Changes in Element 1.1.8 (2021-XX-XX) =================================================== Features ✨: - - + - Improvements 🙌: - - + - Support Jitsi authentication (#3379) Bugfix 🐛: - Fix a problem with database migration on nightly builds (#3335) From 03f81258c4c90eb4e89e343e30d239857e7035fa Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 20 May 2021 20:16:37 +0200 Subject: [PATCH 153/202] Jitsi auth: clean after Benoits review --- .../matrix/android/sdk/api/session/Session.kt | 6 +++ .../sdk/api/session/openid/OpenIdService.kt | 26 +++++++++++++ .../thirdparty => openid}/OpenIdToken.kt | 23 ++++++----- .../session/thirdparty/ThirdPartyService.kt | 7 ---- .../sdk/internal/session/DefaultSession.kt | 4 ++ .../sdk/internal/session/SessionModule.kt | 5 +++ .../session/identity/IdentityAuthAPI.kt | 4 +- .../session/identity/IdentityRegisterTask.kt | 6 +-- .../session/openid/DefaultOpenIdService.kt | 28 +++++++++++++ .../session/openid/GetOpenIdTokenTask.kt | 5 ++- .../sdk/internal/session/openid/OpenIdAPI.kt | 3 +- .../thirdparty/DefaultThirdPartyService.kt | 8 +--- .../session/thirdparty/GetOpenIdTokenTask.kt | 39 ------------------- .../session/thirdparty/ThirdPartyAPI.kt | 16 -------- .../session/thirdparty/ThirdPartyModule.kt | 3 -- .../internal/session/widgets/WidgetsAPI.kt | 4 +- .../java/im/vector/app/core/utils/Base32.kt | 2 +- .../java/im/vector/app/core/utils/UrlUtils.kt | 11 +----- .../features/call/conference/JitsiService.kt | 9 ++--- .../JitsiWidgetPropertiesFactory.kt | 2 +- .../call/conference/jwt/JitsiJWTFactory.kt | 8 ++-- 21 files changed, 106 insertions(+), 113 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/{room/model/thirdparty => openid}/OpenIdToken.kt (70%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetOpenIdTokenTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 86252665a6..b5f90e87ea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -233,6 +234,11 @@ interface Session : */ fun spaceService(): SpaceService + /** + * Returns the open id service associated with the session + */ + fun openIdService(): OpenIdService + /** * Add a listener to the session. * @param listener the listener to add. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt new file mode 100644 index 0000000000..65f6214f93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.openid + +interface OpenIdService { + + /** + * Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix. + * The generated token is only valid for exchanging for user information from the federation API for OpenID. + */ + suspend fun getOpenIdToken(): OpenIdToken +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/OpenIdToken.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt similarity index 70% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/OpenIdToken.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt index 67b39b57c6..2c2ea65681 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/OpenIdToken.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt @@ -14,32 +14,35 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.room.model.thirdparty +package org.matrix.android.sdk.api.session.openid import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -/** - * This class holds the response for openId request_token API - * See https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token - */ @JsonClass(generateAdapter = true) data class OpenIdToken( /** * Required. An access token the consumer may use to verify the identity of the person who generated the token. * This is given to the federation API GET /openid/userinfo to verify the user's identity. */ - @Json(name = "access_token") val accessToken: String, + @Json(name = "access_token") + val accessToken: String, + /** - * Required. The string Bearer. + * Required. The string "Bearer". */ - @Json(name = "token_type") val tokenType: String, + @Json(name = "token_type") + val tokenType: String, + /** * Required. The homeserver domain the consumer should use when attempting to verify the user's identity. */ - @Json(name = "matrix_server_name") val matrix_server_name: String, + @Json(name = "matrix_server_name") + val matrixServerName: String, + /** * Required. The number of seconds before this token expires and a new one must be generated. */ - @Json(name = "expires_in") val expires_in: Int + @Json(name = "expires_in") + val expiresIn: Int ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt index 28ac3832f2..2ae4562b0b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.thirdparty -import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser @@ -37,10 +36,4 @@ interface ThirdPartyService { * @param fields One or more custom fields that are passed to the AS to help identify the user. */ suspend fun getThirdPartyUser(protocol: String, fields: Map = emptyMap()): List - - /** - * Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix. - * The generated token is only valid for exchanging for user information from the federation API for OpenID. - */ - suspend fun getOpenIdToken(): OpenIdToken } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 53e13c14ec..b100a336a7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesServi import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -125,6 +126,7 @@ internal class DefaultSession @Inject constructor( private val thirdPartyService: Lazy, private val callSignalingService: Lazy, private val spaceService: Lazy, + private val openIdService: Lazy, @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy ) : Session, @@ -289,6 +291,8 @@ internal class DefaultSession @Inject constructor( override fun spaceService(): SpaceService = spaceService.get() + override fun openIdService(): OpenIdService = openIdService.get() + override fun getOkHttpClient(): OkHttpClient { return unauthenticatedWithCertificateOkHttpClient.get() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 63423b72c6..de74b34818 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService @@ -82,6 +83,7 @@ import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapab import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor @@ -373,6 +375,9 @@ internal abstract class SessionModule { @Binds abstract fun bindPermalinkService(service: DefaultPermalinkService): PermalinkService + @Binds + abstract fun bindOpenIdTokenService(service: DefaultOpenIdService): OpenIdService + @Binds abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt index 1671859585..9d990d4d8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.session.identity +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -52,5 +52,5 @@ internal interface IdentityAuthAPI { * The request body is the same as the values returned by /openid/request_token in the Client-Server API. */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") - suspend fun register(@Body openIdToken: RequestOpenIdTokenResponse): IdentityRegisterResponse + suspend fun register(@Body openIdToken: OpenIdToken): IdentityRegisterResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt index 8cc854bd94..1800d0eebe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt @@ -16,16 +16,16 @@ package org.matrix.android.sdk.internal.session.identity +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject internal interface IdentityRegisterTask : Task { data class Params( val identityAuthAPI: IdentityAuthAPI, - val openIdTokenResponse: RequestOpenIdTokenResponse + val openIdToken: OpenIdToken ) } @@ -33,7 +33,7 @@ internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegis override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { return executeRequest(null) { - params.identityAuthAPI.register(params.openIdTokenResponse) + params.identityAuthAPI.register(params.openIdToken) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt new file mode 100644 index 0000000000..b90a2435f7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.openid + +import org.matrix.android.sdk.api.session.openid.OpenIdService +import org.matrix.android.sdk.api.session.openid.OpenIdToken +import javax.inject.Inject + +internal class DefaultOpenIdService @Inject constructor(private val getOpenIdTokenTask: GetOpenIdTokenTask): OpenIdService { + + override suspend fun getOpenIdToken(): OpenIdToken { + return getOpenIdTokenTask.execute(Unit) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt index 8481a6ab93..a6ad025b8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt @@ -16,20 +16,21 @@ package org.matrix.android.sdk.internal.session.openid +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface GetOpenIdTokenTask : Task +internal interface GetOpenIdTokenTask : Task internal class DefaultGetOpenIdTokenTask @Inject constructor( @UserId private val userId: String, private val openIdAPI: OpenIdAPI, private val globalErrorReceiver: GlobalErrorReceiver) : GetOpenIdTokenTask { - override suspend fun execute(params: Unit): RequestOpenIdTokenResponse { + override suspend fun execute(params: Unit): OpenIdToken { return executeRequest(globalErrorReceiver) { openIdAPI.openIdToken(userId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt index ed090b845d..eb8c841d57 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.openid +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants import retrofit2.http.Body @@ -34,5 +35,5 @@ internal interface OpenIdAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") suspend fun openIdToken(@Path("userId") userId: String, - @Body body: JsonDict = emptyMap()): RequestOpenIdTokenResponse + @Body body: JsonDict = emptyMap()): OpenIdToken } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt index 8634a20bba..13829c400a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt @@ -16,15 +16,13 @@ package org.matrix.android.sdk.internal.session.thirdparty -import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser import javax.inject.Inject internal class DefaultThirdPartyService @Inject constructor(private val getThirdPartyProtocolTask: GetThirdPartyProtocolsTask, - private val getThirdPartyUserTask: GetThirdPartyUserTask, - private val getOpenIdTokenTask: GetOpenIdTokenTask) + private val getThirdPartyUserTask: GetThirdPartyUserTask) : ThirdPartyService { override suspend fun getThirdPartyProtocols(): Map { @@ -38,8 +36,4 @@ internal class DefaultThirdPartyService @Inject constructor(private val getThird ) return getThirdPartyUserTask.execute(taskParams) } - - override suspend fun getOpenIdToken(): OpenIdToken { - return getOpenIdTokenTask.execute(Unit) - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetOpenIdTokenTask.kt deleted file mode 100644 index e9d82d2a4c..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetOpenIdTokenTask.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.session.thirdparty - -import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver -import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.task.Task -import javax.inject.Inject - -internal interface GetOpenIdTokenTask : Task - -internal class DefaultGetOpenIdTokenTask @Inject constructor( - private val thirdPartyAPI: ThirdPartyAPI, - private val globalErrorReceiver: GlobalErrorReceiver, - @UserId private val userId: String -) : GetOpenIdTokenTask { - - override suspend fun execute(params: Unit): OpenIdToken { - return executeRequest(globalErrorReceiver) { - thirdPartyAPI.requestOpenIdToken(userId) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt index 3c3f57a504..2e03bc7a86 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -16,14 +16,10 @@ package org.matrix.android.sdk.internal.session.thirdparty -import org.matrix.android.sdk.api.session.room.model.thirdparty.OpenIdToken import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser -import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants -import retrofit2.http.Body import retrofit2.http.GET -import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.QueryMap @@ -45,16 +41,4 @@ internal interface ThirdPartyAPI { @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}") suspend fun getThirdPartyUser(@Path("protocol") protocol: String, @QueryMap params: Map?): List - - /** - * Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix. - * The generated token is only valid for exchanging for user information from the federation API for OpenID. - * The access token generated is only valid for the OpenID API. It cannot be used to request another OpenID access token or call /sync, for example. - * - * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token - */ - @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") - suspend fun requestOpenIdToken(@Path("userId") userId: String, - // We should post an empty body - @Body body: JsonDict = HashMap()): OpenIdToken } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt index 62bcca4850..d3acd7a9f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt @@ -44,7 +44,4 @@ internal abstract class ThirdPartyModule { @Binds abstract fun bindGetThirdPartyUserTask(task: DefaultGetThirdPartyUserTask): GetThirdPartyUserTask - - @Binds - abstract fun bindGetOpenIdTokenTask(task: DefaultGetOpenIdTokenTask): GetOpenIdTokenTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt index 6652628026..bfc243c213 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt @@ -15,7 +15,7 @@ */ package org.matrix.android.sdk.internal.session.widgets -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import org.matrix.android.sdk.api.session.openid.OpenIdToken import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -29,7 +29,7 @@ internal interface WidgetsAPI { * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) */ @POST("register") - suspend fun register(@Body body: RequestOpenIdTokenResponse, + suspend fun register(@Body body: OpenIdToken, @Query("v") version: String?): RegisterWidgetResponse @GET("account") diff --git a/vector/src/main/java/im/vector/app/core/utils/Base32.kt b/vector/src/main/java/im/vector/app/core/utils/Base32.kt index 9f220e08eb..3ccc562057 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Base32.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Base32.kt @@ -22,6 +22,6 @@ fun String.toBase32String(padding: Boolean = true): String { return if (padding) { base32 } else { - base32.replace("=", "") + base32.trimEnd('=') } } diff --git a/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt b/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt index d292612e54..70cda17ae6 100644 --- a/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt @@ -30,7 +30,7 @@ fun String.isValidUrl(): Boolean { /** * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty */ -internal fun String.ensureProtocol(): String { +fun String.ensureProtocol(): String { return when { isEmpty() -> this !startsWith("http") -> "https://$this" @@ -38,14 +38,7 @@ internal fun String.ensureProtocol(): String { } } -/** - * Ensure string do not starts with "http" or "https" protocol. - */ -internal fun String.ensureNoProtocol(): String { - return removePrefix("https://").removePrefix("http://") -} - -internal fun String.ensureTrailingSlash(): String { +fun String.ensureTrailingSlash(): String { return when { isEmpty() -> this !endsWith("/") -> "$this/" diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt index d49ffc1306..c9fb8fbccd 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -19,7 +19,6 @@ package im.vector.app.features.call.conference import im.vector.app.R import im.vector.app.core.network.await import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.ensureNoProtocol import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.toBase32String import im.vector.app.features.call.conference.jwt.JitsiJWTFactory @@ -60,9 +59,8 @@ class JitsiService @Inject constructor( rawService.getElementWellknown(session.myUserId) ?.jitsiServer ?.preferredDomain - ?.ensureNoProtocol() } - val jitsiDomain = (preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain)) + val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) val jitsiAuth = getJitsiAuth(jitsiDomain) val confId = createConferenceId(roomId, jitsiAuth) @@ -133,11 +131,10 @@ class JitsiService @Inject constructor( } private suspend fun getOpenIdJWTToken(roomId: String, domain: String, userDisplayName: String, userAvatar: String): String { - val openIdToken = session.thirdPartyService().getOpenIdToken() + val openIdToken = session.openIdService().getOpenIdToken() return jitsiJWTFactory.create( - homeServerName = session.sessionParams.homeServerUrl.ensureNoProtocol(), + openIdToken = openIdToken, jitsiServerDomain = domain, - openIdAccessToken = openIdToken.accessToken, roomId = roomId, userAvatarUrl = userAvatar, userDisplayName = userDisplayName diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt index 8ba8ec0c75..8014e01fb2 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt @@ -37,7 +37,7 @@ class JitsiWidgetPropertiesFactory @Inject constructor( .orEmpty() return JitsiWidgetProperties( - domain = (configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain)), + domain = configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain), confId = configs["conferenceId"], displayName = configs["displayName"], avatarUrl = configs["avatarUrl"] diff --git a/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt index 68475232c7..39b87c5d63 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt @@ -20,6 +20,7 @@ import im.vector.app.core.utils.ensureProtocol import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.security.Keys +import org.matrix.android.sdk.api.session.openid.OpenIdToken import javax.inject.Inject class JitsiJWTFactory @Inject constructor() { @@ -28,9 +29,8 @@ class JitsiJWTFactory @Inject constructor() { * Create a JWT token for jitsi openidtoken-jwt authentication * See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification */ - fun create(homeServerName: String, + fun create(openIdToken: OpenIdToken, jitsiServerDomain: String, - openIdAccessToken: String, roomId: String, userAvatarUrl: String, userDisplayName: String): String { @@ -38,9 +38,9 @@ class JitsiJWTFactory @Inject constructor() { val key = Keys.secretKeyFor(SignatureAlgorithm.HS256) val context = mapOf( "matrix" to mapOf( - "token" to openIdAccessToken, + "token" to openIdToken.accessToken, "room_id" to roomId, - "server_name" to homeServerName + "server_name" to openIdToken.matrixServerName ), "user" to mapOf( "name" to userDisplayName, From 0d0b6a88100fb9d10fc878900332067e7eec2305 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 14 May 2021 13:10:13 +0200 Subject: [PATCH 154/202] Fix empty states for spaces --- .../ui/list/GenericEmptyWithActionItem.kt | 92 +++++++++++++++++++ .../explore/SpaceDirectoryController.kt | 24 ++++- .../spaces/explore/SpaceDirectoryFragment.kt | 45 +++++++++ .../spaces/explore/SpaceDirectoryState.kt | 3 +- .../spaces/explore/SpaceDirectoryViewModel.kt | 20 ++++ .../manage/SpaceManageRoomsController.kt | 35 ++++--- .../main/res/drawable/ic_empty_icon_room.xml | 13 +++ .../layout/fragment_room_directory_picker.xml | 2 +- .../res/layout/item_generic_empty_state.xml | 70 ++++++++++++++ .../main/res/menu/menu_space_directory.xml | 15 +++ vector/src/main/res/values/strings.xml | 8 +- 11 files changed, 308 insertions(+), 19 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/ui/list/GenericEmptyWithActionItem.kt create mode 100644 vector/src/main/res/drawable/ic_empty_icon_room.xml create mode 100644 vector/src/main/res/layout/item_generic_empty_state.xml create mode 100644 vector/src/main/res/menu/menu_space_directory.xml diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericEmptyWithActionItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericEmptyWithActionItem.kt new file mode 100644 index 0000000000..f8eb968268 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericEmptyWithActionItem.kt @@ -0,0 +1,92 @@ +/* + * 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.app.core.ui.list + +import android.content.res.ColorStateList +import android.view.View +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextOrHide + +/** + * A generic list item to display when there is no results, with an optional CTA + */ +@EpoxyModelClass(layout = R.layout.item_generic_empty_state) +abstract class GenericEmptyWithActionItem : VectorEpoxyModel() { + + class Action(var title: String) { + var perform: Runnable? = null + } + + @EpoxyAttribute + var title: CharSequence? = null + + @EpoxyAttribute + var description: CharSequence? = null + + @EpoxyAttribute + @DrawableRes + var iconRes: Int = -1 + + @EpoxyAttribute + @ColorInt + var iconTint: Int? = null + + @EpoxyAttribute + var buttonAction: Action? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.titleText.setTextOrHide(title) + holder.descriptionText.setTextOrHide(description) + + if (iconRes != -1) { + holder.imageView.setImageResource(iconRes) + holder.imageView.isVisible = true + if (iconTint != null) { + ImageViewCompat.setImageTintList(holder.imageView, ColorStateList.valueOf(iconTint!!)) + } else { + ImageViewCompat.setImageTintList(holder.imageView, null) + } + } else { + holder.imageView.isVisible = false + } + + holder.actionButton.setTextOrHide(buttonAction?.title) + holder.actionButton.setOnClickListener { + buttonAction?.perform?.run() + } + } + + class Holder : VectorEpoxyHolder() { + val root by bind(R.id.item_generic_root) + val titleText by bind(R.id.emptyItemTitleView) + val descriptionText by bind(R.id.emptyItemMessageView) + val imageView by bind(R.id.emptyItemImageView) + val actionButton by bind